This document explains how PerlOnJava implements Perl's dynamic scoping via the local keyword and how the same mechanism is used for other features.
Perl's local keyword provides dynamic scoping: it temporarily saves a variable's value and restores it when the current scope exits. This is different from lexical scoping (my), which creates a new variable visible only in the current block.
$x = "global";
sub foo {
local $x = "local";
bar(); # sees $x = "local"
}
sub bar {
print $x; # prints "local" when called from foo()
}
foo();
print $x; # prints "global"All values that can be dynamically scoped implement DynamicState:
public interface DynamicState {
void dynamicSaveState(); // Save current state
void dynamicRestoreState(); // Restore saved state
}Implementations:
RuntimeScalar- scalar variablesRuntimeArray- array variablesRuntimeHash- hash variablesRuntimeGlob- typeglobsGlobalRuntimeArray- global array localization (local @array)GlobalRuntimeHash- global hash localization (local %hash)DeferBlock- defer block executionRegexState- regex match state ($1, $2, etc.)
Manages a stack of saved states:
public class DynamicVariableManager {
private static final Deque<DynamicState> variableStack = new ArrayDeque<>();
// Save current state and push onto stack (4 overloads)
public static RuntimeBase pushLocalVariable(RuntimeBase variable) // returns variable
public static RuntimeScalar pushLocalVariable(RuntimeScalar variable) // returns variable
public static RuntimeGlob pushLocalVariable(RuntimeGlob variable) // special: returns new glob from GlobalVariable.getGlobalIO()
public static void pushLocalVariable(DynamicState variable) // for DeferBlock, RegexState
// Each overload calls variable.dynamicSaveState() and variableStack.addLast(variable).
// The RuntimeGlob overload has special behavior: it returns the new glob obtained
// from GlobalVariable.getGlobalIO(), not the original variable.
// Restore all states back to a saved level
public static void popToLocalLevel(int targetLocalLevel) {
while (variableStack.size() > targetLocalLevel) {
DynamicState variable = variableStack.removeLast();
variable.dynamicRestoreState();
}
}
// Get current stack level (saved at block entry)
public static int getLocalLevel() {
return variableStack.size();
}
}Each RuntimeScalar has its own save stack:
public class RuntimeScalar implements DynamicState {
private static final Stack<RuntimeScalar> dynamicStateStack = new Stack<>();
@Override
public void dynamicSaveState() {
// Save a copy of current state
RuntimeScalar copy = new RuntimeScalar();
copy.type = this.type;
copy.value = this.value;
copy.blessId = this.blessId;
dynamicStateStack.push(copy);
// Reset to undef — this is the key `local` behavior:
// the variable is cleared after saving
this.type = UNDEF;
this.value = null;
this.blessId = 0;
}
@Override
public void dynamicRestoreState() {
RuntimeScalar saved = dynamicStateStack.pop();
this.type = saved.type;
this.value = saved.value;
this.blessId = saved.blessId;
}
}When the compiler sees local $x:
-
Block Entry: Save current local level
int savedLevel = DynamicVariableManager.getLocalLevel();
-
Local Assignment: Save and modify variable
DynamicVariableManager.pushLocalVariable(variable); variable.set(newValue);
-
Block Exit: Restore all local variables (in finally block)
DynamicVariableManager.popToLocalLevel(savedLevel);
FindDeclarationVisitor scans AST blocks to detect if local is used:
public static boolean containsLocalOrDefer(Node blockNode) {
FindDeclarationVisitor visitor = new FindDeclarationVisitor();
visitor.operatorName = "local";
blockNode.accept(visitor);
return visitor.containsLocalOperator || visitor.containsDefer;
}This allows the compiler to skip local setup/teardown for blocks that don't need it.
The same mechanism is used for several other features:
defer { ... } blocks execute code when scope exits:
{
defer { print "cleanup\n" }
print "work\n";
} # prints: work, cleanupImplementation:
public class DeferBlock implements DynamicState {
private final RuntimeScalar codeRef;
private final RuntimeArray capturedArgs; // captures enclosing subroutine's @_
@Override
public void dynamicRestoreState() {
// Execute the defer block (static call, uses capturedArgs)
RuntimeCode.apply(codeRef, capturedArgs, RuntimeContextType.VOID);
}
}Match variables ($1, $2, $&, etc.) are saved/restored:
public class RegexState implements DynamicState {
// Saves: captureGroups, lastMatch, prematch, postmatch, etc.
}This ensures regex state is properly scoped in nested matches.
Runtime warning suppression uses local semantics:
{
no warnings 'DateTime'; # Sets local ${^WARNING_SCOPE} = scopeId
DateTime->new(...); # warnif() checks ${^WARNING_SCOPE}
} # ${^WARNING_SCOPE} restored to 0The CompilerFlagNode emits:
GlobalRuntimeScalar.makeLocal("${^WARNING_SCOPE}");
scopeVar.set(scopeId);local $SIG{__WARN__} and local $SIG{__DIE__} use the same mechanism:
{
local $SIG{__WARN__} = sub { ... };
# warnings go to custom handler
} # original handler restoredpopToLocalLevel() is exception-safe:
public static void popToLocalLevel(int targetLevel) {
Throwable pendingException = null;
while (variableStack.size() > targetLevel) {
DynamicState variable = variableStack.removeLast();
try {
variable.dynamicRestoreState();
} catch (Throwable t) {
// Continue cleanup, remember last exception
pendingException = t;
}
}
// Re-throw after all cleanup
if (pendingException != null) {
throw pendingException;
}
}This ensures:
- All local variables are restored even if one throws
- Defer blocks all execute even if one throws
- The last exception "wins" (Perl semantics)
- Stack Allocation: Uses
ArrayDeque(no synchronization overhead) - Lazy Detection:
containsLocalOrDefer()avoids setup for blocks withoutlocal - Per-Variable Stacks: Each variable type manages its own save stack
| File | Purpose |
|---|---|
DynamicState.java |
Interface for saveable state |
DynamicVariableManager.java |
Central stack management |
RuntimeScalar.java |
Scalar save/restore |
RuntimeArray.java |
Array save/restore |
RuntimeHash.java |
Hash save/restore |
GlobalRuntimeArray.java |
Implements DynamicState for global array localization |
GlobalRuntimeHash.java |
Implements DynamicState for global hash localization |
GlobalRuntimeScalar.java |
Contains makeLocal(), the primary entry point for local $scalar |
DeferBlock.java |
Defer block execution |
RegexState.java |
Regex state save/restore |
Local.java |
Code generation helpers |
FindDeclarationVisitor.java |
Detection of local usage |
- lexical-pragmas.md - How warnings/strict use this mechanism
- ../design/lexical-warnings.md - Warning scope design