Extend compile-time constant folding to inline user-defined constant subroutines
(use constant, sub FOO () { 123 }) into the AST, enabling cascading optimizations.
For example, use constant PI => 3.14159; my $x = PI * 2; should compile as if
written my $x = 6.28318;.
-
ConstantFoldingVisitor— Pure AST optimizer that folds expressions composed of literalNumberNode/StringNodevalues and a hardcoded set of built-in functions (sqrt,sin,abs,int,chr,ord,length,atan2, etc.). Example:2 + 3→5,sqrt(4)→2.0. -
RuntimeCode.constantValue— Runtime short-circuit: when a constant subroutine is called,apply()checksconstantValue != nulland returns the value directly, avoiding full method invocation overhead. -
Dead code elimination —
EmitStatement.getConstantConditionValue()(JVM backend) and the identical method inBytecodeCompilerresolve constant subs at compile time but only forif/unlessconditions. This enables patterns like:use constant WINDOWS => 0; if (WINDOWS) { ... } # Dead code eliminated at compile time
-
The
ConstantFoldingVisitoris not used in the main compilation pipeline. The calls inPerlLanguageProvider.java(line 170) andRuntimeCode.java(line 625) are commented out. It is only active inStringSegmentParserfor regex(?{...})code blocks. -
User-defined constants are not inlined in expressions. For
use constant PI => 3.14; my $x = PI * 2;, the AST contains a subroutine call node forPI, not a literal3.14. The multiplication happens at runtime. -
No cascading optimization. Even if we inlined
PIto3.14, the folding visitor is not running, so3.14 * 2would not be folded to6.28. -
getConstantConditionValueis duplicated identically in bothEmitStatement.javaandBytecodeCompiler.java(~50 lines each).
constant.pm creates constant subs by assigning to the symbol table:
# For Perl > 5.009002 (our case):
Internals::SvREADONLY($scalar, 1);
$symtab->{$name} = \$scalar;
# Fallback:
*$full_name = sub () { $scalar };In PerlOnJava, this results in a RuntimeCode object with constantValue set,
stored via GlobalVariable.getGlobalCodeRef().
The parser creates a SubroutineNode with prototype "" (empty string). After
compilation, if the subroutine body is a constant expression, it becomes a
RuntimeCode with constantValue set.
*foo = \42 creates a constant sub via RuntimeStashEntry, setting constantValue
on a new RuntimeCode.
Since use constant runs in a BEGIN block, constants are defined during parsing.
By the time parser.parse() returns, all constants are available in the global symbol
table. This means a post-parse AST transformation can safely look them up.
Goal: Avoid the overhead of RuntimeCode.apply() for constant subroutine calls.
Where: EmitSubroutine.handleApplyOperator() (JVM backend)
How: When the call target is a named subroutine (resolved via
GlobalVariable.getGlobalCodeRef()), check if it has constantValue set and the
argument list is empty. If so, emit the constant value as a literal (ldc instruction)
instead of the full call machinery (code ref resolution, argument array creation,
RuntimeCode.apply() invocation).
AST patterns to recognize:
BinaryOperatorNode("(", OperatorNode("&", IdentifierNode("PI")), ListNode())
What to emit: For a scalar constant value, emit the equivalent of a NumberNode
or StringNode literal. For non-scalar constants, fall back to the normal call path.
Also apply to: BytecodeCompiler for the interpreter backend.
Files changed:
src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.javasrc/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java
Benefit: Eliminates runtime overhead for every constant sub call. No cascading
folding yet, but each constant reference becomes a simple ldc instead of a method
call chain.
Goal: Replace constant sub references with literal nodes in the AST, enabling
cascading constant folding (e.g., PI * 2 → 3.14159 * 2 → 6.28318).
How:
-
Add a
currentPackagefield toConstantFoldingVisitor(set via constructor or a static method parameter). -
In
visit(BinaryOperatorNode)for operator"(":- Extract the subroutine name from the AST pattern
BinaryOperatorNode("(", OperatorNode("&", IdentifierNode(name)), ListNode()) - Resolve via
GlobalVariable.getGlobalCodeRef(fullName) - If the sub has
constantValuewith exactly oneRuntimeScalarelement, and the argument list is empty, replace the entire call node with aNumberNodeorStringNode - Then the parent expression (e.g.,
PI * 2) has both operands as constants and gets folded by the existing binary operation folding
- Extract the subroutine name from the AST pattern
-
In
visit(IdentifierNode):- Resolve the identifier as a potential constant sub
- If it resolves to a scalar constant, replace with a literal node
- Note: bare identifiers used as constants (without
()) appear asIdentifierNodein some contexts
-
Only inline scalar constants — skip list constants (
WEEKDAYS), reference constants (ARRAYREF => [1,2,3]), and non-scalar values.
Files changed:
src/main/java/org/perlonjava/frontend/analysis/ConstantFoldingVisitor.java
Benefit: Cascading optimization — expressions involving constants are fully evaluated at compile time.
Goal: Activate the folding visitor for all compiled code.
How:
-
Uncomment/add the
ConstantFoldingVisitor.foldConstants(ast)call in:PerlLanguageProvider.java(main script compilation, line ~170)RuntimeCode.java(eval compilation, line ~625)
-
Pass the current package name to the visitor so it can resolve identifiers.
-
The visitor runs after parsing (so BEGIN blocks have executed and constants are defined) and before code emission.
-
For the bytecode interpreter path, apply the same transformation before
BytecodeCompilerprocesses the AST.
Files changed:
src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.javasrc/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java
Benefit: All code paths benefit from constant folding and constant sub inlining.
Goal: Eliminate the duplicated getConstantConditionValue() method.
How: Extract the logic into a shared static method in a utility class (e.g.,
ConstantFoldingVisitor.getConstantConditionValue() or a new
CompileTimeConstants utility). Update both EmitStatement and BytecodeCompiler
to call the shared method.
Files changed:
src/main/java/org/perlonjava/backend/jvm/EmitStatement.javasrc/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java- New or existing utility class
If a constant sub is redefined after folding, the folded value will be stale:
use constant FOO => 1;
my $x = FOO + 1; # Folded to 2 at compile time
# Later: *FOO = sub () { 99 }; # Redefinition — $x is still 2This matches Perl 5 behavior. Perl 5 warns "Constant subroutine FOO redefined" and
the inlined values remain unchanged. We should emit the same warning if not already done.
use constant WEEKDAYS => qw(Sun Mon Tue ...) creates a list constant where
constantValue.elements.size() > 1. These must NOT be folded to a single scalar.
Phase 2 should only inline when constantValue.elements.size() == 1 and the element
is a RuntimeScalar with a simple type (INTEGER, DOUBLE, STRING).
use constant HASHREF => {a => 1} creates a constant returning a reference. References
are mutable objects and must not be folded — each call site must get the same reference
object, not a copy.
The folding visitor must not fold expressions that have side effects. The current
implementation is safe because isConstantNode() only returns true for NumberNode
and StringNode, which are pure values. User-defined constant resolution should
similarly only produce these pure node types.
The ConstantFoldingVisitor currently has no notion of the current package. Since
use constant FOO => 1 defines main::FOO (or CurrentPackage::FOO), the visitor
needs package context to resolve bare identifiers. This should track correctly through
block/package boundaries in the AST, but the simplest initial approach is to use the
package at parse completion time (works for single-package scripts; multi-package
support can be refined later).
src/test/resources/unit/constant.t— basicuse constantfunctionalitysrc/test/resources/unit/regex/code_block_constants.t— constant folding in regex code blocks
# Constant folding in expressions
use constant X => 10;
use constant Y => 20;
is(X + Y, 30, 'constant addition folded');
is(X * 2, 20, 'constant * literal folded');
is(X + Y + 5, 35, 'cascading constant fold');
# Dead code elimination (already works, verify preserved)
use constant DEBUG => 0;
if (DEBUG) { die "should not reach here" }
pass('dead code eliminated');
# List constants NOT folded
use constant DAYS => qw(Mon Tue Wed);
is((DAYS)[0], 'Mon', 'list constant not broken by folding');
# Reference constants NOT folded
use constant AREF => [1, 2, 3];
push @{AREF()}, 4;
is(scalar @{AREF()}, 4, 'reference constant is same object');# Count bytecode instructions — should decrease for constant expressions
./jperl --disassemble -e 'use constant X=>10; my $y = X + 5' 2>&1 | wc -lmake # Must pass
make test-all # Full regression-
Phase 2: AST-level constant sub resolution (2026-03-30)
- Added
currentPackagefield andfoldConstants(Node, String)overload toConstantFoldingVisitor - Added
resolveConstantSubValue()helper for looking upRuntimeCode.constantValuefrom global symbol table visit(BinaryOperatorNode): resolvesCONSTANT()calls (bothOperatorNode("&", IdentifierNode)and bareIdentifierNodeAST patterns) with empty argsvisit(IdentifierNode): resolves bare constant identifiers (e.g.,PI)- Added
foldChild()instance method to propagatecurrentPackagethrough recursive folding - Only inlines scalar constants (INTEGER, DOUBLE, STRING); skips list constants, references, undef
- Files:
ConstantFoldingVisitor.java
- Added
-
Phase 3: Enable visitor in compilation pipeline (2026-03-30)
- Enabled
ConstantFoldingVisitor.foldConstants(ast, currentPackage)inPerlLanguageProvider.java(main script compilation) - Enabled in
RuntimeCode.java(eval compilation) - Runs after parsing (BEGIN blocks have executed, constants are defined) and before code emission
- Files:
PerlLanguageProvider.java,RuntimeCode.java
- Enabled
-
Phase 4: Extract shared utility (2026-03-30)
- Moved
getConstantConditionValue()fromEmitStatement.javaandBytecodeCompiler.javatoConstantFoldingVisitor - Added
resolveConstantSubBoolean()private helper to deduplicate constant sub boolean resolution - Both
EmitStatementandBytecodeCompilernow callConstantFoldingVisitor.getConstantConditionValue() - Eliminated ~100 lines of duplicated code
- Files:
ConstantFoldingVisitor.java,EmitStatement.java,BytecodeCompiler.java
- Moved
- Phase 1: Emit-time constant sub inlining — redundant after Phase 2+3; the AST-level folding already replaces constant sub calls with literal nodes before emission reaches
EmitSubroutine.handleApplyOperator()
- Extended
src/test/resources/unit/constant.tfrom 10 to 31 tests - New tests cover: arithmetic folding with constants, cascading folds, string concatenation, dead code elimination, list/reference constant safety, nested expressions, ternary conditions, logical operators, eval context
- The
--interpreterbackend cannot runuse constantdue to a pre-existingexists() slow path not yet implementederror inSlowOpcodeHandler.java. This is unrelated to constant folding but blocks interpreter-mode testing of constant subs.