From 6a310ee755ec84ca9184f860fbd98efc0588956f Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 7 Sep 2026 17:24:24 +0200 Subject: [PATCH 1/4] wip: capture issue 1283 regression Add a focused Params::Validate regression test for omitted optional arguments before diagnosing the shared backend failure. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- .../unit/params_validate_optional_argument.t | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/test/resources/unit/params_validate_optional_argument.t diff --git a/src/test/resources/unit/params_validate_optional_argument.t b/src/test/resources/unit/params_validate_optional_argument.t new file mode 100644 index 000000000..a7a79e2a8 --- /dev/null +++ b/src/test/resources/unit/params_validate_optional_argument.t @@ -0,0 +1,31 @@ +use strict; +use warnings; +use Test::More; + +use Params::Validate qw(validate SCALAR); + +sub validate_named_arguments { + my %arguments = validate( + @_, { + required => { type => SCALAR }, + optional => { type => SCALAR, optional => 1 }, + }, + ); + + return exists $arguments{optional} ? 'explicit-undef' : 'omitted'; +} + +is( + validate_named_arguments(required => 'value'), + 'omitted', + 'omitted optional named arguments are not materialized as undef', +); + +eval { validate_named_arguments(required => 'value', optional => undef) }; +like( + $@, + qr/The 'optional' parameter \(undef\).*not one of the allowed types: scalar/, + 'explicit undef remains distinguishable from an omitted argument', +); + +done_testing; From 21621072cd7f64f89a068010829c834ba68146a0 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 7 Sep 2026 17:34:02 +0200 Subject: [PATCH 2/4] fix: return undef for false if expressions without else Initialize false conditional paths rather than reusing a stale branch register. This preserves omitted optional arguments in Params::Validate and fixes DateTime::Format::Builder loading (#1283). Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- docs/about/changelog.md | 3 +++ .../backend/bytecode/BytecodeCompiler.java | 19 +++++++++++++++---- .../perlonjava/backend/jvm/EmitStatement.java | 15 ++++++--------- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/docs/about/changelog.md b/docs/about/changelog.md index f7d3298da..b25da30fd 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -8,6 +8,9 @@ priorities and future plans. - Preserve UTF-8 HTML octets through HTML::Parser and no-op entity decoding, restoring complete Thai text in HTML::Formatter output. + +- Return `undef` from false `if` expressions without an `else`, preserving + omitted optional arguments for `Params::Validate` and DateTime formatters. - Preserve caller-owned array and hash lifetimes across generated coercion callbacks, so `Types::Const` freezes cloned values without modifying the original reference. diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 0c1b4b302..30ae9a805 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -7417,11 +7417,22 @@ public void visit(IfNode node) { lastResultReg = thenResultReg >= 0 ? thenResultReg : elseResultReg; } else { - // No else block - patch if-false jump to here (after then block) - int endPos = bytecode.size(); - patchIntOffset(ifFalsePos + 2, endPos); + // A statement-form if without an else returns undef when false. + // Without an explicit false-path value, the register used by the + // then branch can retain stale data from an earlier expression. + int resultReg = thenResultReg >= 0 ? thenResultReg : allocateOutputRegister(); + int gotoEndPos = bytecode.size(); + emit(Opcodes.GOTO); + emitInt(0); + + int falseStart = bytecode.size(); + patchIntOffset(ifFalsePos + 2, falseStart); + emit(Opcodes.LOAD_UNDEF); + emitReg(resultReg); - lastResultReg = thenResultReg; + int endPos = bytecode.size(); + patchIntOffset(gotoEndPos + 1, endPos); + lastResultReg = resultReg; } symbolTable.exitScope(ifScopeIndex); } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java b/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java index 5fda88ef7..f156365b3 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java @@ -464,10 +464,10 @@ public static void emitIf(EmitterVisitor emitterVisitor, IfNode node) { emitterVisitor.ctx.javaClassInfo.popGotoLabels(); } } else { - // No else branch - emit condition value if not void context - // Perl returns the condition value when no branch is taken + // A statement-form if without an else yields undef when + // its condition is false. if (emitterVisitor.ctx.contextType != RuntimeContextType.VOID) { - node.condition.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + EmitOperator.emitUndef(emitterVisitor.ctx.mv); } } } @@ -488,9 +488,7 @@ public static void emitIf(EmitterVisitor emitterVisitor, IfNode node) { Label elseLabel = new Label(); Label endLabel = new Label(); - // When there's no else branch and we need a result value, DUP the condition - // so the condition value is returned when no branch is taken (Perl semantics) - boolean needConditionValue = (node.elseBranch == null && emitterVisitor.ctx.contextType != RuntimeContextType.VOID); + boolean needConditionValue = false; // An elsif is an else-branch AST child rather than an EmitBlock statement, // so publish its own COP while compiling its condition. This prevents a @@ -531,10 +529,9 @@ public static void emitIf(EmitterVisitor emitterVisitor, IfNode node) { // Visit the else branch if it exists if (node.elseBranch != null) { node.elseBranch.accept(branchVisitor(emitterVisitor, node.elseBranch)); - } else if (!needConditionValue) { - // VOID context, no value needed on stack + } else if (emitterVisitor.ctx.contextType != RuntimeContextType.VOID) { + EmitOperator.emitUndef(emitterVisitor.ctx.mv); } - // else: needConditionValue is true, DUPed condition value is already on stack // Visit the end label emitterVisitor.ctx.mv.visitLabel(endLabel); From f98870f2d34cf3a6f2b96bf75b060a5fd96e3700 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 7 Sep 2026 19:56:28 +0200 Subject: [PATCH 3/4] fix: preserve false if condition values Return the false condition value from an if expression without an else while explicitly materializing the interpreter result register. This keeps omitted optional arguments distinct without regressing Perl conditional semantics. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- .../backend/bytecode/BytecodeCompiler.java | 13 +++++++------ .../perlonjava/backend/jvm/EmitStatement.java | 15 +++++++++------ .../unit/params_validate_optional_argument.t | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 30ae9a805..c3ce53ec9 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -7350,7 +7350,9 @@ public void visit(IfNode node) { if (node.elseBranch != null) { node.elseBranch.accept(this); } else { - lastResultReg = -1; + // Perl returns the evaluated condition when an if without + // an else does not take its branch. + compileNode(node.condition, -1, RuntimeContextType.SCALAR); } } return; @@ -7417,9 +7419,9 @@ public void visit(IfNode node) { lastResultReg = thenResultReg >= 0 ? thenResultReg : elseResultReg; } else { - // A statement-form if without an else returns undef when false. - // Without an explicit false-path value, the register used by the - // then branch can retain stale data from an earlier expression. + // Perl returns the evaluated condition when an if without an else + // does not take its branch. Materialize it in the conditional's + // result register so a skipped then branch cannot expose stale data. int resultReg = thenResultReg >= 0 ? thenResultReg : allocateOutputRegister(); int gotoEndPos = bytecode.size(); emit(Opcodes.GOTO); @@ -7427,8 +7429,7 @@ public void visit(IfNode node) { int falseStart = bytecode.size(); patchIntOffset(ifFalsePos + 2, falseStart); - emit(Opcodes.LOAD_UNDEF); - emitReg(resultReg); + emitAliasWithTarget(resultReg, condReg); int endPos = bytecode.size(); patchIntOffset(gotoEndPos + 1, endPos); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java b/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java index f156365b3..246a271d1 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitStatement.java @@ -464,10 +464,10 @@ public static void emitIf(EmitterVisitor emitterVisitor, IfNode node) { emitterVisitor.ctx.javaClassInfo.popGotoLabels(); } } else { - // A statement-form if without an else yields undef when - // its condition is false. + // No else branch - Perl returns the condition value when + // no branch is taken. if (emitterVisitor.ctx.contextType != RuntimeContextType.VOID) { - EmitOperator.emitUndef(emitterVisitor.ctx.mv); + node.condition.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); } } } @@ -488,7 +488,9 @@ public static void emitIf(EmitterVisitor emitterVisitor, IfNode node) { Label elseLabel = new Label(); Label endLabel = new Label(); - boolean needConditionValue = false; + // When there's no else branch and we need a result value, DUP the condition + // so the condition value is returned when no branch is taken (Perl semantics). + boolean needConditionValue = (node.elseBranch == null && emitterVisitor.ctx.contextType != RuntimeContextType.VOID); // An elsif is an else-branch AST child rather than an EmitBlock statement, // so publish its own COP while compiling its condition. This prevents a @@ -529,9 +531,10 @@ public static void emitIf(EmitterVisitor emitterVisitor, IfNode node) { // Visit the else branch if it exists if (node.elseBranch != null) { node.elseBranch.accept(branchVisitor(emitterVisitor, node.elseBranch)); - } else if (emitterVisitor.ctx.contextType != RuntimeContextType.VOID) { - EmitOperator.emitUndef(emitterVisitor.ctx.mv); + } else if (!needConditionValue) { + // VOID context, no value needed on stack } + // else: needConditionValue is true, DUPed condition value is already on stack // Visit the end label emitterVisitor.ctx.mv.visitLabel(endLabel); diff --git a/src/test/resources/unit/params_validate_optional_argument.t b/src/test/resources/unit/params_validate_optional_argument.t index a7a79e2a8..17893df5f 100644 --- a/src/test/resources/unit/params_validate_optional_argument.t +++ b/src/test/resources/unit/params_validate_optional_argument.t @@ -15,6 +15,22 @@ sub validate_named_arguments { return exists $arguments{optional} ? 'explicit-undef' : 'omitted'; } +sub false_if_result { + if ($_[0]) { 'taken' } +} + +is( + false_if_result(0), + 0, + 'an untaken if without else returns its false condition value', +); + +is( + false_if_result(1), + 'taken', + 'a taken if without else returns its branch value', +); + is( validate_named_arguments(required => 'value'), 'omitted', From 5628d08cff6bdea61d0e6132c0259baf532f0763 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 7 Sep 2026 20:47:37 +0200 Subject: [PATCH 4/4] test: remove Params::Validate CI dependency Keep the issue 1283 regression self-contained so it runs in the standard unit test environment without requiring a locally installed CPAN module. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- .../unit/params_validate_optional_argument.t | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/test/resources/unit/params_validate_optional_argument.t b/src/test/resources/unit/params_validate_optional_argument.t index 17893df5f..c1c3766d1 100644 --- a/src/test/resources/unit/params_validate_optional_argument.t +++ b/src/test/resources/unit/params_validate_optional_argument.t @@ -2,15 +2,26 @@ use strict; use warnings; use Test::More; -use Params::Validate qw(validate SCALAR); - sub validate_named_arguments { - my %arguments = validate( - @_, { - required => { type => SCALAR }, - optional => { type => SCALAR, optional => 1 }, - }, + my %input = @_; + my %specification = ( + required => { optional => 0 }, + optional => { optional => 1 }, ); + my %arguments; + + OUTER: for my $name (qw(required optional)) { + my $value = do { + if (exists $specification{$name}{default}) { + $specification{$name}{default}; + } + } || do { + next OUTER if $specification{$name}{optional} && !exists $input{$name}; + $input{$name}; + }; + + $arguments{$name} = $value; + } return exists $arguments{optional} ? 'explicit-undef' : 'omitted'; } @@ -37,10 +48,9 @@ is( 'omitted optional named arguments are not materialized as undef', ); -eval { validate_named_arguments(required => 'value', optional => undef) }; -like( - $@, - qr/The 'optional' parameter \(undef\).*not one of the allowed types: scalar/, +is( + validate_named_arguments(required => 'value', optional => undef), + 'explicit-undef', 'explicit undef remains distinguishable from an omitted argument', );