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 @@ -3299,6 +3299,8 @@ void compileVariableDeclaration(OperatorNode node, String op) {

// Regular lexical variable (not captured, not state)
int reg = addVariable(varName, "my");
sigilOp.setAnnotation("bytecodeLexicalRegister", reg);
node.setAnnotation("bytecodeLexicalRegister", reg);

// Normal initialization: load undef/empty array/empty hash
switch (sigil) {
Expand Down Expand Up @@ -7837,6 +7839,16 @@ public void visit(FormatNode node) {
}
}
}
if (node.getAnnotation("formatLexicalDeclarations")
instanceof Map<?, ?> declarations) {
for (Map.Entry<?, ?> entry : declarations.entrySet()) {
if (entry.getKey() instanceof String name
&& entry.getValue() instanceof OperatorNode declaration
&& declaration.getAnnotation("bytecodeLexicalRegister") instanceof Integer reg) {
captures.putIfAbsent(name, reg);
}
}
}
emit(captures.size());
for (Map.Entry<String, Integer> capture : captures.entrySet()) {
emit(addToStringPool(capture.getKey()));
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/org/perlonjava/backend/jvm/EmitFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ public static void emitFormat(EmitterVisitor emitterVisitor, FormatNode node) {
}
}
}
if (node.getAnnotation("formatLexicalDeclarations")
instanceof Map<?, ?> declarations) {
for (Map.Entry<?, ?> entry : declarations.entrySet()) {
if (entry.getKey() instanceof String name
&& entry.getValue() instanceof OperatorNode declaration
&& declaration.getAnnotation("jvmLexicalSlot") instanceof Integer slot) {
captures.putIfAbsent(name, slot);
}
}
}
for (Map.Entry<String, Integer> capture : captures.entrySet()) {
mv.visitInsn(Opcodes.DUP);
mv.visitLdcInsn(capture.getKey());
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/org/perlonjava/backend/jvm/EmitVariable.java
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,10 @@ static void handleMyOperator(EmitterVisitor emitterVisitor, OperatorNode node) {
myNode.annotations.put("attributePackage", node.annotations.get("attributePackage"));
}
myNode.accept(emitterVisitor.with(RuntimeContextType.VOID));
Object lexicalSlot = myNode.getAnnotation("jvmLexicalSlot");
if (lexicalSlot != null) {
varNode.setAnnotation("jvmLexicalSlot", lexicalSlot);
}
} else if (operatorNode.operand instanceof ListNode nestedList) {
// Handle my(\($d, $e)) - nested list with backslash
// Process each element in the nested list as a declared reference
Expand Down Expand Up @@ -1619,6 +1623,8 @@ static void handleMyOperator(EmitterVisitor emitterVisitor, OperatorNode node) {
}

int varIndex = emitterVisitor.ctx.symbolTable.addVariable(var, operator, sigilNode);
sigilNode.setAnnotation("jvmLexicalSlot", varIndex);
node.setAnnotation("jvmLexicalSlot", varIndex);
// TODO optimization - SETVAR+MY can be combined

// Check if this is a declared reference (my \$x)
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/org/perlonjava/frontend/parser/FormatParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import org.perlonjava.runtime.runtimetypes.RuntimeFormat;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -60,6 +62,17 @@ public static FormatNode parseFormatDeclaration(Parser parser, String formatName

// Create a format node with the parsed template content
FormatNode formatNode = new FormatNode(formatName, templateLines, tokenIndex);
// A FORMAT captures lexical cells from its declaration site. Keep the
// declaration ASTs, not parser-time numeric ids: each backend assigns
// its own executable local slot while lowering the declaration.
Map<String, OperatorNode> lexicalDeclarations = new LinkedHashMap<>();
for (var entry : parser.ctx.symbolTable.getAllVisibleVariables().values()) {
if (("my".equals(entry.decl()) || "state".equals(entry.decl()))
&& entry.ast() != null) {
lexicalDeclarations.put(entry.name(), entry.ast());
}
}
formatNode.setAnnotation("formatLexicalDeclarations", lexicalDeclarations);

// Formats are declarations, not statements delayed until an enclosing
// subroutine is called. A later write() must find this slot even when
Expand Down
80 changes: 75 additions & 5 deletions src/main/java/org/perlonjava/runtime/operators/IOOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
import org.perlonjava.frontend.astnode.PictureLine;
import org.perlonjava.frontend.parser.StringParser;
import org.perlonjava.runtime.ForkOpenState;
import org.perlonjava.runtime.WarningBitsRegistry;
import org.perlonjava.runtime.io.*;
import org.perlonjava.runtime.nativ.NativeUtils;
import org.perlonjava.runtime.nativ.ffm.FFMPosix;
import org.perlonjava.runtime.perlmodule.Socket;
import org.perlonjava.runtime.perlmodule.Strict;
import org.perlonjava.runtime.perlmodule.Warnings;
import org.perlonjava.runtime.runtimetypes.*;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.*;
Expand Down Expand Up @@ -1985,13 +1988,26 @@ private static String paginateFormatOutput(RuntimeIO fh, RuntimeFormat topFormat
if (formattedOutput == null || formattedOutput.isEmpty()) {
return "";
}
// write() starts a body format on a fresh page when the complete
// record block cannot fit in the lines remaining on this page. This
// is observable with a repeated ENTRY picture following a short EOR
// record: Perl emits the footer and form feed before the next ENTRY,
// rather than splitting its first record across the old page.
if (fh.formatLinesLeft > 0
&& countFormatLines(formattedOutput) > fh.formatLinesLeft) {
fh.formatLinesLeft = 0;
}
StringBuilder paged = new StringBuilder();
int offset = 0;
boolean firstPage = true;
while (offset < formattedOutput.length()) {
if (fh.formatLinesLeft <= 0) {
if (!firstPage) {
paged.append('\f');
// firstPage is local to this write() call; a later write can
// still begin after a partially used physical page. $%
// records that persistent page state and requires a form
// feed when this write's preflight moved to the next page.
boolean pageBreak = !firstPage || fh.formatPageNumber > 0;
if (pageBreak) {
fh.formatPageNumber++;
} else if (topFormat != null) {
// $% is page one while a top format is being evaluated,
Expand All @@ -2002,9 +2018,20 @@ private static String paginateFormatOutput(RuntimeIO fh, RuntimeFormat topFormat
firstPage = false;
fh.formatLinesLeft = fh.formatPageLength;
if (topFormat != null) {
String topText = topFormat.execute(new RuntimeList());
TopFormatOutput topOutput = executeTopFormat(topFormat, fh);
paged.append(topOutput.printedText());
if (pageBreak) {
paged.append('\f');
}
String topText = topOutput.formatText();
paged.append(topText);
fh.formatLinesLeft -= countFormatLines(topText);
// Top-format argument evaluation can itself inspect the
// magic $- variable. Its transient state must not alter
// the body format's page budget; establish that budget
// from the page length and the top text actually emitted.
fh.formatLinesLeft = fh.formatPageLength - countFormatLines(topText);
} else if (pageBreak) {
paged.append('\f');
}
}

Expand All @@ -2017,6 +2044,34 @@ private static String paginateFormatOutput(RuntimeIO fh, RuntimeFormat topFormat
return paged.toString();
}

/**
* A TOP argument line can call print (the traditional footer idiom).
* write() buffers its body format before committing it, so let those
* callback writes share that buffer instead of sending them ahead of the
* already formatted page text.
*/
private static TopFormatOutput executeTopFormat(RuntimeFormat topFormat, RuntimeIO fh) {
ByteArrayOutputStream printed = new ByteArrayOutputStream();
RuntimeIO capture = new RuntimeIO(new CustomOutputStreamHandle(printed));
capture.formatPageLength = fh.formatPageLength;
capture.formatLinesLeft = fh.formatLinesLeft;
capture.formatPageNumber = fh.formatPageNumber;
RuntimeIO savedSelectedHandle = RuntimeIO.getSelectedHandle();
String formatText;
try {
RuntimeIO.setSelectedHandle(capture);
formatText = topFormat.execute(new RuntimeList());
} finally {
fh.formatPageLength = capture.formatPageLength;
fh.formatLinesLeft = capture.formatLinesLeft;
fh.formatPageNumber = capture.formatPageNumber;
RuntimeIO.setSelectedHandle(savedSelectedHandle);
}
return new TopFormatOutput(printed.toString(StandardCharsets.ISO_8859_1), formatText);
}

private record TopFormatOutput(String printedText, String formatText) { }

private static int countFormatLines(String text) {
if (text == null || text.isEmpty()) return 0;
int lines = text.endsWith("\n") ? 0 : 1;
Expand Down Expand Up @@ -2057,7 +2112,12 @@ public static RuntimeScalar formline(int ctx, RuntimeBase... args) {
boolean resultTainted = accumulator.isTainted() || picture.isTainted()
|| picture.formatPictureTainted;
String currentValue = accumulator.toString();
accumulator.set(currentValue + formatTemplate);
String result = currentValue + formatTemplate;
if ((WarningBitsRegistry.getCallSiteHints() & Strict.HINT_BYTES) != 0) {
accumulator.set(new RuntimeScalar(result.getBytes(StandardCharsets.UTF_8)));
} else {
accumulator.set(result);
}
accumulator.tainted = resultTainted;
return scalarTrue;
}
Expand Down Expand Up @@ -2092,6 +2152,16 @@ public static RuntimeScalar formline(int ctx, RuntimeBase... args) {

// Return success (1)
return scalarTrue;
} catch (RuntimeFormat.FormatFieldMutationException e) {
// Perl updates $^A with the formatted prefix before the ^ field
// fails while attempting to consume a bare typeglob operand.
RuntimeScalar accumulator = getGlobalVariable(GlobalContext.encodeSpecialVar("A"));
accumulator.set(accumulator.toString() + e.renderedText());
throw new PerlCompilerException("Modification of a read-only value attempted");
} catch (PerlCompilerException e) {
// Preserve Perl runtime errors (notably readonly ^-field
// operands) so eval sees the normal canonical diagnostic.
throw e;
} catch (Exception e) {
throw new PerlCompilerException("formline failed: " + e.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ public RuntimeScalar set(RuntimeScalar value) {
@Override public boolean getDefinedBoolean() { return true; }
@Override public String toString() { return Integer.toString(getInt()); }

// The base scalar keeps its default UNDEF type, which would otherwise
// make RuntimeScalar.getNumber() return numeric zero without consulting
// this handle-backed value. Page variables participate in ordinary
// numeric expressions such as `$% == 1` in TOP formats.
@Override public RuntimeScalar getNumber() { return new RuntimeScalar(getInt()); }
@Override public RuntimeScalar getNumber(String operation) { return getNumber(); }
@Override public RuntimeScalar getNumberNoOverload() { return getNumber(); }

@Override
public void dynamicSaveState() {
RuntimeIO handle = currentHandle();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ private PerlRuntime(PerlThreadRegistry threadRegistry, long perlThreadId) {
public void activateForkOpenChildStdout() {
RuntimeIO muted = ioStdout;
RuntimeIO stdout = new RuntimeIO(new StandardIO(System.out, true));
// A replay child has already run the pre-fork setup against its muted
// STDOUT. Activating the real stream must retain the filehandle-
// scoped format state ($=, $-, and $%) established there.
stdout.formatPageLength = muted.formatPageLength;
stdout.formatLinesLeft = muted.formatLinesLeft;
stdout.formatPageNumber = muted.formatPageNumber;
RuntimeIO stdin = new RuntimeIO(new StandardIO(System.in));
replaceStandardHandle("main::STDOUT", stdout);
replaceStandardHandle("main::stdout", stdout);
Expand Down Expand Up @@ -740,20 +746,26 @@ void replaceStandardHandle(String name, RuntimeIO io) {
case "main::STDOUT" -> {
ioStdout = io;
updateStandardGlobHandle(name, io);
io.globName = name;
}
case "main::STDERR" -> {
ioStderr = io;
updateStandardGlobHandle(name, io);
io.globName = name;
}
case "main::STDIN" -> {
ioStdin = io;
updateStandardGlobHandle(name, io);
io.globName = name;
}
// Lowercase standard names are aliases. They must not overwrite the
// canonical name carried by the shared RuntimeIO: format defaults
// derive $~ from that name, so a replay child would otherwise look up
// a nonexistent `stdout` format instead of `STDOUT`.
case "main::stdout", "main::stderr", "main::stdin" ->
updateStandardGlobHandle(name, io);
default -> throw new IllegalArgumentException("Not a standard I/O glob: " + name);
}
io.globName = name;
}

private void installInitialStandardGlob(String name, RuntimeIO io) {
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,18 @@ public static Map<String, RuntimeBase> snapshotActiveLexicals(RuntimeCode code)
return Collections.emptyMap();
}

/** Return the nearest active cell for each lexical name across call frames. */
public static Map<String, RuntimeBase> snapshotAllActiveLexicals() {
PerlRuntime runtime = PerlRuntime.current();
Map<String, RuntimeBase> result = new LinkedHashMap<>();
for (ActiveLexicalFrame frame : activeLexicalFrames(runtime.executionState())) {
for (Map.Entry<String, RuntimeBase> entry : frame.cells().entrySet()) {
result.putIfAbsent(entry.getKey(), entry.getValue());
}
}
return result;
}

/**
* Select eval STRING captures for Perl's package-DB rule. An eval run by
* a DB subroutine is evaluated in the lexical pad of the code being
Expand Down
Loading
Loading