diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 3fb350138..7ca1aaf81 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -4,6 +4,8 @@ Release history of PerlOnJava. See [Roadmap](roadmap.md) for future plans. ## Work in progress +- Fix format declarations being discarded during compilation, unblocking `write` execution. + - Clear weakened references after a nested method releases its final array-slot owner, restoring `Algorithm::SlidingWindow` eviction and clear behavior on both execution backends. diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 8a739bacb..437c3ae9a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -7296,9 +7296,13 @@ public void visit(CompilerFlagNode node) { @Override public void visit(FormatNode node) { - // Format declarations are handled at the JVM compilation stage. - // When the interpreter backend processes the AST, formats are already - // registered, so this is a no-op. + // 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. + RuntimeFormat format = new RuntimeFormat(node.formatName); + format.setCompiledLines(node.templateLines); + emit(Opcodes.REGISTER_FORMAT); + emit(addToConstantPool(format)); } @Override diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 52046fd37..4f0391002 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -790,6 +790,12 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { registers[rd] = (RuntimeBase) code.constants[constIndex]; } + case Opcodes.REGISTER_FORMAT -> { + int constIndex = bytecode[pc++]; + RuntimeFormat format = (RuntimeFormat) code.constants[constIndex]; + GlobalVariable.setGlobalFormatRef(format.formatName, format); + } + case Opcodes.LOAD_INT -> { // Load integer: rd = immediate (create NEW mutable scalar, not cached) int rd = bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 42d6e724f..8173c236f 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -2534,6 +2534,9 @@ public class Opcodes { /** Resolve a direct named call with a call-site CV cache. Format: rd nameStringIdx cacheConstIdx. */ public static final short DIRECT_NAMED_CODE_CALL = 535; + /** Register a format declaration from a constant RuntimeFormat. Format: REGISTER_FORMAT constantIdx. */ + public static final short REGISTER_FORMAT = 536; + /** Return the mutable {@code $#array} cell. Format: ARRAY_LAST_INDEX_LVALUE rd arrayReg. */ public static final short ARRAY_LAST_INDEX_LVALUE = 518; diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitBlock.java b/src/main/java/org/perlonjava/backend/jvm/EmitBlock.java index 7577f17bd..9d42d92ce 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitBlock.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitBlock.java @@ -165,7 +165,6 @@ public static void emitBlock(EmitterVisitor emitterVisitor, BlockNode node) { Node elem = list.get(i); if (elem != null && !(elem instanceof CompilerFlagNode) - && !(elem instanceof FormatNode) && !(elem instanceof AbstractNode ab && (ab.getBooleanAnnotation("compileTimeOnly") || ab.getBooleanAnnotation("noReturnValue")))) { lastNonNullIndex = i; break; @@ -306,6 +305,10 @@ public static void emitBlock(EmitterVisitor emitterVisitor, BlockNode node) { Object resultRegObj = node.getAnnotation("resultRegister"); int resultReg = (resultRegObj instanceof Integer) ? (Integer) resultRegObj : -1; + // Format declarations have no runtime value, but their registration + // is a required compile-time side effect. Visit them in VOID + // context so write FILEHANDLE can find the declared format. + // EmitFormat itself discards the resulting RuntimeFormat value. // Emit the statement with current context if (i == lastNonNullIndex) { // Special case for the last element diff --git a/src/main/java/org/perlonjava/frontend/analysis/ConstantFoldingVisitor.java b/src/main/java/org/perlonjava/frontend/analysis/ConstantFoldingVisitor.java index c5181e8df..29735cebf 100644 --- a/src/main/java/org/perlonjava/frontend/analysis/ConstantFoldingVisitor.java +++ b/src/main/java/org/perlonjava/frontend/analysis/ConstantFoldingVisitor.java @@ -244,7 +244,11 @@ public void visit(FormatLine node) { @Override public void visit(FormatNode node) { - // Default implementation - no action needed for format nodes + // Format declarations are side-effecting nodes. Preserve them while + // folding their enclosing block so the backend can register the + // declaration before a later write() executes. + result = node; + isConstant = false; } @Override diff --git a/src/test/resources/unit/format_write_regression.t b/src/test/resources/unit/format_write_regression.t new file mode 100644 index 000000000..3f4ca2fbf --- /dev/null +++ b/src/test/resources/unit/format_write_regression.t @@ -0,0 +1,15 @@ +use strict; +use warnings; +use Test::More tests => 1; + +format STDOUT = +. + +my $error; +{ + local $@; + eval { write }; + $error = $@; +} + +ok !$error, 'write registers and executes a declared format';