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
Original file line number Diff line number Diff line change
Expand Up @@ -2424,6 +2424,11 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) {
pc = OpcodeHandlerExtended.executeReadline(bytecode, pc, registers);
}

case Opcodes.SET_LAST_READLINE_HANDLE_NAME -> {
int nameIndex = bytecode[pc++];
RuntimeIO.setLastReadlineHandleName(code.stringPool[nameIndex]);
}

case Opcodes.MATCH_REGEX -> {
// Match regex
// Format: MATCH_REGEX rd stringReg regexReg ctx bytesMode targetNameIndex
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,16 @@ else if (node.right instanceof BinaryOperatorNode rightCall) {
bytecodeCompiler.compileNode(node.left, -1, leftCtx);
int rs1 = bytecodeCompiler.lastResultReg;

// Anonymous lexical and aggregate-element handles have no globName,
// but Perl includes their source spelling in $.-context diagnostics.
if (node.operator.equals("readline")) {
Object handleName = node.getAnnotation("handleName");
if (handleName instanceof String name) {
bytecodeCompiler.emit(Opcodes.SET_LAST_READLINE_HANDLE_NAME);
bytecodeCompiler.emit(bytecodeCompiler.addToStringPool(name));
}
}

int rightCtx;
if (isListOp) {
rightCtx = RuntimeContextType.LIST;
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,11 @@ private static void visitOpen(BytecodeCompiler bc, OperatorNode node) {
bc.throwCompilerException("open requires arguments");
return;
}
String fileHandleName = openTargetName(argsList.elements.getFirst());
if (fileHandleName != null) {
bc.emit(Opcodes.SET_LAST_READLINE_HANDLE_NAME);
bc.emit(bc.addToStringPool(fileHandleName));
}
int argsReg = bc.allocateRegister();
bc.emit(Opcodes.NEW_ARRAY);
bc.emitReg(argsReg);
Expand Down Expand Up @@ -492,6 +497,24 @@ private static void visitOpen(BytecodeCompiler bc, OperatorNode node) {
bc.lastResultReg = rd;
}

private static String openTargetName(Node node) {
if (node instanceof OperatorNode wrapper && wrapper.operator.equals("scalar")) {
node = wrapper.operand;
}
if (node instanceof BinaryOperatorNode element
&& (element.operator.equals("[") || element.operator.equals("{"))
&& element.left instanceof OperatorNode scalar
&& scalar.operator.equals("$")
&& scalar.operand instanceof IdentifierNode identifier) {
return "$" + identifier.name + (element.operator.equals("[") ? "[...]" : "{...}");
}
if (node instanceof OperatorNode scalar && scalar.operator.equals("$")
&& scalar.operand instanceof IdentifierNode identifier) {
return "$" + identifier.name;
}
return null;
}

private static void visitSubstr(BytecodeCompiler bc, OperatorNode node) {
if (node.operand == null || !(node.operand instanceof ListNode args) || args.elements.size() < 2) {
bc.throwCompilerException("substr requires at least 2 arguments");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,11 @@ public static String disassemble(InterpretedCode interpretedCode) {
int readCtx = interpretedCode.bytecode[pc++];
sb.append("READLINE r").append(rd).append(" = readline(r").append(fhReg).append(", ctx=").append(readCtx).append(")\n");
break;
case Opcodes.SET_LAST_READLINE_HANDLE_NAME:
int handleNameIndex = interpretedCode.bytecode[pc++];
sb.append("SET_LAST_READLINE_HANDLE_NAME ")
.append(interpretedCode.stringPool[handleNameIndex]).append("\n");
break;
case Opcodes.MATCH_REGEX:
rd = interpretedCode.bytecode[pc++];
int strReg = interpretedCode.bytecode[pc++];
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/perlonjava/backend/bytecode/Opcodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -2566,6 +2566,9 @@ public class Opcodes {
/** Array element fetch that preserves tied-array lvalue semantics for local(). */
public static final short ARRAY_GET_FOR_LOCAL = 562;

/** Record a readline handle's source spelling for $. diagnostics. Format: nameStringIdx. */
public static final short SET_LAST_READLINE_HANDLE_NAME = 566;

/**
* Resolve a statically named CODE reference at runtime. This preserves the
* current CV snapshot while allowing an earlier runtime glob assignment in
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/org/perlonjava/backend/jvm/EmitOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,15 @@ static void handleSubstrOperator(EmitterVisitor emitterVisitor, OperatorNode nod
index++;
}

String openTargetName = node.operator.equals("open") && !operand.elements.isEmpty()
? openTargetName(operand.elements.getFirst()) : null;
if (openTargetName != null) {
mv.visitLdcInsn(openTargetName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeIO",
"setLastReadlineHandleName", "(Ljava/lang/String;)V", false);
}

emitterVisitor.pushCallContext();
mv.visitVarInsn(Opcodes.ALOAD, argsArraySlot);

Expand Down Expand Up @@ -426,6 +435,24 @@ static void handleSubstrOperator(EmitterVisitor emitterVisitor, OperatorNode nod
}
}

private static String openTargetName(Node node) {
if (node instanceof OperatorNode wrapper && wrapper.operator.equals("scalar")) {
node = wrapper.operand;
}
if (node instanceof BinaryOperatorNode element
&& (element.operator.equals("[") || element.operator.equals("{"))
&& element.left instanceof OperatorNode scalar
&& scalar.operator.equals("$")
&& scalar.operand instanceof IdentifierNode identifier) {
return "$" + identifier.name + (element.operator.equals("[") ? "[...]" : "{...}");
}
if (node instanceof OperatorNode scalar && scalar.operator.equals("$")
&& scalar.operand instanceof IdentifierNode identifier) {
return "$" + identifier.name;
}
return null;
}

// Handle an operator that was parsed using a Perl prototype.
static void handleOperator(EmitterVisitor emitterVisitor, OperatorNode node) {
EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ private void addLayer(String layerSpec) {
ViaLayer layer = new ViaLayer(className, currentLowerHandle(), currentMode());
activeLayers.add(layer);
} else {
throw new IllegalArgumentException("Unknown layer: " + layerSpec);
throw new IllegalArgumentException("Unknown PerlIO layer \"" + layerSpec + "\"");
}
}
}
Expand Down
49 changes: 41 additions & 8 deletions src/main/java/org/perlonjava/runtime/io/ScalarBackedIO.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@
import org.perlonjava.runtime.runtimetypes.RuntimeIO;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
import org.perlonjava.runtime.runtimetypes.RuntimeScalarCache;
import org.perlonjava.runtime.runtimetypes.GlobalVariable;
import org.perlonjava.runtime.runtimetypes.WarningFlags;
import org.perlonjava.runtime.operators.WarnDie;
import org.perlonjava.runtime.perlmodule.Warnings;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

public class ScalarBackedIO implements IOHandle {
private static final String NON_BYTE_SCALAR_MESSAGE =
"Strings with code points over 0xFF may not be mapped into in-memory file handles\n";
private final RuntimeScalar backingScalar;
private int position = 0;
private boolean isEOF = false;
Expand Down Expand Up @@ -45,6 +51,10 @@ public RuntimeScalar doRead(int maxBytes, Charset charset) {
}

String content = backingScalar.toString();
if (!isByteMappable(content)) {
reportNonByteScalar();
return RuntimeScalarCache.scalarUndef;
}
byte[] contentBytes = content.getBytes(StandardCharsets.ISO_8859_1);

if (position >= contentBytes.length) {
Expand Down Expand Up @@ -77,6 +87,10 @@ public RuntimeScalar write(String string) {
}

String currentContent = backingScalar.toString();
if (!isByteMappable(currentContent)) {
reportNonByteScalar();
return RuntimeScalarCache.scalarFalse;
}
byte[] currentBytes = currentContent.getBytes(StandardCharsets.ISO_8859_1);

if (appendMode) {
Expand All @@ -88,8 +102,9 @@ public RuntimeScalar write(String string) {
int newLength = Math.max(position + newBytes.length, currentBytes.length);
byte[] resultBytes = new byte[newLength];

// Copy existing content
System.arraycopy(currentBytes, 0, resultBytes, 0, Math.min(position, currentBytes.length));
// Preserve all existing bytes. In particular, a seek beyond EOF leaves a
// gap which PerlIO::scalar fills with NULs on the following write.
System.arraycopy(currentBytes, 0, resultBytes, 0, currentBytes.length);

// Write new content at position
System.arraycopy(newBytes, 0, resultBytes, position, newBytes.length);
Expand Down Expand Up @@ -139,9 +154,6 @@ public RuntimeScalar tell() {
*/
@Override
public RuntimeScalar seek(long pos, int whence) {
String content = backingScalar.toString();
int contentLength = content.getBytes(StandardCharsets.ISO_8859_1).length;

long newPosition;

switch (whence) {
Expand All @@ -152,16 +164,25 @@ public RuntimeScalar seek(long pos, int whence) {
newPosition = position + pos;
break;
case SEEK_END: // from end
// SEEK_END is the sole form which needs the current scalar
// length. SEEK_SET/CUR must not FETCH a tied scalar merely to
// move its file position.
int contentLength = backingScalar.toString().getBytes(StandardCharsets.ISO_8859_1).length;
newPosition = contentLength + pos;
break;
default:
return RuntimeIO.handleIOError("Invalid whence value: " + whence);
}

// Clamp position to valid range [0, contentLength]
position = (int) Math.max(0, Math.min(newPosition, contentLength));
if (newPosition < 0 || newPosition > Integer.MAX_VALUE) {
GlobalVariable.getGlobalVariable("main::!").set(22); // EINVAL
return RuntimeScalarCache.scalarFalse;
}

isEOF = position >= contentLength;
// Perl permits seeking past EOF. The gap is materialized as NUL bytes
// only if a subsequent write reaches it.
position = (int) newPosition;
isEOF = false;
return RuntimeScalarCache.scalarTrue;
}

Expand Down Expand Up @@ -286,4 +307,16 @@ public void setAppendMode(boolean appendMode) {
position = currentContent.getBytes(StandardCharsets.ISO_8859_1).length;
}
}

/** Whether a Perl scalar can be represented by PerlIO::scalar's byte buffer. */
public static boolean isByteMappable(String value) {
return value.codePoints().noneMatch(codePoint -> codePoint > 0xFF);
}

/** Report the EINVAL/warnings::utf8 contract shared by open, read and write. */
public static void reportNonByteScalar() {
GlobalVariable.getGlobalVariable("main::!").set(22); // EINVAL
WarnDie.warnWithCategory(new RuntimeScalar(NON_BYTE_SCALAR_MESSAGE),
new RuntimeScalar(""), "utf8");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,15 @@ public static RuntimeScalar chown(int ctx, RuntimeBase... args) {
// before taking this no-op fast path.
for (int i = 2; i < args.length; i++) {
for (RuntimeScalar fileArg : args[i]) {
RuntimeScalar.checkTaint(
RuntimeScalar.dereferenceAndFetchOnce(fileArg), "chown");
RuntimeScalar pathArg = RuntimeScalar.dereferenceAndFetchOnce(fileArg);
RuntimeScalar.checkTaint(pathArg, "chown");
if (pathArg.type != RuntimeScalarType.GLOB
&& pathArg.type != RuntimeScalarType.GLOBREFERENCE) {
Path path = RuntimeIO.resolvePath(pathArg.toString(), "chown");
if (path == null || !Files.exists(path)) {
GlobalVariable.getGlobalVariable("main::!").set(2); // ENOENT
}
}
}
}
return new RuntimeScalar(0);
Expand Down
48 changes: 48 additions & 0 deletions src/main/java/org/perlonjava/runtime/operators/IOOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,22 @@ public static RuntimeScalar open(int ctx, RuntimeBase... args) {
// For array/hash elements like $fh0[0], this is the actual lvalue that can be modified
// We assert it's a RuntimeScalar rather than calling .scalar() which would create a copy
RuntimeScalar fileHandle = (RuntimeScalar) args[0];
String openTargetSource = RuntimeIO.getLastReadlineHandleName();
// The compiler uses this one-shot channel to preserve the source
// spelling of open's first argument. Do not let it leak into a
// later, unrelated open/readline operation.
RuntimeIO.setLastReadlineHandleName(null);
// Numbered capture variables are readonly aliases. Unlike a numeric
// literal used as a one-argument handle name, an attempt to install an
// IO slot into one must report Perl's normal readonly-lvalue error.
if (fileHandle.type == RuntimeScalarType.READONLY_SCALAR
|| fileHandle instanceof RuntimeScalarReadOnly
|| fileHandle instanceof ScalarSpecialVariable specialVariable
&& specialVariable.isNumberedCapture()
|| openTargetSource != null && openTargetSource.matches("\\$[0-9]+")
|| fileHandle == scalarUndef) {
throw new PerlCompilerException("Modification of a read-only value attempted");
}
if (args.length < 2) {
// 1-argument open: open FILEHANDLE
// Per Perl semantics, the global scalar variable of the same name as the
Expand Down Expand Up @@ -958,11 +974,43 @@ else if (secondArg.type == RuntimeScalarType.GLOB || secondArg.type == RuntimeSc
RuntimeScalar assignedHandle = fileHandle.set(newGlob);
RuntimeScalar.retainUnstashedIoForDurableSlot(assignedHandle);
}
String aggregateName = aggregateHandleName(fileHandle);
if (aggregateName != null) {
fh.setDiagnosticReadlineHandleName(aggregateName);
} else {
String diagnosticName = normalizeAggregateHandleName(openTargetSource);
if (diagnosticName != null) {
fh.setDiagnosticReadlineHandleName(diagnosticName);
}
}
long pid = fh.getPid();
if (pid > 0) return new RuntimeScalar(pid);
return scalarTrue;
}

private static String aggregateHandleName(RuntimeScalar fileHandle) {
RuntimeBase aggregate = fileHandle instanceof RuntimeArrayProxyEntry arrayEntry
? arrayEntry.getParent()
: fileHandle instanceof RuntimeHashProxyEntry hashEntry ? hashEntry.getParent() : null;
if (aggregate == null) return null;
String name = RuntimeCode.findActiveLexicalName(aggregate);
if (name == null) name = GlobalVariable.findGlobalAggregateName(aggregate);
if (name == null || name.length() < 2) return null;
return "$" + name.substring(1) + (aggregate instanceof RuntimeArray ? "[...]" : "{...}");
}

private static String normalizeAggregateHandleName(String sourceName) {
if (sourceName == null) return null;
int array = sourceName.indexOf('[');
int hash = sourceName.indexOf('{');
int delimiter = array >= 0 ? array : hash;
if (delimiter < 0) return null;
String base = sourceName.substring(0, delimiter);
if (base.startsWith("@") || base.startsWith("%")) base = "$" + base.substring(1);
if (!base.startsWith("$")) return null;
return base + (array >= 0 && (hash < 0 || array < hash) ? "[...]" : "{...}");
}

/**
* Close a file handle.
*
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/org/perlonjava/runtime/operators/Readline.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ public class Readline {
* @return A RuntimeBase with the line(s).
*/
public static RuntimeBase readline(RuntimeScalar fileHandle, int ctx) {
// Resolving a lexical/aggregate element can retire statement-temporary
// diagnostic state. Save the compiler-provided source spelling before
// that resolution and attach it to the resulting IO below.
String sourceName = RuntimeIO.getLastReadlineHandleName();
if (fileHandle == null || fileHandle.type == RuntimeScalarType.UNDEF) {
RuntimeIO.setLastAccessedHandle(null);
}
Expand Down Expand Up @@ -59,6 +63,17 @@ public static RuntimeBase readline(RuntimeScalar fileHandle, int ctx) {
return ctx == RuntimeContextType.LIST ? new RuntimeList() : scalarUndef;
}

if (sourceName != null && (fh.getDiagnosticReadlineHandleName() == null
|| !(fh.getDiagnosticReadlineHandleName().contains("[")
|| fh.getDiagnosticReadlineHandleName().contains("{")))) {
fh.setDiagnosticReadlineHandleName(sourceName);
}
// Perl's delayed warn/die filehandle context follows a scalar readline.
// A list read is consumed as a scoped aggregate and must not leave
// context after its lexical handle is released.
RuntimeIO.setLastReadlineHandle(
ctx == RuntimeContextType.LIST ? null : fh);

if (fh instanceof TieHandle tieHandle) {
return TieHandle.tiedReadline(tieHandle, ctx);
}
Expand All @@ -70,6 +85,7 @@ public static RuntimeBase readline(RuntimeScalar fileHandle, int ctx) {
while ((line = readline(fh)).type != RuntimeScalarType.UNDEF) {
lines.elements.add(line);
}
RuntimeIO.setLastReadlineHandle(null);
return lines;
} else {
// Handle SCALAR context (original behavior)
Expand All @@ -84,6 +100,7 @@ public static RuntimeScalar readline(RuntimeIO runtimeIO) {
// Check if the IO object is set up for reading
// Set this as the last accessed handle for $. (INPUT_LINE_NUMBER) special variable
RuntimeIO.setLastAccessedHandle(runtimeIO);
RuntimeIO.setLastReadlineHandle(runtimeIO);

// Get the input record separator (equivalent to Perl's $/)
RuntimeScalar rsScalar = getGlobalVariable("main::/");
Expand Down
Loading
Loading