diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 65ed07e205..60bb0b9890 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -42,7 +42,7 @@ priorities and future plans. - Preserve named typeglob stringification when copying package stash entries (#1927). -- Reduce eval STRING overhead with lazy goto collections, parser-set AST flags that skip unneeded BytecodeCompiler analysis passes, ScopedSymbolTable snapshot caching, per-thread eval caches without synchronization, cached constructor/class-field metadata, and a re-entrant fast-path in `PerlRuntime.bind()` (#1911). +- Reduce eval STRING overhead with lazy goto collections, parser-set AST flags that skip unneeded BytecodeCompiler analysis passes, ScopedSymbolTable snapshot caching, per-thread eval caches without synchronization, cached compiler symbol tables and captured-variable metadata for repeated eval sites, lexer token flyweights, raw-source deparse reuse, in-place captured-variable setup, cached constructor/class-field metadata, a re-entrant fast-path in `PerlRuntime.bind()`, and a lexer `sawBeginOrSub` flag that skips the O(visible_vars) outer-lexical alias loop on the common path (#1911, #1961). - Preserve arbitrary bytes in SQLite text columns across DBI fetches (#1923). diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 0457d365f5..b2e49175af 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -620,7 +620,7 @@ private void emitNormalLoopResult() { // Declaration metadata for captured package variables. Keep this // separately from the register table because closure analysis may first // materialize a reference as a lexical before the body reaches `local`. - private final Set capturedOurVariableNames = new HashSet<>(); + private Set capturedOurVariableNames = new HashSet<>(); // EmitterContext for strict checks and other compile-time options private EmitterContext emitterContext; // Register allocation @@ -785,6 +785,31 @@ public BytecodeCompiler(String sourceName, int sourceLine, ErrorMessageUtil erro } } + /** + * Cached-path constructor for repeated eval STRING invocations from the same eval site. + * Skips all per-variable iteration by using a pre-built SymbolTable template and + * sharing the pre-built capturedVarIndices / capturedOurVariableNames directly. + * Called from RuntimeCode.evalStringWithInterpreter when the per-site cache is warm. + */ + public BytecodeCompiler(String sourceName, int sourceLine, ErrorMessageUtil errorUtil, + SymbolTable cachedCompilerSymbolTable, + Map cachedCapturedVarIndices, + Set cachedCapturedOurVarNames, + int cachedNextRegister) { + this.sourceName = sourceName; + this.sourceLine = sourceLine; + this.errorUtil = errorUtil; + this.isEvalString = true; + // One HashMap copy replaces N addVariableWithIndex calls + N SymbolEntry allocations + symbolTable.useInitialTable(cachedCompilerSymbolTable); + // Share read-only structures (never mutated after eval STRING construction) + this.capturedVarIndices = cachedCapturedVarIndices; + this.capturedOurVariableNames = cachedCapturedOurVarNames; + this.nextRegister = cachedNextRegister; + this.maxRegisterEverUsed = cachedNextRegister - 1; + this.baseRegisterForStatement = cachedNextRegister; + } + /** * Helper: Check if a variable is a built-in special length-one variable. * Single-character non-letter variables like $_, $0, $!, $; are always allowed under strict. diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index 1a6bf693cf..ecad31ba5b 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -53,8 +53,8 @@ public ScalarPin scalarPin(int nameIdx) { } return pin; } - public final int maxRegisters; // Number of registers needed - public final RuntimeBase[] capturedVars; // Closure support (captured from outer scope) + public int maxRegisters; // Number of registers needed + public RuntimeBase[] capturedVars; // Closure support (captured from outer scope) public final Map variableRegistry; // Variable name → register index (for eval STRING) public final List> evalSiteRegistries; // Per-eval-site variable registries public final List> evalSiteLexicalSubroutineBindings; @@ -299,6 +299,11 @@ private static String sourceTextFromErrorUtil(ErrorMessageUtil errorUtil) { if (errorUtil == null) { return null; } + // Fast path: eval strings store the raw source directly to avoid token reconstruction. + String raw = errorUtil.getRawSourceText(); + if (raw != null) { + return raw.length() <= DEPARSE_SOURCE_TEXT_LIMIT ? raw : null; + } String[] lines = errorUtil.extractSourceLines(); if (lines == null || lines.length <= 1) { return null; @@ -697,6 +702,33 @@ public InterpretedCode withCapturedVars(RuntimeBase[] capturedVars) { return copy; } + /** + * Set captured variables in-place for eval STRING. + * Only safe when this InterpretedCode is freshly compiled and exclusively owned — + * i.e. the caller holds the only reference and will not share it before calling this. + * Avoids allocating a copy via withCapturedVars, and skips the duplicate + * scanMyVarRegisters and WarningBitsRegistry.register calls the copy constructor would make. + */ + public void setEvalCapturedVars(RuntimeBase[] capturedVars) { + if (capturedVars == null || capturedVars.length == 0) { + return; + } + this.capturedVars = capturedVars; + int capturedRegisterCount = 3 + capturedVars.length; + if (capturedRegisterCount > this.maxRegisters) { + this.maxRegisters = capturedRegisterCount; + } + // Build closedOverVariables for PadWalker support (same logic as withCapturedVars). + this.closedOverVariables = new java.util.LinkedHashMap<>(); + for (Map.Entry entry : this.variableRegistry.entrySet()) { + int capturedIndex = entry.getValue() - 3; + if (capturedIndex >= 0 && capturedIndex < capturedVars.length + && capturedVars[capturedIndex] != null) { + this.closedOverVariables.put(entry.getKey(), capturedVars[capturedIndex]); + } + } + } + private void validateAsyncSignature(RuntimeArray args) { if (!futureAsyncAwaitSub || signatureMinArgs < 0) { return; diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterContext.java b/src/main/java/org/perlonjava/backend/jvm/EmitterContext.java index e6b97fadb8..aef7436f5a 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterContext.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterContext.java @@ -13,6 +13,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.perlonjava.frontend.semantic.SymbolTable; /** * The EmitterContext class holds the context information required for parsing and emitting bytecode. @@ -110,6 +111,29 @@ public class EmitterContext { */ public boolean isLvalueSubroutine; + // ------------------------------------------------------------------------- + // Per-eval-site cache for repeated eval STRING invocations. + // capturedEnv and symbolTable are both fixed at compile time, so these maps + // are stable for the lifetime of this EmitterContext and can be reused + // across every runtime call to the same eval{} site. + // Only accessed from the eval-compilation thread (PerlRuntime is ThreadLocal). + // ------------------------------------------------------------------------- + + /** adjustedRegistry map, built once from capturedEnv indices. */ + public Map evalCachedRegistry; + /** adjustedDecls map (varName → decl kind), built once from capturedEnv + symbolTable. */ + public Map evalCachedDecls; + /** adjustedOurPackages map, built once from capturedEnv + symbolTable. */ + public Map evalCachedOurPackages; + /** Pre-built SymbolTable used as BytecodeCompiler's initial variable table template. */ + public SymbolTable evalCachedCompilerSymbolTable; + /** capturedVarIndices for BytecodeCompiler — read-only after first eval, shared directly. */ + public Map evalCachedCapturedVarIndices; + /** capturedOurVariableNames for BytecodeCompiler — read-only after first eval, shared directly. */ + public Set evalCachedCapturedOurVarNames; + /** nextRegister value to use for BytecodeCompiler on the cached path. */ + public int evalCachedNextRegister; + /** * Constructs a new EmitterContext with the specified parameters. * diff --git a/src/main/java/org/perlonjava/frontend/lexer/Lexer.java b/src/main/java/org/perlonjava/frontend/lexer/Lexer.java index 16a72baf0a..051ff18f47 100644 --- a/src/main/java/org/perlonjava/frontend/lexer/Lexer.java +++ b/src/main/java/org/perlonjava/frontend/lexer/Lexer.java @@ -73,6 +73,10 @@ public class Lexer { public int position; // Length of the input public int length; + // Set to true if the token stream contains BEGIN, sub, use, or no — + // keywords that require the outer-lexical alias loop in evalStringWithInterpreter(). + // False on the common path (simple expressions, while loops, etc.). + public boolean sawBeginOrSub = false; // Constructor to initialize the Lexer with input string public Lexer(String input) { @@ -168,8 +172,8 @@ public List tokenize() { while ((token = nextToken()) != null) { tokens.add(token); } - tokens.add(new LexerToken(LexerTokenType.EOF, EOF)); - tokens.add(new LexerToken(LexerTokenType.EOF, EOF)); + tokens.add(LexerToken.TOKEN_EOF); + tokens.add(LexerToken.TOKEN_EOF); this.input = null; // Throw away input to spare memory return tokens; @@ -214,7 +218,7 @@ public LexerToken nextToken() { if (isAsciiWhitespace(current)) { if (current == '\n' || current == '\r') { position++; - return new LexerToken(LexerTokenType.NEWLINE, String.valueOf(current)); + return LexerToken.of(LexerTokenType.NEWLINE, String.valueOf(current)); } else { return consumeWhitespace(); } @@ -321,7 +325,7 @@ public LexerToken consumeWhitespace() { || input.charAt(position) == '\r' || input.charAt(position) == '\f')) { position++; } - return new LexerToken(LexerTokenType.WHITESPACE, input.substring(start, position)); + return LexerToken.of(LexerTokenType.WHITESPACE, input.substring(start, position)); } public LexerToken consumeNumber() { @@ -368,7 +372,11 @@ public LexerToken consumeIdentifier() { } } // Build token text using substring to preserve surrogate pairs correctly - return new LexerToken(LexerTokenType.IDENTIFIER, input.substring(start, position)); + String text = input.substring(start, position); + if (!sawBeginOrSub + && (text.equals("BEGIN") || text.equals("sub") + || text.equals("use") || text.equals("no"))) sawBeginOrSub = true; + return new LexerToken(LexerTokenType.IDENTIFIER, text); } public LexerToken consumeOperator() { @@ -672,6 +680,6 @@ public LexerToken consumeOperator() { } position++; - return new LexerToken(LexerTokenType.OPERATOR, input.substring(start, start + 1)); + return LexerToken.of(LexerTokenType.OPERATOR, input.substring(start, start + 1)); } } diff --git a/src/main/java/org/perlonjava/frontend/lexer/LexerToken.java b/src/main/java/org/perlonjava/frontend/lexer/LexerToken.java index 5a1af9c480..368235b0aa 100644 --- a/src/main/java/org/perlonjava/frontend/lexer/LexerToken.java +++ b/src/main/java/org/perlonjava/frontend/lexer/LexerToken.java @@ -16,16 +16,52 @@ public class LexerToken { * The type of the token, represented by an instance of the LexerTokenType enum. * This field categorizes the token (e.g., keyword, identifier, operator). */ - public LexerTokenType type; + public final LexerTokenType type; /** * The text of the token, represented as a string. * This field contains the actual string of characters that make up the token. */ - public String text; + public final String text; + + // Flyweight cache: single-char OPERATOR tokens and common constant tokens. + // Fields are final so sharing instances across parses is safe. + private static final LexerToken[] OPERATOR_CACHE = new LexerToken[128]; + public static final LexerToken TOKEN_EOF = new LexerToken(LexerTokenType.EOF, ""); + public static final LexerToken TOKEN_NEWLINE = new LexerToken(LexerTokenType.NEWLINE, "\n"); + public static final LexerToken TOKEN_SPACE = new LexerToken(LexerTokenType.WHITESPACE, " "); + + static { + for (char c : "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~".toCharArray()) { + OPERATOR_CACHE[c] = new LexerToken(LexerTokenType.OPERATOR, String.valueOf(c)); + } + } + + /** + * Factory that returns a cached instance for common tokens, or allocates a new one. + * Use this instead of {@code new LexerToken(...)} everywhere except the static + * initialiser above. + */ + public static LexerToken of(LexerTokenType type, String text) { + if (type == LexerTokenType.OPERATOR && text.length() == 1) { + char c = text.charAt(0); + if (c < 128) { + LexerToken cached = OPERATOR_CACHE[c]; + if (cached != null) return cached; + } + } else if (type == LexerTokenType.EOF) { + return TOKEN_EOF; + } else if (type == LexerTokenType.NEWLINE && "\n".equals(text)) { + return TOKEN_NEWLINE; + } else if (type == LexerTokenType.WHITESPACE && " ".equals(text)) { + return TOKEN_SPACE; + } + return new LexerToken(type, text); + } /** * Constructs a new LexerToken with the specified type and text. + * Prefer the {@link #of} factory for common token types. * * @param type the type of the token, represented by an instance of the LexerTokenType enum * @param text the text of the token, represented as a string diff --git a/src/main/java/org/perlonjava/frontend/parser/FunctionParametersKeywordPlugin.java b/src/main/java/org/perlonjava/frontend/parser/FunctionParametersKeywordPlugin.java index ec01a0ed22..bbe0cf3cfe 100644 --- a/src/main/java/org/perlonjava/frontend/parser/FunctionParametersKeywordPlugin.java +++ b/src/main/java/org/perlonjava/frontend/parser/FunctionParametersKeywordPlugin.java @@ -140,7 +140,7 @@ public Node parse(Parser parser, String keyword) { && !validCandidateName)) { parser.throwError(originalText + " expects a parameter list"); } - keywordToken.text = "sub"; + parser.tokens.set(declarationIndex, LexerToken.of(keywordToken.type, "sub")); parser.tokenIndex++; parser.parsingFunctionParametersDeclaration = true; parser.parsingFunctionParametersMethod = method; @@ -222,7 +222,7 @@ public Node parse(Parser parser, String keyword) { parser.functionParametersAttributes = previousAttributes; parser.functionParametersRuntime = previousRuntime; parser.functionParametersDefaultArguments = previousDefaultArguments; - keywordToken.text = originalText; + parser.tokens.set(declarationIndex, LexerToken.of(parser.tokens.get(declarationIndex).type, originalText)); } } diff --git a/src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java b/src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java index 496edf7c50..76c7e47aea 100644 --- a/src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java @@ -506,7 +506,7 @@ public static String parseComplexIdentifierInner(Parser parser, boolean insideBr int underscore = token.text.indexOf('_'); if (underscore >= 0) { variableName.append(token.text, 0, underscore); - token.text = token.text.substring(underscore); + parser.tokens.set(parser.tokenIndex, LexerToken.of(token.type, token.text.substring(underscore))); } else { variableName.append(token.text); parser.tokenIndex++; diff --git a/src/main/java/org/perlonjava/frontend/parser/Parser.java b/src/main/java/org/perlonjava/frontend/parser/Parser.java index 5db5a6cc43..cab2ebb767 100644 --- a/src/main/java/org/perlonjava/frontend/parser/Parser.java +++ b/src/main/java/org/perlonjava/frontend/parser/Parser.java @@ -643,10 +643,11 @@ public Node parseExpression(int precedence) { if ("CORE".equals(token.text) && tokenIndex + 2 < tokens.size() && "::".equals(tokens.get(tokenIndex + 1).text) && ParserTables.INFIX_OP.contains(tokens.get(tokenIndex + 2).text)) { - token.text = tokens.get(tokenIndex + 2).text; - token.type = LexerTokenType.OPERATOR; + String collapsedOpText = tokens.get(tokenIndex + 2).text; tokens.remove(tokenIndex + 2); tokens.remove(tokenIndex + 1); + token = LexerToken.of(LexerTokenType.OPERATOR, collapsedOpText); + tokens.set(tokenIndex, token); } // Check if we have reached the end of the input (EOF) or a terminator (like `;`). @@ -689,11 +690,10 @@ public Node parseExpression(int precedence) { break; // Stop parsing infix, let 'x' be parsed as a bareword argument } // Combine 'x' and '=' into a single token 'x=' - token.text = "x="; - // Set the token type to OPERATOR to reflect its usage - token.type = LexerTokenType.OPERATOR; // Remove the '=' token from the list as it is now part of 'x=' tokens.remove(tokenIndex + 1); + token = LexerToken.of(LexerTokenType.OPERATOR, "x="); + tokens.set(tokenIndex, token); } // Check for the special case of 'x3' style tokens @@ -707,8 +707,8 @@ public Node parseExpression(int precedence) { // position, leaving an ordinary bareword such as x1e3 alone. if (remainder.matches("[0-9][0-9_]*(?:[eE][+]?[0-9_]+)?")) { // Split the token into 'x' operator and the number - token.text = "x"; - token.type = LexerTokenType.OPERATOR; + token = LexerToken.of(LexerTokenType.OPERATOR, "x"); + tokens.set(tokenIndex, token); // Insert a new token for the number after the current position LexerToken numberToken = new LexerToken(LexerTokenType.NUMBER, remainder); tokens.add(tokenIndex + 1, numberToken); diff --git a/src/main/java/org/perlonjava/frontend/parser/StringParser.java b/src/main/java/org/perlonjava/frontend/parser/StringParser.java index efad3d896c..9e771fe7d4 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StringParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StringParser.java @@ -451,17 +451,15 @@ private static ParsedString parseRawStringWithDelimiter(EmitterContext ctx, List && tokens.get(tokPos + 1).text.equals("~")) { remainStr = remainStr + "~"; // Neutralize the consumed ~ token so it won't be parsed again - tokens.get(tokPos + 1).text = " "; - tokens.get(tokPos + 1).type = LexerTokenType.WHITESPACE; + tokens.set(tokPos + 1, LexerToken.TOKEN_SPACE); } else if (remainStr.equals("=") && tokPos + 1 < tokens.size() && tokens.get(tokPos + 1).text.equals(">")) { remainStr = "=>"; // Neutralize the consumed > token so it won't be parsed again - tokens.get(tokPos + 1).text = " "; - tokens.get(tokPos + 1).type = LexerTokenType.WHITESPACE; + tokens.set(tokPos + 1, LexerToken.TOKEN_SPACE); } - tokens.get(tokPos).text = remainStr; // Put the remaining string back in the tokens list + tokens.set(tokPos, LexerToken.of(tokens.get(tokPos).type, remainStr)); // Put the remaining string back in the tokens list } ParsedString parsed = new ParsedString(index, tokPos, buffers, startDelim, endDelim, secondBufferStartDelim, secondBufferEndDelim); diff --git a/src/main/java/org/perlonjava/frontend/parser/TokenUtils.java b/src/main/java/org/perlonjava/frontend/parser/TokenUtils.java index 39d4990ee1..d04a832482 100644 --- a/src/main/java/org/perlonjava/frontend/parser/TokenUtils.java +++ b/src/main/java/org/perlonjava/frontend/parser/TokenUtils.java @@ -72,12 +72,14 @@ public static String consumeChar(Parser parser) { parser.tokenIndex++; } else { str = token.text.substring(0, 1); - token.text = token.text.substring(1); + String remaining = token.text.substring(1); + token = LexerToken.of(token.type, remaining); + parser.tokens.set(parser.tokenIndex, token); if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("consumeChar left: " + token); - if (token.text.equals("=")) { + if (remaining.equals("=")) { LexerToken next = parser.tokens.get(parser.tokenIndex + 1); if (next.text.equals("=")) { - next.text = "=="; + parser.tokens.set(parser.tokenIndex + 1, LexerToken.of(next.type, "==")); parser.tokenIndex++; if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("consumeChar resync: " + TokenUtils.peek(parser)); } diff --git a/src/main/java/org/perlonjava/frontend/parser/Variable.java b/src/main/java/org/perlonjava/frontend/parser/Variable.java index 4e284348cf..c6055b13ff 100644 --- a/src/main/java/org/perlonjava/frontend/parser/Variable.java +++ b/src/main/java/org/perlonjava/frontend/parser/Variable.java @@ -109,8 +109,8 @@ public static Node parseVariable(Parser parser, String sigil) { // two characters as the ^= operator, so recover the contextual // split before identifier parsing consumes the special variable. if (sigil.equals("*") && nextToken.text.equals("^=")) { - nextToken.text = "^"; - parser.tokens.add(nextTokIdx + 1, new LexerToken(LexerTokenType.OPERATOR, "=")); + parser.tokens.set(nextTokIdx, LexerToken.of(LexerTokenType.OPERATOR, "^")); + parser.tokens.add(nextTokIdx + 1, LexerToken.of(LexerTokenType.OPERATOR, "=")); } // Special case 1: $${...} - nested scalar dereference diff --git a/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java b/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java index 7ef1f3fed7..824b92ff0b 100644 --- a/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java +++ b/src/main/java/org/perlonjava/frontend/semantic/ScopedSymbolTable.java @@ -207,6 +207,16 @@ public ScopedSymbolTable() { symbolTableStack.push(new SymbolTable(0)); } + /** + * Replace the initial (bottom) symbol table with a copy of the given template. + * Called from BytecodeCompiler's cached eval-string path to avoid N individual + * addVariableWithIndex calls — one HashMap copy replaces N puts + N SymbolEntry allocs. + */ + public void useInitialTable(SymbolTable template) { + symbolTableStack.pop(); + symbolTableStack.push(new SymbolTable(template)); + } + public static String stringifyFeatureFlags(int featureFlags) { StringBuilder result = new StringBuilder(); for (Map.Entry entry : featureBitPositions.entrySet()) { @@ -1062,15 +1072,18 @@ public ScopedSymbolTable snapShot(SymbolTable prebuiltTable) { // Clone warning flags st.warningFlagsStack.pop(); // Remove the initial value pushed by enterScope - st.warningFlagsStack.push((BitSet) this.warningFlagsStack.peek().clone()); + BitSet wf = this.warningFlagsStack.peek(); + st.warningFlagsStack.push(wf.isEmpty() ? new BitSet() : (BitSet) wf.clone()); // Clone disabled warnings flags st.warningDisabledStack.pop(); // Remove the initial value pushed by enterScope - st.warningDisabledStack.push((BitSet) this.warningDisabledStack.peek().clone()); + BitSet wd = this.warningDisabledStack.peek(); + st.warningDisabledStack.push(wd.isEmpty() ? new BitSet() : (BitSet) wd.clone()); // Clone fatal warnings flags st.warningFatalStack.pop(); // Remove the initial value pushed by enterScope - st.warningFatalStack.push((BitSet) this.warningFatalStack.peek().clone()); + BitSet wft = this.warningFatalStack.peek(); + st.warningFatalStack.push(wft.isEmpty() ? new BitSet() : (BitSet) wft.clone()); // Clone feature flags st.featureFlagsStack.pop(); // Remove the initial value pushed by enterScope diff --git a/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java b/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java index d0036797d2..73dfc1e823 100644 --- a/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java +++ b/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java @@ -19,6 +19,12 @@ public SymbolTable(int index) { this.index = index; } + /** Copy constructor: shares immutable SymbolEntry values, copies only the HashMap shell. */ + public SymbolTable(SymbolTable template) { + this.variableIndex = new HashMap<>(template.variableIndex); + this.index = template.index; + } + /** * Adds a variable to the symbol table if it does not already exist. * diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java index d1b8b32e5c..d0f71978a7 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java @@ -16,6 +16,8 @@ public class ErrorMessageUtil { private final String originalFileName; private String fileName; private int tokenIndex; + /** Raw source text (set for eval strings to avoid token reconstruction). */ + private String rawSourceText; private int lastLineNumber; private volatile int[] physicalLineNumbers; private volatile SourceDirectiveIndex sourceDirectiveIndex; @@ -35,6 +37,17 @@ public ErrorMessageUtil(String fileName, List tokens) { this.lastLineNumber = 1; } + /** Constructor that also stores the raw source text for fast deparse extraction. */ + public ErrorMessageUtil(String fileName, List tokens, String rawSourceText) { + this(fileName, tokens); + this.rawSourceText = rawSourceText; + } + + /** Returns the raw source text, or null if not available. */ + public String getRawSourceText() { + return rawSourceText; + } + /** Register a literal quote body whose apparent #line comments are data, not directives. */ public void addLiteralQuoteRange(int startInclusive, int endExclusive) { literalQuoteRanges.add(new int[]{startInclusive, endExclusive}); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 17c985980f..9a1eb5267a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -4877,7 +4877,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje try { // Create the AST // Create an instance of ErrorMessageUtil with the file name and token list - evalCtx.errorUtil = new ErrorMessageUtil(evalCtx.compilerOptions.fileName, tokens); + evalCtx.errorUtil = new ErrorMessageUtil(evalCtx.compilerOptions.fileName, tokens, evalString); Parser parser = new Parser(evalCtx, tokens); // Parse the tokens parser.parsingEvalString = true; BHooksEndOfScope.beginFileLoad(evalCompilerOptions.fileName); @@ -4897,7 +4897,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // not ctx.compilerOptions.fileName (the outer file, e.g. "-e") so that // anonymous subs compiled inside eval STRING get the correct source filename // for #line directives and caller() reporting - evalCtx.errorUtil = new ErrorMessageUtil(evalCtx.compilerOptions.fileName, tokens); + evalCtx.errorUtil = new ErrorMessageUtil(evalCtx.compilerOptions.fileName, tokens, evalString); ScopedSymbolTable postParseSymbolTable = evalCtx.symbolTable; evalCtx.symbolTable = capturedSymbolTable; evalCtx.symbolTable.copyFlagsFrom(postParseSymbolTable); @@ -5393,10 +5393,14 @@ public static RuntimeList evalStringWithInterpreter( : topLevelMain ? runtimePackage : capturedSymbolTable.getCurrentPackage(); try { + // Parse the eval string (tokenize first so sawBegin is available for the alias gate below) + Lexer lexer = new Lexer(evalString); + tokens = lexer.tokenize(); // Alias captured outer variables before parsing. // BEGIN/use execute during parsing, so their aliases must be in place first. // Named subs in eval also access outer lexicals via these aliases. - { + // Skip this O(visible_vars) loop on the common path (no BEGIN or named sub). + if (lexer.sawBeginOrSub) { Map capturedVars = capturedSymbolTable.getAllVisibleVariables(); for (SymbolTable.SymbolEntry entry : capturedVars.values()) { if (!entry.name().equals("@_") && !entry.decl().isEmpty() && !entry.name().startsWith("&")) { @@ -5452,9 +5456,6 @@ public static RuntimeList evalStringWithInterpreter( } evalCompilerOptions.fileName = evalFilename; warnSignatureArgsInEval(evalString, evalFilename); - // Parse the eval string - Lexer lexer = new Lexer(evalString); - tokens = lexer.tokenize(); // Create parser context ScopedSymbolTable parseSymbolTable = capturedSymbolTable.snapShot(); parseSymbolTable.setCurrentPackage( @@ -5491,7 +5492,7 @@ public static RuntimeList evalStringWithInterpreter( null, callContext, // Use the runtime calling context, not the saved one! true, - new ErrorMessageUtil(evalCompilerOptions.fileName, tokens), + new ErrorMessageUtil(evalCompilerOptions.fileName, tokens, evalString), evalCompilerOptions, new RuntimeArray()); @@ -5521,55 +5522,11 @@ public static RuntimeList evalStringWithInterpreter( // Run UNITCHECK blocks runUnitcheckBlocks(evalCtx.unitcheckBlocks); - // Build adjusted registry for captured variables - // Map variable names to register indices (3+ for captured variables) - Map adjustedRegistry = new HashMap<>(); - adjustedRegistry.put("this", 0); - adjustedRegistry.put("@_", 1); - adjustedRegistry.put("wantarray", 2); - - // IMPORTANT: Captured variables are loaded into registers starting at 3 - // (see BytecodeInterpreter's `System.arraycopy(code.capturedVars, 0, registers, 3, ...)`). - // Therefore the variable registry must map captured variable names to packed - // register indices 3+(i-skipVariables), in the same order as runtimeValues[]. - // Do NOT pack indices: runtimeValues[] is sized for the full capturedEnv range - // and may contain null gaps, so the corresponding registers must keep the same - // slot numbering. - if (ctx.capturedEnv != null) { - for (int i = 3; i < ctx.capturedEnv.length; i++) { - String varName = ctx.capturedEnv[i]; - if (varName == null) { - continue; - } - adjustedRegistry.put(varName, i); - } - } - - // Build a parallel map of declaration kinds so the BytecodeCompiler can distinguish - // 'our' (package) variables from true lexicals. 'our' variables must resolve via - // GlobalVariable.getGlobalVariable() on each access so that `local $OurVar` in the - // caller is visible inside the eval. Without this hint, captured 'our' vars are - // treated as lexical 'my' vars bound to the scalar captured at eval-entry time. - Map adjustedDecls = new HashMap<>(); - // Parallel map of `our` var name → declaring package. Seeded so the eval's - // BytecodeCompiler keeps the caller's lexical alias intact even after the - // eval body changes package (e.g. `package Foo; $x`). - Map adjustedOurPackages = new HashMap<>(); - if (ctx.capturedEnv != null) { - for (int i = 3; i < ctx.capturedEnv.length; i++) { - String varName = ctx.capturedEnv[i]; - if (varName == null) continue; - SymbolTable.SymbolEntry entry = capturedSymbolTable.getSymbolEntry(varName); - if (entry != null && !entry.decl().isEmpty()) { - adjustedDecls.put(varName, entry.decl()); - if ("our".equals(entry.decl()) && entry.perlPackage() != null) { - adjustedOurPackages.put(varName, entry.perlPackage()); - } - } - } - } - - // Compile to InterpretedCode with variable registry. + // Build BytecodeCompiler for the eval body. + // On the first call from this eval site (cold cache), build all maps from + // capturedEnv, populate the per-site cache on ctx, and use the cached-path + // constructor. On subsequent calls (warm cache), skip all map building and + // use the cached-path constructor directly. // // setCompilePackage() is safe here (unlike EvalStringHandler) because: // - evalCtx.errorUtil uses evalCompilerOptions.fileName (the outer script name), @@ -5580,13 +5537,79 @@ public static RuntimeList evalStringWithInterpreter( // in the bytecode string pool. // - Without this call, the BytecodeCompiler defaults to "main", causing // eval q[*named{CODE}] to look up main::named instead of FOO3::named. + if (ctx.evalCachedCompilerSymbolTable == null) { + // ---- Cold path: build maps and populate cache ---- + // Build adjustedRegistry (variable name → register index) + Map adjustedRegistry = new HashMap<>(); + adjustedRegistry.put("this", 0); + adjustedRegistry.put("@_", 1); + adjustedRegistry.put("wantarray", 2); + // IMPORTANT: Captured variables are loaded into registers starting at 3. + // Do NOT pack indices: runtimeValues[] may contain null gaps. + if (ctx.capturedEnv != null) { + for (int i = 3; i < ctx.capturedEnv.length; i++) { + String varName = ctx.capturedEnv[i]; + if (varName != null) adjustedRegistry.put(varName, i); + } + } + // Build adjustedDecls and adjustedOurPackages + Map adjustedDecls = new HashMap<>(); + Map adjustedOurPackages = new HashMap<>(); + if (ctx.capturedEnv != null) { + for (int i = 3; i < ctx.capturedEnv.length; i++) { + String varName = ctx.capturedEnv[i]; + if (varName == null) continue; + SymbolTable.SymbolEntry entry = capturedSymbolTable.getSymbolEntry(varName); + if (entry != null && !entry.decl().isEmpty()) { + adjustedDecls.put(varName, entry.decl()); + if ("our".equals(entry.decl()) && entry.perlPackage() != null) { + adjustedOurPackages.put(varName, entry.perlPackage()); + } + } + } + } + // Build the SymbolTable template: 3 reserved + all captured vars + SymbolTable compilerSymbolTableTemplate = new SymbolTable(0); + compilerSymbolTableTemplate.addVariableWithIndex("this", 0, "reserved", null); + compilerSymbolTableTemplate.addVariableWithIndex("@_", 1, "reserved", null); + compilerSymbolTableTemplate.addVariableWithIndex("wantarray", 2, "reserved", null); + Map capturedVarIndices = new HashMap<>(); + Set capturedOurVarNames = new HashSet<>(); + int maxRegister = 2; + for (Map.Entry entry : adjustedRegistry.entrySet()) { + String varName = entry.getKey(); + int regIndex = entry.getValue(); + if (regIndex >= 3) { + String decl = adjustedDecls.getOrDefault(varName, "my"); + if (decl.isEmpty()) decl = "my"; + if ("our".equals(decl)) capturedOurVarNames.add(varName); + String perlPkg = "our".equals(decl) ? adjustedOurPackages.get(varName) : null; + if (perlPkg != null) { + compilerSymbolTableTemplate.addVariableWithIndex(varName, regIndex, decl, perlPkg); + } else { + compilerSymbolTableTemplate.addVariableWithIndex(varName, regIndex, decl, null); + } + capturedVarIndices.put(varName, regIndex); + if (regIndex > maxRegister) maxRegister = regIndex; + } + } + // Populate the per-site cache (EmitterContext is per-thread, no synchronization needed) + ctx.evalCachedRegistry = adjustedRegistry; + ctx.evalCachedDecls = adjustedDecls; + ctx.evalCachedOurPackages = adjustedOurPackages; + ctx.evalCachedCompilerSymbolTable = compilerSymbolTableTemplate; + ctx.evalCachedCapturedVarIndices = capturedVarIndices; + ctx.evalCachedCapturedOurVarNames = capturedOurVarNames; + ctx.evalCachedNextRegister = maxRegister + 1; + } BytecodeCompiler compiler = new BytecodeCompiler( evalCompilerOptions.fileName, 1, evalCtx.errorUtil, - adjustedRegistry, - adjustedDecls, - adjustedOurPackages); + ctx.evalCachedCompilerSymbolTable, + ctx.evalCachedCapturedVarIndices, + ctx.evalCachedCapturedOurVarNames, + ctx.evalCachedNextRegister); compiler.setCompilePackage(evalCurrentPackage); interpretedCode = compiler.compile(ast, evalCtx); compiledSuccessfully = true; @@ -5603,13 +5626,13 @@ public static RuntimeList evalStringWithInterpreter( storeSourceLines(evalString, evalFilename, ast, tokens, true, debuggerFlagsAtEntry); sourceLinesStored = true; - // Set captured variables + // Set captured variables in-place (freshly compiled, exclusively owned here). if (runtimeValues.length > 0) { RuntimeBase[] capturedVars2 = new RuntimeBase[runtimeValues.length]; for (int i = 0; i < runtimeValues.length; i++) { capturedVars2[i] = (RuntimeBase) runtimeValues[i]; } - interpretedCode = interpretedCode.withCapturedVars(capturedVars2); + interpretedCode.setEvalCapturedVars(capturedVars2); } } catch (Throwable e) {