Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ priorities and future plans.

## Work in progress

- Fix parsing of dense Mo::Inline expressions that use `::` as a bareword.

- Preserve UTF-8 HTML octets through HTML::Parser and no-op entity decoding,
restoring complete Thai text in HTML::Formatter output.
- Preserve caller-owned array and hash lifetimes across generated coercion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1347,7 +1347,9 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) {

case Opcodes.COMPARE_NUM, Opcodes.COMPARE_STR, Opcodes.EQ_NUM, Opcodes.NE_NUM,
Opcodes.LT_NUM, Opcodes.GT_NUM, Opcodes.LE_NUM, Opcodes.GE_NUM, Opcodes.EQ_STR,
Opcodes.NE_STR, Opcodes.NOT, Opcodes.NOT_NO_OVERLOAD -> {
Opcodes.NE_STR, Opcodes.EQU_STR, Opcodes.NEU_STR,
Opcodes.STRICT_EQ_NUM, Opcodes.STRICT_NE_NUM,
Opcodes.NOT, Opcodes.NOT_NO_OVERLOAD -> {
pc = executeComparisons(opcode, bytecode, pc, registers);
}

Expand Down Expand Up @@ -3580,6 +3582,23 @@ private static int executeComparisons(int opcode, int[] bytecode, int pc,
return pc;
}

case Opcodes.EQU_STR, Opcodes.NEU_STR, Opcodes.STRICT_EQ_NUM, Opcodes.STRICT_NE_NUM -> {
int rd = bytecode[pc++];
int rs1 = bytecode[pc++];
int rs2 = bytecode[pc++];
RuntimeBase val1 = registers[rs1];
RuntimeBase val2 = registers[rs2];
RuntimeScalar s1 = (val1 instanceof RuntimeScalar) ? (RuntimeScalar) val1 : val1.scalar();
RuntimeScalar s2 = (val2 instanceof RuntimeScalar) ? (RuntimeScalar) val2 : val2.scalar();
registers[rd] = switch (opcode) {
case Opcodes.EQU_STR -> CompareOperators.equ(s1, s2);
case Opcodes.NEU_STR -> CompareOperators.neu(s1, s2);
case Opcodes.STRICT_EQ_NUM -> CompareOperators.strictEqual(s1, s2);
default -> CompareOperators.strictNotEqual(s1, s2);
};
return pc;
}

case Opcodes.NOT -> {
int rd = bytecode[pc++];
int rs = bytecode[pc++];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import org.perlonjava.runtime.runtimetypes.RuntimeContextType;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CompileBinaryOperator {
static void visitBinaryOperator(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node) {
int savedCallerLineOverride = bytecodeCompiler.callerLineTokenOverride;
Expand All @@ -30,6 +34,16 @@ private static void visitBinaryOperatorBody(BytecodeCompiler bytecodeCompiler, B
// Track token index for error reporting
bytecodeCompiler.currentTokenIndex = node.getIndex();

// Perl evaluates chained comparisons left-to-right, evaluating each
// operand once and stopping as soon as a comparison is false. The JVM
// backend has a dedicated emitter for this shape; keep the bytecode
// interpreter backend equivalent instead of compiling the nested AST as
// ordinary boolean comparisons.
if (isChainedComparison(node)) {
compileChainedComparison(bytecodeCompiler, node);
return;
}

// Handle print/say early (special handling for filehandle)
if (node.operator.equals("print") || node.operator.equals("say")) {
// print/say FILEHANDLE LIST
Expand Down Expand Up @@ -798,6 +812,71 @@ else if (node.right instanceof BinaryOperatorNode rightCall) {
bytecodeCompiler.lastResultReg = rd;
}

private static final List<String> CHAIN_COMPARISON_OPS =
Arrays.asList("<", ">", "<=", ">=", "lt", "gt", "le", "ge");
private static final List<String> CHAIN_EQUALITY_OPS =
Arrays.asList("==", "!=", "===", "!==", "eq", "ne", "equ", "neu");

private static boolean isChainedComparison(BinaryOperatorNode node) {
if (!(node.left instanceof BinaryOperatorNode left)) {
return false;
}
boolean equality = CHAIN_EQUALITY_OPS.contains(node.operator);
boolean comparison = CHAIN_COMPARISON_OPS.contains(node.operator);
if (!equality && !comparison) {
return false;
}
return (equality && CHAIN_EQUALITY_OPS.contains(left.operator))
|| (comparison && CHAIN_COMPARISON_OPS.contains(left.operator));
}

private static void compileChainedComparison(BytecodeCompiler bytecodeCompiler,
BinaryOperatorNode node) {
List<Node> operands = new ArrayList<>();
List<String> operators = new ArrayList<>();
BinaryOperatorNode current = node;
boolean equality = CHAIN_EQUALITY_OPS.contains(node.operator);
while (true) {
operators.add(0, current.operator);
operands.add(0, current.right);
if (current.left instanceof BinaryOperatorNode left
&& ((equality && CHAIN_EQUALITY_OPS.contains(left.operator))
|| (!equality && CHAIN_COMPARISON_OPS.contains(left.operator)))) {
current = left;
} else {
operands.add(0, current.left);
break;
}
}

int resultReg = bytecodeCompiler.allocateOutputRegister();
bytecodeCompiler.compileNode(operands.get(0), -1, RuntimeContextType.SCALAR);
int leftReg = bytecodeCompiler.lastResultReg;
List<Integer> falseJumpPositions = new ArrayList<>();

for (int i = 0; i < operators.size(); i++) {
bytecodeCompiler.compileNode(operands.get(i + 1), -1, RuntimeContextType.SCALAR);
int rightReg = bytecodeCompiler.lastResultReg;
int comparisonReg = CompileBinaryOperatorHelper.compileBinaryOperatorSwitch(
bytecodeCompiler, operators.get(i), leftReg, rightReg, node.getIndex());
bytecodeCompiler.emitAliasWithTarget(resultReg, comparisonReg);

if (i + 1 < operators.size()) {
bytecodeCompiler.emit(bytecodeCompiler.gotoIfFalseOpcode());
bytecodeCompiler.emitReg(resultReg);
falseJumpPositions.add(bytecodeCompiler.bytecode.size());
bytecodeCompiler.emitInt(0);
}
leftReg = rightReg;
}

int endPc = bytecodeCompiler.bytecode.size();
for (int patchPosition : falseJumpPositions) {
bytecodeCompiler.patchIntOffset(patchPosition, endPc);
}
bytecodeCompiler.lastResultReg = resultReg;
}

private static boolean isDirectScalarLvalue(Node node) {
if (node instanceof OperatorNode operator) {
return operator.operator.equals("$");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,30 @@ private static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler
bytecodeCompiler.emitReg(rs1);
bytecodeCompiler.emitReg(rs2);
}
case "equ" -> {
bytecodeCompiler.emit(Opcodes.EQU_STR);
bytecodeCompiler.emitReg(rd);
bytecodeCompiler.emitReg(rs1);
bytecodeCompiler.emitReg(rs2);
}
case "neu" -> {
bytecodeCompiler.emit(Opcodes.NEU_STR);
bytecodeCompiler.emitReg(rd);
bytecodeCompiler.emitReg(rs1);
bytecodeCompiler.emitReg(rs2);
}
case "===" -> {
bytecodeCompiler.emit(Opcodes.STRICT_EQ_NUM);
bytecodeCompiler.emitReg(rd);
bytecodeCompiler.emitReg(rs1);
bytecodeCompiler.emitReg(rs2);
}
case "!==" -> {
bytecodeCompiler.emit(Opcodes.STRICT_NE_NUM);
bytecodeCompiler.emitReg(rd);
bytecodeCompiler.emitReg(rs1);
bytecodeCompiler.emitReg(rs2);
}
case "lt", "gt", "le", "ge" -> {
// String comparisons using COMPARE_STR (like cmp)
// cmp returns: -1 if $a lt $b, 0 if equal, 1 if $a gt $b
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/org/perlonjava/backend/bytecode/Opcodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,18 @@ public class Opcodes {
*/
public static final short NE_STR = 38;

/** Defined string equality: equ. */
public static final short EQU_STR = 547;

/** Defined string inequality: neu. */
public static final short NEU_STR = 548;

/** Defined numeric equality: ===. */
public static final short STRICT_EQ_NUM = 552;

/** Defined numeric inequality: !==. */
public static final short STRICT_NE_NUM = 553;

// =================================================================
// LOGICAL OPERATORS (39-41)
// =================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ public static void emitBinaryOperatorNode(EmitterVisitor emitterVisitor, BinaryO

// Comparison operators (chained)
case "<", ">", "<=", ">=", "lt", "gt", "le", "ge",
"==", "!=", "eq", "ne" -> EmitOperatorChained.emitChainedComparison(emitterVisitor, node);
"==", "!=", "===", "!==", "eq", "ne", "equ", "neu" ->
EmitOperatorChained.emitChainedComparison(emitterVisitor, node);

// Binary operators
case "%", "&", "&.", "binary&", "*", "**", "+", "-", "/",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

public class EmitOperatorChained {
public static final String[] CHAIN_COMPARISON_OP = new String[]{"<", ">", "<=", ">=", "lt", "gt", "le", "ge"};
public static final String[] CHAIN_EQUALITY_OP = new String[]{"==", "!=", "eq", "ne"};
public static final String[] CHAIN_EQUALITY_OP = new String[]{"==", "!=", "===", "!==", "eq", "ne", "equ", "neu"};

static public void emitChainedComparison(EmitterVisitor emitterVisitor, BinaryOperatorNode node) {
EmitterVisitor scalarVisitor =
Expand Down
15 changes: 3 additions & 12 deletions src/main/java/org/perlonjava/frontend/parser/ParseInfix.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public class ParseInfix {
private static final List<String> NON_CHAINABLE_RELATIONAL_OPS = List.of("isa");

// Chainable equality operators (can chain with each other)
private static final List<String> CHAINABLE_EQUALITY_OPS = Arrays.asList("==", "!=", "eq", "ne");
private static final List<String> CHAINABLE_EQUALITY_OPS =
Arrays.asList("==", "!=", "===", "!==", "eq", "ne", "equ", "neu");

// Chainable relational operators (can chain with each other)
private static final List<String> CHAINABLE_RELATIONAL_OPS = Arrays.asList("<", ">", "<=", ">=", "lt", "gt", "le", "ge");
Expand All @@ -59,17 +60,7 @@ public static Node parseInfixOperation(Parser parser, Node left, int precedence)
Node right;

if (ParserTables.INFIX_OP.contains(token.text)) {
String operator = switch (token.text) {
// Perl 5.44's undef-aware comparison operators currently share
// the established numeric/string comparison execution paths.
// Preserve their precedence and parseability while the runtime
// comparison implementation remains centralized.
case "===" -> "==";
case "!==" -> "!=";
case "equ" -> "eq";
case "neu" -> "ne";
default -> token.text;
};
String operator = token.text;

// Check if left operand is a DECLARED REFERENCE (my \$a, our \@arr, etc.)
// Most operators cannot be applied to declared references
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,14 @@ static Node parseOperator(Parser parser, LexerToken token, String operator) {
return new StringNode("::" + identifierName, parser.tokenIndex);
}
}
// In an expression such as `$pkg.::.':E'`, Perl treats `::`
// as a bareword string. This spelling is emitted by
// Mo::Inline and is especially common inside braced hash
// dereferences. It is not a package-qualified subroutine
// when the following token is the concatenation operator.
if (nextToken2.text.equals(".")) {
return new StringNode("::", parser.tokenIndex);
}
throw new PerlCompilerException(parser.tokenIndex, "syntax error", parser.ctx.errorUtil);

case "\\":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public class ParserTables {
CORE_PROTOTYPES.put("endservent", "");
CORE_PROTOTYPES.put("eof", ";*");
CORE_PROTOTYPES.put("eq", null);
CORE_PROTOTYPES.put("equ", null);
CORE_PROTOTYPES.put("eval", null);
CORE_PROTOTYPES.put("evalbytes", "_");
CORE_PROTOTYPES.put("exec", null);
Expand Down Expand Up @@ -195,6 +196,7 @@ public class ParserTables {
CORE_PROTOTYPES.put("msgsnd", "$$$");
CORE_PROTOTYPES.put("my", null);
CORE_PROTOTYPES.put("ne", null);
CORE_PROTOTYPES.put("neu", null);
CORE_PROTOTYPES.put("next", null);
CORE_PROTOTYPES.put("no", null);
CORE_PROTOTYPES.put("not", "$;");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,71 @@ public static RuntimeScalar ne(RuntimeScalar runtimeScalar, RuntimeScalar arg2)
return getScalarBoolean(!stringEquals(runtimeScalar, arg2));
}

/**
* Defined string equality ({@code equ}). Unlike {@code eq}, undef is a
* value in its own right: two undefs compare equal, while undef and any
* defined value compare unequal without producing an uninitialized warning.
*/
public static RuntimeScalar equ(RuntimeScalar arg1, RuntimeScalar arg2) {
arg1 = fetchDefinedComparisonOperand(arg1);
arg2 = fetchDefinedComparisonOperand(arg2);
boolean defined1 = arg1.getDefinedBoolean();
boolean defined2 = arg2.getDefinedBoolean();
if (!defined1 || !defined2) {
return getScalarBoolean(defined1 == defined2);
}
return eq(arg1, arg2);
}

/** Defined string inequality ({@code neu}), the inverse of {@code equ}. */
public static RuntimeScalar neu(RuntimeScalar arg1, RuntimeScalar arg2) {
arg1 = fetchDefinedComparisonOperand(arg1);
arg2 = fetchDefinedComparisonOperand(arg2);
boolean defined1 = arg1.getDefinedBoolean();
boolean defined2 = arg2.getDefinedBoolean();
if (!defined1 || !defined2) {
return getScalarBoolean(defined1 != defined2);
}
return ne(arg1, arg2);
}

/**
* Defined numeric equality ({@code ===}). It has the same overload
* behavior as {@code ==}, but does not coerce undef or warn about it.
*/
public static RuntimeScalar strictEqual(RuntimeScalar arg1, RuntimeScalar arg2) {
arg1 = fetchDefinedComparisonOperand(arg1);
arg2 = fetchDefinedComparisonOperand(arg2);
boolean defined1 = arg1.getDefinedBoolean();
boolean defined2 = arg2.getDefinedBoolean();
if (!defined1 || !defined2) {
return getScalarBoolean(defined1 == defined2);
}
return equalTo(arg1, arg2);
}

/** Defined numeric inequality ({@code !==}), the inverse of {@code ===}. */
public static RuntimeScalar strictNotEqual(RuntimeScalar arg1, RuntimeScalar arg2) {
arg1 = fetchDefinedComparisonOperand(arg1);
arg2 = fetchDefinedComparisonOperand(arg2);
boolean defined1 = arg1.getDefinedBoolean();
boolean defined2 = arg2.getDefinedBoolean();
if (!defined1 || !defined2) {
return getScalarBoolean(defined1 != defined2);
}
return notEqualTo(arg1, arg2);
}

/**
* Fetch a tied operand once before the definedness check and subsequent
* comparison. Calling {@code getDefinedBoolean()} on the tied wrapper and
* then passing that same wrapper to the ordinary comparison would invoke
* FETCH twice.
*/
private static RuntimeScalar fetchDefinedComparisonOperand(RuntimeScalar arg) {
return arg.type == RuntimeScalarType.TIED_SCALAR ? arg.tiedFetch() : arg;
}

/**
* Throws a Perl-5-style "Operation '<op>': no method found" error when
* the overloaded package on either side does not permit fallback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ public record OperatorHandler(String className, String methodName, int methodTyp
put("<=>", "spaceship", "org/perlonjava/runtime/operators/CompareOperators");
put("eq", "eq", "org/perlonjava/runtime/operators/CompareOperators");
put("ne", "ne", "org/perlonjava/runtime/operators/CompareOperators");
put("equ", "equ", "org/perlonjava/runtime/operators/CompareOperators");
put("neu", "neu", "org/perlonjava/runtime/operators/CompareOperators");
put("===", "strictEqual", "org/perlonjava/runtime/operators/CompareOperators");
put("!==", "strictNotEqual", "org/perlonjava/runtime/operators/CompareOperators");
put("lt", "lt", "org/perlonjava/runtime/operators/CompareOperators");
put("le", "le", "org/perlonjava/runtime/operators/CompareOperators");
put("gt", "gt", "org/perlonjava/runtime/operators/CompareOperators");
Expand Down
10 changes: 7 additions & 3 deletions src/main/perl/lib/CPAN/Meta.pm
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use strict;
use warnings;
package CPAN::Meta;

our $VERSION = '2.150013';
our $VERSION = '2.150015';

#pod =head1 SYNOPSIS
#pod
Expand Down Expand Up @@ -649,7 +649,7 @@ CPAN::Meta - the distribution metadata for a CPAN dist

=head1 VERSION

version 2.150013
version 2.150015

=head1 SYNOPSIS

Expand Down Expand Up @@ -1038,7 +1038,7 @@ Adam Kennedy <adamk@cpan.org>

=head1 CONTRIBUTORS

=for stopwords Ansgar Burchardt Avar Arnfjord Bjarmason Benjamin Noggle Christopher J. Madsen Chuck Adams Cory G Watson Damyan Ivanov Dan Book Eric Wilhelm Graham Knop Gregor Hermann Karen Etheridge Kenichi Ishigaki Kent Fredric Ken Williams Lars Dieckow Leon Timmermans majensen Mark Fowler Matt S Trout Michael G. Schwern Mohammad Anwar mohawk2 moznion Niko Tyni Olaf Alders Olivier Mengué Philippe Bruhat (BooK) Randy Sims Ricardo Signes Tomohiro Hosaka
=for stopwords Ansgar Burchardt Avar Arnfjord Bjarmason Benjamin Noggle Christopher J. Madsen Chuck Adams Cory G Watson Damyan Ivanov Dan Book Eric Wilhelm Graham Knop Gregor Hermann James E Keenan Karen Etheridge Kenichi Ishigaki Kent Fredric Ken Williams Lars Dieckow Leon Timmermans majensen Mark Fowler Matt S Trout Michael G. Schwern Mohammad Anwar mohawk2 moznion Niko Tyni Olaf Alders Olivier Mengué Philippe Bruhat (BooK) Randy Sims Ricardo Signes Tomohiro Hosaka

=over 4

Expand Down Expand Up @@ -1088,6 +1088,10 @@ Gregor Hermann <gregoa@debian.org>

=item *

James E Keenan <jkeenan@cpan.org>

=item *

Karen Etheridge <ether@cpan.org>

=item *
Expand Down
Loading
Loading