diff --git a/README.md b/README.md
index 439711bf..1123cf61 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@ String result = awk.script("{ print toupper($0) }").input("hello world").execute
```
When writing custom extensions, annotate associative array parameters with `@JawkAssocArray` and declare them as `Map` values rather than concrete map implementations.
+If you embed Jawk through [`AVM`](https://jawk.io/apidocs/io/jawk/backend/AVM.html), use `executePersistingGlobals(...)` when you want user-defined globals to survive across sequential runs on the same runtime instance.
## Documentation
diff --git a/src/main/java/io/jawk/backend/AVM.java b/src/main/java/io/jawk/backend/AVM.java
index f9538311..09a0a35f 100644
--- a/src/main/java/io/jawk/backend/AVM.java
+++ b/src/main/java/io/jawk/backend/AVM.java
@@ -25,6 +25,7 @@
import java.io.Closeable;
import java.io.IOException;
import java.io.PrintStream;
+import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.ArrayDeque;
@@ -133,6 +134,7 @@ private void push(Object o) {
private boolean inputSourceFilelistAssignmentsApplied;
private InputSource resolvedInputSource;
private AwkExpression installedEvalExpression;
+ private boolean mergedGlobalLayoutActive;
/**
* Construct the interpreter.
@@ -335,9 +337,7 @@ public void execute(
AwkProgram compiledProgram = Objects.requireNonNull(program, "program");
InputSource resolvedSource = Objects.requireNonNull(inputSource, "inputSource");
resetRuntimeState(runtimeArguments, variableOverrides);
- globalVariableOffsets = compiledProgram.getGlobalVariableOffsetMap();
- globalVariableArrays = compiledProgram.getGlobalVariableAarrayMap();
- functionNames = compiledProgram.getFunctionNameSet();
+ installProgramMetadata(compiledProgram);
jrt.prepareForExecution(settings.getFieldSeparator(), settings.getDefaultRS());
if (!executionSpecialVariables.isEmpty()) {
@@ -347,6 +347,94 @@ public void execute(
executeTuples(compiledProgram.top());
}
+ /**
+ * Executes a compiled AWK program while persisting user-defined global
+ * variables across repeated executions on this AVM instance.
+ *
+ * Before the new program starts, this method imports any user-defined
+ * globals currently materialized in the AVM and remaps them onto the
+ * incoming program's compiled global slots.
+ *
+ * @param program compiled program to execute
+ * @param inputSource input source providing records
+ * @throws ExitException when the program terminates via {@code exit}
+ * @throws IOException if execution fails
+ */
+ public void executePersistingGlobals(AwkProgram program, InputSource inputSource)
+ throws ExitException,
+ IOException {
+ executePersistingGlobals(program, inputSource, Collections.emptyList(), null);
+ }
+
+ /**
+ * Executes a compiled AWK program while persisting user-defined global
+ * variables across repeated executions on this AVM instance.
+ *
+ * Before the new program starts, this method imports any user-defined
+ * globals currently materialized in the AVM and remaps them onto the
+ * incoming program's compiled global slots.
+ *
+ * @param program compiled program to execute
+ * @param inputSource input source providing records
+ * @param runtimeArguments name=value or filename entries from the command line
+ * @throws ExitException when the program terminates via {@code exit}
+ * @throws IOException if execution fails
+ */
+ public void executePersistingGlobals(
+ AwkProgram program,
+ InputSource inputSource,
+ List runtimeArguments)
+ throws ExitException,
+ IOException {
+ executePersistingGlobals(program, inputSource, runtimeArguments, null);
+ }
+
+ /**
+ * Executes a compiled AWK program while persisting user-defined global
+ * variables across repeated executions on this AVM instance.
+ *
+ * Before the new program starts, this method imports any user-defined
+ * globals currently materialized in the AVM and remaps them onto the
+ * incoming program's compiled global slots.
+ *
+ * @param program compiled program to execute
+ * @param inputSource input source providing records
+ * @param runtimeArguments name=value or filename entries from the command line
+ * @param variableOverrides additional variable assignments applied on top of
+ * the settings-level variables (may be {@code null})
+ * @throws ExitException when the program terminates via {@code exit}
+ * @throws IOException if execution fails
+ */
+ public void executePersistingGlobals(
+ AwkProgram program,
+ InputSource inputSource,
+ List runtimeArguments,
+ Map variableOverrides)
+ throws ExitException,
+ IOException {
+ AwkProgram compiledProgram = Objects.requireNonNull(program, "program");
+ InputSource resolvedSource = Objects.requireNonNull(inputSource, "inputSource");
+ mergeRuntimeState(runtimeArguments, variableOverrides, compiledProgram);
+
+ jrt.prepareForExecution(settings.getFieldSeparator(), settings.getDefaultRS());
+ if (!executionSpecialVariables.isEmpty()) {
+ jrt.applySpecialVariables(executionSpecialVariables);
+ }
+ rebindResolvedInputSource(resolvedSource);
+ executeTuples(compiledProgram.top());
+ }
+
+ /**
+ * Clears the user-defined globals retained in the current runtime stack.
+ *
+ * The next {@link #executePersistingGlobals(AwkProgram, InputSource, List, Map)}
+ * call will therefore start from an empty persistent global bank.
+ */
+ public void clearPersistentGlobals() {
+ runtimeStack.clearGlobals();
+ mergedGlobalLayoutActive = false;
+ }
+
private void initExtensions() {
if (extensionInstances.isEmpty()) {
return;
@@ -459,6 +547,11 @@ private boolean prepareForEval(
}
private void resetRuntimeState(List runtimeArguments, Map variableOverrides) {
+ resetTransientRuntimeState(runtimeArguments, variableOverrides);
+ runtimeStack.clearGlobals();
+ }
+
+ private void resetTransientRuntimeState(List runtimeArguments, Map variableOverrides) {
// Reset the AVM-owned state that must not leak across executions.
operandStack.clear();
environOffset = NULL_OFFSET;
@@ -475,27 +568,12 @@ private void resetRuntimeState(List runtimeArguments, Map(runtimeArguments) : Collections.emptyList();
-
- if (variableOverrides == null || variableOverrides.isEmpty()) {
- executionInitialVariables = baseInitialVariables;
- executionSpecialVariables = baseSpecialVariables;
- } else {
- executionInitialVariables = new HashMap(baseInitialVariables);
- executionInitialVariables.putAll(variableOverrides);
-
- Map specialOverrides = JRT.copySpecialVariables(variableOverrides);
- if (specialOverrides.isEmpty()) {
- executionSpecialVariables = baseSpecialVariables;
- } else {
- executionSpecialVariables = new HashMap(baseSpecialVariables);
- executionSpecialVariables.putAll(specialOverrides);
- }
- }
+ prepareExecutionInputs(runtimeArguments, variableOverrides);
}
private void installExpressionMetadata(AwkExpression compiledExpression) {
@@ -508,6 +586,12 @@ private void installExpressionMetadata(AwkExpression compiledExpression) {
installedEvalExpression = compiledExpression;
}
+ private void installProgramMetadata(AwkProgram compiledProgram) {
+ globalVariableOffsets = compiledProgram.getGlobalVariableOffsetMap();
+ globalVariableArrays = compiledProgram.getGlobalVariableAarrayMap();
+ functionNames = compiledProgram.getFunctionNameSet();
+ }
+
private void rebindResolvedInputSource(InputSource resolvedSource) {
InputSource previousResolvedSource = resolvedInputSource;
if (previousResolvedSource != null && previousResolvedSource != resolvedSource) {
@@ -524,6 +608,315 @@ private boolean hasCompatibleEvalGlobalLayout(long numGlobals) {
&& Objects.equals(initializedEvalGlobalVariableArrays, globalVariableArrays);
}
+ /**
+ * Resets transient execution state, installs the new program metadata, and
+ * merges the previously retained user globals into the new compiled global
+ * layout.
+ *
+ * @param runtimeArguments name=value or filename entries for this execution
+ * @param variableOverrides per-call variable overrides for this execution
+ * @param compiledProgram program whose global layout should become active
+ */
+ private void mergeRuntimeState(
+ List runtimeArguments,
+ Map variableOverrides,
+ AwkProgram compiledProgram) {
+ Map carriedGlobals = collectPersistentGlobalValues();
+ resetTransientRuntimeState(runtimeArguments, variableOverrides);
+ installProgramMetadata(compiledProgram);
+
+ Map basePersistentSeeds = collectBasePersistentGlobalSeeds();
+ Map executionUserSeeds = collectExecutionUserGlobalSeeds(runtimeArguments, variableOverrides);
+ List mergedGlobalNamesByOffset = buildMergedGlobalNamesByOffset(
+ carriedGlobals,
+ basePersistentSeeds,
+ executionUserSeeds);
+
+ runtimeStack.rebindGlobals(mergedGlobalNamesByOffset);
+ restoreNamedGlobals(carriedGlobals);
+ seedMissingNamedGlobals(carriedGlobals, basePersistentSeeds);
+ applyNamedGlobalOverrides(executionUserSeeds);
+ mergedGlobalLayoutActive = true;
+ }
+
+ /**
+ * Returns whether the current runtime stack already contains a merged global
+ * layout for the compiled program about to execute.
+ *
+ * Persistent execution may append previously retained globals after the
+ * compiled globals of the incoming program. The tuple stream only dereferences
+ * the prefix defined by {@code SET_NUM_GLOBALS}, so appended globals are valid
+ * as long as the compiled prefix still matches name-for-name and offset-for-offset.
+ *
+ * @param numGlobals number of globals compiled into the active program
+ * @return {@code true} when the merged layout is compatible with the active
+ * program
+ */
+ private boolean hasCompatiblePersistentGlobalLayout(long numGlobals) {
+ Object[] globals = runtimeStack.getNumGlobals();
+ if (!mergedGlobalLayoutActive
+ || globals == null
+ || globalVariableOffsets == null
+ || globals.length < numGlobals) {
+ return false;
+ }
+ for (Map.Entry entry : globalVariableOffsets.entrySet()) {
+ int offset = entry.getValue().intValue();
+ if (offset < 0 || offset >= globals.length || !entry.getKey().equals(runtimeStack.getGlobalName(offset))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Prepares the per-execution runtime arguments and variable overrides.
+ *
+ * Base settings-level variables remain the default source. Per-call overrides
+ * are layered on top without mutating the base snapshot held by this AVM.
+ *
+ * @param runtimeArguments name=value or filename entries for this execution
+ * @param variableOverrides per-call variable overrides for this execution
+ */
+ private void prepareExecutionInputs(
+ List runtimeArguments,
+ Map variableOverrides) {
+ this.arguments = runtimeArguments != null ? new ArrayList<>(runtimeArguments) : Collections.emptyList();
+
+ if (variableOverrides == null || variableOverrides.isEmpty()) {
+ executionInitialVariables = baseInitialVariables;
+ executionSpecialVariables = baseSpecialVariables;
+ } else {
+ executionInitialVariables = new HashMap(baseInitialVariables);
+ executionInitialVariables.putAll(variableOverrides);
+
+ Map specialOverrides = JRT.copySpecialVariables(variableOverrides);
+ if (specialOverrides.isEmpty()) {
+ executionSpecialVariables = baseSpecialVariables;
+ } else {
+ executionSpecialVariables = new HashMap(baseSpecialVariables);
+ executionSpecialVariables.putAll(specialOverrides);
+ }
+ }
+ }
+
+ /**
+ * Collects the current user-defined globals retained in the runtime stack.
+ *
+ * @return retained user globals keyed by name, in current runtime order
+ */
+ private Map collectPersistentGlobalValues() {
+ Map retainedGlobals = new LinkedHashMap();
+ for (Map.Entry entry : runtimeStack.snapshotGlobalVariables().entrySet()) {
+ if (isPersistentEligibleGlobal(entry.getKey())) {
+ retainedGlobals.put(entry.getKey(), entry.getValue());
+ }
+ }
+ return retainedGlobals;
+ }
+
+ /**
+ * Collects the AVM-wide baseline variables that should seed persistent
+ * globals when no retained value exists yet for the same name.
+ *
+ * @return baseline user globals keyed by name
+ */
+ private Map collectBasePersistentGlobalSeeds() {
+ Map basePersistentSeeds = new LinkedHashMap();
+ for (Map.Entry entry : baseInitialVariables.entrySet()) {
+ String name = entry.getKey();
+ if (isPersistentEligibleGlobal(name)) {
+ validateSeededGlobal(name, entry.getValue());
+ basePersistentSeeds.put(name, entry.getValue());
+ }
+ }
+ return basePersistentSeeds;
+ }
+
+ /**
+ * Collects the user-defined variables that should override the retained
+ * global bank for the current persistent execution.
+ *
+ * Only user globals are included here. JRT-managed special variables still
+ * flow through the normal execution setup.
+ *
+ * @param runtimeArguments name=value or filename entries for this execution
+ * @param variableOverrides per-call variable overrides for this execution
+ * @return insertion-ordered overriding seed values keyed by variable name
+ */
+ private Map collectExecutionUserGlobalSeeds(
+ List runtimeArguments,
+ Map variableOverrides) {
+ Map executionUserSeeds = new LinkedHashMap();
+ if (variableOverrides != null) {
+ for (Map.Entry entry : variableOverrides.entrySet()) {
+ String name = entry.getKey();
+ if (isPersistentEligibleGlobal(name)) {
+ validateSeededGlobal(name, entry.getValue());
+ executionUserSeeds.put(name, entry.getValue());
+ }
+ }
+ }
+ for (String argument : runtimeArguments != null ? runtimeArguments : Collections.emptyList()) {
+ if (argument.indexOf('=') <= 0) {
+ continue;
+ }
+ NameValueAssignment assignment = parseNameValueAssignment(argument);
+ if (isPersistentEligibleGlobal(assignment.name)) {
+ validateSeededGlobal(assignment.name, assignment.value);
+ executionUserSeeds.put(assignment.name, assignment.value);
+ }
+ }
+ return executionUserSeeds;
+ }
+
+ /**
+ * Builds the slot order for the next persistent execution.
+ *
+ * The compiled globals are always installed first in their compiled offset
+ * order. Retained globals and seeded user globals that are not compiled by
+ * the incoming program are appended afterwards so future runs can still reuse
+ * them without changing the current program's compiled offsets.
+ *
+ * @param carriedGlobals retained user globals from the previous execution
+ * @param basePersistentSeeds baseline user globals coming from the AVM settings
+ * @param executionUserSeeds per-call user overrides for this execution
+ * @return merged slot-to-name layout for the next persistent run
+ */
+ private List buildMergedGlobalNamesByOffset(
+ Map carriedGlobals,
+ Map basePersistentSeeds,
+ Map executionUserSeeds) {
+ LinkedHashSet orderedNames = new LinkedHashSet();
+ List> compiledGlobals = new ArrayList>(
+ globalVariableOffsets.entrySet());
+ Collections.sort(compiledGlobals, (left, right) -> left.getValue().compareTo(right.getValue()));
+ for (Map.Entry entry : compiledGlobals) {
+ orderedNames.add(entry.getKey());
+ }
+ for (String name : carriedGlobals.keySet()) {
+ orderedNames.add(name);
+ }
+ for (String name : basePersistentSeeds.keySet()) {
+ orderedNames.add(name);
+ }
+ for (String name : executionUserSeeds.keySet()) {
+ orderedNames.add(name);
+ }
+ return new ArrayList(orderedNames);
+ }
+
+ /**
+ * Restores retained global values into the runtime stack after the merged
+ * layout has been installed.
+ *
+ * @param carriedGlobals retained user globals from the previous execution
+ */
+ private void restoreNamedGlobals(Map carriedGlobals) {
+ for (Map.Entry entry : carriedGlobals.entrySet()) {
+ runtimeStack.setGlobalVariable(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Applies baseline user globals when no retained value exists yet for the
+ * same name.
+ *
+ * @param carriedGlobals retained user globals from the previous execution
+ * @param basePersistentSeeds baseline user globals coming from the AVM settings
+ */
+ private void seedMissingNamedGlobals(
+ Map carriedGlobals,
+ Map basePersistentSeeds) {
+ for (Map.Entry entry : basePersistentSeeds.entrySet()) {
+ if (!carriedGlobals.containsKey(entry.getKey())) {
+ runtimeStack.setGlobalVariable(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ /**
+ * Applies per-execution user overrides on top of the merged runtime globals.
+ *
+ * @param executionUserSeeds per-call user overrides for this execution
+ */
+ private void applyNamedGlobalOverrides(Map executionUserSeeds) {
+ for (Map.Entry entry : executionUserSeeds.entrySet()) {
+ runtimeStack.setGlobalVariable(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Returns whether the given global name should participate in persistent
+ * memory.
+ *
+ * @param name global variable name
+ * @return {@code true} when the variable should persist across runs
+ */
+ private boolean isPersistentEligibleGlobal(String name) {
+ return name != null
+ && !JRT.isJrtManagedSpecialVariable(name)
+ && !"ARGV".equals(name)
+ && !"ARGC".equals(name)
+ && !"ENVIRON".equals(name)
+ && !"RSTART".equals(name)
+ && !"RLENGTH".equals(name)
+ && !"IGNORECASE".equals(name);
+ }
+
+ /**
+ * Validates that a seeded global value is compatible with the compiled
+ * metadata of the current program.
+ *
+ * @param name variable name to validate
+ * @param value proposed seeded value
+ */
+ private void validateSeededGlobal(String name, Object value) {
+ if (functionNames.contains(name)) {
+ throw new IllegalArgumentException("Cannot assign a scalar to a function name (" + name + ").");
+ }
+ Boolean arrayObj = globalVariableArrays.get(name);
+ if (Boolean.TRUE.equals(arrayObj) && !(value instanceof Map)) {
+ throw new IllegalArgumentException("Cannot assign a scalar to a non-scalar variable (" + name + ").");
+ }
+ }
+
+ /**
+ * Parses a runtime {@code name=value} assignment.
+ *
+ * @param nameValue raw assignment text
+ * @return parsed assignment
+ */
+ private NameValueAssignment parseNameValueAssignment(String nameValue) {
+ int eqIdx = nameValue.indexOf('=');
+ if (eqIdx == 0) {
+ throw new IllegalArgumentException(
+ "Must have a non-blank variable name in a name=value variable assignment argument.");
+ }
+ String name = nameValue.substring(0, eqIdx);
+ String value = nameValue.substring(eqIdx + 1);
+ return new NameValueAssignment(name, coerceVariableAssignmentValue(value));
+ }
+
+ /**
+ * Coerces a runtime assignment value using the same scalar rules as the
+ * existing command-line handling: integer first, then double, then string.
+ *
+ * @param value raw text to coerce
+ * @return coerced scalar value
+ */
+ private Object coerceVariableAssignmentValue(String value) {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException nfe) {
+ try {
+ return Double.parseDouble(value);
+ } catch (NumberFormatException nfe2) {
+ return value;
+ }
+ }
+ }
+
/**
* Executes the tuple stream after the runtime has been fully prepared.
*
@@ -1943,8 +2336,13 @@ private void executeTuples(PositionTracker position)
case SET_NUM_GLOBALS: {
// arg[0] = # of globals
Object[] globals = runtimeStack.getNumGlobals();
- if (globals == null) {
- runtimeStack.setNumGlobals(position.intArg(0));
+ if (mergedGlobalLayoutActive) {
+ if (!hasCompatiblePersistentGlobalLayout(position.intArg(0))) {
+ throw new IllegalStateException(
+ "AVM globals are already initialized for an incompatible persistent layout.");
+ }
+ } else if (globals == null) {
+ runtimeStack.setNumGlobals(position.intArg(0), globalVariableOffsets);
initializedEvalGlobalVariableOffsets = globalVariableOffsets;
initializedEvalGlobalVariableArrays = globalVariableArrays;
@@ -2618,24 +3016,9 @@ public final Object getSUBSEP() {
*/
@SuppressWarnings("unused")
private void setFilelistVariable(String nameValue) {
- int eqIdx = nameValue.indexOf('=');
- // variable name should be non-blank
- if (eqIdx == 0) {
- throw new IllegalArgumentException(
- "Must have a non-blank variable name in a name=value variable assignment argument.");
- }
- String name = nameValue.substring(0, eqIdx);
- String value = nameValue.substring(eqIdx + 1);
- Object obj;
- try {
- obj = Integer.parseInt(value);
- } catch (NumberFormatException nfe) {
- try {
- obj = Double.parseDouble(value);
- } catch (NumberFormatException nfe2) {
- obj = value;
- }
- }
+ NameValueAssignment assignment = parseNameValueAssignment(nameValue);
+ String name = assignment.name;
+ Object obj = assignment.value;
// make sure we're not receiving funcname=value assignments
if (functionNames.contains(name)) {
@@ -2651,8 +3034,9 @@ private void setFilelistVariable(String nameValue) {
} else {
runtimeStack.setFilelistVariable(offsetObj.intValue(), obj);
}
+ } else if (runtimeStack.hasGlobalVariable(name)) {
+ runtimeStack.setGlobalVariable(name, obj);
}
- // otherwise, do nothing
}
/** {@inheritDoc} */
@@ -2682,6 +3066,8 @@ public final void assignVariable(String name, Object obj) {
} else {
runtimeStack.setFilelistVariable(offsetObj.intValue(), obj);
}
+ } else if (runtimeStack.hasGlobalVariable(name)) {
+ runtimeStack.setGlobalVariable(name, obj);
}
}
@@ -2847,6 +3233,16 @@ private Map