diff --git a/dev/design/core-suite-failure-reduction.md b/dev/design/core-suite-failure-reduction.md index e4f9e7f1e7..d108bac4e4 100644 --- a/dev/design/core-suite-failure-reduction.md +++ b/dev/design/core-suite-failure-reduction.md @@ -253,6 +253,23 @@ both commits. - `op/write.t` changed from 273 to 257 explicit JVM Not OK records, repairing sixteen assertions. Remaining failures are separate multiline, continuation, and format-lifecycle clusters. + +- [x] Phase 20: compile format argument expressions and preserve literal-only + pictures (2026-10-25) + - Compile each comma-separated expression as executable code and evaluate + the resulting list at write time, preserving declaration lexical bindings, + tied fetches, and array/hash list behavior across JVM and interpreter. + - Treat unpaired plain-text lines as literal pictures and preserve top-level + commas while parsing argument expressions. + - Added `unit/format_literal_only_pictures.t`; the focused format unit passed + on both backends and the bundled project `make` gate succeeded. + - Parse format templates in a child lexical scope so a `my`/`state` declared + by one argument expression is visible to later expressions on that line, + but is not captured by the format or leaked into its surrounding scope. + - Added `unit/format_argument_line_lexical_scope.t`, validated with system + Perl and both PerlOnJava backends. The full Perl core `op/write.t` + integration now passes 636/636 assertions, including both former #123245 + failures. - Make `@*` consume one terminal record separator before adjacent literal picture text, matching `formline` behavior for values such as `"N\\n"` rendered with `3@*4`. Added regression coverage, validated with system diff --git a/dev/design/function-parameters-compatibility.md b/dev/design/function-parameters-compatibility.md new file mode 100644 index 0000000000..a03310d6de --- /dev/null +++ b/dev/design/function-parameters-compatibility.md @@ -0,0 +1,164 @@ +# Function::Parameters compatibility + +## Goal + +Implement a documented, PerlOnJava-native subset of `Function::Parameters` +using lexical parser keyword providers and the shared AST consumed by both +execution backends. The work follows issues #1292 and #1361; it does not load +the upstream XS parser plugin. + +## Design + +- A compile-time module import registers statement keywords through + `ParserExtensionRegistry`. +- `ScopedSymbolTable` owns plugin registrations in lexical scope stacks, so + nested blocks and eval snapshots inherit or remove syntax with Perl pragma + scope. +- A plugin consumes its declaration and returns ordinary Perl AST nodes. The + first provider delegates positional signature lowering to the existing core + signature implementation. +- Providers must preserve ordinary identifier parsing when their module is + inactive and issue a source-located parser diagnostic for unsupported forms. + +## Progress tracking + +### Current status: Core subset implemented; upstream compatibility work remains (2026-10-26) + +### Completed + +- [x] Add a lexical parser keyword plugin interface and module-provider + registry. +- [x] Add `Function::Parameters` module loading and named `fun` plus implicit- + or explicit-invocant named and anonymous `method` providers, backed by core + signatures. +- [x] Honor selective `fun`/`method` imports and removals, while retaining + default imports when no keyword list is supplied. +- [x] Preserve parameter names and required/optional categories as shared + runtime signature metadata, and implement `Function::Parameters::info` for + the supported subset. +- [x] Add regression coverage for required/default parameters, slurpy arrays, + explicit and default `$self` method invocants, and lexical `no` removal. +- [x] Support anonymous `fun` expressions, lexical blocks, nested `eval`, and + source parsing with both execution backends. +- [x] Cover `//=`/`||=` defaults, ignored parameters, slurpy hashes, named + parameters with a rest hash, and recursive named functions. +- [x] Validate the project unit gate and system Perl behavior where the + upstream module is available. +- [x] Parse scalar type constraints and run their `check`/`get_message` + methods when a declared function is called. +- [x] Honor configured `check_argument_types => 0`; when enabled, apply a + constraint's `coerce` method when `has_coercion` returns true before checking + the coerced parameter value. + Simple quoted type names are reified as type functions before validation, + matching Function::Parameters' `('TypeName')` form. +- [x] Recognize `GraphQL::MaybeTypeCheck`'s delegated import as a provider for + the same lexical `fun`/`method` keywords. +- [x] Parse Type::Tiny bare and composite signature constraints, post-signature + attributes, and method `@_` behavior after invocant binding. +- [x] Register hash-configured function aliases, class methods, and methods + with multiple configured or explicit invocants; support optional explicit + invocants and lax arity configurations; preserve the configured keyword and + invocant list in `Function::Parameters::info` metadata. + +### Current follow-up + +- The default `method` keyword now injects `$self` as its invocant when a + declaration does not provide an explicit `($name: ...)` invocant. Explicit + invocants remain supported. An empty user parameter list is supported too. +- The syntax and introspection regressions pass on JVM and + interpreter backends; the full repository `make` gate passed on this + candidate. +- `Function::Parameters::Info` now exposes invocants, required/optional + positional and named parameters, slurpy metadata, parameter name/type + accessors, keyword, invocant count, and minimum/maximum argument counts. + Named declarations publish this metadata before their first invocation, and + anonymous closures retain it when cloned. Ordinary subs return `undef` from + `info()`. The object layout matches the upstream Info class for this subset. +- Function keyword configurations validate supported options, honor + `check_argument_count`, and apply configured attributes such as `:lvalue`. + Strict slurpy diagnostics identify both the later parameter and the earlier + slurpy parameter. Calls that resolve to a core operator after a same-named + Function::Parameters declaration report the ambiguous-core warning. +- Named argument extraction now starts after positional parameters, guards + empty tails when positional defaults are omitted, and preserves distinct + too-few, missing-name, and odd-pair diagnostics. Required named parameters + are validated with named slurpy arrays as well as ordinary signatures. +- The upstream `named_params.t` suite passes all 134 assertions. The expanded + project unit file passes 107 assertions under system Perl, JVM, and + interpreter; the system Perl oracle uses the cached CPAN XS build in + `/private/tmp/fp-542-source/blib`. +- Configured `reify_type => 'moose'` handles bare/composite type source, + parenthesized expressions, and typed slurpy arrays. The three selected + upstream `types_moose*.t` files pass 49 assertions each on system Perl and + both PerlOnJava backends. JVM anonymous code objects now retain parameter + type-name metadata as well as names. Callback-based `reify_type`, caller + types, and inline checks pass their selected upstream tests on both backends. + A focused project regression covers an anonymous typed slurpy parameter. +- Unparenthesized recursive calls with fat-comma named arguments now have a + project regression and pass both backends. Recursive positional calls are + also recognized as direct calls while the declaration body is compiled. + The upstream `recursion.t` now passes all 26 assertions, including the + `:lax` case where an omitted named parameter has no explicit default. +- Lax Function::Parameters declarations no longer record no-default named + parameters as required. The new omission regression passes under system Perl + and both PerlOnJava backends. +- The latest serialized 56-file upstream run had 45 passing files, 4 errors, + and 7 incomplete files (1,592 planned assertions; no timeouts). Subsequent + focused runs fixed and passed `gorn.t`, `precedence.t`, `types_parse.t`, + `types_custom_2.t`, `types_caller.t`, `types_inline.t`, and all three + `types_moose*.t` files on both backends. Remaining gaps are `types_auto.t` + composite expressions; runtime errors in `elsewhere.t` and + `lineno-torture.t`; the warning-category mismatch in `stringy_h.t`; and + incomplete coverage in `defaults_bare.t`, `bonus.t`, `types_msg.t`, + `lifetime.t`, `lineno.t`, and `eval.t`. These are not claimed as passing. +- Upstream `unicode.t` and `unicode2.t` now pass all 19 and 25 assertions. The + parser recognizes expression-position qualified declarations and reports + invalid Unicode and numeric declaration names with the configured keyword. +- Upstream `info.t`, `strict.t`, `prototype.t`, `types_coerce.t`, and + `attributes.t` pass. +- Scalar type constraints pass focused JVM/interpreter coverage, including + bare Type::Tiny names and bracket-form expressions. Constraint expressions + and checks run as part of signature setup for each call; coercion is applied + before checking when supported, and `check_argument_types => 0` skips those + checks. `Info::Param->type` remains undefined. +- GraphQL's `GraphQL::MaybeTypeCheck` wrapper delegates its import of + `Function::Parameters` into the caller. The parser provider models that + wrapper import. Its CPAN compile test and the GraphQL POST/route-handler + integration cases now pass, including the promise and error request cases. + A direct call to `GraphQL::Language::Receiver::got_value` also passes after + method bodies began seeing `@_` without the bound invocant. The full + `t/basic.t` suite completes. Two WebSocket assertions still differ under + PerlOnJava: `subs response` receives a queued data packet where the test + expects the stop completion, and `subs keepalive` receives a keepalive + packet where it expects the subscription response. A diagnostic run measured + 0.39–7.8 second gaps between resolver callbacks scheduled 100 ms apart on this + busy host; isolated timer and WebSocket probes pass on both Perl and + PerlOnJava. The cached CPAN test with native Function::Parameters under + system Perl passes all nine top-level subtests. The timing failures need + verification on a less-contended runner before attributing them to a runtime + defect. + +### Next steps + +1. Add auto type grammar support for bracket parameterization and composite + operators, then rerun `types_auto.t`. +2. Classify and fix the remaining upstream errors and incomplete files listed + above, preserving focused project regression coverage for each confirmed + PerlOnJava defect. +3. Continue method-modifier coverage and verify GraphQL WebSocket ordering on + an uncongested runner before assigning that timing difference to runtime + behavior. + +### Open questions + +- Which configured keyword aliases should be part of the first public subset? +- How should provider-specific metadata map onto the existing signature + annotations without creating a second backend representation? The current + `Function::Parameters::Info` implementation reuses these annotations. +- Which configured keyword aliases should be part of the first public subset? + +## Related references + +- GitHub issues #1292 and #1361. +- `src/main/java/org/perlonjava/frontend/parser/ParserKeywordPlugin.java`. +- [Parser keyword plugins](../../docs/reference/parser-keyword-plugins.md). diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 402f9193fe..1684cfd953 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -7,6 +7,10 @@ priorities and future plans. ## Work in progress - Keep nested Rose::HTML::Objects form results alive with counted argument owners so weak parent links and qualified field names survive argument evaluation (#1851). +- Preserve multiline regex searches when positive lookaheads constrain a lazy any-character prefix, restoring Text::YAWikiFormater formatting (#1596). +- Compile executable format argument expressions at write time on both backends, scope line-local lexicals correctly, preserve literal-only pictures, and refresh deferred lexical bindings (#1234). +- Diagnose malformed and out-of-class Object::Pad `BUILD` declarations during parsing (#1808). +- Add a lexical parser keyword provider and a Function::Parameters subset with named, anonymous, and runtime-installed `fun`/`method`, `install_sub` callbacks and method modifiers, configured aliases and multiple invocants, attributes, strict/lax arity modes, eager signature introspection including named type metadata, named argument binding after positional parameters, required-name and odd-pair validation, Unicode unknown-name diagnostics, native slurpy signature compatibility, clear invocant and slurpy diagnostics, ambiguous-core warnings, optional type checks and coercions, quoted type names, delegated imports, Type::Tiny parameter checks, post-signature attributes, correct method `@_` forwarding, and method cache updates through whole-glob aliases (#1292, #1361). - Warn for undefined right operands of `.=` while keeping an undefined left operand quiet (#1382). - Fix bundled IO, nested launcher, locale, and core user/group database module compatibility. - Snapshot mutable array entries while marking package roots and during reachability walks so concurrent thread teardown cannot fail with `ConcurrentModificationException`. diff --git a/docs/reference/architecture.md b/docs/reference/architecture.md index ed7f4c8f34..d60b80fdab 100644 --- a/docs/reference/architecture.md +++ b/docs/reference/architecture.md @@ -19,6 +19,13 @@ Both backends share 100% of the same runtime APIs and can call each other seamle This document describes the internal architecture and compilation pipeline. +## Parser keyword extensions + +Parser plugins can register compile-time keywords in lexical scope and lower +their declarations into the shared AST used by both execution backends. See +[Parser keyword plugins](parser-keyword-plugins.md) for the plugin contract +and module-provider lifecycle. + ## Project Structure ``` diff --git a/docs/reference/parser-keyword-plugins.md b/docs/reference/parser-keyword-plugins.md new file mode 100644 index 0000000000..8df4d9171b --- /dev/null +++ b/docs/reference/parser-keyword-plugins.md @@ -0,0 +1,47 @@ +# Parser keyword plugins + +PerlOnJava's parser has an internal extension point for compile-time keywords +that produce the same AST consumed by the JVM and interpreter backends. The +current API is intended for bundled language integrations; it is not yet a +stable third-party plugin SDK. + +## Plugin contract + +Implement `ParserKeywordPlugin` and register it under the keyword text. At a +statement boundary, `parse(Parser parser, String keyword)` receives the live +parser with `parser.tokenIndex` at the keyword. It must consume the tokens that +belong to its declaration and return ordinary AST nodes. A malformed +declaration should use the parser's source-aware diagnostic methods. + +`parseExpression` is an optional hook for keywords that can also appear in +expression position. Return `null` when the keyword does not begin an +expression there; the parser then continues with normal identifier handling. +The statement hook is invoked only for a registered keyword and is expected to +return a node or report a parse error. + +The interface is defined in +[`ParserKeywordPlugin.java`](../../src/main/java/org/perlonjava/frontend/parser/ParserKeywordPlugin.java). +`Parser.registerKeywordPlugin` and `removeKeywordPlugin` update the active +`ScopedSymbolTable`, so registrations follow lexical parser scopes. + +## Module providers and lexical scope + +`ParserExtensionRegistry` maps a module name to a provider callback. The +statement parser invokes that provider when it compiles `use Module` or +`no Module`; the callback registers or removes its keyword names in the +current lexical scope. Nested blocks and `eval` use the parser's existing +scope and snapshot behavior. + +The built-in `Function::Parameters` provider is a small example. It registers +`fun` and `method` by default, honors selective keyword imports, and lowers +named and anonymous declarations into the ordinary subroutine/signature AST. +See +[`ParserExtensionRegistry.java`](../../src/main/java/org/perlonjava/frontend/parser/ParserExtensionRegistry.java) +and +[`FunctionParametersKeywordPlugin.java`](../../src/main/java/org/perlonjava/frontend/parser/FunctionParametersKeywordPlugin.java). + +Plugins operate on mutable parser state. Preserve the previous value of any +parser mode flags that the plugin changes, restore them in `finally`, and avoid +leaving temporary token edits or scope state behind after parsing. Parser +registration is compile-time and lexical; runtime `import` alone cannot make +new syntax available to source that has already been parsed. diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 64db38185f..7d9b7c7892 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -486,6 +486,7 @@ private void predeclareGotoLabels(Node node, boolean expressionContext, boolean // Used by both eval STRING (for variable capture) and debugger (for lexical access). final List> evalSiteRegistries = new ArrayList<>(); final List> evalSiteLexicalSubroutineBindings = new ArrayList<>(); + final List> evalSiteParserKeywordPlugins = new ArrayList<>(); final List evalSitePragmaFlags = new ArrayList<>(); // Cache for DEBUG site registry deduplication - reuse index if scope unchanged private Map lastDebugRegistry = null; @@ -619,6 +620,8 @@ private void emitNormalLoopResult() { // Runtime regex interpolation can synthesize executable source through // overload, so the containing CV must expose all of its live lexical cells. private boolean tracksRuntimeRegexLexicals; + // Deferred format arguments also resolve lexical cells when write() runs. + private boolean tracksRuntimeFormatLexicals; // True when compiling inside a defer block (control flow out of defer is prohibited) private boolean isInDeferBlock; @@ -1632,6 +1635,7 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { symbolTable.getCurrentPackage(), evalSiteRegistries.isEmpty() ? null : evalSiteRegistries, evalSiteLexicalSubroutineBindings.isEmpty() ? null : evalSiteLexicalSubroutineBindings, + evalSiteParserKeywordPlugins.isEmpty() ? null : evalSiteParserKeywordPlugins, evalSitePragmaFlags.isEmpty() ? null : evalSitePragmaFlags, warningBitsString ); @@ -6649,6 +6653,10 @@ boolean tracksRuntimeRegexLexicals() { return tracksRuntimeRegexLexicals; } + boolean tracksRuntimeFormatLexicals() { + return tracksRuntimeFormatLexicals; + } + /** * Apply Perl's context conversion to an array value. This is shared by * named arrays and dereferenced arrays: scalar {@code @$ref} is the array @@ -7480,6 +7488,10 @@ private void visitAnonymousSubroutine(SubroutineNode node) { subCode.attributes = node.attributes; subCode.packageName = node.getAnnotation("regexCallbackPackage") instanceof String pkg ? pkg : getCurrentPackage(); + if (node.getAnnotation("functionParametersRuntimeName") instanceof String runtimeName) { + RuntimeCode.configureFunctionParametersRuntime( + new RuntimeScalar(subCode), runtimeName); + } if (node.getAnnotation("lexicalSubDisplayName") instanceof String lexicalName) { subCode.subName = lexicalName; subCode.lexicalSubDisplayName = true; @@ -9247,6 +9259,18 @@ public void visit(CompilerFlagNode node) { @Override public void visit(FormatNode node) { + tracksRuntimeFormatLexicals = true; + for (int index = 0; index < node.templateLines.size(); index++) { + FormatLine line = node.templateLines.get(index); + if (index > 0 && node.templateLines.get(index - 1) instanceof PictureLine + && line instanceof ArgumentLine argumentLine) { + List expressions = FormatExpressionCompiler.compile( + argumentLine, sourceName, errorUtil, symbolTable, emitterContext); + if (!expressions.isEmpty()) { + argumentLine.setAnnotation("executableFormatExpressions", expressions); + } + } + } // Format declarations are compile-time side effects. Keep the parsed // RuntimeFormat in the constant pool and register it when this code // executes, before a later write() can look it up. @@ -9295,7 +9319,10 @@ public void visit(FormatNode node) { if (entry.getKey() instanceof String name && entry.getValue() instanceof OperatorNode declaration && declaration.getAnnotation("bytecodeLexicalRegister") instanceof Integer reg) { - captures.putIfAbsent(name, reg); + // The frontend symbol-table index is not a bytecode + // register. Prefer the register assigned to the lexical + // declaration when both are present in this capture map. + captures.put(name, reg); } } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 8de586e526..2f754bed52 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -2901,7 +2901,8 @@ && isLocalizedArraySliceReferenceTarget(referenceOp.operand)) { // releases its capture. Refresh the live-cell registry so // later executable runtime source captures the new cell, // not the value that existed before an eval or callback. - if (bytecodeCompiler.tracksRuntimeRegexLexicals()) { + if (bytecodeCompiler.tracksRuntimeRegexLexicals() + || bytecodeCompiler.tracksRuntimeFormatLexicals()) { bytecodeCompiler.emitActiveLexicalBinding(targetReg, varName); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index ce46f62092..09379714d5 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -1623,6 +1623,10 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode node instanceof EvalOperatorNode evalNode ? evalNode.getSymbolTable().getVisibleLexicalSubroutineBindings() : Map.of()); + bytecodeCompiler.evalSiteParserKeywordPlugins.add( + node instanceof EvalOperatorNode evalNode + ? evalNode.getSymbolTable().getVisibleParserKeywordPlugins() + : Map.of()); bytecodeCompiler.evalSitePragmaFlags.add(new int[]{ bytecodeCompiler.symbolTable.strictOptionsStack.peek(), bytecodeCompiler.symbolTable.featureFlagsStack.peek(), diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 10ecd9b7d4..ebd18043d8 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -10,6 +10,7 @@ import org.perlonjava.frontend.lexer.Lexer; import org.perlonjava.frontend.lexer.LexerToken; import org.perlonjava.frontend.parser.Parser; +import org.perlonjava.frontend.parser.ParserKeywordPlugin; import org.perlonjava.frontend.parser.SpecialBlockParser; import org.perlonjava.frontend.semantic.ScopedSymbolTable; import org.perlonjava.runtime.HintHashRegistry; @@ -166,7 +167,7 @@ public static RuntimeList evalStringList(String perlCode, boolean isEvalbytes) { return evalStringList(perlCode, RuntimeScalarType.STRING, currentCode, registers, sourceName, sourceLine, callContext, siteRegistry, siteStrictOptions, - siteFeatureFlags, isEvalbytes, null, -1, false, null); + siteFeatureFlags, isEvalbytes, null, -1, false, null, null); } public static RuntimeList evalStringList(RuntimeScalar codeScalar, @@ -197,7 +198,7 @@ public static RuntimeList evalStringList(RuntimeScalar codeScalar, RuntimeCode.rejectTaintedEval(codeScalar); return evalStringList(codeScalar.toString(), codeScalar.type, currentCode, registers, sourceName, sourceLine, callContext, siteRegistry, siteStrictOptions, - siteFeatureFlags, isEvalbytes, null, -1, false, null); + siteFeatureFlags, isEvalbytes, null, -1, false, null, null); } public static RuntimeList evalStringList(RuntimeScalar codeScalar, @@ -266,11 +267,32 @@ public static RuntimeList evalStringList(RuntimeScalar codeScalar, int siteRegexDebugFlags, boolean siteEnhancedXx, Map siteLexicalSubroutineBindings) { + return evalStringList(codeScalar, currentCode, registers, sourceName, sourceLine, + callContext, siteRegistry, siteStrictOptions, siteFeatureFlags, isEvalbytes, + siteWarningBits, siteRegexDebugFlags, siteEnhancedXx, + siteLexicalSubroutineBindings, null); + } + + public static RuntimeList evalStringList(RuntimeScalar codeScalar, + InterpretedCode currentCode, + RuntimeBase[] registers, + String sourceName, + int sourceLine, + int callContext, + Map siteRegistry, + int siteStrictOptions, + int siteFeatureFlags, + boolean isEvalbytes, + String siteWarningBits, + int siteRegexDebugFlags, + boolean siteEnhancedXx, + Map siteLexicalSubroutineBindings, + Map siteParserKeywordPlugins) { RuntimeCode.rejectTaintedEval(codeScalar); return evalStringList(codeScalar.toString(), codeScalar.type, currentCode, registers, sourceName, sourceLine, callContext, siteRegistry, siteStrictOptions, siteFeatureFlags, isEvalbytes, siteWarningBits, siteRegexDebugFlags, - siteEnhancedXx, siteLexicalSubroutineBindings); + siteEnhancedXx, siteLexicalSubroutineBindings, siteParserKeywordPlugins); } private static RuntimeList evalStringList(String perlCode, @@ -287,7 +309,8 @@ private static RuntimeList evalStringList(String perlCode, String siteWarningBits, int siteRegexDebugFlags, boolean siteEnhancedXx, - Map siteLexicalSubroutineBindings) { + Map siteLexicalSubroutineBindings, + Map siteParserKeywordPlugins) { try (PerlRuntime.Binding runtimeBinding = PerlRuntime.current().bind()) { PerlLanguageProvider.CompilationLockGuard compilationLock = PerlLanguageProvider.acquireCompilationLock(); @@ -516,6 +539,11 @@ private static RuntimeList evalStringList(String perlCode, if (siteLexicalSubroutineBindings != null) { symbolTable.copyLexicalSubroutineBindings(siteLexicalSubroutineBindings); } + if (siteParserKeywordPlugins != null) { + for (Map.Entry entry : siteParserKeywordPlugins.entrySet()) { + symbolTable.registerParserKeywordPlugin(entry.getKey(), entry.getValue()); + } + } // Inherit lexical pragma flags from parent if available if (currentCode != null) { diff --git a/src/main/java/org/perlonjava/backend/bytecode/FormatExpressionCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/FormatExpressionCompiler.java new file mode 100644 index 0000000000..4da9104948 --- /dev/null +++ b/src/main/java/org/perlonjava/backend/bytecode/FormatExpressionCompiler.java @@ -0,0 +1,86 @@ +package org.perlonjava.backend.bytecode; + +import org.perlonjava.app.cli.CompilerOptions; +import org.perlonjava.frontend.astnode.ArgumentLine; +import org.perlonjava.frontend.astnode.Node; +import org.perlonjava.frontend.astnode.ListNode; +import org.perlonjava.backend.jvm.EmitterContext; +import org.perlonjava.backend.jvm.JavaClassInfo; +import org.perlonjava.frontend.semantic.ScopedSymbolTable; +import org.perlonjava.frontend.semantic.SymbolTable; +import org.perlonjava.runtime.runtimetypes.ErrorMessageUtil; +import org.perlonjava.runtime.runtimetypes.RuntimeArray; +import org.perlonjava.runtime.runtimetypes.RuntimeContextType; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Compiles parsed format argument expressions into shared interpreter bytecode. */ +public final class FormatExpressionCompiler { + private FormatExpressionCompiler() {} + + public static List compile(ArgumentLine line, String sourceName, + ErrorMessageUtil errorUtil, + ScopedSymbolTable scope, + EmitterContext context) { + if (line == null || line.getBooleanAnnotation("formatExpressionParseFallback") + || line.expressions.isEmpty()) { + return List.of(); + } + + Map registry = new LinkedHashMap<>(); + if (scope != null) { + // ScopedSymbolTable indices are frontend identities and start at + // zero. InterpretedCode reserves registers 0..2 for `this`, `@_` + // and `wantarray`, so translate visible entries into the captured + // register range used by withCapturedVars(). + scope.getVisibleVariableRegistry().forEach( + (name, index) -> registry.put(name, index + 3)); + } + Map declarations = new LinkedHashMap<>(); + if (scope != null) { + for (SymbolTable.SymbolEntry entry : scope.getAllVisibleVariables().values()) { + if (entry.decl() != null) declarations.put(entry.name(), entry.decl()); + } + } + Map ourPackages = scope == null + ? Map.of() : scope.getVisibleOurRegistry(); + + EmitterContext compileContext = context; + if (compileContext == null) { + CompilerOptions options = new CompilerOptions(); + options.fileName = sourceName; + compileContext = new EmitterContext(new JavaClassInfo(), scope, null, null, + RuntimeContextType.LIST, false, errorUtil, options, new RuntimeArray()); + } else { + compileContext = compileContext.with(RuntimeContextType.LIST); + } + + List compiled = new ArrayList<>(line.expressions.size()); + for (Node expression : line.expressions) { + if (expression instanceof ListNode list && !list.parenthesized) { + for (Node element : list.elements) { + compiled.add(compileOne(element, line, sourceName, errorUtil, + registry, declarations, ourPackages, compileContext)); + } + } else { + compiled.add(compileOne(expression, line, sourceName, errorUtil, + registry, declarations, ourPackages, compileContext)); + } + } + return List.copyOf(compiled); + } + + private static InterpretedCode compileOne(Node expression, ArgumentLine line, + String sourceName, ErrorMessageUtil errorUtil, + Map registry, + Map declarations, + Map ourPackages, + EmitterContext compileContext) { + BytecodeCompiler compiler = new BytecodeCompiler(sourceName, + line.sourceLine, errorUtil, registry, declarations, ourPackages); + return compiler.compile(expression, compileContext); + } +} diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index fcb6814c5e..f128aa3863 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -1,6 +1,7 @@ package org.perlonjava.backend.bytecode; import org.perlonjava.frontend.astnode.OperatorNode; +import org.perlonjava.frontend.parser.ParserKeywordPlugin; import org.perlonjava.runtime.WarningBitsRegistry; import org.perlonjava.runtime.perlmodule.Strict; import org.perlonjava.runtime.runtimetypes.*; @@ -57,6 +58,7 @@ public ScalarPin scalarPin(int nameIdx) { public final Map variableRegistry; // Variable name → register index (for eval STRING) public final List> evalSiteRegistries; // Per-eval-site variable registries public final List> evalSiteLexicalSubroutineBindings; + public final List> evalSiteParserKeywordPlugins; public final List evalSitePragmaFlags; // [strict, features, evalbytes, warningBitsPoolIndex, regexDebug, enhancedXx] // Optimization flags (set by compiler after construction) @@ -191,7 +193,7 @@ public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, int strictOptions, int featureFlags, BitSet warningFlags) { this(bytecode, constants, stringPool, maxRegisters, capturedVars, sourceName, sourceLine, pcToTokenIndex, variableRegistry, errorUtil, - strictOptions, featureFlags, warningFlags, "main", null, null, null, null); + strictOptions, featureFlags, warningFlags, "main", null, null, null, null, null); } public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, @@ -204,7 +206,7 @@ public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, String compilePackage) { this(bytecode, constants, stringPool, maxRegisters, capturedVars, sourceName, sourceLine, pcToTokenIndex, variableRegistry, errorUtil, - strictOptions, featureFlags, warningFlags, compilePackage, null, null, null, null); + strictOptions, featureFlags, warningFlags, compilePackage, null, null, null, null, null); } public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, @@ -217,12 +219,14 @@ public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, String compilePackage, List> evalSiteRegistries, List> evalSiteLexicalSubroutineBindings, + List> evalSiteParserKeywordPlugins, List evalSitePragmaFlags, String warningBitsString) { this(bytecode, constants, stringPool, maxRegisters, capturedVars, sourceName, sourceLine, pcToTokenIndex, variableRegistry, errorUtil, strictOptions, featureFlags, warningFlags, compilePackage, - evalSiteRegistries, evalSiteLexicalSubroutineBindings, evalSitePragmaFlags, + evalSiteRegistries, evalSiteLexicalSubroutineBindings, + evalSiteParserKeywordPlugins, evalSitePragmaFlags, warningBitsString, false, null); } @@ -236,6 +240,7 @@ private InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, String compilePackage, List> evalSiteRegistries, List> evalSiteLexicalSubroutineBindings, + List> evalSiteParserKeywordPlugins, List evalSitePragmaFlags, String warningBitsString, boolean reuseDeparseSourceText, @@ -252,6 +257,7 @@ private InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, this.variableRegistry = variableRegistry; this.evalSiteRegistries = evalSiteRegistries; this.evalSiteLexicalSubroutineBindings = evalSiteLexicalSubroutineBindings; + this.evalSiteParserKeywordPlugins = evalSiteParserKeywordPlugins; this.evalSitePragmaFlags = evalSitePragmaFlags; this.errorUtil = errorUtil; this.strictOptions = strictOptions; @@ -612,6 +618,7 @@ public InterpretedCode withCapturedVars(RuntimeBase[] capturedVars) { this.compilePackage, this.evalSiteRegistries, this.evalSiteLexicalSubroutineBindings, + this.evalSiteParserKeywordPlugins, this.evalSitePragmaFlags, this.warningBitsString, true, @@ -676,10 +683,17 @@ public InterpretedCode withCapturedVars(RuntimeBase[] capturedVars) { copy.signatureMinArgs = this.signatureMinArgs; copy.signatureMaxArgs = this.signatureMaxArgs; copy.signatureSubName = this.signatureSubName; + copy.signatureKeyword = this.signatureKeyword; copy.signatureNamedParams = this.signatureNamedParams; copy.signatureRequiredNamedParams = this.signatureRequiredNamedParams; + copy.signatureInvocants = this.signatureInvocants; + copy.signaturePositionalRequired = this.signaturePositionalRequired; + copy.signaturePositionalOptional = this.signaturePositionalOptional; + copy.signatureNamedRequired = this.signatureNamedRequired; + copy.signatureNamedOptional = this.signatureNamedOptional; copy.signaturePositionalCount = this.signaturePositionalCount; copy.signatureSlurpySigil = this.signatureSlurpySigil; + copy.signatureSlurpyName = this.signatureSlurpyName; return copy; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index 4df3e1b7af..03abca0e68 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -293,6 +293,11 @@ public static int executeEvalString( && evalSiteIndex < code.evalSiteLexicalSubroutineBindings.size()) { siteLexicalSubroutineBindings = code.evalSiteLexicalSubroutineBindings.get(evalSiteIndex); } + Map siteParserKeywordPlugins = null; + if (evalSiteIndex >= 0 && code.evalSiteParserKeywordPlugins != null + && evalSiteIndex < code.evalSiteParserKeywordPlugins.size()) { + siteParserKeywordPlugins = code.evalSiteParserKeywordPlugins.get(evalSiteIndex); + } // Look up per-eval-site pragma flags (strict/feature at compile time of eval site) int siteStrictOptions = -1; @@ -362,7 +367,8 @@ public static int executeEvalString( siteWarningBits, siteRegexDebugFlags, siteEnhancedXx, - siteLexicalSubroutineBindings + siteLexicalSubroutineBindings, + siteParserKeywordPlugins ); registers[rd] = result; evalTrace("EVAL_STRING opcode exit LIST stored=" + (registers[rd] != null ? registers[rd].getClass().getSimpleName() : "null") + @@ -382,7 +388,8 @@ public static int executeEvalString( siteWarningBits, siteRegexDebugFlags, siteEnhancedXx, - siteLexicalSubroutineBindings + siteLexicalSubroutineBindings, + siteParserKeywordPlugins ); // Preserve control-flow markers so the enclosing interpreter frame // can resolve a valid target outside eval STRING (for example, diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitEval.java b/src/main/java/org/perlonjava/backend/jvm/EmitEval.java index c96f4e7f34..97ec1ff0be 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitEval.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitEval.java @@ -146,6 +146,7 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) // Preserve any special flags from the parse-time symbol table newSymbolTable.copyFlagsFrom(evalOperatorNode.getSymbolTable()); newSymbolTable.copyLexicalSubroutineBindingsFrom(evalOperatorNode.getSymbolTable()); + newSymbolTable.copyParserKeywordPluginsFrom(evalOperatorNode.getSymbolTable()); } if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("evalStringHelper newSymbolTable: " + newSymbolTable); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitFormat.java b/src/main/java/org/perlonjava/backend/jvm/EmitFormat.java index f7976a7c1f..5023e0e235 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitFormat.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitFormat.java @@ -1,6 +1,8 @@ package org.perlonjava.backend.jvm; import org.perlonjava.app.cli.CompilerOptions; +import org.perlonjava.backend.bytecode.FormatExpressionCompiler; +import org.perlonjava.backend.bytecode.InterpretedCode; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; @@ -33,6 +35,9 @@ public static void emitFormat(EmitterVisitor emitterVisitor, FormatNode node) { // Always generate format bytecode, even in VOID context // The side effect of storing the format is important + Map executableExpressionFields = + registerExecutableExpressions(ctx, node); + // Create ArrayList for template lines first mv.visitTypeInsn(Opcodes.NEW, "java/util/ArrayList"); mv.visitInsn(Opcodes.DUP); @@ -43,7 +48,9 @@ public static void emitFormat(EmitterVisitor emitterVisitor, FormatNode node) { mv.visitInsn(Opcodes.DUP); // Duplicate ArrayList reference // Create the appropriate FormatLine object based on type - emitFormatLine(ctx, node.templateLines.get(i)); + FormatLine line = node.templateLines.get(i); + emitFormatLine(ctx, line, line instanceof ArgumentLine argumentLine + ? executableExpressionFields.get(argumentLine) : null); // Preserve template-line provenance for runtime warnings emitted // while this format is written. @@ -136,7 +143,10 @@ public static void emitFormat(EmitterVisitor emitterVisitor, FormatNode node) { if (entry.getKey() instanceof String name && entry.getValue() instanceof OperatorNode declaration && declaration.getAnnotation("jvmLexicalSlot") instanceof Integer slot) { - captures.putIfAbsent(name, slot); + // The frontend symbol-table index may not be a JVM local + // slot. The declaration annotation is the backend's + // authoritative location for this lexical cell. + captures.put(name, slot); } } } @@ -158,6 +168,26 @@ public static void emitFormat(EmitterVisitor emitterVisitor, FormatNode node) { if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("FORMAT end: " + node.formatName); } + private static Map registerExecutableExpressions( + EmitterContext ctx, FormatNode node) { + Map fields = new java.util.IdentityHashMap<>(); + for (int index = 0; index < node.templateLines.size(); index++) { + FormatLine formatLine = node.templateLines.get(index); + if (index == 0 || !(node.templateLines.get(index - 1) instanceof PictureLine) + || !(formatLine instanceof ArgumentLine argumentLine)) continue; + java.util.List expressions = FormatExpressionCompiler.compile( + argumentLine, ctx.compilerOptions == null ? "" : ctx.compilerOptions.fileName, + ctx.errorUtil, ctx.symbolTable, ctx); + if (expressions.isEmpty()) continue; + String key = ctx.javaClassInfo.javaClassName + "#format#" + argumentLine.tokenIndex; + org.perlonjava.runtime.runtimetypes.RuntimeFormatExpressionRegistry + .register(key, expressions); + fields.put(argumentLine, + ctx.javaClassInfo.registerFormatExpressionField(key, ctx.cw)); + } + return fields; + } + /** * Emits bytecode to create a FormatLine object. * This method handles the creation of different types of format lines @@ -166,7 +196,8 @@ public static void emitFormat(EmitterVisitor emitterVisitor, FormatNode node) { * @param ctx The emitter context. * @param line The format line to emit. */ - private static void emitFormatLine(EmitterContext ctx, FormatLine line) { + private static void emitFormatLine(EmitterContext ctx, FormatLine line, + String executableExpressionField) { MethodVisitor mv = ctx.mv; if (line instanceof CommentLine commentLine) { @@ -200,11 +231,25 @@ private static void emitFormatLine(EmitterContext ctx, FormatLine line) { mv.visitLdcInsn(argumentLine.content); // Create expressions list - emitNodeList(ctx, argumentLine.expressions); + emitEmptyNodeList(ctx); mv.visitLdcInsn(argumentLine.tokenIndex); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/frontend/astnode/ArgumentLine", "", "(Ljava/lang/String;Ljava/util/List;I)V", false); + if (executableExpressionField != null) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn("executableFormatExpressions"); + mv.visitFieldInsn(Opcodes.GETSTATIC, ctx.javaClassInfo.javaClassName, + executableExpressionField, "Ljava/util/List;"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/frontend/astnode/ArgumentLine", "setAnnotation", + "(Ljava/lang/String;Ljava/lang/Object;)V", false); + } + if (argumentLine.getAnnotation("unavailableLexicalVariableNames") + instanceof java.util.List unavailableNames) { + emitStringListAnnotation(ctx, "unavailableLexicalVariableNames", + unavailableNames); + } if (argumentLine.getAnnotation("unavailableLexicalSubWarning") instanceof String warning) { mv.visitLdcInsn(warning); mv.visitLdcInsn((String) argumentLine.getAnnotation("unavailableLexicalSubError")); @@ -215,6 +260,27 @@ private static void emitFormatLine(EmitterContext ctx, FormatLine line) { } } + private static void emitStringListAnnotation(EmitterContext ctx, String key, + java.util.List values) { + MethodVisitor mv = ctx.mv; + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(key); + mv.visitTypeInsn(Opcodes.NEW, "java/util/ArrayList"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/util/ArrayList", "", "()V", false); + for (Object value : values) { + if (!(value instanceof String text)) continue; + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(text); + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/List", "add", + "(Ljava/lang/Object;)Z", true); + mv.visitInsn(Opcodes.POP); + } + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/frontend/astnode/ArgumentLine", "setAnnotation", + "(Ljava/lang/String;Ljava/lang/Object;)V", false); + } + /** * Emits bytecode to create a list of FormatField objects. * @@ -303,7 +369,7 @@ private static void emitFormatField(EmitterContext ctx, FormatField field) { * @param ctx The emitter context. * @param nodes The list of nodes to emit. */ - private static void emitNodeList(EmitterContext ctx, java.util.List nodes) { + private static void emitEmptyNodeList(EmitterContext ctx) { MethodVisitor mv = ctx.mv; // Create ArrayList @@ -311,32 +377,7 @@ private static void emitNodeList(EmitterContext ctx, java.util.List nodes) mv.visitInsn(Opcodes.DUP); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/util/ArrayList", "", "()V", false); - // Add each node to the list - for (Node node : nodes) { - mv.visitInsn(Opcodes.DUP); // Duplicate ArrayList reference - - // For now, create simple StringNode objects - // In a full implementation, we would emit the actual node bytecode - if (node instanceof StringNode stringNode) { - mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/frontend/astnode/StringNode"); - mv.visitInsn(Opcodes.DUP); - mv.visitLdcInsn(stringNode.value); - mv.visitLdcInsn(stringNode.tokenIndex); - mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/frontend/astnode/StringNode", - "", "(Ljava/lang/String;I)V", false); - } else { - // For other node types, create a placeholder StringNode - mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/frontend/astnode/StringNode"); - mv.visitInsn(Opcodes.DUP); - mv.visitLdcInsn(node.toString()); - mv.visitLdcInsn(0); - mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/frontend/astnode/StringNode", - "", "(Ljava/lang/String;I)V", false); - } - - mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/List", "add", - "(Ljava/lang/Object;)Z", true); - mv.visitInsn(Opcodes.POP); // Pop boolean return value - } + // The executable bytecode templates are attached separately. Do not + // stringify AST nodes into fake StringNode instances here. } } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java index a4ba378758..4436e0f2ca 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java @@ -507,6 +507,7 @@ && isDirectLeafIntegerAddition(node.block, directLeafCaptures, "makeCodeObject", "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;IIII)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); + emitFunctionParametersMetadata(mv, node.block); int endLocationToken = node.block instanceof BlockNode block && block.elements != null && !block.elements.isEmpty() ? block.elements.get(block.elements.size() - 1).getIndex() @@ -812,6 +813,18 @@ && isDirectLeafIntegerAddition(node.block, directLeafCaptures, "Ljava/util/List;"); } + if (node.getAnnotation("functionParametersRuntimeName") instanceof String runtimeName) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(runtimeName); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", + "configureFunctionParametersRuntime", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Ljava/lang/String;)" + + "Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", + false); + mv.visitInsn(Opcodes.POP); + } + RuntimeScalar compileTimeAttributeCodeRef = node.getAnnotation("compileTimeAttributeCodeRef") instanceof RuntimeScalar ref ? ref : null; @@ -1954,4 +1967,74 @@ private static boolean isDirectArrayArgument(Node node) { return node instanceof OperatorNode operator && "@".equals(operator.operator) && operator.operand instanceof IdentifierNode; } + + private static void emitFunctionParametersMetadata(MethodVisitor mv, Node block) { + if (!(block instanceof AbstractNode abstractNode) + || !(abstractNode.getAnnotation("signatureKeyword") instanceof String keyword)) { + return; + } + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(keyword); + Object subName = abstractNode.getAnnotation("signatureSubName"); + if (subName instanceof String name) mv.visitLdcInsn(name); + else mv.visitInsn(Opcodes.ACONST_NULL); + emitSignatureNames(mv, abstractNode, "signatureInvocants"); + emitSignatureNames(mv, abstractNode, "signaturePositionalRequired"); + emitSignatureNames(mv, abstractNode, "signaturePositionalOptional"); + emitSignatureNames(mv, abstractNode, "signatureNamedRequired"); + emitSignatureNames(mv, abstractNode, "signatureNamedOptional"); + emitSignatureNames(mv, abstractNode, "signatureNamedParams"); + emitSignatureNames(mv, abstractNode, "signatureRequiredNamedParams"); + Object positionalCount = abstractNode.getAnnotation("signaturePositionalCount"); + mv.visitLdcInsn(positionalCount instanceof Integer count ? count : 0); + emitSignatureString(mv, abstractNode.getAnnotation("signatureSlurpySigil")); + emitSignatureString(mv, abstractNode.getAnnotation("signatureSlurpyName")); + emitSignatureMap(mv, abstractNode.getAnnotation("signatureParameterTypes")); + emitSignatureMap(mv, abstractNode.getAnnotation("signatureParameterTypeObjectVariables")); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", + "attachFunctionParametersMetadata", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Ljava/lang/String;Ljava/lang/String;" + + "[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;" + + "[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;ILjava/lang/String;" + + "Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)" + + "Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", + false); + mv.visitInsn(Opcodes.POP); + } + + private static void emitSignatureNames(MethodVisitor mv, AbstractNode node, String annotation) { + Object value = node.getAnnotation(annotation); + java.util.List names = value instanceof java.util.List list ? list : java.util.List.of(); + mv.visitLdcInsn(names.size()); + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/String"); + for (int i = 0; i < names.size(); i++) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(i); + mv.visitLdcInsn(String.valueOf(names.get(i))); + mv.visitInsn(Opcodes.AASTORE); + } + } + + private static void emitSignatureMap(MethodVisitor mv, Object value) { + java.util.Map values = value instanceof java.util.Map map ? map : java.util.Map.of(); + mv.visitLdcInsn(values.size() * 2); + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/String"); + int index = 0; + for (var entry : values.entrySet()) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(index++); + mv.visitLdcInsn(String.valueOf(entry.getKey())); + mv.visitInsn(Opcodes.AASTORE); + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(index++); + mv.visitLdcInsn(String.valueOf(entry.getValue())); + mv.visitInsn(Opcodes.AASTORE); + } + } + + private static void emitSignatureString(MethodVisitor mv, Object value) { + if (value instanceof String text) mv.visitLdcInsn(text); + else mv.visitInsn(Opcodes.ACONST_NULL); + } } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index e62078336f..29f7e2dba9 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -28,6 +28,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; /** @@ -380,11 +381,14 @@ public static byte[] getBytecode(EmitterContext ctx, Node ast, boolean useTryCat try { return getBytecodeInternal(ctx, ast, useTryCatch, false); } catch (MethodTooLargeException tooLarge) { + discardPendingFormatExpressions(ctx); throw tooLarge; } catch (InterpreterFallbackException fallback) { + discardPendingFormatExpressions(ctx); // Re-throw - caller will handle interpreter fallback throw fallback; } catch (ArrayIndexOutOfBoundsException frameComputeCrash) { + discardPendingFormatExpressions(ctx); // ASM frame computation failed - fall back to interpreter // This commonly happens with nested defers and complex control flow @@ -430,6 +434,7 @@ public static byte[] getBytecode(EmitterContext ctx, Node ast, boolean useTryCat String[] envNames = ctx.capturedEnv != null ? ctx.capturedEnv : ctx.symbolTable.getVariableNames(); throw new InterpreterFallbackException(interpretedCode, envNames); } catch (PerlCompilerException compileError) { + discardPendingFormatExpressions(ctx); if (asmDebug && causedByAsmFrameMerge(compileError)) { compileError.printStackTrace(); try { @@ -445,6 +450,17 @@ public static byte[] getBytecode(EmitterContext ctx, Node ast, boolean useTryCat } } throw compileError; + } catch (RuntimeException compileError) { + discardPendingFormatExpressions(ctx); + throw compileError; + } + } + + private static void discardPendingFormatExpressions(EmitterContext ctx) { + if (ctx != null && ctx.javaClassInfo != null + && ctx.javaClassInfo.javaClassName != null) { + RuntimeFormatExpressionRegistry.discardClass( + ctx.javaClassInfo.javaClassName); } } @@ -538,19 +554,6 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean cw.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, "WARNING_BITS", "Ljava/lang/String;", null, warningBits).visitEnd(); - // Create static initializer to register warning bits with WarningBitsRegistry - MethodVisitor clinit = cw.visitMethod(Opcodes.ACC_STATIC, "", "()V", null, null); - clinit.visitCode(); - clinit.visitLdcInsn(className.replace('/', '.')); // Convert to Java class name format - clinit.visitLdcInsn(warningBits); - clinit.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/WarningBitsRegistry", - "register", - "(Ljava/lang/String;Ljava/lang/String;)V", false); - clinit.visitInsn(Opcodes.RETURN); - clinit.visitMaxs(2, 0); - clinit.visitEnd(); - // Add a constructor with parameters for initializing the fields // Include ALL env slots (even nulls) so signature matches caller expectations StringBuilder constructorDescriptor = new StringBuilder("("); @@ -1406,6 +1409,29 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean mv.visitMaxs(0, 0); // Automatically computed mv.visitEnd(); + // Initialize warning metadata and the compiled format-expression + // templates collected while the apply method was emitted. + MethodVisitor clinit = cw.visitMethod(Opcodes.ACC_STATIC, "", "()V", null, null); + clinit.visitCode(); + clinit.visitLdcInsn(className.replace('/', '.')); + clinit.visitLdcInsn(warningBits); + clinit.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/WarningBitsRegistry", + "register", + "(Ljava/lang/String;Ljava/lang/String;)V", false); + for (Map.Entry entry + : ctx.javaClassInfo.getFormatExpressionFields().entrySet()) { + clinit.visitLdcInsn(entry.getKey()); + clinit.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeFormatExpressionRegistry", + "take", "(Ljava/lang/String;)Ljava/util/List;", false); + clinit.visitFieldInsn(Opcodes.PUTSTATIC, className, entry.getValue(), + "Ljava/util/List;"); + } + clinit.visitInsn(Opcodes.RETURN); + clinit.visitMaxs(0, 0); + clinit.visitEnd(); + // Complete the class cw.visitEnd(); classData = cw.toByteArray(); // Generate the bytecode diff --git a/src/main/java/org/perlonjava/backend/jvm/JavaClassInfo.java b/src/main/java/org/perlonjava/backend/jvm/JavaClassInfo.java index 81789dbc7d..299fff38de 100644 --- a/src/main/java/org/perlonjava/backend/jvm/JavaClassInfo.java +++ b/src/main/java/org/perlonjava/backend/jvm/JavaClassInfo.java @@ -65,6 +65,7 @@ public TryFinallyContext(Label finallyStart, int returnValueSlot, /** Static fields holding this class's package-scalar pins, keyed by the pinned name. */ private final Map scalarPinFields = new LinkedHashMap<>(); + private final Map formatExpressionFields = new LinkedHashMap<>(); private final IdentityHashMap stringLiteralFields = new IdentityHashMap<>(); private final IdentityHashMap integerLiteralFields = new IdentityHashMap<>(); @@ -96,6 +97,21 @@ public String registerScalarPinField(String name) { return field; } + /** Reserve a static field initialized from a compiled format-expression template. */ + public String registerFormatExpressionField(String registryKey, ClassWriter cw) { + String field = formatExpressionFields.get(registryKey); + if (field != null) return field; + field = "formatExpressions$" + formatExpressionFields.size(); + cw.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, + field, "Ljava/util/List;", null, null).visitEnd(); + formatExpressionFields.put(registryKey, field); + return field; + } + + public Map getFormatExpressionFields() { + return Collections.unmodifiableMap(formatExpressionFields); + } + /** Register an instance field for one compiled string literal occurrence. */ public String stringLiteralField(StringNode node, ClassWriter cw) { String field = stringLiteralFields.get(node); diff --git a/src/main/java/org/perlonjava/frontend/parser/FormatParser.java b/src/main/java/org/perlonjava/frontend/parser/FormatParser.java index a84b0fb585..e95e266c42 100644 --- a/src/main/java/org/perlonjava/frontend/parser/FormatParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/FormatParser.java @@ -57,11 +57,6 @@ public static FormatNode parseFormatDeclaration(Parser parser, String formatName if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("Parsing format declaration: " + formatName); - // Parse format template content immediately - List templateLines = parseFormatTemplateContentImmediate(parser); - - // Create a format node with the parsed template content - FormatNode formatNode = new FormatNode(formatName, templateLines, tokenIndex); // A FORMAT captures lexical cells from its declaration site. Keep the // declaration ASTs, not parser-time numeric ids: each backend assigns // its own executable local slot while lowering the declaration. @@ -72,6 +67,21 @@ public static FormatNode parseFormatDeclaration(Parser parser, String formatName lexicalDeclarations.put(entry.name(), entry.ast()); } } + + // Format argument lines are deferred code. Lexicals declared by an + // argument expression (for example, `my $x = ..., $x`) belong to that + // expression and must not leak into the declaration's surrounding + // scope or become captured by the RuntimeFormat itself. + int formatScope = parser.ctx.symbolTable.enterScope(); + List templateLines; + try { + templateLines = parseFormatTemplateContentImmediate(parser); + } finally { + parser.ctx.symbolTable.exitScope(formatScope); + } + + // Create a format node with the parsed template content + FormatNode formatNode = new FormatNode(formatName, templateLines, tokenIndex); formatNode.setAnnotation("formatLexicalDeclarations", lexicalDeclarations); // Formats are declarations, not statements delayed until an enclosing @@ -155,7 +165,8 @@ private static List parseFormatTemplateContentImmediate(Parser parse } // Parse the line and add to template - FormatLine formatLine = parseFormatLine(parser, line, lineIndex); + FormatLine formatLine = parseFormatLine(parser, line, lineIndex, + expectsArgumentLine(templateLines)); setSourceLocation(parser, formatLine, lineIndex); templateLines.add(formatLine); currentLine.setLength(0); @@ -180,7 +191,8 @@ private static List parseFormatTemplateContentImmediate(Parser parse if (line.trim().equals(".")) { foundTerminator = true; } else { - FormatLine formatLine = parseFormatLine(parser, line, lineIndex); + FormatLine formatLine = parseFormatLine(parser, line, lineIndex, + expectsArgumentLine(templateLines)); setSourceLocation(parser, formatLine, lineIndex); templateLines.add(formatLine); } @@ -263,7 +275,8 @@ public static void parseFormatTemplateContent(Parser parser) { } // Parse the line and add to template - FormatLine formatLine = parseFormatLine(parser, line, lineIndex); + FormatLine formatLine = parseFormatLine(parser, line, lineIndex, + expectsArgumentLine(templateLines)); setSourceLocation(parser, formatLine, lineIndex); templateLines.add(formatLine); currentLine.setLength(0); @@ -319,7 +332,18 @@ public static void parseFormatTemplateContent(Parser parser) { * @param tokenIndex The token index for this line * @return FormatLine representing the parsed line */ - private static FormatLine parseFormatLine(Parser parser, String line, int tokenIndex) { + private static boolean expectsArgumentLine(List templateLines) { + for (int index = templateLines.size() - 1; index >= 0; index--) { + FormatLine previous = templateLines.get(index); + if (previous instanceof CommentLine) continue; + return previous instanceof PictureLine pictureLine + && !pictureLine.fields.isEmpty(); + } + return false; + } + + private static FormatLine parseFormatLine(Parser parser, String line, int tokenIndex, + boolean expectsArgumentLine) { if (line.trim().equals(".//")) { var location = parser.ctx.errorUtil.getSourceLocationAccurate(tokenIndex); throw new PerlCompilerException("syntax error at " + location.fileName() @@ -345,6 +369,15 @@ private static FormatLine parseFormatLine(Parser parser, String line, int tokenI return new PictureLine(line, fields, literalText, tokenIndex); } + // Lines in a format are either pictures (including literal-only + // pictures) or argument expressions paired with the preceding + // picture. Treat an unpaired plain-text line as a literal picture; + // attempting to parse words like "body" as expressions both rejects + // valid formats under strict subs and executes text as code. + if (!expectsArgumentLine && !multilineBracedArgument) { + return new PictureLine(line, List.of(), line, tokenIndex); + } + // Otherwise, treat as argument line // A format argument may not substitute into the @_ aggregate. Perl // diagnoses this while compiling the FORMAT and does not install the @@ -357,19 +390,31 @@ private static FormatLine parseFormatLine(Parser parser, String line, int tokenI "Can't modify array dereference in substitution (s///)", parser.ctx.errorUtil); } parser.formatArgumentLexicalSubName = null; - List expressions = parseArgumentExpressions(parser, line, tokenIndex); + List expressions = parseArgumentExpressions( + parser, line, tokenIndex, multilineBracedArgument); ArgumentLine argumentLine = new ArgumentLine(line, expressions, tokenIndex); + if (argumentLineExpressionsUseRuntimeFallback(line, expressions)) { + argumentLine.setAnnotation("formatExpressionParseFallback", true); + } annotateUnavailableLexicalSub(parser, argumentLine); annotateUnavailableLexicalVariables(parser, argumentLine); return argumentLine; } + private static boolean argumentLineExpressionsUseRuntimeFallback(String line, + List expressions) { + return !line.trim().isEmpty() && expressions.size() == 1 + && expressions.getFirst() instanceof StringNode stringNode + && stringNode.value.equals(line.trim()); + } + /** Preserve deferred closure warnings for lexical scalar/array/hash uses. */ private static void annotateUnavailableLexicalVariables(Parser parser, ArgumentLine argumentLine) { List unavailable = new ArrayList<>(); Matcher matcher = Pattern.compile("[$@%][A-Za-z_]\\w*").matcher(argumentLine.content); while (matcher.find()) { String name = matcher.group(); + if (declaredInArgumentLine(argumentLine.content, name)) continue; boolean lexical = parser.ctx.symbolTable.getAllVisibleVariables().values().stream() .anyMatch(entry -> name.equals(entry.name()) && ("my".equals(entry.decl()) || "state".equals(entry.decl()))); @@ -381,6 +426,20 @@ private static void annotateUnavailableLexicalVariables(Parser parser, ArgumentL } } + /** A lexical declared by the argument expression is local to that line. */ + private static boolean declaredInArgumentLine(String source, String name) { + Pattern declaration = Pattern.compile("\\b(?:my|state)\\b([^;=]*)"); + Matcher declarations = declaration.matcher(source); + Pattern variable = Pattern.compile("[$@%][A-Za-z_]\\w*"); + while (declarations.find()) { + Matcher variables = variable.matcher(declarations.group(1)); + while (variables.find()) { + if (name.equals(variables.group())) return true; + } + } + return false; + } + private static boolean hasPriorLexicalDeclaration(Parser parser, String name) { List tokens = parser.tokens; for (int i = 0; i + 1 < tokens.size(); i++) { @@ -595,7 +654,8 @@ private static String extractLiteralText(String line) { * @param tokenIndex The token index * @return List of expression nodes */ - private static List parseArgumentExpressions(Parser parser, String line, int tokenIndex) { + private static List parseArgumentExpressions(Parser parser, String line, int tokenIndex, + boolean multilineBracedArgument) { List expressions = new ArrayList<>(); if (line.trim().isEmpty()) { @@ -604,7 +664,8 @@ private static List parseArgumentExpressions(Parser parser, String line, i try { // Create a lexer for the argument line - Lexer lexer = new Lexer(line); + String expressionSource = multilineBracedArgument ? "do " + line : line; + Lexer lexer = new Lexer(expressionSource); List tokens = lexer.tokenize(); // Create a parser for the tokens @@ -613,14 +674,21 @@ private static List parseArgumentExpressions(Parser parser, String line, i argParser.parsingFormatArgumentLine = true; // Parse comma-separated expressions + boolean complete = false; while (argParser.tokenIndex < tokens.size()) { LexerToken token = tokens.get(argParser.tokenIndex); if (token.type == LexerTokenType.EOF) { + complete = true; break; } // Parse the next expression - Node expr = argParser.parseExpression(0); + // Keep the top-level comma for the argument-list loop below. + // Parsing with precedence zero absorbs the entire line into a + // ListNode, which can leave the parser positioned before a + // trailing token and silently switch valid expressions to the + // runtime source-eval fallback. + Node expr = argParser.parseExpression(5); if (expr != null) { expressions.add(expr); } @@ -632,11 +700,18 @@ private static List parseArgumentExpressions(Parser parser, String line, i argParser.tokenIndex++; // consume comma } else if (nextToken.type != LexerTokenType.EOF) { // If not comma and not EOF, we might have a syntax error - // For now, just break to avoid infinite loop + // Keep runtime parsing for syntax the format parser + // cannot represent completely. break; + } else { + complete = true; } } } + if (!complete) { + expressions.clear(); + expressions.add(new StringNode(line.trim(), tokenIndex)); + } parser.formatArgumentLexicalSubName = argParser.formatArgumentLexicalSubName; } catch (Exception e) { // If parsing fails, fall back to treating the whole line as a string literal diff --git a/src/main/java/org/perlonjava/frontend/parser/FunctionParametersKeywordPlugin.java b/src/main/java/org/perlonjava/frontend/parser/FunctionParametersKeywordPlugin.java new file mode 100644 index 0000000000..ec01a0ed22 --- /dev/null +++ b/src/main/java/org/perlonjava/frontend/parser/FunctionParametersKeywordPlugin.java @@ -0,0 +1,277 @@ +package org.perlonjava.frontend.parser; + +import org.perlonjava.frontend.astnode.Node; +import org.perlonjava.frontend.lexer.LexerToken; +import org.perlonjava.frontend.lexer.LexerTokenType; +import org.perlonjava.frontend.parser.SubroutineParser; +import org.perlonjava.runtime.runtimetypes.RuntimeScalar; + +import java.util.List; +import java.util.Set; + +/** Parses Function::Parameters declarations using core signature lowering. */ +final class FunctionParametersKeywordPlugin implements ParserKeywordPlugin { + static final FunctionParametersKeywordPlugin FUNCTION = + new FunctionParametersKeywordPlugin(false, 0, true, true, false, null, false, + List.of(), "auto", null, true); + static final FunctionParametersKeywordPlugin METHOD = + new FunctionParametersKeywordPlugin(true, 0, true, true, false, null, false, + List.of("$self"), "auto", null, true); + + private final boolean method; + private final int optionalInvocantCount; + private final boolean strict; + private final boolean checkArgumentTypes; + private final boolean runtime; + private final String installSub; + private final boolean requiresName; + private final boolean prohibitsName; + private final boolean defaultArguments; + private final boolean types; + private final String typeReifier; + private final RuntimeScalar typeReifierCallback; + private final List invocants; + private final List defaultAttributes; + + private FunctionParametersKeywordPlugin( + boolean method, int optionalInvocantCount, boolean strict, + boolean checkArgumentTypes, boolean runtime, String installSub, + boolean requiresName, List invocants, String typeReifier, + RuntimeScalar typeReifierCallback, boolean types) { + this(method, optionalInvocantCount, strict, checkArgumentTypes, runtime, + installSub, requiresName, false, true, invocants, List.of(), typeReifier, + typeReifierCallback, types); + } + + private FunctionParametersKeywordPlugin( + boolean method, int optionalInvocantCount, boolean strict, + boolean checkArgumentTypes, boolean runtime, String installSub, boolean requiresName, + boolean prohibitsName, boolean defaultArguments, + List invocants, + List defaultAttributes, + String typeReifier, + RuntimeScalar typeReifierCallback, + boolean types) { + this.method = method; + this.optionalInvocantCount = optionalInvocantCount; + this.strict = strict; + this.checkArgumentTypes = checkArgumentTypes; + this.runtime = runtime; + this.installSub = installSub; + this.requiresName = requiresName; + this.prohibitsName = prohibitsName; + this.defaultArguments = defaultArguments; + this.types = types; + this.typeReifier = typeReifier; + this.typeReifierCallback = typeReifierCallback; + this.invocants = List.copyOf(invocants); + this.defaultAttributes = List.copyOf(defaultAttributes); + } + + static FunctionParametersKeywordPlugin configured( + String defaults, List shift, int optionalInvocantCount, + boolean strict, boolean checkArgumentTypes, boolean runtime, String installSub, + List defaultAttributes, String namePolicy, boolean defaultArguments, + String typeReifier, RuntimeScalar typeReifierCallback, boolean types) { + boolean method = Set.of("method", "classmethod", "before", "after", "around", + "augment", "override").contains(defaults); + boolean requiresName = Set.of("before", "after", "around", "augment", "override") + .contains(defaults); + if (method) { + List invocants = shift; + if (invocants == null || invocants.isEmpty()) { + invocants = "around".equals(defaults) + ? List.of("$orig", "$self") + : List.of("classmethod".equals(defaults) ? "$class" : "$self"); + } + return new FunctionParametersKeywordPlugin( + true, 0, strict, checkArgumentTypes, runtime, installSub, + "required".equals(namePolicy) || requiresName, + "prohibited".equals(namePolicy), defaultArguments, invocants, defaultAttributes, + typeReifier, typeReifierCallback, types); + } + if (optionalInvocantCount == 0 && strict && checkArgumentTypes && !runtime && installSub == null + && defaultAttributes.isEmpty() && "optional".equals(namePolicy) && defaultArguments + && "auto".equals(typeReifier) && typeReifierCallback == null && types) { + return FUNCTION; + } + return new FunctionParametersKeywordPlugin( + false, optionalInvocantCount, strict, checkArgumentTypes, runtime, installSub, + "required".equals(namePolicy) || requiresName, + "prohibited".equals(namePolicy), defaultArguments, List.of(), defaultAttributes, + typeReifier, typeReifierCallback, types); + } + + @Override + public Node parse(Parser parser, String keyword) { + int declarationIndex = parser.tokenIndex; + LexerToken keywordToken = parser.tokens.get(declarationIndex); + String originalText = keywordToken.text; + int implicitInvocantInsertIndex = -1; + boolean previousFunctionParametersDeclaration = parser.parsingFunctionParametersDeclaration; + boolean previousFunctionParametersMethod = parser.parsingFunctionParametersMethod; + boolean previousOptionalInvocant = parser.parsingFunctionParametersOptionalInvocant; + int previousInvocantCount = parser.functionParametersInvocantCount; + boolean previousStrict = parser.functionParametersStrict; + boolean previousCheckArgumentTypes = parser.functionParametersCheckArgumentTypes; + boolean previousTypes = parser.functionParametersTypes; + String previousKeyword = parser.functionParametersKeyword; + String previousInstallSub = parser.functionParametersInstallSub; + String previousTypeReifier = parser.functionParametersTypeReifier; + RuntimeScalar previousTypeReifierCallback = parser.functionParametersTypeReifierCallback; + List previousAttributes = parser.functionParametersAttributes; + boolean previousRuntime = parser.functionParametersRuntime; + boolean previousDefaultArguments = parser.functionParametersDefaultArguments; + parser.tokenIndex++; + boolean startsWithApostrophe = "'".equals(TokenUtils.peek(parser).text); + if ("{".equals(TokenUtils.peek(parser).text)) { + parser.throwError("In " + keyword + + " (anon): I was expecting a parameter list, not \"{\""); + } + int declarationNameIndex = parser.tokenIndex; + String candidateName = null; + if (!"(".equals(TokenUtils.peek(parser).text) && !startsWithApostrophe) { + candidateName = IdentifierParser.parseSubroutineIdentifier(parser); + } + boolean validCandidateName = isValidCandidateName(candidateName); + parser.tokenIndex = declarationIndex; + if (startsWithApostrophe || (!"(".equals(parser.tokens.get( + Whitespace.skipWhitespace(parser, declarationNameIndex, parser.tokens)).text) + && !validCandidateName)) { + parser.throwError(originalText + " expects a parameter list"); + } + keywordToken.text = "sub"; + parser.tokenIndex++; + parser.parsingFunctionParametersDeclaration = true; + parser.parsingFunctionParametersMethod = method; + parser.parsingFunctionParametersOptionalInvocant = optionalInvocantCount > 0; + parser.functionParametersInvocantCount = method ? invocants.size() : optionalInvocantCount; + parser.functionParametersStrict = strict; + parser.functionParametersCheckArgumentTypes = checkArgumentTypes; + parser.functionParametersTypes = types; + parser.functionParametersKeyword = keyword; + parser.functionParametersAttributes = defaultAttributes; + parser.functionParametersRuntime = runtime; + parser.functionParametersDefaultArguments = defaultArguments; + parser.functionParametersInstallSub = installSub; + parser.functionParametersTypeReifier = typeReifier; + parser.functionParametersTypeReifierCallback = typeReifierCallback; + try { + LexerToken declarationStart = TokenUtils.peek(parser); + boolean named = declarationStart.type == LexerTokenType.IDENTIFIER + || declarationStart.text.equals("::") + || declarationStart.text.equals("'"); + if (named && parser.expressionParseDepth > 0) { + parser.throwError("expecting a parameter list"); + } + if (requiresName && !named) { + parser.throwError("expecting a function name"); + } + if (prohibitsName && named) parser.throwError("expecting a parameter list"); + if (method) { + int openParen = parser.tokenIndex; + while (openParen < parser.tokens.size() + && !"(".equals(parser.tokens.get(openParen).text) + && !"{".equals(parser.tokens.get(openParen).text)) { + openParen++; + } + if (openParen < parser.tokens.size() + && "(".equals(parser.tokens.get(openParen).text) + && !hasExplicitInvocant(parser, openParen)) { + implicitInvocantInsertIndex = openParen + 1; + int insertAt = implicitInvocantInsertIndex; + for (int i = 0; i < invocants.size(); i++) { + String invocant = invocants.get(i); + if (i > 0) { + parser.tokens.add(insertAt++, + new LexerToken(LexerTokenType.OPERATOR, ",")); + } + parser.tokens.add(insertAt++, + new LexerToken(LexerTokenType.OPERATOR, "$")); + parser.tokens.add(insertAt++, + new LexerToken(LexerTokenType.IDENTIFIER, + invocant.startsWith("$") ? invocant.substring(1) : invocant)); + } + parser.tokens.add(insertAt, + new LexerToken(LexerTokenType.OPERATOR, ":")); + } + } + Node declaration = SubroutineParser.parseSubroutineDefinition(parser, named, "our"); + if (installSub != null && !runtime) { + SpecialBlockParser.runSpecialBlock(parser, "BEGIN", declaration); + return new org.perlonjava.frontend.astnode.ListNode(parser.tokenIndex); + } + return declaration; + } finally { + if (implicitInvocantInsertIndex >= 0) { + int inserted = invocants.size() * 2 + (invocants.size() - 1) + 1; + for (int i = 0; i < inserted; i++) parser.tokens.remove(implicitInvocantInsertIndex); + parser.tokenIndex -= inserted; + } + parser.parsingFunctionParametersDeclaration = previousFunctionParametersDeclaration; + parser.parsingFunctionParametersMethod = previousFunctionParametersMethod; + parser.parsingFunctionParametersOptionalInvocant = previousOptionalInvocant; + parser.functionParametersInvocantCount = previousInvocantCount; + parser.functionParametersStrict = previousStrict; + parser.functionParametersCheckArgumentTypes = previousCheckArgumentTypes; + parser.functionParametersTypes = previousTypes; + parser.functionParametersKeyword = previousKeyword; + parser.functionParametersInstallSub = previousInstallSub; + parser.functionParametersTypeReifier = previousTypeReifier; + parser.functionParametersTypeReifierCallback = previousTypeReifierCallback; + parser.functionParametersAttributes = previousAttributes; + parser.functionParametersRuntime = previousRuntime; + parser.functionParametersDefaultArguments = previousDefaultArguments; + keywordToken.text = originalText; + } + } + + private static boolean hasExplicitInvocant(Parser parser, int openParen) { + int first = openParen + 1; + while (first < parser.tokens.size() + && (parser.tokens.get(first).type == LexerTokenType.WHITESPACE + || parser.tokens.get(first).type == LexerTokenType.NEWLINE)) { + first++; + } + if (first >= parser.tokens.size() + || !Set.of("$", "@", "%").contains(parser.tokens.get(first).text)) return false; + for (int i = first + 1; i < parser.tokens.size(); i++) { + String text = parser.tokens.get(i).text; + if (":".equals(text)) return true; + if (")".equals(text) || "{".equals(text)) return false; + } + return false; + } + + private static boolean isValidCandidateName(String name) { + if (name == null || name.isEmpty()) return false; + String[] components = name.split("::", -1); + int firstComponent = name.startsWith("::") ? 1 : 0; + for (int componentIndex = firstComponent; + componentIndex < components.length; componentIndex++) { + String component = components[componentIndex]; + if (component.isEmpty()) return false; + int firstCodePoint = component.codePointAt(0); + if (!IdentifierParser.isPerlIdentifierStart(firstCodePoint)) return false; + for (int offset = Character.charCount(firstCodePoint); offset < component.length();) { + int codePoint = component.codePointAt(offset); + if (!Character.isUnicodeIdentifierPart(codePoint) && codePoint != '_') return false; + offset += Character.charCount(codePoint); + } + } + return true; + } + + @Override + public Node parseExpression(Parser parser, String keyword) { + int keywordIndex = parser.tokenIndex; + parser.tokenIndex++; + LexerToken declarationStart = TokenUtils.peek(parser); + parser.tokenIndex = keywordIndex; + if (declarationStart.type == LexerTokenType.IDENTIFIER + || Set.of("(", "::", "'").contains(declarationStart.text)) { + return parse(parser, keyword); + } + return null; + } +} diff --git a/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java b/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java index b203df2a14..01ba74d93f 100644 --- a/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java +++ b/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java @@ -79,6 +79,14 @@ public static Node parsePrimary(Parser parser) { if (token.text.getBytes(StandardCharsets.UTF_8).length >= 1020) { parser.throwCleanError("Identifier too long"); } + ParserKeywordPlugin keywordPlugin = parser.getKeywordPlugin(token.text); + if (keywordPlugin != null) { + int afterKeyword = parser.tokenIndex; + parser.tokenIndex = startIndex; + Node keywordExpression = keywordPlugin.parseExpression(parser, token.text); + if (keywordExpression != null) return keywordExpression; + parser.tokenIndex = afterKeyword; + } if (parser.ctx.compilerOptions.isByteStringSource) { for (int i = 0; i < token.text.length(); ) { int codePoint = token.text.codePointAt(i); @@ -345,10 +353,6 @@ private static Node parseIdentifier(Parser parser, int startIndex, LexerToken to if (codeRef.value instanceof RuntimeCode code && code.isDeclared && !code.isBuiltin - && code.subroutine == null - && code.methodHandle == null - && code.compilerSupplier == null - && code.constantValue == null && (code.attributes == null || !code.attributes.contains("method"))) { String location = parser.ctx.errorUtil == null ? "" : parser.ctx.errorUtil.warningLocation(startIndex); diff --git a/src/main/java/org/perlonjava/frontend/parser/Parser.java b/src/main/java/org/perlonjava/frontend/parser/Parser.java index ffb9057ce3..0c378e56d9 100644 --- a/src/main/java/org/perlonjava/frontend/parser/Parser.java +++ b/src/main/java/org/perlonjava/frontend/parser/Parser.java @@ -195,6 +195,8 @@ public boolean hasVisibleLexicalSubroutine(String name) { private final List specialBlockPrototypeWarnings = new ArrayList<>(); // Current index in the token list. public int tokenIndex = 0; + /** Nesting depth of expression parsing, used by parser plugins to reject statements in expressions. */ + public int expressionParseDepth = 0; // Flags to indicate special parsing states. public boolean parsingForLoopVariable = false; // Nesting depth of runtime loop bodies currently being parsed. A lexical @@ -247,6 +249,31 @@ public boolean hasVisibleLexicalSubroutine(String name) { // path for signature lexicals; ordinary named subs must retain selective // capture to avoid retaining the entire surrounding lexical environment. public boolean parsingSignaturedSubroutine = false; + /** True while a parser extension delegates a function declaration to core signatures. */ + public boolean parsingFunctionParametersDeclaration = false; + /** True while parsing an explicit-invocant Function::Parameters method. */ + public boolean parsingFunctionParametersMethod = false; + /** True for configurations where a colon may optionally mark invocants. */ + public boolean parsingFunctionParametersOptionalInvocant = false; + /** Number of leading positional parameters used as Function::Parameters invocants. */ + public int functionParametersInvocantCount = 1; + /** Whether configured Function::Parameters signatures enforce exact arity. */ + public boolean functionParametersStrict = true; + /** Whether configured Function::Parameters signatures run type constraints. */ + public boolean functionParametersCheckArgumentTypes = true; + public boolean functionParametersTypes = true; + /** Active Function::Parameters keyword, retained for signature metadata. */ + public String functionParametersKeyword; + /** Default attributes supplied by the active Function::Parameters keyword. */ + public List functionParametersAttributes = List.of(); + /** Install a named Function::Parameters declaration when its statement runs. */ + public boolean functionParametersRuntime; + /** Whether Function::Parameters signatures accept default arguments. */ + public boolean functionParametersDefaultArguments = true; + public String functionParametersInstallSub; + public String functionParametersTypeReifier; + /** Optional compile-time callback used by Function::Parameters reify_type. */ + public org.perlonjava.runtime.runtimetypes.RuntimeScalar functionParametersTypeReifierCallback; // Are we parsing the top level script? public boolean isTopLevelScript = false; // Are we parsing inside a class block? @@ -336,6 +363,22 @@ public Parser(EmitterContext ctx, List tokens, List sh } } + public void registerKeywordPlugin(String keyword, ParserKeywordPlugin plugin) { + ctx.symbolTable.registerParserKeywordPlugin(keyword, plugin); + } + + public void removeKeywordPlugin(String keyword) { + ctx.symbolTable.removeParserKeywordPlugin(keyword); + } + + public ParserKeywordPlugin getKeywordPlugin(String keyword) { + return ctx.symbolTable.getParserKeywordPlugin(keyword); + } + + public java.util.Map getVisibleParserKeywordPlugins() { + return ctx.symbolTable.getVisibleParserKeywordPlugins(); + } + private void indexSourceLines() { int lineOffset = 0; for (LexerToken token : tokens) { @@ -555,6 +598,8 @@ public void deferErrorAtTokenWithoutTrailingCommaWhitespace(int index, String me * @return The root node of the parsed expression. */ public Node parseExpression(int precedence) { + expressionParseDepth++; + try { // First, parse the primary expression (like a number or a variable). Node left = ParsePrimary.parsePrimary(this); @@ -669,6 +714,9 @@ public Node parseExpression(int precedence) { // Return the root node of the constructed expression tree. return left; + } finally { + expressionParseDepth--; + } } /** diff --git a/src/main/java/org/perlonjava/frontend/parser/ParserExtensionRegistry.java b/src/main/java/org/perlonjava/frontend/parser/ParserExtensionRegistry.java new file mode 100644 index 0000000000..0e5596f09c --- /dev/null +++ b/src/main/java/org/perlonjava/frontend/parser/ParserExtensionRegistry.java @@ -0,0 +1,270 @@ +package org.perlonjava.frontend.parser; + +import org.perlonjava.runtime.runtimetypes.RuntimeBase; +import org.perlonjava.runtime.runtimetypes.RuntimeArray; +import org.perlonjava.runtime.runtimetypes.RuntimeHash; +import org.perlonjava.runtime.runtimetypes.RuntimeList; +import org.perlonjava.runtime.runtimetypes.RuntimeScalar; +import org.perlonjava.runtime.runtimetypes.RuntimeScalarType; + +import java.util.Map; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.concurrent.ConcurrentHashMap; + +/** Connects compile-time module imports to lexical parser keyword plugins. */ +public final class ParserExtensionRegistry { + @FunctionalInterface + public interface ModuleProvider { + void apply(Parser parser, boolean enabled, RuntimeList arguments); + } + + private static final Map PROVIDERS = new ConcurrentHashMap<>(); + + static { + ModuleProvider functionParameters = ParserExtensionRegistry::applyFunctionParameters; + registerModuleProvider("Function::Parameters", functionParameters); + // GraphQL::MaybeTypeCheck delegates Function::Parameters->import::into + // to its caller. That dynamic import is not visible to the incremental + // parser, so mirror the resulting default lexical keyword registration. + registerModuleProvider("GraphQL::MaybeTypeCheck", + (parser, enabled, arguments) -> applyFunctionParameters(parser, enabled, null)); + } + + private ParserExtensionRegistry() { } + + public static void registerModuleProvider(String moduleName, ModuleProvider provider) { + PROVIDERS.put(moduleName, provider); + } + + public static void applyModulePragma( + Parser parser, String moduleName, boolean enabled, RuntimeList arguments) { + ModuleProvider provider = PROVIDERS.get(moduleName); + if (provider != null) provider.apply(parser, enabled, arguments); + } + + private static void applyFunctionParameters( + Parser parser, boolean enabled, RuntimeList arguments) { + if (!enabled && (arguments == null || arguments.isEmpty())) { + parser.getVisibleParserKeywordPlugins().forEach((keyword, plugin) -> { + if (plugin instanceof FunctionParametersKeywordPlugin) { + parser.removeKeywordPlugin(keyword); + } + }); + return; + } + + if (arguments == null || arguments.isEmpty()) { + if (enabled) registerDefaults(parser); + return; + } + + for (RuntimeBase argument : arguments.elements) { + RuntimeBase value = unwrap(argument); + if (value instanceof RuntimeHash definitions) { + for (var entry : definitions.elements.entrySet()) { + if (enabled) { + parser.registerKeywordPlugin(entry.getKey(), + configuredPlugin(entry.getKey(), entry.getValue())); + } else { + parser.removeKeywordPlugin(entry.getKey()); + } + } + continue; + } + + String name = argument.toString(); + if (":std".equals(name)) { + if (enabled) registerDefaults(parser); + else removeDefaults(parser); + } else if (":modifiers".equals(name)) { + if (enabled) registerModifiers(parser); + else removeModifiers(parser); + } else if (":strict".equals(name) || ":lax".equals(name)) { + if (enabled) registerStrictnessDefaults(parser, ":strict".equals(name)); + else removeDefaults(parser); + } else if ("fun".equals(name) || "method".equals(name)) { + if (enabled) parser.registerKeywordPlugin(name, + "fun".equals(name) ? FunctionParametersKeywordPlugin.FUNCTION + : FunctionParametersKeywordPlugin.METHOD); + else parser.removeKeywordPlugin(name); + } else if (!enabled && !name.isEmpty()) { + parser.removeKeywordPlugin(name); + } + } + } + + private static void registerDefaults(Parser parser) { + parser.registerKeywordPlugin("fun", FunctionParametersKeywordPlugin.FUNCTION); + parser.registerKeywordPlugin("method", FunctionParametersKeywordPlugin.METHOD); + } + + private static void registerStrictnessDefaults(Parser parser, boolean strict) { + parser.registerKeywordPlugin("fun", FunctionParametersKeywordPlugin.configured( + "function", null, 0, strict, true, false, null, List.of(), "optional", true, + null, null, true)); + parser.registerKeywordPlugin("method", FunctionParametersKeywordPlugin.configured( + "method", null, 0, strict, true, false, null, List.of(), "optional", true, + null, null, true)); + } + + private static void removeDefaults(Parser parser) { + parser.removeKeywordPlugin("fun"); + parser.removeKeywordPlugin("method"); + } + + private static void registerModifiers(Parser parser) { + for (String keyword : List.of("before", "after", "around", "augment", "override")) { + parser.registerKeywordPlugin(keyword, configuredPlugin(keyword, new RuntimeScalar(keyword))); + } + } + + private static void removeModifiers(Parser parser) { + for (String keyword : List.of("before", "after", "around", "augment", "override")) { + parser.removeKeywordPlugin(keyword); + } + } + + private static FunctionParametersKeywordPlugin configuredPlugin(String keyword, RuntimeScalar definition) { + RuntimeBase value = unwrap(definition); + String defaults; + List shift = null; + int optionalInvocantCount = 0; + boolean strict = true; + boolean strictSpecified = false; + boolean checkArgumentTypes = true; + boolean runtime = false; + boolean defaultArguments = true; + boolean types = true; + String namePolicy = "optional"; + String installSub = null; + String reifyType = null; + RuntimeScalar reifyTypeCallback = null; + List defaultAttributes = List.of(); + if (value instanceof RuntimeHash configuration) { + Set supportedOptions = Set.of( + "defaults", "shift", "invocant", "strict", + "check_argument_count", "check_argument_types", "attributes", "runtime", + "install_sub", "default_arguments", "name", "reify_type", "types"); + for (String option : configuration.elements.keySet()) { + if (!supportedOptions.contains(option)) { + throw new IllegalArgumentException("Invalid keyword property: " + option); + } + } + RuntimeBase reifyTypeValue = configuration.elements.get("reify_type"); + RuntimeBase reifyTypeBase = unwrap(reifyTypeValue); + if (reifyTypeValue instanceof RuntimeScalar reifyMode + && reifyMode.type == RuntimeScalarType.CODE) { + reifyTypeCallback = reifyMode; + } else if (reifyTypeBase != null + && (!(reifyTypeBase instanceof RuntimeScalar reifyMode) + || !Set.of("auto", "moose").contains(reifyMode.toString()))) { + throw new IllegalArgumentException( + "Invalid reify_type: expected 'auto', 'moose', or a code reference"); + } + if (reifyTypeBase instanceof RuntimeScalar reifyTypeScalar) { + reifyType = reifyTypeScalar.toString(); + } + RuntimeScalar defaultValue = configuration.elements.get("defaults"); + String keywordDefaults = Set.of("method", "classmethod", "before", "after", "around", + "augment", "override").contains(keyword) ? keyword : "function"; + defaults = defaultValue == null ? keywordDefaults : defaultValue.toString(); + RuntimeScalar shiftValue = configuration.elements.get("shift"); + RuntimeBase shiftBase = shiftValue == null ? null : unwrap(shiftValue); + if (shiftBase instanceof RuntimeArray names) { + shift = new ArrayList<>(); + for (RuntimeScalar name : names.elements) shift.add(name.toString()); + } else if (shiftValue != null) { + shift = List.of(shiftValue.toString()); + } + RuntimeScalar invocantValue = configuration.elements.get("invocant"); + if (invocantValue != null) optionalInvocantCount = Math.max(0, invocantValue.getInt()); + RuntimeScalar strictValue = configuration.elements.get("strict"); + if (strictValue != null) { + strict = strictValue.getBoolean(); + strictSpecified = true; + } + RuntimeScalar checkArgumentCountValue = configuration.elements.get("check_argument_count"); + if (checkArgumentCountValue != null) { + strict = checkArgumentCountValue.getBoolean() + || (strictSpecified && strict); + strictSpecified = true; + } + RuntimeScalar checkArgumentTypesValue = configuration.elements.get("check_argument_types"); + if (checkArgumentTypesValue != null) { + checkArgumentTypes = checkArgumentTypesValue.getBoolean(); + } + RuntimeScalar runtimeValue = configuration.elements.get("runtime"); + if (runtimeValue != null) { + runtime = runtimeValue.getBoolean(); + } + RuntimeScalar installSubValue = configuration.elements.get("install_sub"); + if (installSubValue != null && installSubValue.getDefinedBoolean()) { + installSub = installSubValue.toString(); + } + RuntimeScalar attributesValue = configuration.elements.get("attributes"); + if (attributesValue != null) { + defaultAttributes = parseConfiguredAttributes(attributesValue.toString()); + } + RuntimeScalar defaultArgumentsValue = configuration.elements.get("default_arguments"); + if (defaultArgumentsValue != null) defaultArguments = defaultArgumentsValue.getBoolean(); + RuntimeScalar typesValue = configuration.elements.get("types"); + if (typesValue != null) types = typesValue.getBoolean(); + RuntimeScalar nameValue = configuration.elements.get("name"); + if (nameValue != null) { + namePolicy = nameValue.toString(); + if (!Set.of("required", "prohibited", "optional").contains(namePolicy)) { + throw new IllegalArgumentException("Invalid keyword property name: " + namePolicy); + } + } + } else { + defaults = definition.toString(); + } + if (defaults.endsWith("_strict") || defaults.endsWith("_lax")) { + if (!strictSpecified) strict = defaults.endsWith("_strict"); + defaults = defaults.substring(0, defaults.lastIndexOf('_')); + } + Set modifiers = Set.of("before", "after", "around", "augment", "override"); + if (modifiers.contains(defaults)) { + if (installSub == null) installSub = defaults; + // Function::Parameters modifier keywords default to runtime + // installation. Explicit configuration can still select parse-time. + if (!(value instanceof RuntimeHash configuration) + || !configuration.elements.containsKey("runtime")) runtime = true; + } + if (!Set.of("function", "method", "classmethod", "before", "after", "around", + "augment", "override").contains(defaults)) { + throw new IllegalArgumentException( + "Unsupported Function::Parameters keyword defaults: " + defaults); + } + return FunctionParametersKeywordPlugin.configured( + defaults, shift, optionalInvocantCount, strict, checkArgumentTypes, + runtime, installSub, defaultAttributes, namePolicy, defaultArguments, + reifyType, reifyTypeCallback, types); + } + + private static List parseConfiguredAttributes(String source) { + String trimmed = source.trim(); + String attributePattern = "[A-Za-z_]\\w*(?:\\([^()]*\\))?"; + if (!trimmed.matches("\\s*:\\s*" + attributePattern + + "(?:\\s*:?\\s*" + attributePattern + ")*\\s*")) { + throw new IllegalArgumentException( + "\"" + source + "\" doesn't look like valid attributes"); + } + Matcher matcher = Pattern.compile("[A-Za-z_]\\w*(?:\\([^()]*\\))?").matcher(trimmed); + List attributes = new ArrayList<>(); + while (matcher.find()) attributes.add(matcher.group()); + return List.copyOf(attributes); + } + + private static RuntimeBase unwrap(RuntimeBase value) { + if (value instanceof RuntimeScalar scalar && scalar.value instanceof RuntimeBase nested) { + return nested; + } + return value; + } +} diff --git a/src/main/java/org/perlonjava/frontend/parser/ParserKeywordPlugin.java b/src/main/java/org/perlonjava/frontend/parser/ParserKeywordPlugin.java new file mode 100644 index 0000000000..45688ae06d --- /dev/null +++ b/src/main/java/org/perlonjava/frontend/parser/ParserKeywordPlugin.java @@ -0,0 +1,14 @@ +package org.perlonjava.frontend.parser; + +import org.perlonjava.frontend.astnode.Node; + +/** Parses a registered lexical keyword at a statement boundary. */ +@FunctionalInterface +public interface ParserKeywordPlugin { + Node parse(Parser parser, String keyword); + + /** Parse a keyword in expression position, or return {@code null}. */ + default Node parseExpression(Parser parser, String keyword) { + return null; + } +} diff --git a/src/main/java/org/perlonjava/frontend/parser/ParserTables.java b/src/main/java/org/perlonjava/frontend/parser/ParserTables.java index d2b4eb05de..1fde8837b4 100644 --- a/src/main/java/org/perlonjava/frontend/parser/ParserTables.java +++ b/src/main/java/org/perlonjava/frontend/parser/ParserTables.java @@ -32,7 +32,7 @@ public class ParserTables { "exec", "exit", "fork", "getgrgid", "gethostbyname", "getlogin", "getpwuid", "glob", - "hex", + "hex", "join", "kill", "lock", "length", "localtime", "log", "pop", "oct", "open", diff --git a/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java b/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java index 6c6021883f..75773a5776 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java @@ -1,14 +1,26 @@ package org.perlonjava.frontend.parser; import org.perlonjava.frontend.astnode.*; +import org.perlonjava.frontend.lexer.Lexer; import org.perlonjava.frontend.lexer.LexerToken; import org.perlonjava.frontend.lexer.LexerTokenType; +import org.perlonjava.frontend.semantic.SymbolTable; import org.perlonjava.runtime.runtimetypes.NameNormalizer; import org.perlonjava.runtime.runtimetypes.PerlCompilerException; +import org.perlonjava.runtime.runtimetypes.RuntimeList; +import org.perlonjava.runtime.runtimetypes.RuntimeContextType; +import org.perlonjava.runtime.runtimetypes.RuntimeScalar; +import org.perlonjava.runtime.runtimetypes.RuntimeScalarType; +import org.perlonjava.runtime.runtimetypes.GlobalVariable; +import org.perlonjava.runtime.runtimetypes.ErrorMessageUtil; +import java.util.concurrent.atomic.AtomicLong; import org.perlonjava.runtime.operators.WarnDie; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Set; import static org.perlonjava.frontend.parser.OperatorParser.dieWarnNode; import static org.perlonjava.frontend.parser.ParserNodeUtils.atUnderscore; @@ -33,6 +45,7 @@ * */ public class SignatureParser { + private static final AtomicLong CUSTOM_TYPE_REIFIER_SEQUENCE = new AtomicLong(); private final Parser parser; private final List astNodes = new ArrayList<>(); @@ -46,23 +59,36 @@ public class SignatureParser { private int positionalParameterCount = 0; private final List namedParameterNames = new ArrayList<>(); private final List requiredNamedParameterNames = new ArrayList<>(); + private final List invocantNames = new ArrayList<>(); + private final List positionalRequiredNames = new ArrayList<>(); + private final List positionalOptionalNames = new ArrayList<>(); + private final List namedRequiredMetadataNames = new ArrayList<>(); + private final List namedOptionalNames = new ArrayList<>(); + private final Set functionParametersParameterNames = new java.util.HashSet<>(); + private final Map parameterTypeNames = new LinkedHashMap<>(); + private final Map parameterTypeObjects = new LinkedHashMap<>(); + private final Map parameterTypeObjectVariables = new LinkedHashMap<>(); + private RuntimeScalar lastCustomReifiedTypeValue; private final List defaultValueNodes = new ArrayList<>(); private boolean hasOptional = false; private boolean hasNamedParameter = false; private String namedArgsHashName = null; // Track the hash name for named parameters + private String slurpyParameterName; + private int typedParameterCount; private String subroutineName = null; // Optional subroutine name for error messages private boolean isMethod = false; // True if parsing method signature (has implicit $self) + private boolean parsingFunctionParametersInvocant; private int signatureOpenParenIndex; private SignatureParser(Parser parser) { this.parser = parser; - this.isMethod = parser.isInMethod; + this.isMethod = parser.isInMethod && !parser.parsingFunctionParametersMethod; } private SignatureParser(Parser parser, String subroutineName) { this.parser = parser; this.subroutineName = subroutineName; - this.isMethod = parser.isInMethod; + this.isMethod = parser.isInMethod && !parser.parsingFunctionParametersMethod; } /** @@ -111,6 +137,9 @@ private ListNode parse() { // Handle empty signature if (peekToken().text.equals(")")) { + if (parser.parsingFunctionParametersMethod) { + parser.throwError("Method signature must declare an invocant"); + } consumeCloseParen(); return generateSignatureAST(); } @@ -119,13 +148,54 @@ private ListNode parse() { parser.throwErrorAtToken(previousSignificantToken(parser.tokenIndex), "syntax error"); } + int explicitInvocantCount = countParametersBeforeInvocantSeparator(signatureOpenParenIndex); + if (parser.parsingFunctionParametersDeclaration + && !parser.parsingFunctionParametersMethod + && !parser.parsingFunctionParametersOptionalInvocant + && explicitInvocantCount >= 0) { + parser.throwError(functionParametersInvocantName(signatureOpenParenIndex) + + " not allowed here"); + } + int configuredInvocants = parser.functionParametersInvocantCount; + if (parser.parsingFunctionParametersMethod && explicitInvocantCount >= 0 + && explicitInvocantCount != configuredInvocants) { + throwInvocantCountMismatch(explicitInvocantCount, configuredInvocants); + } + + boolean functionParametersInvocantSeparatorConsumed = false; + boolean functionParametersHasInvocants = parser.parsingFunctionParametersMethod + || (parser.parsingFunctionParametersOptionalInvocant + && hasOptionalInvocantSeparator(signatureOpenParenIndex)); + int functionParametersInvocantCount = functionParametersHasInvocants + ? parser.functionParametersInvocantCount : 0; // Parse parameters while (true) { + parsingFunctionParametersInvocant = functionParametersHasInvocants + && positionalParameterCount < functionParametersInvocantCount; parseParameter(); + parsingFunctionParametersInvocant = false; // Check what comes after the parameter LexerToken next = peekToken(); - if (next.text.equals(")")) { + if (functionParametersHasInvocants + && !functionParametersInvocantSeparatorConsumed) { + if (next.text.equals(":")) { + if (invocantNames.size() != functionParametersInvocantCount) { + throwInvocantCountMismatch(invocantNames.size(), functionParametersInvocantCount); + } + consumeToken(); + functionParametersInvocantSeparatorConsumed = true; + if (peekToken().text.equals(")")) { + break; + } + } else if (next.text.equals(",") + && invocantNames.size() < functionParametersInvocantCount) { + consumeCommas(); + continue; + } else { + parser.throwError("Method signature must separate its invocant with ':'"); + } + } else if (next.text.equals(")")) { break; } else if (next.text.equals(",")) { consumeCommas(); @@ -156,6 +226,65 @@ private void parseParameter() { isNamed = true; } + Node typeConstraint = null; + boolean parenthesizedTypeConstraint = false; + RuntimeScalar parenthesizedTypeValue = null; + RuntimeScalar customReifiedTypeValue = null; + if (parser.parsingFunctionParametersDeclaration && parser.functionParametersTypes) { + if (peekToken().text.equals("(")) { + consumeToken(); + parenthesizedTypeConstraint = true; + int typeSourceStart = parser.tokenIndex; + typeConstraint = parser.parseExpression(0); + int typeSourceEnd = parser.tokenIndex; + if (!peekToken().text.equals(")")) { + parser.throwError("Malformed Function::Parameters type constraint"); + } + consumeToken(); + if (parser.functionParametersTypeReifierCallback != null) { + customReifiedTypeValue = callCustomTypeReifier( + typeSource(typeSourceStart, typeSourceEnd), paramStartIndex); + typeConstraint = customTypeConstraintReference(customReifiedTypeValue, paramStartIndex); + } + } else if (peekToken().type == LexerTokenType.IDENTIFIER) { + typeConstraint = parseUnparenthesizedTypeConstraint(); + customReifiedTypeValue = lastCustomReifiedTypeValue; + } + if (parenthesizedTypeConstraint + && "moose".equals(parser.functionParametersTypeReifier)) { + RuntimeList evaluatedType = SpecialBlockParser.runSpecialBlock( + parser, "BEGIN", typeConstraint, RuntimeContextType.SCALAR); + parenthesizedTypeValue = evaluatedType.scalar(); + if (RuntimeScalarType.blessedId(parenthesizedTypeValue) == 0) { + Node reifierCall = mooseTypeReifierCall( + parenthesizedTypeValue.toString(), paramStartIndex); + customReifiedTypeValue = SpecialBlockParser.runSpecialBlock( + parser, "BEGIN", reifierCall, RuntimeContextType.SCALAR).scalar(); + typeConstraint = customTypeConstraintReference( + customReifiedTypeValue, paramStartIndex); + } else { + customReifiedTypeValue = parenthesizedTypeValue; + typeConstraint = customTypeConstraintReference( + customReifiedTypeValue, paramStartIndex); + } + } else if (customReifiedTypeValue == null) { + typeConstraint = reifyQuotedTypeName(typeConstraint, paramStartIndex); + } + if (typeConstraint != null && customReifiedTypeValue == null) { + RuntimeScalar evaluatedType = SpecialBlockParser.runSpecialBlock( + parser, "BEGIN", typeConstraint, RuntimeContextType.SCALAR).scalar(); + if (RuntimeScalarType.blessedId(evaluatedType) != 0) { + customReifiedTypeValue = evaluatedType; + typeConstraint = customTypeConstraintReference( + customReifiedTypeValue, paramStartIndex); + } + } + if (typeConstraint != null && peekToken().text.equals(":")) { + consumeToken(); + isNamed = true; + } + } + LexerToken sigilToken = consumeToken(); String sigil = sigilToken.text; @@ -170,7 +299,9 @@ private void parseParameter() { // collect their unrecognized arguments. Only a later ordinary // positional scalar is forbidden. if (!isNamed && hasNamedParameter && sigil.equals("$")) { - parser.throwError(paramStartIndex, "Positional parameter follows named parameter"); + parser.throwError(paramStartIndex, parser.parsingFunctionParametersDeclaration + ? "positional parameter follows named parameter" + : "Positional parameter follows named parameter"); } // Check if this is a slurpy parameter @@ -179,7 +310,12 @@ private void parseParameter() { // Keep parsing after a slurpy parameter so Perl can report every // following invalid parameter in the same signature. if (hasSlurpy) { - String message = isSlurpy ? "Multiple slurpy parameters not allowed" : "Slurpy parameter not last"; + String followingName = sigil + + (peekToken().type == LexerTokenType.IDENTIFIER ? peekToken().text : ""); + String message = parser.parsingFunctionParametersDeclaration + ? "\"" + followingName + "\" can't appear after slurpy parameter \"" + + slurpyParameterName + "\"" + : isSlurpy ? "Multiple slurpy parameters not allowed" : "Slurpy parameter not last"; if (!isSlurpy && hasDefaultValueBeforeCloseParen()) { parser.deferErrorAtToken(lastSignatureTokenBeforeCloseParen(), message); } else { @@ -205,6 +341,24 @@ private void parseParameter() { + sigil + "_\"\n"); } + String metadataName = paramName == null ? sigil : sigil + paramName; + if (parenthesizedTypeValue != null + && RuntimeScalarType.blessedId(parenthesizedTypeValue) != 0) { + parameterTypeObjects.put(metadataName, parenthesizedTypeValue); + } + if (customReifiedTypeValue != null + && customReifiedTypeValue.type != RuntimeScalarType.UNDEF) { + parameterTypeObjects.put(metadataName, customReifiedTypeValue); + } + if (typeConstraint instanceof AbstractNode typedNode + && typedNode.getAnnotation("functionParametersTypeObjectVariable") instanceof String variableName) { + parameterTypeObjectVariables.put(metadataName, variableName); + } + if (parser.parsingFunctionParametersDeclaration && paramName != null + && !functionParametersParameterNames.add(metadataName)) { + parser.throwError(paramStartIndex, + metadataName + " can't appear twice in the same parameter list"); + } if (isNamed && paramName != null && namedParameterNames.contains(paramName)) { parser.throwError(paramStartIndex, "Duplicated subroutine parameter name"); } @@ -217,9 +371,14 @@ private void parseParameter() { // Check for illegal operator after parameter (e.g. $b += 1) LexerToken afterParam = peekToken(); + if (parser.parsingFunctionParametersMethod && isNamed && afterParam.text.equals(":")) { + parser.throwError(paramStartIndex, + "invocant " + sigil + paramName + " not allowed as a named parameter here"); + } if (paramName != null && !afterParam.text.equals(",") && !afterParam.text.equals(")") && !afterParam.text.equals("=") && !afterParam.text.equals("//=") && !afterParam.text.equals("||=") - && !afterParam.text.equals("$") && !afterParam.text.equals("@") && !afterParam.text.equals("%")) { + && !afterParam.text.equals("$") && !afterParam.text.equals("@") && !afterParam.text.equals("%") + && !(parsingFunctionParametersInvocant && afterParam.text.equals(":"))) { if (afterParam.type == LexerTokenType.OPERATOR) { throwMalformedSeparator(); } @@ -227,27 +386,71 @@ private void parseParameter() { // Create parameter variable or undef placeholder Node paramVariable = createParameterVariable(sigil, paramName); + OperatorNode typeConstraintVariable = null; + if (typeConstraint != null) { + if (paramName == null || (isSlurpy && !"@".equals(sigil))) { + parser.throwError(paramStartIndex, + "Function::Parameters type constraints require a named scalar or array parameter"); + } + if (parser.functionParametersCheckArgumentTypes) { + String typeVariableName = "__function_parameters_type_" + (++typedParameterCount); + OperatorNode typeVariable = new OperatorNode("$", + new IdentifierNode(typeVariableName, paramStartIndex), paramStartIndex); + OperatorNode typeDeclaration = new OperatorNode("my", typeVariable, paramStartIndex); + typeDeclaration.setAnnotation("signatureParameterDeclaration", true); + astNodes.add(new BinaryOperatorNode("=", typeDeclaration, typeConstraint, paramStartIndex)); + parser.ctx.symbolTable.addVariable("$" + typeVariableName, "my", typeVariable); + typeConstraintVariable = typeVariable; + } + String typeName = metadataTypeName(typeConstraint); + if (typeName != null) { + parameterTypeNames.put(metadataName, typeName); + if (typeName.startsWith("moose:") && !parameterTypeObjects.containsKey(metadataName)) { + Node reifiedMetadataType = mooseTypeReifierCall( + typeName.substring("moose:".length()), paramStartIndex); + RuntimeScalar evaluatedType = SpecialBlockParser.runSpecialBlock( + parser, "BEGIN", reifiedMetadataType, RuntimeContextType.SCALAR).scalar(); + parameterTypeObjects.put(metadataName, evaluatedType); + } + } + } if (isNamed) { hasNamedParameter = true; LexerToken namedDefault = peekToken(); if (hasOptional && !namedDefault.text.equals("=") && !namedDefault.text.equals("//=") && !namedDefault.text.equals("||=")) { - parser.throwError(paramStartIndex, "Mandatory parameter follows optional parameter"); + parser.throwError(paramStartIndex, parser.parsingFunctionParametersDeclaration + ? "optional positional parameter is followed by required named parameter" + : "Mandatory parameter follows optional parameter"); } // Named parameters are handled separately, not part of @_ unpacking namedParameterNodes.add(paramVariable); namedParameterNames.add(paramName); handleNamedParameter(paramVariable, paramName); + if (requiredNamedParameterNames.contains(paramName)) { + namedRequiredMetadataNames.add(metadataName); + } else { + namedOptionalNames.add(metadataName); + } } else { parameterVariables.add(paramVariable); if (isSlurpy) { slurpyVariable = paramVariable; slurpySigil = sigil; + slurpyParameterName = metadataName; handleSlurpyParameter(); } else { positionalParameterCount++; + boolean wasOptional = hasOptional; handleScalarParameter(paramVariable, paramStartIndex); + if (parsingFunctionParametersInvocant) { + invocantNames.add(metadataName); + } else if (hasOptional) { + positionalOptionalNames.add(metadataName); + } else { + positionalRequiredNames.add(metadataName); + } } } @@ -267,6 +470,420 @@ private void parseParameter() { } parser.ctx.symbolTable.addVariable(sigil + paramName, "my", (OperatorNode) paramVariable); } + Node inlineTypeCheck = null; + if (customReifiedTypeValue != null && typeConstraintVariable != null + && parser.functionParametersCheckArgumentTypes) { + inlineTypeCheck = inlineCustomTypeCheck( + typeConstraint, sigil + paramName, paramStartIndex); + } + if (typeConstraintVariable != null && parser.functionParametersCheckArgumentTypes) { + Node hasCanMethod = typeConstraintMethodCall(typeConstraintVariable, + "can", new StringNode("has_coercion", paramStartIndex), paramStartIndex); + Node hasCoercion = new BinaryOperatorNode("&&", hasCanMethod, + typeConstraintNoArgumentMethodCall( + typeConstraintVariable, "has_coercion", paramStartIndex), + paramStartIndex); + Node coercedValue = new BinaryOperatorNode("=", paramVariable, + typeConstraintMethodCall( + typeConstraintVariable, "coerce", paramVariable, paramStartIndex), + paramStartIndex); + astNodes.add(new IfNode("if", hasCoercion, + new BlockNode(new ArrayList<>(List.of(coercedValue)), paramStartIndex), + null, paramStartIndex)); + + Node failure; + Node message; + if (inlineTypeCheck != null) { + failure = new OperatorNode("!", inlineTypeCheck, paramStartIndex); + message = typeConstraintMethodCall( + typeConstraintVariable, "get_message", paramVariable, paramStartIndex); + } else { + Node check = typeConstraintMethodCall( + typeConstraintVariable, "check", paramVariable, paramStartIndex); + failure = new OperatorNode("!", check, paramStartIndex); + message = typeConstraintMethodCall( + typeConstraintVariable, "get_message", paramVariable, paramStartIndex); + } + if (parser.parsingFunctionParametersDeclaration) { + int parameterIndex = isNamed + ? positionalParameterCount + namedParameterNames.size() + : positionalParameterCount; + String keyword = parser.functionParametersKeyword == null + ? "fun" : parser.functionParametersKeyword; + String name = subroutineName == null ? "(anon)" : subroutineName; + String prefix = "In " + keyword + " " + name + ": parameter " + + parameterIndex + " (" + sigil + paramName + "): "; + message = new BinaryOperatorNode(".", new StringNode(prefix, paramStartIndex), + message, paramStartIndex); + } + Node die = new OperatorNode("die", new ListNode(List.of(message), paramStartIndex), paramStartIndex); + astNodes.add(new IfNode("if", failure, + new BlockNode(new ArrayList<>(List.of(die)), paramStartIndex), null, paramStartIndex)); + } + } + + private static Node requireMooseTypeConstraints(Node expression, int index) { + Node require = new OperatorNode("require", ListNode.makeList( + new StringNode("Moose/Util/TypeConstraints.pm", index)), index); + return new TernaryOperatorNode("?", require, expression, + new OperatorNode("undef", null, index), index); + } + + private static Node mooseTypeReifierCall(String typeName, int index) { + Node reifier = new OperatorNode("&", new IdentifierNode( + "Moose::Util::TypeConstraints::find_or_create_isa_type_constraint", index), index); + Node call = new BinaryOperatorNode("(", reifier, + new ListNode(List.of(new StringNode(typeName, index)), index), index); + return requireMooseTypeConstraints(call, index); + } + + private boolean hasOptionalInvocantSeparator(int openParenIndex) { + int expected = parser.functionParametersInvocantCount; + if (expected <= 0) return false; + int depthParen = 0; + int depthSquare = 0; + int depthBrace = 0; + int parameterSigils = 0; + int commas = 0; + int pendingTernaries = 0; + boolean segmentHasParameter = false; + for (int i = openParenIndex + 1; i < parser.tokens.size(); i++) { + LexerToken token = parser.tokens.get(i); + if (token.type == LexerTokenType.WHITESPACE || token.type == LexerTokenType.NEWLINE) continue; + String text = token.text; + if (depthParen == 0 && depthSquare == 0 && depthBrace == 0) { + if (")".equals(text)) return false; + if (!segmentHasParameter + && ("$".equals(text) || "@".equals(text) || "%".equals(text))) { + parameterSigils++; + segmentHasParameter = true; + } else if (",".equals(text)) { + commas++; + segmentHasParameter = false; + } else if ("?".equals(text)) { + pendingTernaries++; + } else if (":".equals(text)) { + if (pendingTernaries > 0) { + pendingTernaries--; + } else { + return parameterSigils == expected && commas == expected - 1; + } + } + } + switch (text) { + case "(" -> depthParen++; + case ")" -> depthParen--; + case "[" -> depthSquare++; + case "]" -> depthSquare--; + case "{" -> depthBrace++; + case "}" -> depthBrace--; + } + } + return false; + } + + private int countParametersBeforeInvocantSeparator(int openParenIndex) { + int depthParen = 0; + int depthSquare = 0; + int depthBrace = 0; + int parameterCount = 0; + int pendingTernaries = 0; + boolean segmentHasParameter = false; + for (int i = openParenIndex + 1; i < parser.tokens.size(); i++) { + LexerToken token = parser.tokens.get(i); + if (token.type == LexerTokenType.WHITESPACE || token.type == LexerTokenType.NEWLINE) continue; + String text = token.text; + if (depthParen == 0 && depthSquare == 0 && depthBrace == 0) { + if (")".equals(text)) return -1; + if (":".equals(text)) { + if (pendingTernaries > 0) { + pendingTernaries--; + segmentHasParameter = true; + continue; + } + if (segmentHasParameter && parameterCount > 0) return parameterCount; + // A leading colon starts a named parameter, not an + // invocant separator. + segmentHasParameter = true; + continue; + } + if (",".equals(text)) { + segmentHasParameter = false; + continue; + } + if (!segmentHasParameter + && ("$".equals(text) || "@".equals(text) || "%".equals(text))) { + parameterCount++; + segmentHasParameter = true; + continue; + } + if ("?".equals(text)) pendingTernaries++; + segmentHasParameter = true; + } + switch (text) { + case "(" -> depthParen++; + case ")" -> depthParen--; + case "[" -> depthSquare++; + case "]" -> depthSquare--; + case "{" -> depthBrace++; + case "}" -> depthBrace--; + } + } + return -1; + } + + private String functionParametersInvocantName(int openParenIndex) { + for (int i = openParenIndex + 1; i < parser.tokens.size(); i++) { + LexerToken token = parser.tokens.get(i); + if (token.type == LexerTokenType.WHITESPACE || token.type == LexerTokenType.NEWLINE) continue; + if (!"$".equals(token.text) && !"@".equals(token.text) && !"%".equals(token.text)) { + return "invocant"; + } + String sigil = token.text; + for (int j = i + 1; j < parser.tokens.size(); j++) { + LexerToken name = parser.tokens.get(j); + if (name.type == LexerTokenType.WHITESPACE || name.type == LexerTokenType.NEWLINE) continue; + if (name.type == LexerTokenType.IDENTIFIER) return "invocant " + sigil + name.text; + return "invocant " + sigil; + } + } + return "invocant"; + } + + private void throwInvocantCountMismatch(int actual, int expected) { + String keyword = parser.functionParametersKeyword == null + ? "method" : parser.functionParametersKeyword; + String name = subroutineName == null ? "(anon)" : subroutineName; + parser.throwError("In " + keyword + " " + name + + ": number of invocants in parameter list (" + actual + + ") differs from number of invocants in keyword definition (" + expected + ")"); + } + + private static Node typeConstraintMethodCall( + Node invocant, String method, Node argument, int index) { + Node call = new BinaryOperatorNode("(", + new OperatorNode("&", new IdentifierNode(method, index), index), + new ListNode(List.of(argument), index), index); + return new BinaryOperatorNode("->", invocant, call, index); + } + + private static Node typeConstraintNoArgumentMethodCall(Node invocant, String method, int index) { + Node call = new BinaryOperatorNode("(", + new OperatorNode("&", new IdentifierNode(method, index), index), + new ListNode(List.of(), index), index); + return new BinaryOperatorNode("->", invocant, call, index); + } + + private Node inlineCustomTypeCheck( + Node typeReference, String parameterName, int index) { + Node canMethod = typeConstraintMethodCall( + typeReference, "can", new StringNode("can_be_inlined", index), index); + if (!SpecialBlockParser.runSpecialBlock( + parser, "BEGIN", canMethod, RuntimeContextType.SCALAR).scalar().getDefinedBoolean()) { + return null; + } + RuntimeScalar canInline = SpecialBlockParser.runSpecialBlock( + parser, "BEGIN", + typeConstraintNoArgumentMethodCall(typeReference, "can_be_inlined", index), + RuntimeContextType.SCALAR).scalar(); + if (!canInline.getDefinedBoolean()) return null; + + RuntimeScalar inlineMethod = SpecialBlockParser.runSpecialBlock( + parser, "BEGIN", + typeConstraintMethodCall( + typeReference, "can", new StringNode("inline_check", index), index), + RuntimeContextType.SCALAR).scalar(); + String methodName = inlineMethod.getDefinedBoolean() ? "inline_check" : "_inline_check"; + Node sourceCall = typeConstraintMethodCall( + typeReference, methodName, new StringNode(parameterName, index), index); + String source = SpecialBlockParser.runSpecialBlock( + parser, "BEGIN", sourceCall, RuntimeContextType.SCALAR).scalar().toString(); + + List inlineTokens = new Lexer(source).tokenize(); + ErrorMessageUtil previousErrorUtil = parser.ctx.errorUtil; + ErrorMessageUtil.SourceLocation declarationLocation = + previousErrorUtil.getSourceLocationAccurate(index); + String inlineFile = "(inline_check:" + declarationLocation.fileName() + + ":" + declarationLocation.lineNumber() + ")"; + parser.ctx.errorUtil = new ErrorMessageUtil(inlineFile, inlineTokens); + try { + Parser inlineParser = new Parser(parser.ctx, inlineTokens); + Node expression = inlineParser.parseExpression(0); + if (expression == null || inlineParser.tokens.get(inlineParser.tokenIndex).type + != LexerTokenType.EOF) { + inlineParser.throwError("syntax error in inlined type constraint"); + } + return expression; + } catch (PerlCompilerException exception) { + throw exception; + } finally { + parser.ctx.errorUtil = previousErrorUtil; + } + } + + private static Node reifyQuotedTypeName(Node typeConstraint, int index) { + if (!(typeConstraint instanceof StringNode string) + || !string.value.matches("[A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*")) { + return typeConstraint; + } + Node typeFunction = new OperatorNode("&", new IdentifierNode(string.value, index), index); + return new BinaryOperatorNode("(", typeFunction, new ListNode(List.of(), index), index); + } + + private Node parseUnparenthesizedTypeConstraint() { + int start = parser.tokenIndex; + int parameterSigil = findTypeConstraintParameterSigil(start); + if (parameterSigil < 0) { + int last = parser.tokens.size() - 1; + while (last >= start && (parser.tokens.get(last).type == LexerTokenType.EOF + || parser.tokens.get(last).type == LexerTokenType.WHITESPACE + || parser.tokens.get(last).type == LexerTokenType.NEWLINE)) { + last--; + } + if (last >= start && "[".equals(parser.tokens.get(last).text)) { + parser.throwError(start, "missing type name"); + } + return parser.parseExpression(0); + } + if (parser.functionParametersTypeReifierCallback != null && parameterSigil > start) { + String source = typeSource(start, parameterSigil); + parser.tokenIndex = parameterSigil; + lastCustomReifiedTypeValue = callCustomTypeReifier(source, start); + return customTypeConstraintReference(lastCustomReifiedTypeValue, start); + } + if ("moose".equals(parser.functionParametersTypeReifier) + && parameterSigil > start) { + StringBuilder typeSource = new StringBuilder(); + for (int index = start; index < parameterSigil; index++) { + LexerToken token = parser.tokens.get(index); + if (token.type != LexerTokenType.WHITESPACE + && token.type != LexerTokenType.NEWLINE) { + typeSource.append(token.text); + } + } + Node reifier = new OperatorNode("&", new IdentifierNode( + "Moose::Util::TypeConstraints::find_or_create_isa_type_constraint", start), start); + parser.tokenIndex = parameterSigil; + Node reifiedType = new BinaryOperatorNode("(", reifier, + new ListNode(List.of(new StringNode(typeSource.toString(), start)), start), start); + return requireMooseTypeConstraints(reifiedType, start); + } + + int first = nextSignificantToken(start); + int afterFirst = nextSignificantToken(first + 1); + if (first == start && afterFirst == parameterSigil + && parser.tokens.get(start).type == LexerTokenType.IDENTIFIER) { + LexerToken typeName = consumeToken(); + Node typeSub = new OperatorNode("&", + new IdentifierNode(typeName.text, start), start); + return new BinaryOperatorNode("(", typeSub, new ListNode(List.of(), start), start); + } + + // Bound expression parsing at the parameter sigil. Without this + // sentinel, Perl's `Type $value` indirect-call syntax consumes the + // parameter as an argument to Type. + LexerToken boundary = new LexerToken(LexerTokenType.OPERATOR, ")"); + parser.tokens.add(parameterSigil, boundary); + try { + Node expression = parser.parseExpression(0); + if (!peekToken().text.equals(")")) { + parser.throwError("Malformed Function::Parameters type constraint"); + } + consumeToken(); + return expression; + } finally { + int boundaryIndex = parser.tokenIndex - 1; + if (boundaryIndex >= 0 && boundaryIndex < parser.tokens.size() + && parser.tokens.get(boundaryIndex) == boundary) { + parser.tokens.remove(boundaryIndex); + parser.tokenIndex--; + } else { + parser.tokens.remove(boundary); + } + } + } + + private String typeSource(int start, int end) { + StringBuilder source = new StringBuilder(); + for (int index = start; index < end; index++) { + LexerToken token = parser.tokens.get(index); + if (token.type != LexerTokenType.WHITESPACE + && token.type != LexerTokenType.NEWLINE) { + source.append(token.text); + } + } + return source.toString(); + } + + private RuntimeScalar callCustomTypeReifier(String source, int index) { + long id = CUSTOM_TYPE_REIFIER_SEQUENCE.incrementAndGet(); + String callbackName = "Function::Parameters::Internal::__type_reifier_" + id; + GlobalVariable.globalCodeRefs.put(callbackName, parser.functionParametersTypeReifierCallback); + Node callback = new OperatorNode("&", new IdentifierNode(callbackName, index), index); + Node call = new BinaryOperatorNode("(", callback, + new ListNode(List.of(new StringNode(source, index)), index), index); + try { + RuntimeScalar value = SpecialBlockParser.runSpecialBlock( + parser, "BEGIN", call, RuntimeContextType.SCALAR).scalar(); + if (RuntimeScalarType.blessedId(value) == 0) { + parser.throwError(index, "reify_type callback must return a type constraint object"); + } + return value; + } catch (PerlCompilerException exception) { + String message = exception.getMessage(); + int beginFailure = message == null + ? -1 : message.indexOf("\nBEGIN failed--compilation aborted at "); + if (beginFailure >= 0) { + throw new PerlCompilerException(message.substring(0, beginFailure + 1)); + } + throw exception; + } finally { + GlobalVariable.globalCodeRefs.remove(callbackName); + } + } + + private Node customTypeConstraintReference(RuntimeScalar typeObject, int index) { + long id = CUSTOM_TYPE_REIFIER_SEQUENCE.incrementAndGet(); + String variableName = "Function::Parameters::Internal::__type_constraint_" + id; + GlobalVariable.globalVariables.put(variableName, typeObject); + OperatorNode reference = new OperatorNode("$", new IdentifierNode(variableName, index), index); + reference.setAnnotation("functionParametersTypeObjectVariable", variableName); + return reference; + } + + private int findTypeConstraintParameterSigil(int start) { + int squareDepth = 0; + int roundDepth = 0; + int braceDepth = 0; + for (int index = start; index < parser.tokens.size(); index++) { + LexerToken token = parser.tokens.get(index); + if (token.type == LexerTokenType.WHITESPACE) continue; + if (squareDepth == 0 && roundDepth == 0 && braceDepth == 0 + && ("$".equals(token.text) || "@".equals(token.text) || "%".equals(token.text))) { + return index; + } + switch (token.text) { + case "[" -> squareDepth++; + case "]" -> squareDepth--; + case "(" -> roundDepth++; + case ")" -> { + if (roundDepth == 0) return -1; + roundDepth--; + } + case "{" -> braceDepth++; + case "}" -> braceDepth--; + default -> { } + } + } + return -1; + } + + private int nextSignificantToken(int index) { + while (index < parser.tokens.size() + && parser.tokens.get(index).type == LexerTokenType.WHITESPACE) { + index++; + } + return index; } private int previousSignificantToken(int index) { @@ -280,6 +897,13 @@ private int previousSignificantToken(int index) { private boolean validateSigil(String sigil, int paramStartIndex, boolean isNamed, boolean immediatelyFollowedByHash) { + if (parsingFunctionParametersInvocant && ("@".equals(sigil) || "%".equals(sigil))) { + LexerToken name = peekToken(); + String invocant = sigil + + (name.type == LexerTokenType.IDENTIFIER ? name.text : ""); + parser.throwError(paramStartIndex, "invocant " + invocant + + ("@".equals(sigil) ? " can't be an array" : " can't be a hash")); + } // The lexer may combine a sigil and an immediately following '#' // (for example, "$#") into one token. Diagnose all three sigils at // the preceding open-paren context, which is the source position Perl @@ -560,7 +1184,8 @@ private void handleNamedParameter(Node paramVariable, String paramName) { if (next.text.equals("=") || next.text.equals("||=") || next.text.equals("//=")) { defaultOp = consumeToken().text; defaultValue = parseDefaultValue(paramVariable); - } else { + } else if (!(parser.parsingFunctionParametersDeclaration + && !parser.functionParametersStrict)) { requiredNamedParameterNames.add(paramName); } @@ -580,18 +1205,25 @@ private Node generateDefaultAssignment(Node variable, Node defaultValue, String if (variable == null || (variable instanceof OperatorNode && ((OperatorNode) variable).operator.equals("undef"))) { // An anonymous parameter still evaluates its default expression. // There is simply no lexical slot to assign the result to. - if (op.equals("=")) { - return new BinaryOperatorNode( - "&&", - new BinaryOperatorNode( - "<", - atUnderscore(parser), - new NumberNode(Integer.toString(paramIndex + 1), parser.tokenIndex), - parser.tokenIndex), - defaultValue, + Node missingArgument = new BinaryOperatorNode( + "<", new OperatorNode("scalar", atUnderscore(parser), parser.tokenIndex), + new NumberNode(Integer.toString(paramIndex + 1), parser.tokenIndex), + parser.tokenIndex); + Node condition = missingArgument; + if ("//=".equals(op) || "||=".equals(op)) { + ArrayLiteralNode argumentIndex = new ArrayLiteralNode( + List.of(new NumberNode(Integer.toString(paramIndex), parser.tokenIndex)), + parser.tokenIndex); + Node argument = new BinaryOperatorNode("[", atUnderscore(parser), argumentIndex, + parser.tokenIndex); + Node fallbackCondition = "//=".equals(op) + ? new OperatorNode("!", new OperatorNode("defined", argument, parser.tokenIndex), + parser.tokenIndex) + : new OperatorNode("!", argument, parser.tokenIndex); + condition = new BinaryOperatorNode("||", missingArgument, fallbackCondition, parser.tokenIndex); } - return defaultValue; + return new BinaryOperatorNode("&&", condition, defaultValue, parser.tokenIndex); } if (op.equals("=")) { @@ -617,6 +1249,10 @@ private Node generateDefaultAssignment(Node variable, Node defaultValue, String } private Node parseDefaultValue(Node paramVariable) { + if (parser.parsingFunctionParametersDeclaration && !parser.functionParametersDefaultArguments) { + parser.throwError(previousDefaultOperatorIndex(), parser.functionParametersKeyword + + ": default argument not allowed"); + } // Check if there's actually a default expression LexerToken next = peekToken(); if (next.type == LexerTokenType.EOF || next.text.equals(",") || next.text.equals(")")) { @@ -638,7 +1274,20 @@ private Node parseDefaultValue(Node paramVariable) { if (paramVariable instanceof OperatorNode op && "$".equals(op.operator) && op.operand instanceof IdentifierNode id) { - qualifySelfReference(value, id.name); + SymbolTable.SymbolEntry outerVariable = parser.ctx.symbolTable.getSymbolEntry("$" + id.name); + if (parser.parsingFunctionParametersDeclaration + && outerVariable != null + && Set.of("my", "state").contains(outerVariable.decl()) + && outerVariable.ast() != null) { + String alias = "__fp_outer_" + outerVariable.index() + "_" + paramVariable.getIndex(); + parser.ctx.symbolTable.addVariableWithIndex( + "$" + alias, outerVariable.index(), outerVariable.decl(), + outerVariable.perlPackage(), outerVariable.ast()); + outerVariable.ast().setAnnotation("functionParametersOuterBinding", "$" + id.name); + renameSelfReference(value, id.name, alias); + } else { + qualifySelfReference(value, id.name); + } } return value; } @@ -675,6 +1324,26 @@ private void qualifySelfReference(Node node, String name) { } } + private void renameSelfReference(Node node, String name, String alias) { + if (node instanceof OperatorNode op) { + if ("$".equals(op.operator) && op.operand instanceof IdentifierNode id + && name.equals(id.name)) { + id.name = alias; + } else if (op.operand != null) { + renameSelfReference(op.operand, name, alias); + } + } else if (node instanceof BinaryOperatorNode binary) { + renameSelfReference(binary.left, name, alias); + renameSelfReference(binary.right, name, alias); + } else if (node instanceof ListNode list) { + for (Node element : list.elements) renameSelfReference(element, name, alias); + } else if (node instanceof HashLiteralNode hash) { + for (Node element : hash.elements) renameSelfReference(element, name, alias); + } else if (node instanceof ArrayLiteralNode array) { + for (Node element : array.elements) renameSelfReference(element, name, alias); + } + } + /** * Generates AST for extracting a named parameter from @_. * @@ -738,18 +1407,23 @@ private Node generateNamedParameterExtraction(Node paramVariable, String paramNa if (defaultValue != null) { // (delete $h{named}) // defaultValue if (defaultOp.equals("=")) { - defaultOp = "//"; - } else if (defaultOp.equals("||=")) { - defaultOp = "||"; - } else if (defaultOp.equals("//=")) { - defaultOp = "//"; - } + OperatorNode exists = new OperatorNode( + "exists", new ListNode(List.of(hashAccess), parser.tokenIndex), parser.tokenIndex); + extractionValue = new TernaryOperatorNode("?", exists, deleteExpr, defaultValue, + parser.tokenIndex); + } else { + if (defaultOp.equals("||=")) { + defaultOp = "||"; + } else if (defaultOp.equals("//=")) { + defaultOp = "//"; + } - extractionValue = new BinaryOperatorNode( - defaultOp, - deleteExpr, - defaultValue, - parser.tokenIndex); + extractionValue = new BinaryOperatorNode( + defaultOp, + deleteExpr, + defaultValue, + parser.tokenIndex); + } } else { extractionValue = deleteExpr; } @@ -767,20 +1441,43 @@ private Node generateNamedParameterExtraction(Node paramVariable, String paramNa } private Node namedArgsSource() { + Node namedArguments = atUnderscore(parser); + Node positionalCountAvailable = new OperatorNode("scalar", atUnderscore(parser), parser.tokenIndex); + Node namedArgumentCount = positionalCountAvailable; + if (positionalParameterCount > 0) { + Node start = new NumberNode(Integer.toString(positionalParameterCount), parser.tokenIndex); + Node end = new OperatorNode("$#", new IdentifierNode("_", parser.tokenIndex), parser.tokenIndex); + Node range = new BinaryOperatorNode("..", start, end, parser.tokenIndex); + Node slice = new BinaryOperatorNode("[", atUnderscore(parser), + new ArrayLiteralNode(List.of(range), parser.tokenIndex), parser.tokenIndex); + Node hasNamedTail = new BinaryOperatorNode("<=", + new NumberNode(Integer.toString(positionalParameterCount), parser.tokenIndex), + positionalCountAvailable, parser.tokenIndex); + namedArguments = new TernaryOperatorNode("?", hasNamedTail, slice, + new ListNode(List.of(), parser.tokenIndex), parser.tokenIndex); + namedArgumentCount = new TernaryOperatorNode("?", hasNamedTail, + new BinaryOperatorNode("-", + new OperatorNode("scalar", atUnderscore(parser), parser.tokenIndex), + new NumberNode(Integer.toString(positionalParameterCount), parser.tokenIndex), + parser.tokenIndex), + new NumberNode("0", parser.tokenIndex), parser.tokenIndex); + } Node odd = new BinaryOperatorNode("%", - new OperatorNode("scalar", atUnderscore(parser), parser.tokenIndex), + namedArgumentCount, new NumberNode("2", parser.tokenIndex), parser.tokenIndex); Node pad = new TernaryOperatorNode("?", odd, new ListNode(List.of(new OperatorNode("undef", null, parser.tokenIndex)), parser.tokenIndex), new ListNode(List.of(), parser.tokenIndex), parser.tokenIndex); - return new ListNode(List.of(atUnderscore(parser), pad), parser.tokenIndex); + return new ListNode(List.of(namedArguments, pad), parser.tokenIndex); } private ListNode generateSignatureAST() { List allNodes = new ArrayList<>(); // Add argument count validation - allNodes.add(generateArgCountValidation()); + if (!(parser.parsingFunctionParametersDeclaration && !parser.functionParametersStrict)) { + allNodes.add(generateArgCountValidation()); + } // Add parameter assignment from @_ if (!parameterVariables.isEmpty()) { @@ -810,6 +1507,17 @@ private ListNode generateSignatureAST() { } } + // Function::Parameters methods bind their implicit/explicit + // invocant lexically, then expose only user arguments through @_ in + // the body. This lets method code forward @_ while adding $self. + if (!invocantNames.isEmpty()) { + for (int i = 0; i < invocantNames.size(); i++) { + allNodes.add(new OperatorNode("shift", + new OperatorNode("@", new IdentifierNode("_", parser.tokenIndex), parser.tokenIndex), + parser.tokenIndex)); + } + } + ListNode signature = new ListNode(allNodes, parser.tokenIndex); int adjustment = isMethod ? 1 : 0; signature.setAnnotation("signatureMinArgs", minParams + adjustment); @@ -818,17 +1526,57 @@ private ListNode generateSignatureAST() { signature.setAnnotation("signatureNamedParams", new ArrayList<>(namedParameterNames)); signature.setAnnotation("signatureRequiredNamedParams", new ArrayList<>(requiredNamedParameterNames)); signature.setAnnotation("signaturePositionalCount", positionalParameterCount); + signature.setAnnotation("signatureInvocants", new ArrayList<>(invocantNames)); + if (parser.functionParametersKeyword != null) { + signature.setAnnotation("signatureKeyword", parser.functionParametersKeyword); + } + signature.setAnnotation("signaturePositionalRequired", new ArrayList<>(positionalRequiredNames)); + signature.setAnnotation("signaturePositionalOptional", new ArrayList<>(positionalOptionalNames)); + signature.setAnnotation("signatureNamedRequired", new ArrayList<>(namedRequiredMetadataNames)); + signature.setAnnotation("signatureNamedOptional", new ArrayList<>(namedOptionalNames)); + signature.setAnnotation("signatureParameterTypes", new LinkedHashMap<>(parameterTypeNames)); + signature.setAnnotation("signatureParameterTypeObjects", new LinkedHashMap<>(parameterTypeObjects)); + signature.setAnnotation("signatureParameterTypeObjectVariables", + new LinkedHashMap<>(parameterTypeObjectVariables)); if (slurpySigil != null) { signature.setAnnotation("signatureSlurpySigil", slurpySigil); + signature.setAnnotation("signatureSlurpyName", slurpyParameterName); } signature.setAnnotation("signatureSubName", NameNormalizer.normalizeVariableName( subroutineName == null ? "__ANON__" : subroutineName, parser.ctx.symbolTable.getCurrentPackage())); signature.setAnnotation("signatureDefaultValueNodes", new ArrayList<>(defaultValueNodes)); + if (parser.functionParametersTypeReifier != null) { + signature.setAnnotation("signatureTypeReifier", parser.functionParametersTypeReifier); + } return signature; } + private static String metadataTypeName(Node typeConstraint) { + if (typeConstraint.getAnnotation("signatureMetadataTypeName") instanceof String name) { + return name; + } + if (typeConstraint instanceof TernaryOperatorNode ternary + && "?".equals(ternary.operator)) { + return metadataTypeName(ternary.trueExpr); + } + if (typeConstraint instanceof BinaryOperatorNode sequence && ",".equals(sequence.operator)) { + typeConstraint = sequence.right; + } + if (typeConstraint instanceof BinaryOperatorNode call && "(".equals(call.operator) + && call.left instanceof OperatorNode reference && "&".equals(reference.operator) + && reference.operand instanceof IdentifierNode name) { + if ("Moose::Util::TypeConstraints::find_or_create_isa_type_constraint".equals(name.name) + && call.right instanceof ListNode arguments && arguments.elements.size() == 1 + && arguments.elements.get(0) instanceof StringNode typeSource) { + return "moose:" + typeSource.value; + } + return name.name; + } + return null; + } + private Node generateArgCountValidation() { // If we have named parameters, we need different validation // Named parameters are passed as key-value pairs, so we need to allow extra arguments @@ -917,6 +1665,9 @@ private Node generateOddNamedArgsValidation() { } private Node generateTooFewArgsMessage() { + if (parser.parsingFunctionParametersDeclaration) { + return generateFunctionParametersArgumentCountMessage(true); + } // Generate: "Too few arguments for subroutine 'Package::name' (got " . (scalar(@_) + adjustment) . "; expected at least " . minParams . ")" String fullName = NameNormalizer.normalizeVariableName( subroutineName == null ? "__ANON__" : subroutineName, @@ -951,6 +1702,9 @@ private Node generateTooFewArgsMessage() { } private Node generateTooManyArgsMessage() { + if (parser.parsingFunctionParametersDeclaration) { + return generateFunctionParametersArgumentCountMessage(false); + } // Generate: "Too many arguments for subroutine 'Package::name' (got " . (scalar(@_) + adjustment) . "; expected at most " . maxParams . ")" String fullName = NameNormalizer.normalizeVariableName( subroutineName == null ? "__ANON__" : subroutineName, @@ -984,6 +1738,20 @@ private Node generateTooManyArgsMessage() { parser.tokenIndex); } + private Node generateFunctionParametersArgumentCountMessage(boolean tooFew) { + String keyword = parser.functionParametersKeyword == null + ? "method" : parser.functionParametersKeyword; + String name = subroutineName == null ? "(anon)" : subroutineName; + int expected = tooFew ? minParams : maxParams; + String prefix = "Too " + (tooFew ? "few" : "many") + " arguments for " + + keyword + " " + name + " (expected " + expected + ", got "; + Node argCount = new OperatorNode("scalar", atUnderscore(parser), parser.tokenIndex); + return new BinaryOperatorNode(".", + new BinaryOperatorNode(".", new StringNode(prefix, parser.tokenIndex), + argCount, parser.tokenIndex), + new StringNode(")", parser.tokenIndex), parser.tokenIndex); + } + private Node generateParameterAssignment() { // my ($a, $b, @rest) = @_ OperatorNode declaration = new OperatorNode("my", diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java index 8bb092a222..b91be10644 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java @@ -1661,6 +1661,9 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) { if (FilterUtilCall.wasFilterInstalled()) { applySourceFilterToRemainingTokens(parser); } + + ParserExtensionRegistry.applyModulePragma( + parser, packageName, !isNoDeclaration, args); } } } diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java b/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java index 20a3e9cee2..46147946c8 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java @@ -107,6 +107,13 @@ public static Node parseStatement(Parser parser, String label) { // Store the current source location - this will be used for stack trace generation ByteCodeSourceMapper.saveSourceLocation(parser.ctx, parser.tokenIndex); + if (token.type == LexerTokenType.IDENTIFIER) { + ParserKeywordPlugin plugin = parser.getKeywordPlugin(token.text); + if (plugin != null) { + return plugin.parse(parser, token.text); + } + } + if (token.type == LexerTokenType.IDENTIFIER) { Node result = switch (token.text) { case "CHECK", "INIT", "UNITCHECK", "BEGIN", "END", "ADJUST", "BUILD" -> { @@ -124,12 +131,18 @@ public static Node parseStatement(Parser parser, String label) { if (peek(parser).text.equals("{") || (peek(parser).text.equals("(") && hasSpecialBlockPrototype(parser))) { if ("BUILD".equals(token.text) && !parser.isInClassBlock) { - parser.tokenIndex = currentIndex; - yield null; + parser.throwCleanError(currentIndex, + "BUILD blocks are only allowed inside class blocks"); } parser.tokenIndex = currentIndex; yield SpecialBlockParser.parseSpecialBlock(parser); } + if ("BUILD".equals(token.text)) { + parser.throwCleanError(currentIndex, + parser.isInClassBlock + ? "Malformed BUILD declaration: expected a block body" + : "BUILD blocks are only allowed inside class blocks"); + } // A diamond after a special block name is parsed as an // attempted declaration, not an ordinary subroutine call. // Preserve Perl's dedicated diagnostic instead of allowing diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index f57e40bf2f..e03f738f28 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -288,6 +288,13 @@ static Node parseSubroutineCall(Parser parser, boolean isMethod) { if (!subExists && !isNewMethod && !isMethod) { subExists = GlobalVariable.existsGlobalCodeRefAsScalar(fullName).getBoolean(); } + // A named function's body is parsed before its final RuntimeCode is + // installed. Treat a call back to that same declaration as a direct + // subroutine call so an initial scalar argument is not mistaken for + // indirect-object syntax (for example, `recur $n - 1, ...`). + if (!isMethod && parser.compilingSubroutineBodies.contains(fullName)) { + subExists = true; + } boolean prototypeWasPredeclared = prototype != null; if (prototype == null) { prototype = parser.declaredSubPrototypes.get(fullName); @@ -596,7 +603,8 @@ && isValidIndirectMethod(sourceSubName, parser) if (!terminator && !infixOp && (nextTok.type != LexerTokenType.IDENTIFIER - || (subName.contains("::") + || (hasFatCommaArgumentStart(parser) + || subName.contains("::") && !nextTok.text.equals("or") && !nextTok.text.equals("and") && !nextTok.text.equals("not") @@ -920,6 +928,22 @@ private static boolean isValidIndirectMethod(String subName) { return isValidIndirectMethod(subName, null); } + /** + * An unresolved call followed by a bareword fat-comma key is an ordinary + * unparenthesized subroutine call, as in {@code recursive value => $x}. + * The lookahead distinguishes it from indirect-object syntax where the + * first bareword is the invocant. + */ + private static boolean hasFatCommaArgumentStart(Parser parser) { + int index = parser.tokenIndex + 1; + while (index < parser.tokens.size() + && (parser.tokens.get(index).type == LexerTokenType.WHITESPACE + || parser.tokens.get(index).type == LexerTokenType.NEWLINE)) { + index++; + } + return index < parser.tokens.size() && parser.tokens.get(index).text.equals("=>"); + } + private static boolean isValidIndirectMethod(String subName, Parser parser) { if (subName.startsWith("CORE::")) return false; // `method` is a feature-gated declaration keyword, but it also has @@ -1082,24 +1106,6 @@ public static Node parseSubroutineDefinition( } } - // Ensure attributes.pm is loaded when attribute syntax is used, so that - // attributes::get() is available (Perl 5 implicitly loads attributes.pm) - if (!attributes.isEmpty()) { - org.perlonjava.runtime.operators.ModuleOperators.require(new RuntimeScalar("attributes.pm")); - } - if (subName != null && attributes.contains("const")) { - parser.throwError(":const is not permitted on named subroutines"); - } - if (futureAsyncAwaitSub && attributes.contains("lvalue")) { - String location = parser.ctx.errorUtil == null - ? "" - : parser.ctx.errorUtil.warningLocation(currentIndex); - org.perlonjava.runtime.operators.WarnDie.warn( - new RuntimeScalar("The :lvalue attribute has no effect on an async sub" - + location + ".\n"), - new RuntimeScalar("misc")); - } - ListNode signature = null; boolean signatureArgsWarningsEnabled = false; boolean previousParsingSignaturedSubroutine = parser.parsingSignaturedSubroutine; @@ -1118,7 +1124,8 @@ public static Node parseSubroutineDefinition( // prototypes in a later do FILE load look like signatures after // test.pl's watchdog setup. The Feature manager records actual // lexical imports, so require both sources of evidence here. - boolean signaturesEnabled = parser.ctx.symbolTable.isFeatureCategoryEnabled("signatures") + boolean signaturesEnabled = parser.parsingFunctionParametersDeclaration + || parser.ctx.symbolTable.isFeatureCategoryEnabled("signatures") && org.perlonjava.runtime.perlmodule.Feature.getFeatureManager() .getEnabledFeatures().contains("signatures"); if (signaturesEnabled) { @@ -1133,7 +1140,17 @@ public static Node parseSubroutineDefinition( signature = parseSignature(parser, subName); parser.parsingSignaturedSubroutine = true; if (peek(parser).text.equals(":")) { - parser.throwError("Subroutine attributes must come before the signature"); + if (!parser.parsingFunctionParametersDeclaration) { + parser.throwError("Subroutine attributes must come before the signature"); + } + while (peek(parser).text.equals(":")) { + String attrPrototype = consumeAttributes( + parser, attributes, prototype, attrSubDisplayName, prevAttrProto); + if (attrPrototype != null) { + prevAttrProto = attrPrototype; + prototype = attrPrototype; + } + } } if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("Signature AST: " + signature); if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("next token " + peek(parser)); @@ -1197,6 +1214,38 @@ public static Node parseSubroutineDefinition( } } + // Ensure attributes.pm is loaded after parsing both pre-signature and + // Function::Parameters post-signature attributes. + if (parser.parsingFunctionParametersDeclaration + && !parser.functionParametersAttributes.isEmpty()) { + for (int i = parser.functionParametersAttributes.size() - 1; i >= 0; i--) { + String configuredAttribute = parser.functionParametersAttributes.get(i); + if (configuredAttribute.startsWith("prototype(") + && configuredAttribute.endsWith(")")) { + prototype = configuredAttribute.substring( + "prototype(".length(), configuredAttribute.length() - 1); + } + if (!attributes.contains(configuredAttribute)) { + attributes.add(0, configuredAttribute); + } + } + } + if (!attributes.isEmpty()) { + org.perlonjava.runtime.operators.ModuleOperators.require(new RuntimeScalar("attributes.pm")); + } + if (subName != null && attributes.contains("const")) { + parser.throwError(":const is not permitted on named subroutines"); + } + if (futureAsyncAwaitSub && attributes.contains("lvalue")) { + String location = parser.ctx.errorUtil == null + ? "" + : parser.ctx.errorUtil.warningLocation(currentIndex); + org.perlonjava.runtime.operators.WarnDie.warn( + new RuntimeScalar("The :lvalue attribute has no effect on an async sub" + + location + ".\n"), + new RuntimeScalar("misc")); + } + if (wantName && subName != null && !peek(parser).text.equals("{")) { // A named subroutine can be predeclared without a block of code. String fullName = NameNormalizer.normalizeVariableName(subName, parser.ctx.symbolTable.getCurrentPackage()); @@ -1286,6 +1335,13 @@ public static Node parseSubroutineDefinition( } if (!wantName && !peek(parser).text.equals("{")) { + if (parser.parsingFunctionParametersDeclaration) { + String keyword = parser.functionParametersKeyword == null + ? "fun" : parser.functionParametersKeyword; + parser.throwError("In " + keyword + + " (anon): I was expecting a function body, not \"" + + peek(parser).text + "\""); + } parser.throwCleanError("Illegal declaration of anonymous subroutine"); } @@ -1407,8 +1463,28 @@ public static Node parseSubroutineDefinition( signature.getAnnotation("signatureRequiredNamedParams")); block.setAnnotation("signaturePositionalCount", signature.getAnnotation("signaturePositionalCount")); + block.setAnnotation("signatureInvocants", + signature.getAnnotation("signatureInvocants")); + block.setAnnotation("signatureKeyword", + signature.getAnnotation("signatureKeyword")); + block.setAnnotation("signaturePositionalRequired", + signature.getAnnotation("signaturePositionalRequired")); + block.setAnnotation("signaturePositionalOptional", + signature.getAnnotation("signaturePositionalOptional")); + block.setAnnotation("signatureNamedRequired", + signature.getAnnotation("signatureNamedRequired")); + block.setAnnotation("signatureNamedOptional", + signature.getAnnotation("signatureNamedOptional")); + block.setAnnotation("signatureParameterTypes", + signature.getAnnotation("signatureParameterTypes")); + block.setAnnotation("signatureParameterTypeObjects", + signature.getAnnotation("signatureParameterTypeObjects")); + block.setAnnotation("signatureParameterTypeObjectVariables", + signature.getAnnotation("signatureParameterTypeObjectVariables")); block.setAnnotation("signatureSlurpySigil", signature.getAnnotation("signatureSlurpySigil")); + block.setAnnotation("signatureSlurpyName", + signature.getAnnotation("signatureSlurpyName")); block.elements.addAll(0, signature.elements); } @@ -1419,6 +1495,26 @@ public static Node parseSubroutineDefinition( abstractNode.setAnnotation("futureAsyncAwaitSub", true); } return result; + } else if (parser.parsingFunctionParametersDeclaration + && (parser.functionParametersRuntime + || parser.functionParametersInstallSub != null)) { + String fullName = NameNormalizer.normalizeVariableName( + subName, parser.ctx.symbolTable.getCurrentPackage()); + SubroutineNode runtimeCode = handleAnonSub( + parser, null, prototype, attributes, block, currentIndex, sourceEndTokenIndex); + runtimeCode.setAnnotation("functionParametersRuntimeName", fullName); + if (parser.functionParametersInstallSub != null) { + Node installer = new OperatorNode("&", + new IdentifierNode(parser.functionParametersInstallSub, currentIndex), + currentIndex); + ListNode installerArguments = new ListNode(List.of( + new StringNode(subName, currentIndex), runtimeCode), currentIndex); + return new BinaryOperatorNode("(", installer, installerArguments, currentIndex); + } + OperatorNode glob = new OperatorNode("*", + new IdentifierNode(fullName, currentIndex), currentIndex); + glob.setAnnotation("explicitGlobDereference", true); + return new BinaryOperatorNode("=", glob, runtimeCode, currentIndex); } else { return handleNamedSub(parser, subName, prototype, attributes, block, declaration); } @@ -1615,6 +1711,9 @@ static String consumeAttributes(Parser parser, List attributes, String p // (either in this same call or from a previous :prototype() call) String existingAttrProto = prototype != null ? prototype : prevAttrPrototype; if (existingAttrProto != null && subDisplayName != null) { + if (parser.parsingFunctionParametersDeclaration) { + parser.throwCleanError("Can't redefine prototype"); + } String msg = "Attribute prototype(" + argString + ") discards earlier prototype attribute in same sub"; String loc = parser.ctx.errorUtil.warningLocation(parser.tokenIndex); @@ -2037,6 +2136,9 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S // Initialize placeholder metadata (accessed via codeRef.value) RuntimeCode placeholder = (RuntimeCode) codeRef.value; + if (block != null && block.getAnnotation("signatureMinArgs") instanceof Integer minArgs) { + placeholder.applySignatureMetadata(block); + } if (block == null && placeholder.hasBodyDefinition && attributes != null && attributes.contains("lvalue")) { String location = ""; @@ -2274,12 +2376,19 @@ && isLexicalSubStorageReferenced(explicitlyUsedVars, entry.name()) ast, k -> EmitterMethodCreator.classCounter.getAndIncrement()); } + String capturedName = entry.name().substring(1); + if (ast != null + && ast.getAnnotation("functionParametersOuterBinding") instanceof String sourceName + && sourceName.length() > 1 + && sourceName.charAt(0) == sigil.charAt(0)) { + capturedName = sourceName.substring(1); + } // This is an internal lexical-storage key, not a Perl package // variable lookup. NameNormalizer intentionally forces names such as // STDERR and ARGV into main::, which would make `my $STDERR` captured // by a named sub alias the package glob instead of its lexical cell. variableName = PersistentVariable.beginPackage(beginId) - + "::" + entry.name().substring(1); + + "::" + capturedName; } // Determine the class type based on the sigil classList.add( @@ -2487,9 +2596,20 @@ && isLexicalSubStorageReferenced(explicitlyUsedVars, entry.name()) // interpreter calls validate named arguments at the boundary. placeholder.signatureNamedParams = runtimeCode.signatureNamedParams; placeholder.signatureRequiredNamedParams = runtimeCode.signatureRequiredNamedParams; + placeholder.signatureInvocants = runtimeCode.signatureInvocants; + placeholder.signaturePositionalRequired = runtimeCode.signaturePositionalRequired; + placeholder.signaturePositionalOptional = runtimeCode.signaturePositionalOptional; + placeholder.signatureNamedRequired = runtimeCode.signatureNamedRequired; + placeholder.signatureNamedOptional = runtimeCode.signatureNamedOptional; + placeholder.signatureParameterTypes = runtimeCode.signatureParameterTypes; + placeholder.signatureParameterTypeObjects = runtimeCode.signatureParameterTypeObjects; + placeholder.signatureParameterTypeObjectVariables = + runtimeCode.signatureParameterTypeObjectVariables; placeholder.signaturePositionalCount = runtimeCode.signaturePositionalCount; placeholder.signatureSlurpySigil = runtimeCode.signatureSlurpySigil; + placeholder.signatureSlurpyName = runtimeCode.signatureSlurpyName; placeholder.signatureSubName = runtimeCode.signatureSubName; + placeholder.signatureKeyword = runtimeCode.signatureKeyword; Map compiledOurRegistry = runtimeCode.ourVariableRegistry; if (compiledOurRegistry == null || compiledOurRegistry.isEmpty()) { @@ -3178,8 +3298,8 @@ private static boolean hasMalformedUnderscorePrototype(String prototype) { } /** - * Validates prototype characters and emits "Illegal character in prototype" warnings. - * Valid prototype characters: $ @ % & * ; + \ [ ] _ + * Validates prototype characters and emits the same diagnostic sequence as + * Function::Parameters for malformed escapes, list markers, and character classes. * * @param parser The parser (for location info) * @param proto The prototype string to validate @@ -3188,50 +3308,119 @@ private static boolean hasMalformedUnderscorePrototype(String prototype) { static void emitIllegalProtoWarning(Parser parser, String proto, String subDisplayName) { if (proto == null || proto.isEmpty()) return; String name = subDisplayName != null ? subDisplayName : "?"; - // Check if any character is illegal - boolean hasIllegal = false; - for (int i = 0; i < proto.length(); i++) { - char c = proto.charAt(i); - if ("$@%&*;+\\[]_ ".indexOf(c) < 0) { - hasIllegal = true; - break; - } - } - // Perl emits diagnostics in addition to the general illegal-character - // warning for the malformed prototype shapes below. Whitespace is not - // part of a prototype's shape, so check the compacted form. The - // after-@ diagnostic is reported before the illegal-character one. String shape = proto.replaceAll("\\s+", ""); - int at = -1; + boolean hasIllegalCharacter = !shape.matches("[$@%*;\\[\\]&\\\\_+]*"); + boolean warnedAfterAt = false; for (int i = 0; i < shape.length(); i++) { + if (shape.charAt(i) == '\\' && i + 1 < shape.length()) { + if (shape.charAt(i + 1) == '[') { + int closingBracket = shape.indexOf(']', i + 2); + if (closingBracket >= 0) i = closingBracket; + else i++; + } else { + i++; + } + continue; + } if (shape.charAt(i) != '@') continue; - int backslashes = 0; - for (int j = i - 1; j >= 0 && shape.charAt(j) == '\\'; j--) backslashes++; - if (backslashes % 2 == 0) { - at = i; + if (i + 1 < shape.length()) { + warnIllegalProto(parser, "Prototype after '@' for " + name + " : " + proto); + warnedAfterAt = true; break; } } - if (at >= 0 && at + 1 < shape.length() && shape.charAt(at + 1) != '%' - && shape.charAt(at + 1) != ']' && shape.charAt(at + 1) != ';') { - warnIllegalProto(parser, "Prototype after '@' for " + name + " : " + proto); + if (!warnedAfterAt && hasMalformedUnderscoreForWarning(shape)) { + if (hasIllegalCharacter) { + warnIllegalProto(parser, + "Illegal character in prototype for " + name + " : " + proto); + } + warnMalformedUnderscorePrototype(parser, shape, proto, name); + return; } - if (hasIllegal) { + if (hasIllegalCharacter) { warnIllegalProto(parser, "Illegal character in prototype for " + name + " : " + proto); + return; } + if (warnedAfterAt) return; for (int i = 0; i < shape.length(); i++) { - if (shape.charAt(i) == '_' && i + 1 < shape.length() - && shape.charAt(i + 1) != ';') { - warnIllegalProto(parser, - "Illegal character after '_' in prototype for " + name + " : " + proto); - break; + switch (shape.charAt(i)) { + case '_': + if (i + 1 < shape.length() && ";@%".indexOf(shape.charAt(i + 1)) < 0) { + warnIllegalProto(parser, + "Illegal character after '_' in prototype for " + name + " : " + + proto); + return; + } + break; + case '@': + case '%': + if (i + 1 < shape.length()) { + warnIllegalProto(parser, "Prototype after '" + shape.charAt(i) + "' for " + + name + ": " + shape.substring(i + 1)); + return; + } + break; + case '\\': + i++; + if (i < shape.length() && "$@%&*".indexOf(shape.charAt(i)) >= 0) break; + if (i < shape.length() && shape.charAt(i) == '[') { + i++; + int firstClassCharacter = i; + while (i < shape.length() && shape.charAt(i) != ']' + && "$@%&*".indexOf(shape.charAt(i)) >= 0) { + i++; + } + if (i < shape.length() && shape.charAt(i) == ']' + && i > firstClassCharacter) break; + } + warnIllegalProto(parser, "Illegal character after '\\' in prototype for " + + name + " : " + shape.substring(Math.min(i, shape.length()))); + return; + case '$': + case '*': + case '&': + case ';': + case '+': + break; + default: + warnIllegalProto(parser, "Illegal character in prototype for " + name + " : " + + shape.substring(i)); + if (shape.indexOf('[') >= 0 + && shape.indexOf(']', shape.indexOf('[') + 1) < 0) { + warnIllegalProto(parser, + "Missing ']' in prototype for " + name + " : " + proto); + } + return; } } - if (shape.indexOf('[') >= 0 && shape.indexOf(']', shape.indexOf('[') + 1) < 0) { - warnIllegalProto(parser, "Missing ']' in prototype for " + name + " : " + proto); + } + + private static void warnMalformedUnderscorePrototype( + Parser parser, String shape, String proto, String name) { + if (hasMalformedUnderscoreForWarning(shape)) { + warnIllegalProto(parser, + "Illegal character after '_' in prototype for " + name + " : " + proto); } } + private static boolean hasMalformedUnderscoreForWarning(String shape) { + for (int i = 0; i + 1 < shape.length(); i++) { + if (shape.charAt(i) == '\\') { + if (shape.charAt(i + 1) == '[') { + int closingBracket = shape.indexOf(']', i + 2); + i = closingBracket >= 0 ? closingBracket : i + 1; + } else { + i++; + } + continue; + } + if (shape.charAt(i) == '_' && ";@%".indexOf(shape.charAt(i + 1)) < 0) { + return true; + } + } + return false; + } + /** * Emits an "illegalproto" diagnostic under the lexical warning state of the * code being compiled, so an eval STRING's own {@code use warnings} or diff --git a/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java b/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java index eaff1b6266..0db414f979 100644 --- a/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java +++ b/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java @@ -1,6 +1,7 @@ package org.perlonjava.frontend.semantic; import org.perlonjava.frontend.astnode.OperatorNode; +import org.perlonjava.frontend.parser.ParserKeywordPlugin; import org.perlonjava.runtime.runtimetypes.FeatureFlags; import org.perlonjava.runtime.runtimetypes.PerlCompilerException; import org.perlonjava.runtime.runtimetypes.PerlRuntime; @@ -123,6 +124,8 @@ public void inheritAllWarningsForRegisteredCategory(String category) { // `use VERSION` is lexical: declaration and warning state resets for an // inner block and is restored when that block exits. private final Stack useVersionStack = new Stack<>(); + /** Lexically scoped parser keywords installed by compile-time modules. */ + private final Stack> parserKeywordPluginsStack = new Stack<>(); // A stack to manage nested scopes of symbol tables. private final Stack symbolTableStack = new Stack<>(); private final Stack packageStack = new Stack<>(); @@ -175,6 +178,7 @@ public ScopedSymbolTable() { regexModifierStack.push(""); regexDebugFlagsStack.push(0); useVersionStack.push(null); + parserKeywordPluginsStack.push(new LinkedHashMap<>()); // Initialize the package name packageStack.push(new PackageInfo("main", false, null)); // Initialize the subroutine stack with empty string (no subroutine) @@ -255,6 +259,7 @@ public int enterScope() { regexModifierStack.push(regexModifierStack.peek()); regexDebugFlagsStack.push(regexDebugFlagsStack.peek()); useVersionStack.push(null); + parserKeywordPluginsStack.push(new LinkedHashMap<>(parserKeywordPluginsStack.peek())); // Return the current size of the symbol table stack as the scope index return symbolTableStack.size() - 1; @@ -311,6 +316,7 @@ public void exitScope(int scopeIndex) { regexModifierStack.pop(); regexDebugFlagsStack.pop(); useVersionStack.pop(); + parserKeywordPluginsStack.pop(); } // Propagate the child scope's index to the parent to prevent slot reuse. // This ensures that local variable slots allocated inside conditional branches @@ -329,6 +335,27 @@ public void setUseVersion(String version) { useVersionStack.set(useVersionStack.size() - 1, version); } + public void registerParserKeywordPlugin(String keyword, ParserKeywordPlugin plugin) { + parserKeywordPluginsStack.peek().put(keyword, plugin); + } + + public void removeParserKeywordPlugin(String keyword) { + parserKeywordPluginsStack.peek().remove(keyword); + } + + public ParserKeywordPlugin getParserKeywordPlugin(String keyword) { + return parserKeywordPluginsStack.peek().get(keyword); + } + + public Map getVisibleParserKeywordPlugins() { + return new LinkedHashMap<>(parserKeywordPluginsStack.peek()); + } + + public void copyParserKeywordPluginsFrom(ScopedSymbolTable source) { + parserKeywordPluginsStack.set(parserKeywordPluginsStack.size() - 1, + source.getVisibleParserKeywordPlugins()); + } + /** * Returns the JVM local variable slot indices for all {@code my} variables * declared in the scopes being exited (from the current top of the symbol @@ -643,6 +670,12 @@ public void addVariableWithIndex(String name, int index, String variableDeclType symbolTableStack.peek().addVariableWithIndex(name, index, variableDeclType, perlPackage); } + public void addVariableWithIndex( + String name, int index, String variableDeclType, String perlPackage, OperatorNode ast) { + clearVisibleVariablesCache(); + symbolTableStack.peek().addVariableWithIndex(name, index, variableDeclType, perlPackage, ast); + } + public Map getVisibleVariableRegistry() { Map registry = new HashMap<>(); Map visible = getAllVisibleVariables(); @@ -996,6 +1029,9 @@ public ScopedSymbolTable snapShot() { st.regexModifierStack.push(this.regexModifierStack.peek()); st.regexDebugFlagsStack.pop(); st.regexDebugFlagsStack.push(this.regexDebugFlagsStack.peek()); + st.parserKeywordPluginsStack.pop(); + st.parserKeywordPluginsStack.push(new LinkedHashMap<>( + this.parserKeywordPluginsStack.peek())); st.lexicalRegexPragmaChanged = this.lexicalRegexPragmaChanged; return st; diff --git a/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java b/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java index b73a0435ea..d0036797d2 100644 --- a/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java +++ b/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java @@ -57,7 +57,12 @@ public int addVariable(String name, String variableDeclType, String perlPackage, * @return The index of the variable, or -1 if the variable is not found. */ public void addVariableWithIndex(String name, int specificIndex, String variableDeclType, String perlPackage) { - variableIndex.put(name, new SymbolEntry(specificIndex, name, variableDeclType, perlPackage, null)); + addVariableWithIndex(name, specificIndex, variableDeclType, perlPackage, null); + } + + public void addVariableWithIndex( + String name, int specificIndex, String variableDeclType, String perlPackage, OperatorNode ast) { + variableIndex.put(name, new SymbolEntry(specificIndex, name, variableDeclType, perlPackage, ast)); if (specificIndex >= index) { index = specificIndex + 1; } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/FunctionParameters.java b/src/main/java/org/perlonjava/runtime/perlmodule/FunctionParameters.java new file mode 100644 index 0000000000..01bb8992d8 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/perlmodule/FunctionParameters.java @@ -0,0 +1,121 @@ +package org.perlonjava.runtime.perlmodule; + +import org.perlonjava.runtime.runtimetypes.GlobalVariable; +import org.perlonjava.runtime.runtimetypes.RuntimeArray; +import org.perlonjava.runtime.runtimetypes.RuntimeCode; +import org.perlonjava.runtime.runtimetypes.RuntimeHash; +import org.perlonjava.runtime.runtimetypes.RuntimeList; +import org.perlonjava.runtime.runtimetypes.RuntimeScalar; +import org.perlonjava.runtime.runtimetypes.RuntimeScalarType; +import org.perlonjava.runtime.operators.ModuleOperators; +import org.perlonjava.runtime.operators.ReferenceOperators; + +import java.util.List; +import java.util.ArrayList; + +/** Runtime support for the introspection part of Function::Parameters. */ +public final class FunctionParameters extends PerlModuleBase { + public FunctionParameters() { + super("Function::Parameters", false); + } + + public static void initialize() { + FunctionParameters module = new FunctionParameters(); + try { + module.registerMethod("info", "info", "$"); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Missing Function::Parameters.info implementation", e); + } + } + + public static RuntimeList info(RuntimeArray args, int context) { + if (args.size() == 0) { + throw new IllegalArgumentException("Usage: Function::Parameters::info(CODEREF)"); + } + RuntimeScalar codeRef = args.get(0).scalar(); + if (codeRef.type != RuntimeScalarType.CODE || !(codeRef.value instanceof RuntimeCode code)) { + return new RuntimeScalar().getList(); + } + // Named declarations can still be represented by a lazy CV when + // introspection happens before the subroutine's first call. Materialize + // it first so metadata attached by the backend is visible here too. + if (code.compilerSupplier != null) { + code.compilerSupplier.get(); + if (codeRef.value instanceof RuntimeCode materialized) { + code = materialized; + } + } + if (code.signatureKeyword == null) { + return new RuntimeScalar().getList(); + } + + RuntimeHash info = new RuntimeHash(); + List positionalRequired = new ArrayList<>(code.signatureInvocants); + positionalRequired.addAll(code.signaturePositionalRequired); + putParameters(info, "_positional_required", positionalRequired, code); + putParameters(info, "_positional_optional", code.signaturePositionalOptional, code); + putParameters(info, "_named_required", code.signatureNamedRequired, code); + putParameters(info, "_named_optional", code.signatureNamedOptional, code); + info.put("nshift", new RuntimeScalar(code.signatureInvocants.size())); + info.put("keyword", new RuntimeScalar(code.signatureKeyword != null + ? code.signatureKeyword + : code.signatureInvocants.isEmpty() ? "fun" : "method")); + RuntimeScalar slurpy = code.signatureSlurpyName == null + ? new RuntimeScalar() + : parameter(code.signatureSlurpyName, code); + info.put("slurpy", slurpy); + int min = positionalRequired.size() + + 2 * code.signatureRequiredNamedParams.size(); + info.put("args_min", new RuntimeScalar(min)); + boolean unbounded = !code.signatureNamedParams.isEmpty() || code.signatureSlurpySigil != null; + int max = positionalRequired.size() + code.signaturePositionalOptional.size(); + info.put("args_max", new RuntimeScalar(unbounded ? "Inf" : Integer.toString(max))); + RuntimeScalar object = ReferenceOperators.bless( + info.createReferenceWithTrackedElements(), new RuntimeScalar("Function::Parameters::Info")); + return object.getList(); + } + + private static void putParameters( + RuntimeHash info, String key, List names, RuntimeCode code) { + RuntimeArray values = new RuntimeArray(); + for (String name : names) values.elements.add(parameter(name, code)); + info.put(key, values.createAnonymousReference()); + } + + private static RuntimeScalar parameter(String name, RuntimeCode code) { + RuntimeHash parameter = new RuntimeHash(); + parameter.put("name", new RuntimeScalar(name)); + parameter.put("type", resolveParameterType(name, code)); + return ReferenceOperators.bless(parameter.createReferenceWithTrackedElements(), + new RuntimeScalar("Function::Parameters::Param")); + } + + private static RuntimeScalar resolveParameterType(String parameterName, RuntimeCode code) { + RuntimeScalar objectType = code.signatureParameterTypeObjects.get(parameterName); + if (objectType != null) return objectType; + String typeName = code.signatureParameterTypes.get(parameterName); + if (typeName == null) return new RuntimeScalar(); + if (typeName.startsWith("moose:")) { + ModuleOperators.require(new RuntimeScalar("Moose/Util/TypeConstraints.pm")); + String reifierName = "Moose::Util::TypeConstraints::find_or_create_isa_type_constraint"; + RuntimeScalar reifier = GlobalVariable.getGlobalCodeRefForFreshLookup(reifierName); + if (reifier == null || reifier.type != RuntimeScalarType.CODE) return new RuntimeScalar(); + RuntimeList result = RuntimeCode.apply(reifier, reifierName, + new org.perlonjava.runtime.runtimetypes.RuntimeBase[]{ + new RuntimeScalar(typeName.substring("moose:".length()))}, + org.perlonjava.runtime.runtimetypes.RuntimeContextType.SCALAR); + return result.scalar(); + } + String fullName = typeName.startsWith("::") ? typeName.substring(2) + : typeName.contains("::") ? typeName + : (code.packageName == null ? "main" : code.packageName) + "::" + typeName; + RuntimeScalar typeFunction = GlobalVariable.getGlobalCodeRefForFreshLookup(fullName); + if (typeFunction == null || typeFunction.type != RuntimeScalarType.CODE) { + return new RuntimeScalar(); + } + RuntimeList result = RuntimeCode.apply(typeFunction, fullName, + new org.perlonjava.runtime.runtimetypes.RuntimeBase[0], + org.perlonjava.runtime.runtimetypes.RuntimeContextType.SCALAR); + return result.scalar(); + } +} diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java index 7dfe3b2c54..12419c6ef6 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java @@ -362,6 +362,7 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { XSLoader.initialize(); DynaLoader.initialize(); Builtin.initialize(); + FunctionParameters.initialize(); Base.initialize(); Symbol.initialize(); ScalarUtil.initialize(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 8010a42503..a068fbd521 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -625,9 +625,20 @@ public void setCallDepthTrackingRequired(boolean required) { // accidentally turn an odd list into a hash. public List signatureNamedParams = List.of(); public List signatureRequiredNamedParams = List.of(); + public List signatureInvocants = List.of(); + public List signaturePositionalRequired = List.of(); + public List signaturePositionalOptional = List.of(); + public List signatureNamedRequired = List.of(); + public List signatureNamedOptional = List.of(); + public Map signatureParameterTypes = Map.of(); + public Map signatureParameterTypeObjects = Map.of(); + public Map signatureParameterTypeObjectVariables = Map.of(); + public String signatureTypeReifier; public int signaturePositionalCount = 0; public String signatureSlurpySigil; + public String signatureSlurpyName; public String signatureSubName; + public String signatureKeyword; public void applySignatureMetadata(Node ast) { Object names = ast.getAnnotation("signatureNamedParams"); @@ -638,27 +649,162 @@ public void applySignatureMetadata(Node ast) { if (required instanceof List list) { signatureRequiredNamedParams = list.stream().map(String::valueOf).toList(); } + Object invocants = ast.getAnnotation("signatureInvocants"); + if (invocants instanceof List list) { + signatureInvocants = list.stream().map(String::valueOf).toList(); + } + Object keyword = ast.getAnnotation("signatureKeyword"); + if (keyword instanceof String name) signatureKeyword = name; + Object positionalRequired = ast.getAnnotation("signaturePositionalRequired"); + if (positionalRequired instanceof List list) { + signaturePositionalRequired = list.stream().map(String::valueOf).toList(); + } + Object positionalOptional = ast.getAnnotation("signaturePositionalOptional"); + if (positionalOptional instanceof List list) { + signaturePositionalOptional = list.stream().map(String::valueOf).toList(); + } + Object namedRequired = ast.getAnnotation("signatureNamedRequired"); + if (namedRequired instanceof List list) { + signatureNamedRequired = list.stream().map(String::valueOf).toList(); + } + Object namedOptional = ast.getAnnotation("signatureNamedOptional"); + if (namedOptional instanceof List list) { + signatureNamedOptional = list.stream().map(String::valueOf).toList(); + } + Object parameterTypes = ast.getAnnotation("signatureParameterTypes"); + if (parameterTypes instanceof Map types) { + Map parsed = new LinkedHashMap<>(); + for (var entry : types.entrySet()) { + if (entry.getKey() != null && entry.getValue() != null) { + parsed.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue())); + } + } + signatureParameterTypes = Map.copyOf(parsed); + } + Object typeObjects = ast.getAnnotation("signatureParameterTypeObjects"); + if (typeObjects instanceof Map objectMap) { + Map parsed = new LinkedHashMap<>(); + for (Map.Entry entry : objectMap.entrySet()) { + if (entry.getKey() instanceof String name && entry.getValue() instanceof RuntimeScalar type) { + parsed.put(name, type); + } + } + signatureParameterTypeObjects = Map.copyOf(parsed); + } + Object typeObjectVariables = ast.getAnnotation("signatureParameterTypeObjectVariables"); + if (typeObjectVariables instanceof Map variableMap) { + Map parsed = new LinkedHashMap<>(); + Map resolvedObjects = new LinkedHashMap<>(signatureParameterTypeObjects); + for (Map.Entry entry : variableMap.entrySet()) { + if (entry.getKey() instanceof String name && entry.getValue() instanceof String variableName) { + parsed.put(name, variableName); + RuntimeScalar typeObject = GlobalVariable.globalVariables.get(variableName); + if (typeObject != null) resolvedObjects.putIfAbsent(name, typeObject); + } + } + signatureParameterTypeObjectVariables = Map.copyOf(parsed); + signatureParameterTypeObjects = Map.copyOf(resolvedObjects); + } + Object typeReifier = ast.getAnnotation("signatureTypeReifier"); + signatureTypeReifier = typeReifier instanceof String value ? value : null; Object positional = ast.getAnnotation("signaturePositionalCount"); if (positional instanceof Integer count) signaturePositionalCount = count; Object slurpy = ast.getAnnotation("signatureSlurpySigil"); signatureSlurpySigil = slurpy instanceof String value ? value : null; + Object slurpyName = ast.getAnnotation("signatureSlurpyName"); + signatureSlurpyName = slurpyName instanceof String value ? value : null; Object subName = ast.getAnnotation("signatureSubName"); if (subName instanceof String value) signatureSubName = value; } + /** Attach parser metadata to an anonymous Function::Parameters code object. */ + public static RuntimeScalar attachFunctionParametersMetadata( + RuntimeScalar codeRef, String keyword, String subName, + String[] invocants, String[] positionalRequired, String[] positionalOptional, + String[] namedRequired, String[] namedOptional, String[] namedParams, + String[] requiredNamedParams, int positionalCount, + String slurpySigil, String slurpyName, String[] parameterTypes, + String[] typeObjectVariables) { + if (codeRef != null && codeRef.value instanceof RuntimeCode code) { + code.signatureKeyword = keyword; + code.signatureSubName = subName; + code.signatureInvocants = signatureNames(invocants); + code.signaturePositionalRequired = signatureNames(positionalRequired); + code.signaturePositionalOptional = signatureNames(positionalOptional); + code.signatureNamedRequired = signatureNames(namedRequired); + code.signatureNamedOptional = signatureNames(namedOptional); + code.signatureNamedParams = signatureNames(namedParams); + code.signatureRequiredNamedParams = signatureNames(requiredNamedParams); + code.signaturePositionalCount = positionalCount; + code.signatureSlurpySigil = slurpySigil; + code.signatureSlurpyName = slurpyName; + if (parameterTypes != null) { + Map types = new LinkedHashMap<>(); + for (int i = 0; i + 1 < parameterTypes.length; i += 2) { + types.put(parameterTypes[i], parameterTypes[i + 1]); + } + code.signatureParameterTypes = Map.copyOf(types); + } + if (typeObjectVariables != null) { + Map variables = new LinkedHashMap<>(); + Map objects = new LinkedHashMap<>(code.signatureParameterTypeObjects); + for (int i = 0; i + 1 < typeObjectVariables.length; i += 2) { + String parameterName = typeObjectVariables[i]; + String variableName = typeObjectVariables[i + 1]; + variables.put(parameterName, variableName); + RuntimeScalar typeObject = GlobalVariable.globalVariables.get(variableName); + if (typeObject != null) objects.putIfAbsent(parameterName, typeObject); + } + code.signatureParameterTypeObjectVariables = Map.copyOf(variables); + code.signatureParameterTypeObjects = Map.copyOf(objects); + } + } + return codeRef; + } + + /** Name a runtime-installed Function::Parameters CV. */ + public static RuntimeScalar configureFunctionParametersRuntime( + RuntimeScalar codeRef, String fullName) { + if (codeRef == null || !(codeRef.value instanceof RuntimeCode code)) return codeRef; + int separator = fullName.lastIndexOf("::"); + code.packageName = separator < 0 ? "main" : fullName.substring(0, separator); + code.subName = separator < 0 ? fullName : fullName.substring(separator + 2); + code.isDeclared = true; + code.hasBodyDefinition = true; + return codeRef; + } + + private static List signatureNames(String[] names) { + return names == null ? List.of() : List.of(names.clone()); + } + protected void validateNamedSignatureArguments(RuntimeArray args) { if (signatureNamedParams.isEmpty() && !"%".equals(signatureSlurpySigil)) return; int start = Math.min(signaturePositionalCount, args.size()); int namedCount = args.size() - start; if ("%".equals(signatureSlurpySigil) && (namedCount & 1) != 0) { - WarnDie.die(new RuntimeScalar("Odd name/value argument for subroutine '" - + (signatureSubName == null ? "" : signatureSubName) + "'"), + dieTooFewForMissingRequiredNamed(args, start); + WarnDie.die(new RuntimeScalar(signatureKeyword != null + ? "Odd number of elements in anonymous hash" + : "Odd name/value argument for subroutine '" + + (signatureSubName == null ? "" : signatureSubName) + "'"), new RuntimeScalar("")); } if (signatureNamedParams.isEmpty()) return; - if (!"@".equals(signatureSlurpySigil) && (namedCount & 1) != 0) { - WarnDie.die(new RuntimeScalar("Odd name/value argument for subroutine '" - + (signatureSubName == null ? "" : signatureSubName) + "'"), + // A native signature with a trailing @ slurpy accepts an odd final + // argument as positional slurpy data. It is not a malformed named + // pair, and must not make an otherwise absent named parameter fatal. + if (signatureKeyword == null && "@".equals(signatureSlurpySigil) + && (namedCount & 1) != 0) { + return; + } + if ((signatureKeyword != null || !"@".equals(signatureSlurpySigil)) + && (namedCount & 1) != 0) { + dieTooFewForMissingRequiredNamed(args, start); + WarnDie.die(new RuntimeScalar(signatureKeyword != null + ? "Odd number of elements in anonymous hash" + : "Odd name/value argument for subroutine '" + + (signatureSubName == null ? "" : signatureSubName) + "'"), new RuntimeScalar("")); } @@ -685,18 +831,42 @@ protected void validateNamedSignatureArguments(RuntimeArray args) { WarnDie.die(new RuntimeScalar(message), new RuntimeScalar("")); } } - if (!"@".equals(signatureSlurpySigil)) { - List missing = new ArrayList<>(); - for (String required : signatureRequiredNamedParams) { - if (!values.containsKey(required)) { - missing.add(required); + List missing = new ArrayList<>(); + for (String required : signatureRequiredNamedParams) { + if (!values.containsKey(required)) missing.add(required); + } + if (!missing.isEmpty()) { + int minimumNamedArgumentCount = signatureRequiredNamedParams.size() * 2; + if (signatureKeyword != null && namedCount < minimumNamedArgumentCount) { + WarnDie.die(new RuntimeScalar("Too few arguments for " + signatureKeyword + + " " + signatureSubName), new RuntimeScalar("")); + } + if (signatureKeyword != null) { + String label = missing.size() == 1 ? "named parameter" : "named parameters"; + WarnDie.die(new RuntimeScalar("missing " + label + " '" + + String.join("', '", missing) + "' to " + signatureKeyword + + " " + signatureSubName), new RuntimeScalar("")); + } + String label = missing.size() == 1 ? "parameter" : "parameters"; + WarnDie.die(new RuntimeScalar("Missing required named " + label + " '" + + String.join("', '", missing) + "' to subroutine '" + signatureSubName + "'"), + new RuntimeScalar("")); + } + } + + private void dieTooFewForMissingRequiredNamed(RuntimeArray args, int start) { + if (signatureKeyword == null || signatureRequiredNamedParams.isEmpty()) return; + for (String required : signatureRequiredNamedParams) { + boolean present = false; + for (int i = start; i + 1 < args.size(); i += 2) { + if (required.equals(args.elements.get(i).toString())) { + present = true; + break; } } - if (!missing.isEmpty()) { - String label = missing.size() == 1 ? "parameter" : "parameters"; - WarnDie.die(new RuntimeScalar("Missing required named " + label + " '" - + String.join("', '", missing) + "' to subroutine '" + signatureSubName + "'"), - new RuntimeScalar("")); + if (!present) { + WarnDie.die(new RuntimeScalar("Too few arguments for " + signatureKeyword + + " " + signatureSubName), new RuntimeScalar("")); } } } @@ -3370,6 +3540,22 @@ public RuntimeCode cloneForClosure() { ? null : new java.util.LinkedHashMap<>(this.declaredOurVariables); clone.compilerSupplier = this.compilerSupplier; clone.stableHashMutationMethodKeys = this.stableHashMutationMethodKeys; + clone.signatureNamedParams = this.signatureNamedParams; + clone.signatureRequiredNamedParams = this.signatureRequiredNamedParams; + clone.signatureInvocants = this.signatureInvocants; + clone.signaturePositionalRequired = this.signaturePositionalRequired; + clone.signaturePositionalOptional = this.signaturePositionalOptional; + clone.signatureNamedRequired = this.signatureNamedRequired; + clone.signatureNamedOptional = this.signatureNamedOptional; + clone.signatureParameterTypes = this.signatureParameterTypes; + clone.signatureParameterTypeObjects = this.signatureParameterTypeObjects; + clone.signatureParameterTypeObjectVariables = this.signatureParameterTypeObjectVariables; + clone.signatureTypeReifier = this.signatureTypeReifier; + clone.signaturePositionalCount = this.signaturePositionalCount; + clone.signatureSlurpySigil = this.signatureSlurpySigil; + clone.signatureSlurpyName = this.signatureSlurpyName; + clone.signatureSubName = this.signatureSubName; + clone.signatureKeyword = this.signatureKeyword; clone.jvmClosureFrameRequired = this.jvmClosureFrameRequired; clone.activeLexicalFrameRequired = this.activeLexicalFrameRequired; clone.callDepthTrackingRequired = this.callDepthTrackingRequired; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeFormat.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeFormat.java index 368feef5db..3891b1e97e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeFormat.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeFormat.java @@ -715,18 +715,50 @@ private List materializeLineArguments(ArgumentLine argLine, lexicalVariables.put(name, new RuntimeScalar()); } } + String source = hoistNestedFormatDeclarations(argLine.content); List simpleScalarSlots = resolveSimpleGlobalScalarSlots(argLine.content); if (simpleScalarSlots != null) { lineArgs.addAll(simpleScalarSlots); return lineArgs; } - // A format argument line is executable Perl in list context. The - // parsed nodes are retained for format introspection, but cannot - // be evaluated piecemeal: doing so loses operators, blocks, list - // expansion, and their side effects. Re-evaluate the complete - // source line when write() reaches this picture, just as Perl - // evaluates a format's argument line at write time. - String source = hoistNestedFormatDeclarations(argLine.content); + if (source.trim().matches("^return(?:\\s|;|$).*$")) { + lastExecutionReturned = true; + return lineArgs; + } + if (argLine.getAnnotation("executableFormatExpressions") instanceof List expressions + && !expressions.isEmpty()) { + for (Object expression : expressions) { + if (!(expression instanceof org.perlonjava.backend.bytecode.InterpretedCode template)) { + throw new IllegalStateException( + "Invalid compiled expression in format " + formatName); + } + int highestRegister = 2; + for (Integer register : template.variableRegistry.values()) { + if (register != null) highestRegister = Math.max(highestRegister, register); + } + RuntimeBase[] captured = new RuntimeBase[highestRegister - 2]; + for (Map.Entry entry : template.variableRegistry.entrySet()) { + int register = entry.getValue(); + if (register >= 3 && register - 3 < captured.length) { + captured[register - 3] = lexicalVariables.get(entry.getKey()); + } + } + RuntimeList values = template.withCapturedVars(captured) + .apply(new RuntimeArray(), RuntimeContextType.LIST); + for (RuntimeBase value : values.elements) { + RuntimeScalar scalar = value.scalar(); + if (scalar.type == RuntimeScalarType.TIED_SCALAR) { + scalar = scalar.tiedFetch(); + } + lineArgs.add(scalar); + lastExecutionTainted |= scalar.isTainted(); + } + } + return lineArgs; + } + // Only syntax explicitly marked as beyond the format parser's + // current AST support reaches this source-eval fallback. Parsed + // expressions use the shared bytecode pipeline above. // A bare return in a format argument line exits the format. It is // not an empty list of picture values: write() reports failure to // its caller without rendering or writing this picture. @@ -939,68 +971,6 @@ private record ConsumedText(String text, String remaining) { } private record PictureExecution(String text, boolean hasRemainingText, boolean hasNonemptyFieldValue) { } - /** - * Evaluate an expression node to get its runtime value. - * This is a simplified implementation that handles basic variable access. - */ - private RuntimeScalar evaluateExpression(Node expression) { - if (expression instanceof StringNode stringNode) { - // Handle the case where expressions are stored as StringNode with serialized AST data - // This is a workaround for the parsing issue in FormatParser.parseArgumentExpressions() - String content = stringNode.value; - - // Parse simple variable expressions from the serialized AST data - if (content.contains("OperatorNode: $") && content.contains("IdentifierNode:")) { - // Extract variable name from serialized AST: "OperatorNode: $ pos:3\nIdentifierNode: 'name'" - String[] lines = content.split("\\n"); - for (String line : lines) { - if (line.trim().startsWith("IdentifierNode:")) { - // Extract variable name: "IdentifierNode: 'name'" -> "name" - String varName = line.substring(line.indexOf("'") + 1, line.lastIndexOf("'")); - String fullName = "main::" + varName; - RuntimeScalar result = getGlobalVariable(fullName).scalar(); - return result; - } - } - } - - // If we can't parse it, return the raw content as a fallback - return new RuntimeScalar(content); - } else if (expression instanceof OperatorNode opNode) { - // Handle variable access operators like $ (scalar dereference) - if ("$".equals(opNode.operator) && opNode.operand != null) { - if (opNode.operand instanceof IdentifierNode idNode) { - String varName = idNode.name; - String fullName = "main::" + varName; - RuntimeScalar result = getGlobalVariable(fullName).scalar(); - return result; - } - } - } else if (expression instanceof IdentifierNode idNode) { - // Handle variable access like $name, $version - String varName = idNode.name; - - // Variables in Perl start with sigils ($, @, %) - // For format expressions, we're typically dealing with scalars ($) - if (varName.startsWith("$")) { - // Remove the $ sigil and normalize the variable name - String cleanName = varName.substring(1); - if (!cleanName.contains("::")) { - cleanName = "main::" + cleanName; - } - RuntimeScalar result = getGlobalVariable(cleanName).scalar(); - return result; - } else { - // Handle bare identifiers as scalar variables - String fullName = "main::" + varName; - return getGlobalVariable(fullName).scalar(); - } - } - - // For unsupported expression types, return a placeholder - return new RuntimeScalar(""); - } - /** * Compile the format template into executable format lines. * This is a simplified version that parses the template string. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeFormatExpressionRegistry.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeFormatExpressionRegistry.java new file mode 100644 index 0000000000..a598b37543 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeFormatExpressionRegistry.java @@ -0,0 +1,28 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.perlonjava.backend.bytecode.InterpretedCode; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** Transfers compiled format expressions into generated JVM class constants. */ +public final class RuntimeFormatExpressionRegistry { + private static final ConcurrentHashMap> pending = + new ConcurrentHashMap<>(); + + private RuntimeFormatExpressionRegistry() {} + + public static void register(String key, List expressions) { + pending.put(key, List.copyOf(expressions)); + } + + public static List take(String key) { + List expressions = pending.remove(key); + return expressions == null ? List.of() : expressions; + } + + public static void discardClass(String className) { + String prefix = className + "#format#"; + pending.keySet().removeIf(key -> key.startsWith(prefix)); + } +} diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index 26280eb418..89a6e6227a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -635,6 +635,9 @@ public RuntimeScalar set(RuntimeScalar value) { } // Get or create the code ref container RuntimeScalar codeContainer = GlobalVariable.defineGlobalCodeRef(codeSlotName); + boolean updateWholeGlobAliasGroup = writesThroughGlobAlias + || (!isLocalizedGlob(this.globName) + && GlobalVariable.isInGlobAliasGroup(this.globName)); // A non-local typeglob assignment is a redefinition for // prototype purposes too. Parser-time checks do not see @@ -759,11 +762,10 @@ public RuntimeScalar set(RuntimeScalar value) { codeContainer.set(value); - // `*Alias = *Base` makes assignments made through Alias - // replace Base's CODE slot as well. The converse is not - // true: assigning directly to Base may split its CODE slot - // from alias names while the other glob slots stay shared. - if (writesThroughGlobAlias) { + // Whole-glob aliases share their CODE slot regardless of + // which name receives a non-local definition. Localized + // writes remain temporary and are handled by the save stack. + if (updateWholeGlobAliasGroup) { for (String alias : GlobalVariable.getGlobAliasGroup(this.globName)) { GlobalVariable.globalCodeRefs.put(alias, codeContainer); GlobalVariable.replacePinnedCodeRef(alias, codeContainer); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGraphCloner.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGraphCloner.java index 9d123b8bdb..77d57efaf5 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGraphCloner.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGraphCloner.java @@ -277,6 +277,7 @@ private InterpretedCode cloneInterpretedCode(InterpretedCode source) { source.variableRegistry, source.errorUtil, source.strictOptions, source.featureFlags, source.warningFlags, source.compilePackage, source.evalSiteRegistries, source.evalSiteLexicalSubroutineBindings, + source.evalSiteParserKeywordPlugins, source.evalSitePragmaFlags, source.warningBitsString); } @@ -289,6 +290,7 @@ private InterpretedCode cloneInterpretedCode(InterpretedCode source) { target.signatureMinArgs = source.signatureMinArgs; target.signatureMaxArgs = source.signatureMaxArgs; target.signatureSubName = source.signatureSubName; + target.signatureKeyword = source.signatureKeyword; target.gotoLabelPcs = source.gotoLabelPcs; target.gotoLabelsInsideLoop = source.gotoLabelsInsideLoop; copyCodeMetadata(source, target); @@ -304,6 +306,17 @@ private RuntimeBase[] cloneRuntimeBases(RuntimeBase[] values) { private void copyCodeMetadata(RuntimeCode source, RuntimeCode target) { target.prototype = source.prototype; + target.signatureNamedParams = source.signatureNamedParams; + target.signatureRequiredNamedParams = source.signatureRequiredNamedParams; + target.signatureInvocants = source.signatureInvocants; + target.signaturePositionalRequired = source.signaturePositionalRequired; + target.signaturePositionalOptional = source.signaturePositionalOptional; + target.signatureNamedRequired = source.signatureNamedRequired; + target.signatureNamedOptional = source.signatureNamedOptional; + target.signaturePositionalCount = source.signaturePositionalCount; + target.signatureSlurpySigil = source.signatureSlurpySigil; + target.signatureSlurpyName = source.signatureSlurpyName; + target.signatureSubName = source.signatureSubName; target.attributes = source.attributes == null ? null : new ArrayList<>(source.attributes); target.packageName = source.packageName; target.subName = source.subName; diff --git a/src/main/perl/lib/Function/Parameters.pm b/src/main/perl/lib/Function/Parameters.pm new file mode 100644 index 0000000000..75a3aba6cf --- /dev/null +++ b/src/main/perl/lib/Function/Parameters.pm @@ -0,0 +1,56 @@ +package Function::Parameters; + +use strict; +use warnings; + +our $VERSION = '0.01'; + +require Function::Parameters::Info; + +# PerlOnJava installs syntax through the lexical parser extension. This +# runtime hook validates direct imports while leaving registration to parser. +sub import { + shift if @_ && $_[0] eq __PACKAGE__; + for my $argument (@_) { + if (ref($argument) eq 'HASH') { + for my $keyword (keys %$argument) { + _validate_keyword($keyword); + my $definition = $argument->{$keyword}; + my $defaults = ref($definition) eq 'HASH' + ? ($definition->{defaults} // _keyword_defaults($keyword)) : $definition; + _validate_type($defaults); + } + } elsif (!ref($argument)) { + next if $argument =~ /\A:(?:std|modifiers|strict|lax)\z/; + next if $argument =~ /\A(?:fun|method)\z/; + die "Function::Parameters: $argument is not exported\n"; + } else { + die "Function::Parameters: import argument must be a tag, keyword, or hash\n"; + } + } + return; +} + +sub _keyword_defaults { + my ($keyword) = @_; + return $keyword if $keyword =~ /\A(?:method|classmethod|before|after|around|augment|override)\z/; + return 'function'; +} + +sub _validate_keyword { + my ($keyword) = @_; + die "Function::Parameters: '$keyword' is not a valid identifier for a keyword\n" + unless $keyword =~ /\A[A-Za-z_]\w*\z/; +} + +sub _validate_type { + my ($type) = @_; + $type = "$type"; + $type =~ s/_(?:strict|lax)\z//; + die "Function::Parameters: '$type' is not a valid type\n" + unless $type =~ /\A(?:function|method|classmethod|before|after|around|augment|override)\z/; +} + +sub unimport { return } + +1; diff --git a/src/main/perl/lib/Function/Parameters/Info.pm b/src/main/perl/lib/Function/Parameters/Info.pm new file mode 100644 index 0000000000..9a1f499784 --- /dev/null +++ b/src/main/perl/lib/Function/Parameters/Info.pm @@ -0,0 +1,53 @@ +package Function::Parameters::Info; + +use strict; +use warnings; +use Carp (); + +sub keyword { return $_[0]->{keyword} } +sub nshift { return $_[0]->{nshift} } +sub slurpy { return $_[0]->{slurpy} } +sub named_required { return wantarray ? @{$_[0]->{_named_required}} : scalar @{$_[0]->{_named_required}} } +sub named_optional { return wantarray ? @{$_[0]->{_named_optional}} : scalar @{$_[0]->{_named_optional}} } +sub positional_optional { return wantarray ? @{$_[0]->{_positional_optional}} : scalar @{$_[0]->{_positional_optional}} } + +sub positional_required { + my @parameters = @{$_[0]->{_positional_required}}; + splice @parameters, 0, $_[0]->nshift; + return wantarray ? @parameters : scalar @parameters; +} + +sub invocants { + my @parameters = @{$_[0]->{_positional_required}}; + splice @parameters, $_[0]->nshift; + return wantarray ? @parameters : scalar @parameters; +} + +sub invocant { + my $count = $_[0]->nshift; + return undef if $count == 0; + return $_[0]->{_positional_required}[0] if $count == 1; + Carp::croak "Can't return a single invocant; this function has $count"; +} + +sub args_min { + my ($self) = @_; + return @{$self->{_positional_required}} + 2 * @{$self->{_named_required}}; +} + +sub args_max { + my ($self) = @_; + return 'Inf' if defined $self->slurpy || $self->named_required || $self->named_optional; + return @{$self->{_positional_required}} + @{$self->{_positional_optional}}; +} + +package Function::Parameters::Param; + +use strict; +use warnings; +use overload '""' => sub { $_[0]->{name} }, fallback => 1; + +sub name { return $_[0]->{name} } +sub type { return $_[0]->{type} } + +1; diff --git a/src/test/resources/unit/format_argument_line_lexical_scope.t b/src/test/resources/unit/format_argument_line_lexical_scope.t new file mode 100644 index 0000000000..52314d9ff0 --- /dev/null +++ b/src/test/resources/unit/format_argument_line_lexical_scope.t @@ -0,0 +1,25 @@ +use warnings 'syntax'; +use Test::More; + +format FORMAT_ARGUMENT_LINE_LEXICAL_SCOPE = +^*|^* +my $format_local = q/dd/, $format_local +. + +my $rendered = ''; +open my $format_fh, '>', \$rendered + or die "open scalar format handle: $!"; +my $old_fh = select $format_fh; +local $~ = 'FORMAT_ARGUMENT_LINE_LEXICAL_SCOPE'; +my @format_warnings; +local $SIG{__WARN__} = sub { push @format_warnings, @_ }; +write; +select $old_fh; +close $format_fh or die "close scalar format handle: $!"; + +is($rendered, "dd|\n", + 'a lexical declared in a format argument line is visible to later expressions on that line'); +is(scalar @format_warnings, 0, + 'a format-line lexical is not misreported as an unavailable captured lexical'); + +done_testing; diff --git a/src/test/resources/unit/format_compiled_argument_behaviors.t b/src/test/resources/unit/format_compiled_argument_behaviors.t new file mode 100644 index 0000000000..a46a70bc30 --- /dev/null +++ b/src/test/resources/unit/format_compiled_argument_behaviors.t @@ -0,0 +1,64 @@ +use strict; +use warnings; +use Test::More; + +{ + package FormatArgumentTie; + sub TIESCALAR { bless { fetches => $_[1] }, $_[0] } + sub FETCH { ${$_[0]{fetches}}++; return 'tied' } + + package FormatArgumentOverload; + use overload '""' => sub { return 'overloaded' }, fallback => 1; +} + +my $fetches = 0; +tie my $tied_value, 'FormatArgumentTie', \$fetches; +my $overloaded_value = bless {}, 'FormatArgumentOverload'; +format FORMAT_COMPILED_ARGUMENT_BEHAVIORS = +@<<<<<<<<<<<<<< +$tied_value . ':' . $overloaded_value +. + +my $path = 'format_compiled_argument_behaviors.tmp'; +open my $fh, '>', $path or die "open $path: $!"; +my $old_fh = select((select($fh), $~ = 'FORMAT_COMPILED_ARGUMENT_BEHAVIORS')[0]); +write $fh; +select $old_fh; +close $fh or die "close $path: $!"; +open my $read_fh, '<', $path or die "read $path: $!"; +my $rendered = do { local $/; <$read_fh> }; +close $read_fh or die "close $path after read: $!"; +unlink $path or die "unlink $path: $!"; +is($rendered, "tied:overloaded\n", 'format expression preserves tie and overload behavior'); +is($fetches, 1, 'format expression fetches a tied value once'); + +sub warn_from_format { warn "format expression warning\n"; return 'ok' } +format FORMAT_COMPILED_ARGUMENT_WARNING = +@<< +warn_from_format() +. +my $warning = ''; +{ + local $SIG{__WARN__} = sub { $warning .= join '', @_ }; + $~ = 'FORMAT_COMPILED_ARGUMENT_WARNING'; + write; +} +like($warning, qr/format expression warning/, 'format expression propagates warnings'); + +sub die_from_format { die "format expression failure\n" } +format FORMAT_COMPILED_ARGUMENT_ERROR = +@<< +die_from_format() +. +open my $error_fh, '>', $path or die "open $path for error case: $!"; +$old_fh = select((select($error_fh), $~ = 'FORMAT_COMPILED_ARGUMENT_ERROR')[0]); +my $write_ok = eval { write $error_fh; 1 }; +my $format_error = $@; +select $old_fh; +close $error_fh or die "close $path after error case: $!"; +unlink $path or die "unlink $path after error case: $!"; +ok(!$write_ok, 'format expression error aborts write'); +like($format_error, qr/format expression failure/, + 'format expression propagates Perl errors'); + +done_testing; diff --git a/src/test/resources/unit/format_compiled_argument_expression.t b/src/test/resources/unit/format_compiled_argument_expression.t new file mode 100644 index 0000000000..eccccfa69e --- /dev/null +++ b/src/test/resources/unit/format_compiled_argument_expression.t @@ -0,0 +1,50 @@ +use strict; +use warnings; +use Test::More; + +my $format_value = 10; +my $format_calls = 0; +sub next_format_value { return ++$format_calls; } + +format FORMAT_COMPILED_ARGUMENT_EXPRESSION = +@### @### +($format_value + 0), next_format_value() +. +$format_value = 42; + +my $path = 'format_compiled_argument_expression.tmp'; +open my $fh, '>', $path or die "open $path: $!"; +my $old_fh = select((select($fh), $~ = 'FORMAT_COMPILED_ARGUMENT_EXPRESSION')[0]); +write $fh; +select $old_fh; +close $fh or die "close $path: $!"; + +open my $read_fh, '<', $path or die "read $path: $!"; +my $rendered = do { local $/; <$read_fh> }; +close $read_fh or die "close $path after read: $!"; +unlink $path or die "unlink $path: $!"; + +is($rendered, " 42 1\n", 'compiled format expressions see current values and render in order'); +is($format_calls, 1, 'each format argument expression runs exactly once'); + +{ + package FormatExpressionPackage; + our $package_value = 40; + format FORMAT_COMPILED_PACKAGE_EXPRESSION = +@### +$package_value + 2 +. +} +my $package_path = 'format_compiled_package_expression.tmp'; +open my $package_fh, '>', $package_path or die "open $package_path: $!"; +$old_fh = select((select($package_fh), $~ = 'FormatExpressionPackage::FORMAT_COMPILED_PACKAGE_EXPRESSION')[0]); +write $package_fh; +select $old_fh; +close $package_fh or die "close $package_path: $!"; +open my $package_read_fh, '<', $package_path or die "read $package_path: $!"; +my $package_rendered = do { local $/; <$package_read_fh> }; +close $package_read_fh or die "close $package_path after read: $!"; +unlink $package_path or die "unlink $package_path: $!"; +is($package_rendered, " 42\n", 'format expressions resolve globals in the declaration package'); + +done_testing; diff --git a/src/test/resources/unit/format_executable_hash_argument.t b/src/test/resources/unit/format_executable_hash_argument.t new file mode 100644 index 0000000000..f0bcf49d5c --- /dev/null +++ b/src/test/resources/unit/format_executable_hash_argument.t @@ -0,0 +1,25 @@ +use strict; +use warnings; +use Test::More; + +format FORMAT_EXECUTABLE_HASH_ARGUMENT = +@### +${{qw[ Sun 0 Mon 1 Tue 2 Wed 3 Thu 4 Fri 5 Sat 6 ]}}{"Wed"} +. + +my $path = 'format_executable_hash_argument.tmp'; +open my $fh, '>', $path or die "open $path: $!"; +my $old_fh = select((select($fh), $~ = 'FORMAT_EXECUTABLE_HASH_ARGUMENT')[0]); +write $fh; +select $old_fh; +close $fh or die "close $path: $!"; + +open my $read_fh, '<', $path or die "read $path: $!"; +my $rendered = do { local $/; <$read_fh> }; +close $read_fh or die "close $path after read: $!"; +unlink $path or die "unlink $path: $!"; + +is($rendered, " 3\n", + 'format argument evaluates a nested hash expression at write time'); + +done_testing; diff --git a/src/test/resources/unit/format_lexical_assignment.t b/src/test/resources/unit/format_lexical_assignment.t new file mode 100644 index 0000000000..ba0ce7047c --- /dev/null +++ b/src/test/resources/unit/format_lexical_assignment.t @@ -0,0 +1,27 @@ +use strict; +use warnings; +use Test::More; + +my $format_value = 10; +format FORMAT_LEXICAL_ASSIGNMENT = +@### +$format_value +. +$format_value = 42; + +my $path = 'format_lexical_assignment.tmp'; +open my $fh, '>', $path or die "open $path: $!"; +my $old_fh = select((select($fh), $~ = 'FORMAT_LEXICAL_ASSIGNMENT')[0]); +write $fh; +select $old_fh; +close $fh or die "close $path: $!"; + +open my $read_fh, '<', $path or die "read $path: $!"; +my $rendered = do { local $/; <$read_fh> }; +close $read_fh or die "close $path after read: $!"; +unlink $path or die "unlink $path: $!"; + +is($rendered, " 42\n", + 'interpreter format reads the current lexical cell after assignment'); + +done_testing; diff --git a/src/test/resources/unit/format_literal_only_pictures.t b/src/test/resources/unit/format_literal_only_pictures.t new file mode 100644 index 0000000000..e97ce8484f --- /dev/null +++ b/src/test/resources/unit/format_literal_only_pictures.t @@ -0,0 +1,31 @@ +use strict; +use warnings; +use Test::More; + +format FORMAT_LITERAL_ONLY_FIRST = +body +. + +format FORMAT_LITERAL_ONLY_MULTIPLE = +first line +second line +. + +sub render_format { + my ($format_name) = @_; + my $rendered = ''; + open my $fh, '>', \$rendered or die "open scalar handle: $!"; + my $previous = select $fh; + local $~ = $format_name; + write; + select $previous; + close $fh or die "close scalar handle: $!"; + return $rendered; +} + +is(render_format('FORMAT_LITERAL_ONLY_FIRST'), "body\n", + 'a plain text first line is a literal picture'); +is(render_format('FORMAT_LITERAL_ONLY_MULTIPLE'), "first line\nsecond line\n", + 'consecutive plain text lines remain literal pictures'); + +done_testing; diff --git a/src/test/resources/unit/function_parameters_configured_keywords.t b/src/test/resources/unit/function_parameters_configured_keywords.t new file mode 100644 index 0000000000..89108ba453 --- /dev/null +++ b/src/test/resources/unit/function_parameters_configured_keywords.t @@ -0,0 +1,96 @@ +use strict; +use warnings; +use Test::More; + +my $loaded = eval { require Function::Parameters; 1 }; +if (!$loaded && $@ =~ /Can't locate Function\/Parameters\.pm/) { + plan skip_all => 'Function::Parameters is not installed in system Perl'; +} +die $@ unless $loaded; + +my $configured = eval q{ + use Function::Parameters { + configured_function => 'function', + configured_classmethod => 'classmethod', + configured_method => { + defaults => 'method', + shift => ['$first_invocant', '$second_invocant'], + }, + }; + configured_function custom_alias($value) { $value + 6 } + configured_classmethod class_name() { $class } + configured_method two_invocants($value) { + join ':', $first_invocant, $second_invocant, $value, scalar @_; + } + configured_method explicit_two_invocants($first, $second: $value) { + join ':', $first, $second, $value, scalar @_; + } + 1; +}; +die $@ unless $configured; + +is(custom_alias(4), 10, 'hash configuration registers a function alias'); +is(class_name('Example::Class'), 'Example::Class', + 'a configured classmethod binds its implicit $class invocant'); +is(two_invocants('one', 'two', 'three'), 'one:two:three:1', + 'a configured method binds multiple invocants and removes them from @_'); +is(explicit_two_invocants('left', 'right', 'value'), 'left:right:value:1', + 'a multi-invocant method accepts explicit invocant names'); +my @multi_invocants = Function::Parameters::info(\&two_invocants)->invocants; +is_deeply([map { "$_" } @multi_invocants], ['$first_invocant', '$second_invocant'], + 'signature metadata reports all configured invocants'); +is(Function::Parameters::info(\&two_invocants)->keyword, 'configured_method', + 'signature metadata preserves a configured keyword alias'); + +my $optional_invocant = eval q{ + use Function::Parameters { + configured_optional => { + invocant => 1, + strict => 0, + }, + }; + configured_optional optional_one($x) { join ' ', $x, @_ } + configured_optional optional_explicit($x: $y) { join ' ', $x, $y, @_ } + configured_optional optional_two($x, $y) { join ' ', $x, $y, @_ } + 1; +}; +die $@ unless $optional_invocant; +is(optional_one('a'), 'a a', 'optional invocant config leaves an unmarked signature as a function'); +is(optional_one('a', 'b'), 'a a b', 'lax configured functions preserve extra arguments in @_'); +is(optional_explicit('a', 'b'), 'a b b', 'an explicit colon activates a configured invocant'); +is(optional_explicit('a', 'b', 'c'), 'a b b c', + 'lax configured invocant functions accept additional arguments'); +is(optional_two('a', 'b', 'c'), 'a b a b c', + 'unmarked leading parameters are not consumed as optional invocants'); +my @optional_invocants = Function::Parameters::info(\&optional_explicit)->invocants; +is_deeply([map { "$_" } @optional_invocants], ['$x'], + 'metadata records only an explicitly selected optional invocant'); + +{ + package ConfiguredKeywordNoTypeChecks; + sub check { die 'check_argument_types => 0 must skip type checks' } + sub get_message { die 'check_argument_types => 0 must skip error messages' } +} +my $unchecked_constraint = bless {}, 'ConfiguredKeywordNoTypeChecks'; +my $unchecked_alias = eval q{ + use Function::Parameters { + unchecked => { + defaults => 'function', + check_argument_types => 0, + }, + }; + unchecked no_type_check(($unchecked_constraint) $value) { $value } + 1; +}; +die $@ unless $unchecked_alias; +is(no_type_check('not checked'), 'not checked', + 'check_argument_types false skips custom parameter checks'); + +my $configured_no = eval q{ + no Function::Parameters qw(configured_function); + configured_function unavailable_alias() { 1 } + 1; +}; +ok(!$configured_no, 'no removes a selected configured keyword alias'); + +done_testing; diff --git a/src/test/resources/unit/function_parameters_fun.t b/src/test/resources/unit/function_parameters_fun.t new file mode 100644 index 0000000000..ba7e355407 --- /dev/null +++ b/src/test/resources/unit/function_parameters_fun.t @@ -0,0 +1,198 @@ +use strict; +use warnings; +use Test::More; + +our $anonymous_add; +our $anonymous_method; +our $positive_constraint; +my $loaded = eval q{ + use Function::Parameters; + + package PositiveConstraint; + sub check { $_[1] > 0 } + sub get_message { 'value must be positive' } + package main; + sub InstanceOf { $positive_constraint } + sub PositiveType { $positive_constraint } + sub Maybe { $positive_constraint } + our $positive_constraint = bless {}, 'PositiveConstraint'; + + fun add($left, $right = 1) { $left + $right } + fun constrained(($positive_constraint) $value) { $value } + fun constrained_bare(PositiveType $value) { $value } + fun constrained_composite(Maybe[PositiveType] $value) { $value } + fun constrained_instance((InstanceOf['GraphQL::Type']) $value) { $value } + fun item_count(@items) { scalar @items } +fun option_count(%options) { scalar keys %options } +fun ignore_first($, $value) { $value } +fun default_operators($undefined //= 7, $false ||= 8) { "$undefined:$false" } +fun factorial($value) { $value < 2 ? 1 : $value * factorial($value - 1) } +fun named_options(:$required, :$optional //= 9, %extra) { + "$required:$optional:" . ($extra{other} // '') +} + our $anonymous_add = fun ($left, $right = 3) { $left + $right }; + our $anonymous_method = method (@) { $self->{base} }; + package AnonymousMethodOverload; + use overload '""' => method (@) { $self->{text} }, fallback => 1; + package FunctionParametersMethod; + method sum($self: $value = 2) { $self->{base} + $value } + method default_self($value = 1) { $self->{base} + $value } + method no_arguments() { $self->{base} } + method user_argument_count($first, $second) { scalar @_ } + method forward_value($value) { + unshift @_, $self; + goto &FunctionParametersMethod::accept_value; + } + method accept_value($value) { $value } + package main; + 1; +}; +if (!$loaded && $@ =~ /Can't locate Function\/Parameters\.pm/) { + plan skip_all => 'Function::Parameters is not installed in system Perl'; +} +die $@ unless $loaded; + +is(add(4), 5, 'fun declaration uses an optional signature default'); +is(add(4, 3), 7, 'fun declaration accepts all positional parameters'); +is(constrained(3), 3, 'typed parameters accept values that satisfy their constraint'); +is(constrained_bare(4), 4, 'typed parameters accept Type::Tiny bare-name syntax'); +my $bare_typed_result = eval { constrained_bare(0); 1 }; +ok(!$bare_typed_result, 'bare-name typed parameters reject invalid values'); +like($@, qr/value must be positive/, 'bare-name type failures use the constraint message'); +is(constrained_composite(5), 5, 'typed parameters parse bracketed Type::Tiny expressions'); +is(constrained_instance(3), 3, 'typed parameters accept bracket-form type expressions'); +my $typed_result = eval { constrained(0); 1 }; +ok(!$typed_result, 'typed parameters reject values that fail their constraint'); +like($@, qr/value must be positive/, 'typed parameter failure uses the constraint message'); +my $add_info = Function::Parameters::info(\&add); +is_deeply([map { "$_" } $add_info->positional_required], ['$left'], + 'signature info reports required positional parameter names'); +is_deeply([map { "$_" } $add_info->positional_optional], ['$right'], + 'signature info reports optional positional parameter names'); +is($add_info->args_min, 1, 'signature info reports the minimum argument count'); +is($add_info->args_max, 2, 'signature info reports the maximum argument count'); +is($add_info->keyword, 'fun', 'signature info reports the fun keyword'); +is(item_count(qw(one two three)), 3, 'fun declaration supports a slurpy array'); +is(option_count(one => 1, two => 2), 2, 'fun declaration supports a slurpy hash'); +my $items_info = Function::Parameters::info(\&item_count); +is("" . $items_info->slurpy, '@items', 'signature info reports a slurpy array'); +is($items_info->args_max, 'Inf', 'signature info marks slurpy signatures unbounded'); +is(ignore_first('unused', 'kept'), 'kept', 'fun declarations can ignore a positional parameter'); +is(default_operators(undef, 0), '7:8', 'fun declarations support defined-or and or defaults'); +is(factorial(5), 120, 'named fun declarations can recurse'); +is(named_options(required => 'r', other => 'x'), 'r:9:x', + 'named parameters support defaults and a trailing slurpy hash'); +my $named_info = Function::Parameters::info(\&named_options); +is_deeply([map { "$_" } $named_info->named_required], ['$required'], + 'signature info reports required named parameters'); +is_deeply([map { "$_" } $named_info->named_optional], ['$optional'], + 'signature info reports optional named parameters'); +is("" . $named_info->slurpy, '%extra', 'signature info reports a slurpy hash'); +is($named_info->args_min, 2, 'required named parameters count as key/value arguments'); +is($anonymous_add->(4), 7, 'fun supports anonymous declarations in expression position'); +my $object = bless { base => 40 }, 'FunctionParametersMethod'; +is($anonymous_method->($object), 40, 'anonymous method expressions bind an implicit invocant'); +my $overloaded = bless { text => 'anonymous method stringification' }, 'AnonymousMethodOverload'; +is("$overloaded", 'anonymous method stringification', + 'anonymous method expressions work as overload implementations'); +is($object->sum, 42, 'method declaration binds its explicit invocant and default'); +is($object->sum(3), 43, 'method declaration accepts an explicit positional value'); +is($object->default_self, 41, 'method declaration supplies the default $self invocant'); +is($object->default_self(5), 45, 'method declaration keeps arguments after its implicit invocant'); +is($object->no_arguments, 40, 'method declaration supports an empty user parameter list'); +is($object->user_argument_count(1, 2), 2, + 'method bodies see only user arguments in @_ after binding the invocant'); +is($object->forward_value('forwarded'), 'forwarded', + 'method forwarding preserves one invocant in @_ without duplication'); +my $method_info = Function::Parameters::info(\&FunctionParametersMethod::default_self); +is("" . $method_info->invocant, '$self', 'signature info reports the method invocant'); +is($method_info->args_min, 1, 'method signature info includes its invocant'); +is($method_info->keyword, 'method', 'method signature info reports its keyword'); + +my $block_scope_result = eval q{ + { + use Function::Parameters; + fun block_scoped($value) { $value + 1 } + } + block_scoped(4); +}; +is($block_scope_result, 5, 'declaration parsed while provider is active in a block'); + +my $eval_result = eval q{ + use Function::Parameters; + eval q{ + fun eval_scoped($value) { $value + 2 } + eval_scoped(4); + }; +}; +is($eval_result, 6, 'nested eval inherits the lexical keyword provider'); + +my $fun_only_result = eval q{ + use Function::Parameters qw(fun); + fun only_fun($value) { $value + 3 } + 1; +}; +is($fun_only_result, 1, 'importing fun alone registers only the requested keyword'); +is(only_fun(4), 7, 'selectively imported fun declarations execute'); +my $method_not_imported = eval q{ + use Function::Parameters qw(fun); + method not_imported() { 1 } + 1; +}; +ok(!$method_not_imported, 'unrequested method keyword remains unavailable'); + +my $selective_no_result = eval q{ + use Function::Parameters qw(fun method); + fun before_no($value) { $value + 1 } + no Function::Parameters qw(fun); + package SelectiveMethod; + method value() { $self->{value} } + package main; + 1; +}; +is($selective_no_result, 1, 'no with an import list removes only selected keywords'); +my $selective_object = bless { value => 9 }, 'SelectiveMethod'; +is($selective_object->value, 9, 'unremoved method keyword remains active after selective no'); + +my $delegated_import_result = eval q{ + use lib 'src/test/resources/unit/lib'; + use GraphQL::MaybeTypeCheck; + fun delegated_import($value) { $value + 4 } + fun delegated_return_type($value) :ReturnType(Bool) { $value } + package DelegatedMethod; + use GraphQL::MaybeTypeCheck; + method value() { $self->{value} } + package main; + 1; +}; +is($delegated_import_result, 1, + 'GraphQL MaybeTypeCheck import activates delegated Function::Parameters keywords'); +is(delegated_import(5), 9, 'delegated import enables fun declarations'); +is(delegated_return_type(1), 1, + 'Function::Parameters accepts attributes after a signature'); +my $delegated_object = bless { value => 12 }, 'DelegatedMethod'; +is($delegated_object->value, 12, 'delegated import enables method declarations'); + +my $arity_result = eval { add(); 1 }; +ok(!$arity_result, 'fun declaration rejects too few arguments'); +like($@, qr/Too few arguments/, 'fun declaration checks required argument count'); + +my $disabled = eval q{ + use Function::Parameters; + no Function::Parameters; + fun rejected($value) { $value } + 1; +}; +ok(!$disabled, 'no Function::Parameters removes its lexical keyword'); + +my $block_disabled = eval q{ + { + use Function::Parameters; + fun scoped_only($value) { $value } + } + fun rejected_after_scope($value) { $value } + 1; +}; +ok(!$block_disabled, 'keyword registration ends with its lexical block'); + +done_testing; diff --git a/src/test/resources/unit/function_parameters_info_api.t b/src/test/resources/unit/function_parameters_info_api.t new file mode 100644 index 0000000000..f849d8e76f --- /dev/null +++ b/src/test/resources/unit/function_parameters_info_api.t @@ -0,0 +1,49 @@ +use strict; +use warnings; +use Test::More; + +BEGIN { unshift @INC, 'src/main/perl/lib' } +use Function::Parameters::Info; + +my $self = bless { name => '$self', type => undef }, 'Function::Parameters::Param'; +my $required = bless { name => '$x', type => undef }, 'Function::Parameters::Param'; +my $optional = bless { name => '$y', type => undef }, 'Function::Parameters::Param'; +my $info = bless { + keyword => 'method', + nshift => 1, + _positional_required => [$self, $required], + _positional_optional => [$optional], + _named_required => [], + _named_optional => [], + slurpy => undef, + args_min => 2, + args_max => 3, +}, 'Function::Parameters::Info'; + +is("$required", '$x', 'parameter objects stringify to their names'); +is($required->name, '$x', 'parameter objects expose their names'); +ok(!defined $required->type, 'parameter objects expose an absent type as undef'); +is($info->keyword, 'method', 'info reports the declaration keyword'); +is($info->nshift, 1, 'info reports the invocant count'); +is_deeply([$info->invocants], [$self], 'info returns invocant objects in list context'); +is(scalar $info->invocants, 1, 'info returns an invocant count in scalar context'); +is("" . $info->invocant, '$self', 'info returns its single invocant'); +is_deeply([$info->positional_required], [$required], 'info returns required positional objects'); +is_deeply([$info->positional_optional], [$optional], 'info returns optional positional objects'); +is(scalar $info->named_required, 0, 'info returns a named parameter count in scalar context'); +ok(!defined $info->slurpy, 'info returns undef when the signature is not slurpy'); +is($info->args_min, 2, 'info exposes the minimum argument count'); +is($info->args_max, 3, 'info exposes the maximum argument count including the invocant'); + +my $two_invocants = bless { + nshift => 2, + _positional_required => [$self, $required], + _positional_optional => [], + _named_required => [], + _named_optional => [], +}, 'Function::Parameters::Info'; +my $single_invocant_error = eval { $two_invocants->invocant; 1 }; +ok(!$single_invocant_error, 'invocant rejects a multi-invocant signature'); +like($@, qr/Can't return a single invocant/, 'multi-invocant diagnostic is clear'); + +done_testing; diff --git a/src/test/resources/unit/function_parameters_metadata_and_strict.t b/src/test/resources/unit/function_parameters_metadata_and_strict.t new file mode 100644 index 0000000000..d0c5be303b --- /dev/null +++ b/src/test/resources/unit/function_parameters_metadata_and_strict.t @@ -0,0 +1,452 @@ +use strict; +use warnings; +use Test::More; + +use Function::Parameters { + fun => {}, + lax_count => { check_argument_count => 0 }, + lvalue_function => { attributes => ':lvalue' }, + runtime_fun => { defaults => 'function_strict', runtime => 1 }, + runtime_method => { defaults => 'method_strict', runtime => 1 }, + runtime_installed => { + defaults => 'function_strict', runtime => 1, install_sub => 'capture_installer', + }, + required_modifier => { defaults => 'before', runtime => 1 }, + multi_method => { defaults => 'method', shift => ['$self1', '$self2'] }, + single_method => { defaults => 'method' }, + required_name => { defaults => 'function', name => 'required' }, + prohibited_name => { defaults => 'function', name => 'prohibited' }, + no_defaults => { defaults => 'function', default_arguments => 0 }, + method => { name => 'required', shift => '$configured_self' }, +}; + +our ($installed_name, $installed_code); +sub capture_installer { ($installed_name, $installed_code) = @_ } + +runtime_installed installed_by_callback($value) { $value } +is($installed_name, 'installed_by_callback', + 'runtime install_sub callback receives the declared name'); +is($installed_code->('callback-ready'), 'callback-ready', + 'runtime install_sub callback receives the function body'); +multi_method checked_multi_method($value) { $value } +my $multi_method_error = eval { checked_multi_method('only-one-argument'); 1 }; +ok(!$multi_method_error, 'configured multi-invocant method checks argument count'); +like($@, qr/^Too few arguments for multi_method checked_multi_method \(expected 3, got 1\)/, + 'multi-invocant argument error names the keyword and expected count'); +eval q{ multi_method ($self1) {} }; +like($@, qr/\$self1 can't appear twice in the same parameter list/, + 'configured invocant cannot be repeated as an ordinary parameter'); +eval q{ multi_method wrong_invocants($self:) {} }; +like($@, qr/^In multi_method wrong_invocants: number of invocants in parameter list \(1\) differs from number of invocants in keyword definition \(2\)/, + 'configured method reports an invocant count mismatch'); +eval q{ fun ($self:) {} }; +like($@, qr/invocant \$self not allowed here/, + 'function keyword rejects an explicit invocant with a focused diagnostic'); +eval q{ single_method (@items:) {} }; +like($@, qr/invocant \@items can't be an array/, + 'method keyword rejects an array invocant with a focused diagnostic'); +my $unnamed_modifier_result = eval q{ required_modifier () {} }; +ok(!$unnamed_modifier_result, 'modifier keyword requires a named declaration'); +like($@, qr/expecting a function name/, + 'unnamed modifier diagnostic identifies the missing function name'); +ok(!defined(&runtime_named), 'runtime keyword has no CV before its declaration executes'); +runtime_fun runtime_named($value) { $value } +ok(defined(&runtime_named), 'runtime keyword installs its CV when executed'); +is(runtime_named('ready'), 'ready', 'runtime-installed function is callable'); +required_name named_required() { 1 } +my $required_name_error = eval q{ required_name () {} }; +ok(!$required_name_error, 'required-name keyword rejects anonymous declarations'); +like($@, qr/expecting a function name/, 'required-name diagnostic identifies missing name'); +my $prohibited_name_error = eval q{ prohibited_name named_forbidden() {} }; +ok(!$prohibited_name_error, 'prohibited-name keyword rejects named declarations'); +like($@, qr/expecting a parameter list/, 'prohibited-name diagnostic identifies expected parameter list'); +my $default_argument_error = eval q{ no_defaults no_default($value = 42) { $value } }; +ok(!$default_argument_error, 'default_arguments false rejects signature defaults'); +like($@, qr/no_defaults.*default argument/i, + 'default argument diagnostic names its configured keyword'); +method configured_default_method($key) { $configured_self->{$key} } +is(configured_default_method({ key => 'method-default' }, 'key'), 'method-default', + 'method keyword defaults to method semantics without an explicit defaults option'); +my @anonymous_default_calls; +my $anonymous_defined_or = fun ($ //= push(@anonymous_default_calls, 'defined-or')) { + scalar @anonymous_default_calls; +}; +is($anonymous_defined_or->('present'), 0, + 'anonymous defined-or default does not run for a defined argument'); +is($anonymous_defined_or->(undef), 1, + 'anonymous defined-or default runs for an undef argument'); +my @named_default_calls; +my $named_assignment_default = fun (:$value = (push(@named_default_calls, 'assignment'), 'fallback')) { + $value; +}; +is($named_assignment_default->(value => undef), undef, + 'named assignment default preserves an explicitly supplied undef'); +is_deeply(\@named_default_calls, [], 'named assignment default stays unevaluated for a present key'); +is($named_assignment_default->(), 'fallback', 'named assignment default fills an absent key'); +is_deeply(\@named_default_calls, ['assignment'], 'named assignment default runs once for an absent key'); +my $positional_and_named = fun ($head, :$tail) { [$head, $tail] }; +is_deeply($positional_and_named->('head-value', tail => 'tail-value'), + ['head-value', 'tail-value'], + 'named parameter extraction skips leading positional arguments'); +my $optional_positional_and_named = fun ($head = 'head-default', :$tail = 'tail-default') { + [$head, $tail]; +}; +is_deeply($optional_positional_and_named->(), ['head-default', 'tail-default'], + 'optional positional defaults leave an empty named-argument tail'); +my $required_named = fun (:$required) { $required }; +my $required_named_error = eval { $required_named->(); 1 }; +ok(!$required_named_error, 'required named parameter rejects an omitted argument'); +like($@, qr/Too few arguments/, 'missing required named parameter reports too few arguments'); +my $required_named_slurp = fun (:$key, @rest) { [$key, \@rest] }; +is_deeply($required_named_slurp->(key => 'value'), ['value', []], + 'named slurpy parameter accepts the required key and an empty remainder'); +my $required_named_slurp_missing = eval { $required_named_slurp->(other => 'value'); 1 }; +ok(!$required_named_slurp_missing, 'named slurpy parameter requires its named key'); +like($@, qr/missing named.*key/i, 'named slurpy omission identifies the missing key'); +my $optional_named_only = fun (:$tail = 'tail-default') { $tail }; +my $odd_named_error = eval { $optional_named_only->('orphan'); 1 }; +ok(!$odd_named_error, 'named parameters reject an odd key/value tail'); +like($@, qr/Odd number/, 'odd named argument diagnostic follows Perl wording'); +eval q{ fun (:$duplicate, :$duplicate) {} }; +like($@, qr/\$duplicate.*twice/, 'duplicate named parameter names identify the repeated parameter'); +eval q{ fun (:$named_first, $positional_after) {} }; +like($@, qr/positional.*named/i, 'positional-after-named diagnostic identifies both parameter kinds'); +eval q{ method named_invocant(:$named_invocant:) {} }; +like($@, qr/invocant.*\$named_invocant.*named/i, + 'named method invocant receives a focused diagnostic'); +runtime_fun runtime_caller_name() { (caller 0)[3] } +is(runtime_caller_name(), 'main::runtime_caller_name', + 'runtime-installed function retains its declared caller name'); + +{ + package FunctionParametersMultilineInvocant; + use Function::Parameters; + method echo( + $class: + $value + ) { $value } +} +my $multiline_invocant_object = bless {}, 'FunctionParametersMultilineInvocant'; +is($multiline_invocant_object->echo('multiline-ready'), 'multiline-ready', + 'method preserves an explicit invocant after multiline signature whitespace'); + +{ + package FunctionParametersRuntimeMethod; + runtime_method echo($value) { $value } + runtime_method caller_name() { (caller 0)[3] } +} +my $runtime_method_object = bless {}, 'FunctionParametersRuntimeMethod'; +is($runtime_method_object->echo('method-ready'), 'method-ready', + 'runtime-installed method binds its invocant'); +is($runtime_method_object->caller_name(), 'FunctionParametersRuntimeMethod::caller_name', + 'runtime-installed method retains its declared caller name'); + +my $moose_reifier_keyword = eval q{ + use Function::Parameters { + moose_style => { defaults => 'function', reify_type => 'moose', check_argument_types => 0 }, + }; + moose_style identity($value) { $value } + identity('reifier-ready'); +}; +is($moose_reifier_keyword, 'reifier-ready', + 'configured Moose type reifier can register a Function::Parameters keyword'); + +{ + package FunctionParametersCallbackConstraint; + sub check { $_[1] eq 'accepted' } + sub get_message { "rejected value: $_[1]" } +} +our $custom_reified_type_name; +my $custom_reifier_result = eval q{ + package FunctionParametersCustomReifier; + use Function::Parameters { + custom_fun => { + defaults => 'function', + reify_type => sub { + $main::custom_reified_type_name = $_[0]; + bless {}, 'FunctionParametersCallbackConstraint'; + }, + }, + }; + custom_fun callback_typed(TCustom $value) { $value } + callback_typed('accepted'); +}; +is($custom_reifier_result, 'accepted', + 'callback-based type reifier validates a custom constraint'); +is($custom_reified_type_name, 'TCustom', + 'callback-based type reifier receives normalized type source'); +my @custom_reifier_parameters = Function::Parameters::info( + \&FunctionParametersCustomReifier::callback_typed)->positional_required; +is($custom_reifier_parameters[0]->type->check('accepted'), 1, + 'callback-reified type object is available through Function::Parameters::info'); +my $custom_reifier_error = eval { + FunctionParametersCustomReifier::callback_typed('rejected'); + 1; +}; +ok(!$custom_reifier_error, 'callback-reified type rejects invalid values'); +like($@, qr/parameter 1 \(\$value\): rejected value: rejected/, + 'callback-reified type supplies its custom validation message'); + +SKIP: { + my $moose_available = eval { require Moose; 1 }; + skip('Moose is not installed', 6) unless $moose_available; + my $moose_slurpy_type = eval q{ + package FunctionParametersMooseTypes; + use Moose; + use Function::Parameters { + fun => { defaults => 'function', reify_type => 'moose' }, + }; + fun typed_slurpy(ArrayRef [ Int | CodeRef ] @items) { \@items } + Function::Parameters::info(\&typed_slurpy)->slurpy->type; + }; + ok($moose_slurpy_type, 'Moose reifier returns a type object for a typed slurpy parameter'); + ok($moose_slurpy_type->equals( + Moose::Util::TypeConstraints::find_or_parse_type_constraint('ArrayRef[Int|CodeRef]')), + 'typed slurpy metadata preserves its parameterized Moose type'); + my $moose_anonymous_slurpy_type = eval q{ + package FunctionParametersMooseTypes; + use Moose; + use Function::Parameters { + fun => { defaults => 'function', reify_type => 'moose' }, + }; + Function::Parameters::info(fun (ArrayRef [ Int | CodeRef ] @items) {})->slurpy->type; + }; + ok($moose_anonymous_slurpy_type->equals( + Moose::Util::TypeConstraints::find_or_parse_type_constraint('ArrayRef[Int|CodeRef]')), + 'anonymous typed slurpy metadata preserves its parameterized Moose type'); + my $moose_typed_named = eval q{ + package FunctionParametersMooseTypes; + use Moose; + use Function::Parameters { + fun => { defaults => 'function', reify_type => 'moose' }, + }; + fun typed_named(('Int'):$value) { $value } + typed_named(value => 42); + }; + is($moose_typed_named, 42, + 'parenthesized Moose type expressions can precede named parameters'); + my ($moose_invalid_value, $moose_validation_error) = do { + my $value = eval q{ + package FunctionParametersMooseTypes; + use Function::Parameters { + fun => { defaults => 'function', reify_type => 'moose' }, + }; + fun typed_required(('Int') $value) { $value } + typed_required('not an integer'); + }; + ($value, $@); + }; + ok(!defined($moose_invalid_value), 'Moose type violations reject invalid arguments'); + like($moose_validation_error, qr/parameter 1 \(?\$value\)?.*Validation failed.*Int/s, + 'Moose type errors name the parameter and constraint'); +} + +{ + package FunctionParametersRuntimeConditional; + if (0) { runtime_fun skipped() { 1 } } +} +ok(!defined(&FunctionParametersRuntimeConditional::skipped), + 'runtime declaration in an unexecuted branch remains undefined'); + +{ + package FunctionParametersRuntimeAlias::Base; + runtime_method value() { 'before' } + package FunctionParametersRuntimeAlias::Alias; + package FunctionParametersRuntimeAlias::Derived; + our @ISA = ('FunctionParametersRuntimeAlias::Alias'); +} +*FunctionParametersRuntimeAlias::Alias::value = + *FunctionParametersRuntimeAlias::Base::value; +my $runtime_aliased_method = bless {}, 'FunctionParametersRuntimeAlias::Derived'; +is($runtime_aliased_method->value(), 'before', + 'runtime method resolves through a whole-glob alias'); +{ + package FunctionParametersRuntimeAlias::Base; + no warnings 'redefine'; + runtime_method value() { 'after' } +} +is($runtime_aliased_method->value(), 'after', + 'runtime redefinition updates method dispatch through whole-glob aliases'); + +sub ordinary_sub { return } +ok(!defined(Function::Parameters::info(\&ordinary_sub)), + 'introspection returns undef for a non-Function::Parameters sub'); + +fun inspected_before_call($required, $optional = 1) { $required } +my $info = Function::Parameters::info(\&inspected_before_call); +is($info->keyword, 'fun', 'introspection works before the function is called'); +is_deeply([map { "$_" } $info->positional_required], ['$required'], + 'pre-call metadata includes required parameters'); +is_deeply([map { "$_" } $info->positional_optional], ['$optional'], + 'pre-call metadata includes optional parameters'); +ok(!defined(($info->positional_required)[0]->type), + 'untyped parameters report no type constraint'); +my $anonymous_info = Function::Parameters::info( + fun ($anonymous_required, $anonymous_optional = 1) { $anonymous_required }); +is($anonymous_info->keyword, 'fun', 'anonymous declarations retain signature metadata'); +is_deeply([map { "$_" } $anonymous_info->positional_optional], ['$anonymous_optional'], + 'anonymous metadata includes optional parameters'); + +lax_count permissive_count($value) { $value } +is(permissive_count(1, 2), 1, + 'check_argument_count false allows extra arguments'); +lvalue_function lvalue_value($reference) { $$reference } +my $mutable_value = 'before'; +lvalue_value(\$mutable_value) = 'after'; +is($mutable_value, 'after', 'configured lvalue attributes reach the subroutine'); + +{ + package FunctionParametersAmbiguousCore; + fun join($left, $right) { "$left:$right" } +} +my ($ambiguous_core_call, $ambiguous_core_warning); +{ + local $SIG{__WARN__} = sub { $ambiguous_core_warning .= shift }; + $ambiguous_core_call = eval q{ + package FunctionParametersAmbiguousCore; + use warnings; + join('left', 'right'); + }; +} +is($ambiguous_core_call, 'right', 'the ambiguous call resolves to the core operator'); +like($ambiguous_core_warning, qr/Ambiguous call resolved as CORE::join/, + 'declared Function::Parameters sub triggers the core ambiguity warning'); +my $typed_declaration = eval q{ + use Types::Standard qw(Int); + use Function::Parameters { typed_for_info => { defaults => 'function' } }; + typed_for_info inspected_typed_parameter(Int $typed) { $typed } + 1; +}; +ok($typed_declaration, 'typed Function::Parameters declaration compiles'); +my $typed_info = Function::Parameters::info(\&inspected_typed_parameter); +is(ref(($typed_info->positional_required)[0]->type), 'Type::Tiny', + 'parameter metadata exposes its declared type object before the function is called'); +is(($typed_info->positional_required)[0]->type->name, 'Int', + 'parameter metadata preserves the declared type object'); + +{ + my $default = 'outer'; + fun same_named_default($default = $default) { $default } + is(same_named_default(), 'outer', + 'a default can read an outer lexical with the same name as its parameter'); + $default = 'changed'; + is(same_named_default(), 'changed', + 'same-named default closure observes later outer lexical values'); +} + +my $slurpy_scalar_error = eval q{ + use Function::Parameters; + fun invalid_after_array(@items, $later) { $later } + 1; +}; +ok(!$slurpy_scalar_error, 'strict mode rejects a scalar after an array slurpy'); +like($@, qr/"\$later" can't appear after slurpy parameter "\@items"/, + 'strict mode names the later and slurpy parameters'); + +my $slurpy_hash_error = eval q{ + use Function::Parameters; + fun invalid_after_hash(%options, $later) { $later } + 1; +}; +ok(!$slurpy_hash_error, 'strict mode rejects a scalar after a hash slurpy'); +like($@, qr/"\$later" can't appear after slurpy parameter "%options"/, + 'hash slurpy diagnostic names both parameters'); + +my $second_slurpy_error = eval q{ + use Function::Parameters; + fun invalid_second_slurpy(@items, @more) { @more } + 1; +}; +ok(!$second_slurpy_error, 'strict mode rejects a second slurpy parameter'); +like($@, qr/"\@more" can't appear after slurpy parameter "\@items"/, + 'second slurpy diagnostic names both parameters'); + +my $invalid_rarity = eval q{ + use Function::Parameters { + spike => { rarity => 'best' }, + }; + 1; +}; +ok(!$invalid_rarity, 'reject an unsupported rarity option'); +like($@, qr/Invalid.*rarity/i, 'unsupported rarity has a clear diagnostic'); + +my $invalid_attributes = eval q{ + use Function::Parameters { broken => { attributes => 'nope' } }; + 1; +}; +ok(!$invalid_attributes, 'reject a malformed configured attribute list'); +like($@, qr/nope.*attributes/i, 'malformed configured attributes have a clear diagnostic'); + +my $invalid_attribute_syntax = eval q~ + use Function::Parameters { broken => { attributes => ': in valid {' } }; + 1; +~; +ok(!$invalid_attribute_syntax, 'reject invalid configured attribute syntax'); +like($@, qr/in valid.*attributes/i, 'invalid configured attribute syntax has a clear diagnostic'); + +my $invalid_import_tag = eval { Function::Parameters->import(':not_exported'); 1 }; +ok(!$invalid_import_tag, 'reject an unknown Function::Parameters import tag'); +like($@, qr/not exported/, 'unknown import tag has a clear diagnostic'); +my $invalid_import_keyword = eval { Function::Parameters->import({ '42' => 'function' }); 1 }; +ok(!$invalid_import_keyword, 'reject an invalid configured keyword identifier'); +like($@, qr/valid identifier/, 'invalid keyword identifier has a clear diagnostic'); +my $invalid_import_type = eval { Function::Parameters->import({ bad_type => 'QQQQ' }); 1 }; +ok(!$invalid_import_type, 'reject an invalid configured keyword type'); +like($@, qr/valid type/, 'invalid keyword type has a clear diagnostic'); + +fun recursive_named_call(:$depth, :$value) { + return $value unless $depth; + recursive_named_call value => $value + 1, depth => $depth - 1; +} +is(recursive_named_call(value => 4, depth => 3), 7, + 'recursive named calls accept unparenthesized fat-comma arguments'); + +{ + use Function::Parameters qw(:lax); + fun lax_named_argument(:$value) { defined($value) ? $value : 'missing' } + is(lax_named_argument(), 'missing', + 'lax named parameters may be omitted without a default'); +} + +my $qualified_function_declaration = eval q{ + fun ::function_parameters_absolute_name() { 42 } + 1; +}; +ok($qualified_function_declaration, + 'expression-position declarations accept an absolute qualified name'); +is(function_parameters_absolute_name(), 42, + 'an absolute qualified declaration installs its function'); + +my $invalid_quoted_function_name = eval q{ fun 'hi(){} 1 }; +ok(!$invalid_quoted_function_name, + 'expression-position declarations reject a quoted function name'); +like($@, qr/parameter list/i, + 'a malformed expression-position declaration reports a parameter-list error'); + +my $missing_parameter_list = eval q{ fun {} }; +ok(!$missing_parameter_list, 'anonymous declaration requires a parameter list'); +like($@, qr/expecting a parameter list, not "\{"/, + 'missing parameter list reports the unexpected block'); +my $missing_function_body = eval q{ fun () :() {} }; +ok(!$missing_function_body, 'anonymous declaration rejects a malformed prototype attribute'); +like($@, qr/expecting a function body, not "\("/, + 'malformed prototype attribute reports the unexpected body token'); +my $named_declaration_in_expression = eval q{ my $value = fun forbidden() {} }; +ok(!$named_declaration_in_expression, 'named declarations are not expressions'); +like($@, qr/expect.*parameter list/i, + 'named declaration in an expression reports its syntax error'); +my $missing_type_name = eval q{ fun malformed_type(X[[ }; +ok(!$missing_type_name, 'malformed parameterized type is rejected'); +like($@, qr/missing type name/i, + 'malformed parameterized type reports the missing type name'); + +use Function::Parameters { + named_error_keyword => { defaults => 'function' }, +}; +named_error_keyword function_parameters_named_error($value) { $value } +my $named_error_call = eval { function_parameters_named_error(); 1 }; +ok(!$named_error_call, 'configured keyword checks required positional arguments'); +like($@, qr/named_error_keyword.*function_parameters_named_error/s, + 'argument-count errors retain the configured Function::Parameters keyword'); + +done_testing; diff --git a/src/test/resources/unit/function_parameters_prototype_attributes.t b/src/test/resources/unit/function_parameters_prototype_attributes.t new file mode 100644 index 0000000000..d63ad0a045 --- /dev/null +++ b/src/test/resources/unit/function_parameters_prototype_attributes.t @@ -0,0 +1,33 @@ +use strict; +use warnings FATAL => 'all'; +use Test::More; +use Function::Parameters; + +my @invalid_prototypes = ( + ['[', qr/Illegal character in prototype/], + ['\\;', qr/Illegal character after '\\' in prototype/], + ['\\[_;@]', qr/Illegal character after '\\' in prototype/], + ['\\+', qr/Illegal character after '\\' in prototype/], + ['\\\\', qr/Illegal character after '\\' in prototype/], + ['[$]', qr/Illegal character in prototype/], + ['\\[_$]', qr/Illegal character after '\\' in prototype/], + ['_$', qr/Illegal character after '_' in prototype/], +); + +for my $case (@invalid_prototypes) { + my ($prototype, $diagnostic) = @$case; + my $result = eval "fun () :prototype($prototype) {}"; + ok(!defined($result), "reject malformed prototype $prototype"); + like($@, $diagnostic, "report the malformed prototype $prototype"); +} + +my $duplicate_prototype = eval q{ + fun () :prototype($) prototype(@) {} +}; +ok(!defined($duplicate_prototype), 'reject two prototype attributes'); +like($@, qr/Can't redefine prototype/, 'duplicate prototype diagnostic is clear'); + +ok(eval 'fun () :prototype(_@) {}', 'accept underscore array prototype'); +ok(eval 'fun () :prototype(_%) {}', 'accept underscore hash prototype'); + +done_testing; diff --git a/src/test/resources/unit/function_parameters_type_coercion.t b/src/test/resources/unit/function_parameters_type_coercion.t new file mode 100644 index 0000000000..2fcd4a1969 --- /dev/null +++ b/src/test/resources/unit/function_parameters_type_coercion.t @@ -0,0 +1,35 @@ +use strict; +use warnings; +use Test::More; +use Function::Parameters; + +{ + package FunctionParametersCoercingType; + + sub check { $_[1] eq 'Type_C:good' } + sub get_message { 'value must be Type_C:good' } + sub has_coercion { 1 } + sub coerce { + my $value = $_[1]; + $value =~ s/\?+\z//; + return $value; + } +} + +use constant FunctionParametersCoercingTypeConstraint => + bless {}, 'FunctionParametersCoercingType'; + +fun constrained_value(FunctionParametersCoercingTypeConstraint $value) { $value } +fun constrained_value_quoted(('FunctionParametersCoercingTypeConstraint') $value) { $value } + +is(constrained_value('Type_C:good???'), 'Type_C:good', + 'coerce a value before validating the parameter'); +is(constrained_value_quoted('Type_C:good???'), 'Type_C:good', + 'reify a quoted type name before validating the parameter'); + +my $valid = eval { constrained_value('Type_C:bad'); 1 }; +ok(!$valid, 'reject a value after its coercion fails the type check'); +like($@, qr/value must be Type_C:good/, + 'report the constraint message for a failed coerced value'); + +done_testing; diff --git a/src/test/resources/unit/lib/GraphQL/MaybeTypeCheck.pm b/src/test/resources/unit/lib/GraphQL/MaybeTypeCheck.pm new file mode 100644 index 0000000000..ed626f8a2e --- /dev/null +++ b/src/test/resources/unit/lib/GraphQL/MaybeTypeCheck.pm @@ -0,0 +1,13 @@ +package GraphQL::MaybeTypeCheck; + +use Attribute::Handlers; + +sub import { + my $caller = caller; + no strict 'refs'; + push @{"${caller}::ISA"}, __PACKAGE__; +} + +sub ReturnType : ATTR(CODE) { return } + +1; diff --git a/src/test/resources/unit/lib/Types/Standard.pm b/src/test/resources/unit/lib/Types/Standard.pm new file mode 100644 index 0000000000..718b1404a0 --- /dev/null +++ b/src/test/resources/unit/lib/Types/Standard.pm @@ -0,0 +1,24 @@ +package Types::Standard; + +use strict; +use warnings; +use Exporter qw(import); + +our @EXPORT_OK = qw(Int); + +my $integer_type = bless { name => 'Int' }, 'Type::Tiny'; + +sub Int { + return $integer_type; +} + +package Type::Tiny; + +use strict; +use warnings; + +sub name { + return $_[0]->{name}; +} + +1; diff --git a/src/test/resources/unit/object_pad_build_diagnostics.t b/src/test/resources/unit/object_pad_build_diagnostics.t new file mode 100644 index 0000000000..c2dc78bae4 --- /dev/null +++ b/src/test/resources/unit/object_pad_build_diagnostics.t @@ -0,0 +1,33 @@ +use strict; +use warnings; +use Test::More; + +my $loaded = eval q{ + use Object::Pad 0.66; + 1; +}; +if (!$loaded && $@ =~ /Can't locate Object\/Pad\.pm/) { + plan skip_all => 'Object::Pad is not installed in system Perl'; +} +die $@ unless $loaded; + +my $malformed = eval q{ + use Object::Pad 0.66; + class MalformedBuildDeclaration { + BUILD method value {} + } + 1; +}; +ok(!$malformed, 'BUILD without a block body is rejected'); +like($@, qr/Malformed BUILD declaration: expected a block body/, + 'malformed declaration gives a targeted BUILD diagnostic'); + +my $outside_class = eval q{ + BUILD {} + 1; +}; +ok(!$outside_class, 'BUILD outside a class is rejected'); +like($@, qr/BUILD blocks are only allowed inside class blocks/, + 'out-of-class diagnostic explains where BUILD is allowed'); + +done_testing; diff --git a/src/test/resources/unit/prototype_underscore_warning_diagnostics.t b/src/test/resources/unit/prototype_underscore_warning_diagnostics.t new file mode 100644 index 0000000000..e69b9ebe59 --- /dev/null +++ b/src/test/resources/unit/prototype_underscore_warning_diagnostics.t @@ -0,0 +1,32 @@ +use strict; +use warnings; +use Test::More; + +my @warnings; +{ + local $SIG{__WARN__} = sub { push @warnings, @_ }; + eval q{ + use warnings 'illegalproto'; + sub warning_proto_after_underscore_scalar ($_$) { 1 } + sub warning_proto_underscore_word (_b) { 1 } + sub warning_proto_underscore_class ([_]) { 1 } + 1; + }; + ok(!$@, 'malformed underscore prototypes do not prevent declarations'); +} + +my $warnings = join '', @warnings; +like($warnings, + qr/Illegal character after '_' in prototype for main::warning_proto_after_underscore_scalar : \$_\$/, + 'underscore warning retains the complete prototype'); +like($warnings, + qr/Illegal character in prototype for main::warning_proto_underscore_word : _b/, + 'illegal prototype character warning is retained'); +like($warnings, + qr/Illegal character after '_' in prototype for main::warning_proto_underscore_word : _b/, + 'malformed underscore gets its additional warning'); +like($warnings, + qr/Illegal character after '_' in prototype for main::warning_proto_underscore_class : \[_\]/, + 'underscore warning retains character class delimiters'); + +done_testing; diff --git a/src/test/resources/unit/regex/multiline_lookahead_lazy_capture.t b/src/test/resources/unit/regex/multiline_lookahead_lazy_capture.t new file mode 100644 index 0000000000..18cba9ff6f --- /dev/null +++ b/src/test/resources/unit/regex/multiline_lookahead_lazy_capture.t @@ -0,0 +1,25 @@ +use strict; +use warnings; +use Test::More; + +my $list_start = qr/^(?=[\*\#]+\s)/msix; +my $list_end = qr/(?:^(?![\*\#\s])|\z)/msix; +my $subject = "prefix\n\n----\n\n* list 1\n** list 1.1\n*# list 1.I\n" + . "*** list 1.2.1\n* list 2\n* list 3\n\n----\n"; + +ok($subject =~ m{$list_start(.*?)$list_end}msix, + 'multiline lookahead finds a list after preceding text'); +is($1, "* list 1\n** list 1.1\n*# list 1.I\n*** list 1.2.1\n" + . "* list 2\n* list 3\n\n", + 'lazy capture stops at the first non-list line'); + +my $single_line = "prefix\n* only item\nend\n"; +ok($single_line =~ m{$list_start(.*?)$list_end}msix, + 'multiline lookahead also matches a single list line'); +is($1, "* only item\n", 'single-line capture has the expected boundary'); + +my $no_list = "prefix\nplain text\nend\n"; +ok($no_list !~ m{$list_start(.*?)$list_end}msix, + 'multiline lookahead rejects text without a list line'); + +done_testing; diff --git a/src/test/resources/unit/signature_named_validation_regressions.t b/src/test/resources/unit/signature_named_validation_regressions.t new file mode 100644 index 0000000000..9aee42c5f2 --- /dev/null +++ b/src/test/resources/unit/signature_named_validation_regressions.t @@ -0,0 +1,34 @@ +use strict; +use warnings; +use utf8; +use Test::More; + +my $supports_named_signatures = eval q{ + use feature 'signatures'; + no warnings 'experimental::signature_named_parameters'; + sub _named_validation_probe (:$required, @rest) { 1 } + 1; +}; + +SKIP: { + skip 'Perl does not support named signature parameters', 4 + unless $supports_named_signatures; + + eval q{ + use feature 'signatures'; + no warnings 'experimental::signature_named_parameters'; + sub _named_validation_unicode (:$ĉevaloj) { $ĉevaloj } + sub _named_validation_slurpy (:$required, @rest) { 'ok' } + }; + ok(!$@, 'named signature regression declarations compile'); + + my $unknown = eval { _named_validation_unicode('ŝafoj' => 5); 1 }; + ok(!$unknown, 'unknown named key is rejected'); + like($@, qr/^Unrecognized named parameter 'ŝafoj'/, + 'unknown named key is diagnosed before a missing required key'); + + is(_named_validation_slurpy('one positional value'), 'ok', + 'native @ slurpy accepts an odd trailing positional argument'); +} + +done_testing; diff --git a/third_party/joni/src/org/joni/Analyser.java b/third_party/joni/src/org/joni/Analyser.java index 3607326316..e55fada10e 100644 --- a/third_party/joni/src/org/joni/Analyser.java +++ b/third_party/joni/src/org/joni/Analyser.java @@ -4258,7 +4258,17 @@ protected final void setOptimizedInfoFromTree(Node node) { AnchorType.ANYCHAR_STAR_ML | AnchorType.LOOK_BEHIND); - if ((opt.anchor.leftAnchor & (AnchorType.LOOK_BEHIND | AnchorType.PREC_READ_NOT)) != 0) regex.anchor &= ~AnchorType.ANYCHAR_STAR_ML; + if ((opt.anchor.leftAnchor & (AnchorType.LOOK_BEHIND + | AnchorType.PREC_READ | AnchorType.PREC_READ_NOT)) != 0) { + regex.anchor &= ~AnchorType.ANYCHAR_STAR_ML; + } + // A positive assertion can reject the earliest line start even when + // a later line satisfies it. In that case the multiline anychar-star + // shortcut cannot limit search to the initial candidate position. + if ((regex.anchor & AnchorType.ANYCHAR_STAR_ML) != 0 + && containsPositiveLookahead(node)) { + regex.anchor &= ~AnchorType.ANYCHAR_STAR_ML; + } regex.anchor |= opt.anchor.rightAnchor & (AnchorType.END_BUF | AnchorType.SEMI_END_BUF | @@ -4420,6 +4430,26 @@ private boolean containsBeginLineAnchor(Node node) { }; } + private boolean containsPositiveLookahead(Node node) { + if (node instanceof AnchorNode anchor + && anchor.type == AnchorType.PREC_READ) { + return true; + } + return switch (node.getType()) { + case NodeType.LIST, NodeType.ALT -> { + ListNode list = (ListNode) node; + boolean found = false; + do { + found |= containsPositiveLookahead(list.value); + } while (!found && (list = list.tail) != null); + yield found; + } + case NodeType.QTFR -> containsPositiveLookahead(((QuantifierNode) node).target); + case NodeType.ENCLOSE -> containsPositiveLookahead(((EncloseNode) node).target); + default -> false; + }; + } + /** Recognize fixed-string alternations reported by Perl as an * AHOCORASICK-EXACT start class. The native map remains the matcher * implementation; this only preserves the public optimization label. */ diff --git a/third_party/joni/test/org/joni/TestPerlPositiveLookaheadAnyCharStarSearch.java b/third_party/joni/test/org/joni/TestPerlPositiveLookaheadAnyCharStarSearch.java new file mode 100644 index 0000000000..e1b1c5831b --- /dev/null +++ b/third_party/joni/test/org/joni/TestPerlPositiveLookaheadAnyCharStarSearch.java @@ -0,0 +1,66 @@ +/* + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.joni; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.nio.charset.StandardCharsets; + +import org.jcodings.specific.UTF8Encoding; +import org.junit.Test; + +public class TestPerlPositiveLookaheadAnyCharStarSearch { + private static final String PATTERN = + "(?msix)^(?=[\\*\\#]+\\s)(.*?)(?:^(?![\\*\\#\\s])|\\z)"; + + private static Regex compile() { + byte[] pattern = PATTERN.getBytes(StandardCharsets.UTF_8); + return new Regex(pattern, 0, pattern.length, Option.NONE, + UTF8Encoding.INSTANCE, Syntax.Perl); + } + + private static Matcher matcher(Regex regex, String input) { + byte[] subject = input.getBytes(StandardCharsets.UTF_8); + return regex.matcher(subject, 0, subject.length); + } + + @Test + public void findsLookaheadConstrainedMultilineCaptureAfterPrefix() { + String input = "prefix\n\n----\n\n* list 1\n** list 2\n\n----\n"; + Regex regex = compile(); + assertFalse("lookahead-constrained lazy wildcard must not use the anychar-star shortcut; anchor=" + + regex.getOptimizationInfo().anchor() + ", beginLine=" + + regex.getOptimizationInfo().beginLineAnchored(), + regex.getOptimizationInfo().implicitMultiLineAnchor()); + Matcher matcher = matcher(regex, input); + + assertEquals(input.indexOf("* list"), + matcher.search(0, input.length(), Option.NONE)); + assertEquals(input.indexOf("* list"), matcher.getBegin()); + } + + @Test + public void doesNotInventMatchWhenConstrainedLineIsAbsent() { + String input = "prefix\nplain text\nend\n"; + assertEquals(Matcher.FAILED, + matcher(compile(), input).search(0, input.length(), Option.NONE)); + } +}