View Javadoc
1   package io.jawk.backend;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * Jawk
6    * ჻჻჻჻჻჻
7    * Copyright (C) 2006 - 2026 MetricsHub
8    * ჻჻჻჻჻჻
9    * This program is free software: you can redistribute it and/or modify
10   * it under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation, either version 3 of the
12   * License, or (at your option) any later version.
13   *
14   * This program is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Lesser Public License for more details.
18   *
19   * You should have received a copy of the GNU General Lesser Public
20   * License along with this program.  If not, see
21   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
22   * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
23   */
24  
25  import java.io.Closeable;
26  import java.io.IOException;
27  import java.io.PrintStream;
28  import java.util.AbstractSet;
29  import java.util.Iterator;
30  import java.util.Arrays;
31  import java.util.HashSet;
32  import java.util.LinkedHashMap;
33  import java.util.LinkedHashSet;
34  import java.util.Objects;
35  import java.util.ArrayDeque;
36  import java.util.ArrayList;
37  import java.util.Collections;
38  import java.util.Deque;
39  import java.util.Enumeration;
40  import java.util.HashMap;
41  import java.util.IdentityHashMap;
42  import java.util.List;
43  import java.util.Locale;
44  import java.util.Map;
45  import java.util.Set;
46  import java.util.function.BiConsumer;
47  import java.util.regex.Pattern;
48  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
49  import io.jawk.AwkExpression;
50  import io.jawk.AwkProgram;
51  import io.jawk.AwkSandboxException;
52  import io.jawk.ExitException;
53  import io.jawk.ext.AbstractExtension;
54  import io.jawk.ext.ExtensionFunction;
55  import io.jawk.ext.ForInKeyOrder;
56  import io.jawk.ext.JawkExtension;
57  import io.jawk.intermediate.Address;
58  import io.jawk.intermediate.BuiltinFunction;
59  import io.jawk.intermediate.Opcode;
60  import io.jawk.intermediate.PositionTracker;
61  import io.jawk.intermediate.Tuple;
62  import io.jawk.intermediate.Tuple.BooleanTuple;
63  import io.jawk.intermediate.Tuple.CallFunctionTuple;
64  import io.jawk.intermediate.Tuple.ClassTuple;
65  import io.jawk.intermediate.Tuple.CountAndAppendTuple;
66  import io.jawk.intermediate.Tuple.CountTuple;
67  import io.jawk.intermediate.Tuple.DereferenceTuple;
68  import io.jawk.intermediate.Tuple.ExtensionTuple;
69  import io.jawk.intermediate.Tuple.IndirectCallTuple;
70  import io.jawk.intermediate.Tuple.IndirectFunctionTarget;
71  import io.jawk.intermediate.Tuple.InputFieldTuple;
72  import io.jawk.intermediate.Tuple.LongTuple;
73  import io.jawk.intermediate.Tuple.PushDoubleTuple;
74  import io.jawk.intermediate.Tuple.PushLongTuple;
75  import io.jawk.intermediate.Tuple.PushStringTuple;
76  import io.jawk.intermediate.Tuple.RegexTuple;
77  import io.jawk.intermediate.Tuple.ScalarPopTuple;
78  import io.jawk.intermediate.Tuple.SubstitutionVariableTuple;
79  import io.jawk.intermediate.Tuple.VariableTuple;
80  import io.jawk.intermediate.UninitializedObject;
81  import io.jawk.intermediate.UntypedObject;
82  import io.jawk.jrt.AssocArray;
83  import io.jawk.jrt.AwkRuntimeException;
84  import io.jawk.jrt.AwkSink;
85  import io.jawk.jrt.ConditionPair;
86  import io.jawk.jrt.InputSource;
87  import io.jawk.jrt.JRT;
88  import io.jawk.jrt.VariableManager;
89  import io.jawk.util.AwkSettings;
90  import io.jawk.jrt.BSDRandom;
91  
92  /**
93   * The Jawk interpreter.
94   * <p>
95   * It takes tuples constructed by the intermediate step
96   * and executes each tuple in accordance to their instruction semantics.
97   * The tuples correspond to the Awk script compiled by the parser.
98   * The interpreter consists of an instruction processor (interpreter),
99   * a runtime stack, and machinery to support the instruction set
100  * contained within the tuples.
101  * <p>
102  * The interpreter runs completely independent of the frontend/intermediate step.
103  * In fact, an intermediate file produced by Jawk is sufficient to
104  * execute on this interpreter. The binding data-structure is
105  * the {@link AwkSettings}, which can contain options pertinent to
106  * the interpreter. For example, the interpreter must know about
107  * the -v command line argument values, as well as the file/variable list
108  * parameter values (ARGC/ARGV) after the script on the command line.
109  * However, if programmatic access to the AVM is required, meaningful
110  * {@link AwkSettings} are not required.
111  * <p>
112  * Semantic analysis has occurred prior to execution of the interpreter.
113  * Therefore, the interpreter throws AwkRuntimeExceptions upon most
114  * errors/conditions. It can also throw a <code>java.lang.Error</code> if an
115  * interpreter error is encountered.
116  * <p>
117  * AVM instances are reusable, but they are not thread-safe. Reuse the same
118  * interpreter only sequentially, or create one AVM per concurrent execution.
119  * When AVM is used directly, callers own its lifecycle and must
120  * {@link #close()} it when done.
121  *
122  * @author Danny Daglas
123  */
124 public class AVM implements VariableManager, Closeable {
125 
126 	private RuntimeStack runtimeStack = new RuntimeStack();
127 
128 	// operand stack: a flat Object[] with an explicit top index so push/pop
129 	// stay single array accesses; depth is bounded by expression nesting, so
130 	// the array stays small and rarely grows
131 	private Object[] operandStack = new Object[INITIAL_OPERAND_STACK_CAPACITY];
132 	private int operandStackSize;
133 	// Untyped array elements currently passed as user-function arguments, in
134 	// call order. Each reference lives exactly as long as the call frame that
135 	// received it, so this stack stays empty unless an untyped element
136 	// argument is in flight.
137 	private final Deque<IndirectArrayArgumentReference> elementArgumentReferences = new ArrayDeque<IndirectArrayArgumentReference>();
138 	private List<String> arguments;
139 	private boolean sortedArrayKeys;
140 	private final Map<String, Object> baseInitialVariables;
141 	private final Map<String, Object> baseSpecialVariables;
142 	private Map<String, Object> executionInitialVariables;
143 	private Map<String, Object> executionSpecialVariables;
144 	private JRT jrt;
145 	private Map<String, JawkExtension> extensionInstances;
146 
147 	private static final int INITIAL_OPERAND_STACK_CAPACITY = 64;
148 
149 	// stack methods
150 	private Object pop() {
151 		Object value = operandStack[--operandStackSize];
152 		operandStack[operandStackSize] = null;
153 		return value;
154 	}
155 
156 	private void push(Object o) {
157 		if (operandStackSize == operandStack.length) {
158 			operandStack = Arrays.copyOf(operandStack, operandStack.length * 2);
159 		}
160 		operandStack[operandStackSize++] = o;
161 	}
162 
163 	private void clearOperandStack() {
164 		Arrays.fill(operandStack, 0, operandStackSize, null);
165 		operandStackSize = 0;
166 	}
167 
168 	private final AwkSettings settings;
169 	private final boolean profiling;
170 	private final Map<Opcode, ProfilingReport.Accumulator> tupleProfilingStats;
171 	private final Map<String, ProfilingReport.Accumulator> functionProfilingStats;
172 	private final Deque<ActiveFunction> activeProfilingFunctions;
173 	private InputSource resolvedInputSource;
174 	private AwkExpression installedEvalExpression;
175 	private boolean mergedGlobalLayoutActive;
176 
177 	/** Optional extension-provided ordering for {@code for (index in array)} traversal. */
178 	private ForInKeyOrder forInKeyOrder;
179 
180 	/** Description of the primary script source, for runtime diagnostics. */
181 	private String sourceDescription;
182 
183 	/** Script line of the extension call currently being dispatched. */
184 	private int currentLineNumber;
185 
186 	/** Whether the extension beforeStart hooks already ran for this AVM. */
187 	private boolean beforeStartHooksExecuted;
188 
189 	/** Offset of a materialized SYMTAB array, for live operand-assignment updates. */
190 	private long symtabOffset = NULL_OFFSET;
191 
192 	/**
193 	 * Construct the interpreter.
194 	 * <p>
195 	 * Provided to allow programmatic construction of the interpreter
196 	 * outside of the framework which is used by Jawk.
197 	 */
198 	public AVM() {
199 		this(null, Collections.<String, JawkExtension>emptyMap());
200 	}
201 
202 	/**
203 	 * Construct the interpreter, accepting parameters which may have been
204 	 * set on the command-line arguments to the JVM.
205 	 *
206 	 * @param parameters The parameters affecting the behavior of the
207 	 *        interpreter.
208 	 * @param extensionInstances Map of the extensions to load
209 	 */
210 	public AVM(final AwkSettings parameters,
211 			final Map<String, JawkExtension> extensionInstances) {
212 		this(parameters, extensionInstances, false);
213 	}
214 
215 	/**
216 	 * Construct the interpreter, optionally enabling runtime profiling.
217 	 *
218 	 * @param parameters The parameters affecting the behavior of the
219 	 *        interpreter.
220 	 * @param extensionInstances Map of the extensions to load
221 	 * @param profilingEnabled Whether to collect profiling statistics
222 	 */
223 	public AVM(
224 			final AwkSettings parameters,
225 			final Map<String, JawkExtension> extensionInstances,
226 			final boolean profilingEnabled) {
227 		this.settings = parameters != null ? parameters : AwkSettings.DEFAULT_SETTINGS;
228 		this.extensionInstances = extensionInstances == null ?
229 				Collections.<String, JawkExtension>emptyMap() : extensionInstances;
230 		this.profiling = profilingEnabled;
231 		if (profilingEnabled) {
232 			this.tupleProfilingStats = new java.util.EnumMap<Opcode, ProfilingReport.Accumulator>(Opcode.class);
233 			this.functionProfilingStats = new LinkedHashMap<String, ProfilingReport.Accumulator>();
234 			this.activeProfilingFunctions = new ArrayDeque<ActiveFunction>();
235 		} else {
236 			this.tupleProfilingStats = null;
237 			this.functionProfilingStats = null;
238 			this.activeProfilingFunctions = null;
239 		}
240 
241 		arguments = Collections.emptyList();
242 		sortedArrayKeys = this.settings.isUseSortedArrayKeys();
243 		baseInitialVariables = new HashMap<String, Object>(this.settings.getVariables());
244 		baseSpecialVariables = JRT.copySpecialVariables(baseInitialVariables);
245 		executionInitialVariables = baseInitialVariables;
246 		executionSpecialVariables = baseSpecialVariables;
247 
248 		jrt = createJrt();
249 		initExtensions();
250 	}
251 
252 	/**
253 	 * Creates the runtime this interpreter drives.
254 	 * <p>
255 	 * Called once from the constructor, so that a subclass can install its own
256 	 * runtime — {@link SandboxedAVM} returns a
257 	 * {@link io.jawk.jrt.SandboxedJRT} this way. It runs before the subclass
258 	 * constructor body, so an override must not depend on the subclass's own
259 	 * fields.
260 	 * </p>
261 	 *
262 	 * @return the runtime bound to this interpreter, never {@code null}
263 	 */
264 	protected JRT createJrt() {
265 		return new JRT(this, this.settings.getLocale(), AwkSink.NOP_SINK, null);
266 	}
267 
268 	/**
269 	 * Returns the runtime settings associated with this interpreter.
270 	 *
271 	 * @return the settings, never {@code null}
272 	 */
273 	protected AwkSettings getSettings() {
274 		return settings;
275 	}
276 
277 	/**
278 	 * Returns the JRT (Jawk Runtime) instance associated with this interpreter.
279 	 *
280 	 * @return the JRT instance, never {@code null}
281 	 */
282 	@SuppressFBWarnings("EI_EXPOSE_REP")
283 	public JRT getJrt() {
284 		return jrt;
285 	}
286 
287 	/**
288 	 * Sets the sink used by default {@code print} and {@code printf}
289 	 * operations on this runtime.
290 	 *
291 	 * @param sink sink to use
292 	 */
293 	public void setAwkSink(AwkSink sink) {
294 		jrt.setAwkSink(Objects.requireNonNull(sink, "sink"));
295 	}
296 
297 	/**
298 	 * Sets the stream used for the stderr output of spawned processes
299 	 * (e.g.&nbsp;{@code system("...")}).
300 	 *
301 	 * @param errorStream stream to receive process stderr
302 	 */
303 	public void setErrorStream(PrintStream errorStream) {
304 		jrt.setErrorStream(errorStream);
305 	}
306 
307 	/**
308 	 * Sets the stream that receives runtime warning messages (gawk-style
309 	 * diagnostics). Warnings default to {@link System#err}.
310 	 *
311 	 * @param warningStream stream to receive runtime warnings
312 	 */
313 	public void setWarningStream(PrintStream warningStream) {
314 		jrt.setWarningStream(warningStream);
315 	}
316 
317 	/**
318 	 * Registers the hook that decides the key traversal order of
319 	 * {@code for (index in array)} statements.
320 	 * <p>
321 	 * When no hook is registered, iteration uses the array's natural key order.
322 	 * Extensions typically register a hook from their {@code beforeStart}
323 	 * method; the last registration wins.
324 	 * </p>
325 	 *
326 	 * @param keyOrder traversal-order provider, or {@code null} to restore the
327 	 *        natural key order
328 	 */
329 	public void setForInKeyOrder(ForInKeyOrder keyOrder) {
330 		forInKeyOrder = keyOrder;
331 	}
332 
333 	/**
334 	 * Returns the default sink used by this runtime.
335 	 *
336 	 * @return the current AWK sink
337 	 */
338 	public AwkSink getAwkSink() {
339 		return jrt.getAwkSink();
340 	}
341 
342 	/**
343 	 * Returns the locale configured for this runtime.
344 	 *
345 	 * @return runtime locale
346 	 */
347 	protected Locale getLocale() {
348 		return jrt.getLocale();
349 	}
350 
351 	/**
352 	 * Evaluates a compiled expression against the AVM state exactly as it
353 	 * currently stands.
354 	 *
355 	 * @param expression compiled expression to evaluate
356 	 * @return the resulting value
357 	 * @throws IOException if evaluation fails
358 	 */
359 	public Object eval(AwkExpression expression) throws IOException {
360 		AwkExpression compiledExpression = Objects.requireNonNull(expression, "expression");
361 		installExpressionMetadata(compiledExpression);
362 
363 		try {
364 			executeTuples(compiledExpression.top());
365 		} catch (ExitException e) {
366 			// Expression tuples must never contain EXIT opcodes. If callers pass an
367 			// invalid compiled expression, fail fast without poisoning later evals.
368 			throwExitException = false;
369 			exitCode = 0;
370 			throw new IllegalStateException("eval(AwkExpression) cannot execute EXIT opcodes.", e);
371 		}
372 		return operandStackSize == 0 ? null : JRT.toJavaScalar(pop());
373 	}
374 
375 	/**
376 	 * Evaluates a compiled expression against the supplied input source.
377 	 *
378 	 * @param expression compiled expression to evaluate
379 	 * @param inputSource input source providing the current record
380 	 * @return the resulting value
381 	 * @throws IOException if evaluation fails
382 	 */
383 	public Object eval(AwkExpression expression, InputSource inputSource) throws IOException {
384 		return eval(expression, inputSource, null);
385 	}
386 
387 	/**
388 	 * Evaluates a compiled expression against the supplied input source with
389 	 * per-call variable overrides.
390 	 *
391 	 * @param expression compiled expression to evaluate
392 	 * @param inputSource input source providing the current record
393 	 * @param variableOverrides additional variable assignments applied on top of
394 	 *        the settings-level variables (may be {@code null})
395 	 * @return the resulting value
396 	 * @throws IOException if evaluation fails
397 	 */
398 	public Object eval(
399 			AwkExpression expression,
400 			InputSource inputSource,
401 			Map<String, Object> variableOverrides)
402 			throws IOException {
403 		prepareForEval(inputSource, Collections.<String>emptyList(), variableOverrides);
404 		return eval(expression);
405 	}
406 
407 	/**
408 	 * Executes a compiled AWK program with the current runtime defaults.
409 	 *
410 	 * @param program compiled program to execute
411 	 * @param inputSource input source providing records
412 	 * @throws ExitException when the program terminates via {@code exit}
413 	 * @throws IOException if execution fails
414 	 */
415 	public void execute(AwkProgram program, InputSource inputSource) throws ExitException, IOException {
416 		execute(program, inputSource, Collections.<String>emptyList(), null);
417 	}
418 
419 	/**
420 	 * Executes a compiled AWK program with explicit runtime arguments.
421 	 *
422 	 * @param program compiled program to execute
423 	 * @param inputSource input source providing records
424 	 * @param runtimeArguments name=value or filename entries from the command line
425 	 * @throws ExitException when the program terminates via {@code exit}
426 	 * @throws IOException if execution fails
427 	 */
428 	public void execute(AwkProgram program, InputSource inputSource, List<String> runtimeArguments)
429 			throws ExitException,
430 			IOException {
431 		execute(program, inputSource, runtimeArguments, null);
432 	}
433 
434 	/**
435 	 * Executes a compiled AWK program with explicit runtime arguments and
436 	 * variable overrides.
437 	 *
438 	 * @param program compiled program to execute
439 	 * @param inputSource input source providing records
440 	 * @param runtimeArguments name=value or filename entries from the command line
441 	 * @param variableOverrides additional variable assignments applied on top of
442 	 *        the settings-level variables (may be {@code null})
443 	 * @throws ExitException when the program terminates via {@code exit}
444 	 * @throws IOException if execution fails
445 	 */
446 	public void execute(
447 			AwkProgram program,
448 			InputSource inputSource,
449 			List<String> runtimeArguments,
450 			Map<String, Object> variableOverrides)
451 			throws ExitException,
452 			IOException {
453 		AwkProgram compiledProgram = Objects.requireNonNull(program, "program");
454 		InputSource resolvedSource = Objects.requireNonNull(inputSource, "inputSource");
455 		resetRuntimeState(runtimeArguments, variableOverrides);
456 		installProgramMetadata(compiledProgram);
457 
458 		jrt.prepareForExecution(settings.getFieldSeparator(), settings.getDefaultRS());
459 		if (!executionSpecialVariables.isEmpty()) {
460 			jrt.applySpecialVariables(executionSpecialVariables);
461 		}
462 		rebindResolvedInputSource(resolvedSource);
463 		executeTuples(compiledProgram.top());
464 	}
465 
466 	/**
467 	 * Executes a compiled AWK program while persisting user-defined global
468 	 * variables across repeated executions on this AVM instance.
469 	 * <p>
470 	 * Before the new program starts, this method imports any user-defined
471 	 * globals currently materialized in the AVM and remaps them onto the
472 	 * incoming program's compiled global slots.
473 	 *
474 	 * @param program compiled program to execute
475 	 * @param inputSource input source providing records
476 	 * @throws ExitException when the program terminates via {@code exit}
477 	 * @throws IOException if execution fails
478 	 */
479 	public void executePersistingGlobals(AwkProgram program, InputSource inputSource)
480 			throws ExitException,
481 			IOException {
482 		executePersistingGlobals(program, inputSource, Collections.<String>emptyList(), null);
483 	}
484 
485 	/**
486 	 * Executes a compiled AWK program while persisting user-defined global
487 	 * variables across repeated executions on this AVM instance.
488 	 * <p>
489 	 * Before the new program starts, this method imports any user-defined
490 	 * globals currently materialized in the AVM and remaps them onto the
491 	 * incoming program's compiled global slots.
492 	 *
493 	 * @param program compiled program to execute
494 	 * @param inputSource input source providing records
495 	 * @param runtimeArguments name=value or filename entries from the command line
496 	 * @throws ExitException when the program terminates via {@code exit}
497 	 * @throws IOException if execution fails
498 	 */
499 	public void executePersistingGlobals(
500 			AwkProgram program,
501 			InputSource inputSource,
502 			List<String> runtimeArguments)
503 			throws ExitException,
504 			IOException {
505 		executePersistingGlobals(program, inputSource, runtimeArguments, null);
506 	}
507 
508 	/**
509 	 * Executes a compiled AWK program while persisting user-defined global
510 	 * variables across repeated executions on this AVM instance.
511 	 * <p>
512 	 * Before the new program starts, this method imports any user-defined
513 	 * globals currently materialized in the AVM and remaps them onto the
514 	 * incoming program's compiled global slots.
515 	 *
516 	 * @param program compiled program to execute
517 	 * @param inputSource input source providing records
518 	 * @param runtimeArguments name=value or filename entries from the command line
519 	 * @param variableOverrides additional variable assignments applied on top of
520 	 *        the settings-level variables (may be {@code null})
521 	 * @throws ExitException when the program terminates via {@code exit}
522 	 * @throws IOException if execution fails
523 	 */
524 	public void executePersistingGlobals(
525 			AwkProgram program,
526 			InputSource inputSource,
527 			List<String> runtimeArguments,
528 			Map<String, Object> variableOverrides)
529 			throws ExitException,
530 			IOException {
531 		AwkProgram compiledProgram = Objects.requireNonNull(program, "program");
532 		InputSource resolvedSource = Objects.requireNonNull(inputSource, "inputSource");
533 		mergeRuntimeState(runtimeArguments, variableOverrides, compiledProgram);
534 
535 		jrt.prepareForExecution(settings.getFieldSeparator(), settings.getDefaultRS());
536 		if (!executionSpecialVariables.isEmpty()) {
537 			jrt.applySpecialVariables(executionSpecialVariables);
538 		}
539 		rebindResolvedInputSource(resolvedSource);
540 		executeTuples(compiledProgram.top());
541 	}
542 
543 	/**
544 	 * Clears the user-defined globals retained in the current runtime stack.
545 	 * <p>
546 	 * The next {@link #executePersistingGlobals(AwkProgram, InputSource, List, Map)}
547 	 * call will therefore start from an empty persistent global bank.
548 	 */
549 	public void clearPersistentGlobals() {
550 		runtimeStack.clearGlobals();
551 		mergedGlobalLayoutActive = false;
552 	}
553 
554 	/**
555 	 * Captures the user-defined globals currently retained by this AVM for
556 	 * persistent execution.
557 	 * <p>
558 	 * The returned snapshot is serializable and can later be fed back into
559 	 * {@link #restorePersistentMemory(Map)} on this or another AVM instance.
560 	 *
561 	 * @return serializable snapshot of the persistent user-global bank
562 	 */
563 	public Map<String, Object> snapshotPersistentMemory() {
564 		return new LinkedHashMap<>(collectPersistentGlobalValues());
565 	}
566 
567 	/**
568 	 * Restores the user-defined globals retained by this AVM from a previously
569 	 * captured persistent-memory snapshot.
570 	 * <p>
571 	 * Restoring a snapshot replaces the current retained global bank. The next
572 	 * {@link #executePersistingGlobals(AwkProgram, InputSource, List, Map)} call
573 	 * will merge these globals into the compiled layout of the incoming program.
574 	 *
575 	 * @param snapshot snapshot to restore
576 	 */
577 	public void restorePersistentMemory(Map<String, Object> snapshot) {
578 		Map<String, Object> restoredSnapshot = Objects.requireNonNull(snapshot, "snapshot");
579 		Map<String, Object> restoredGlobals = filterToPersistentEligible(restoredSnapshot);
580 		runtimeStack.clearGlobals();
581 		if (!restoredGlobals.isEmpty()) {
582 			runtimeStack.rebindGlobals(new ArrayList<>(restoredGlobals.keySet()));
583 			applyGlobalsToStack(restoredGlobals);
584 		}
585 		mergedGlobalLayoutActive = false;
586 	}
587 
588 	private void initExtensions() {
589 		if (extensionInstances.isEmpty()) {
590 			return;
591 		}
592 		Set<JawkExtension> initialized = new LinkedHashSet<JawkExtension>();
593 		for (JawkExtension extension : extensionInstances.values()) {
594 			if (initialized.add(extension)) {
595 				extension.init(this, jrt, settings); // this = VariableManager
596 			}
597 		}
598 	}
599 
600 	// Offsets for globals that remain runtime-managed by the tuple stream.
601 	// ARGC is always materialized; ENVIRON and ARGV are emitted on demand.
602 	private long environOffset = NULL_OFFSET;
603 	private long argcOffset = NULL_OFFSET;
604 	private long argvOffset = NULL_OFFSET;
605 
606 	private static final Integer ZERO = Integer.valueOf(0);
607 	private static final Integer ONE = Integer.valueOf(1);
608 
609 	/** Random number generator used for rand() */
610 	private final BSDRandom randomNumberGenerator = new BSDRandom(1);
611 
612 	/**
613 	 * Address of the END blocks section, read from the compiled program;
614 	 * {@code null} for expression streams.
615 	 */
616 	private Address exitAddress = null;
617 
618 	/**
619 	 * Address of the ENDFILE section, read from the compiled program when it
620 	 * has BEGINFILE/ENDFILE rules; {@code null} otherwise.
621 	 */
622 	private Address endFileAddress = null;
623 
624 	/**
625 	 * Address of the NEXT_FILE tuple that opens each input file, read from
626 	 * the compiled program when it requires per-file input stepping
627 	 * (BEGINFILE/ENDFILE rules or a {@code nextfile} statement);
628 	 * {@code null} otherwise.
629 	 */
630 	private Address nextFileAddress = null;
631 
632 	/**
633 	 * Address of the main input loop's next-record entry point, read from the
634 	 * compiled program when it consumes input, where a {@code next} statement
635 	 * executed from a user-defined function resumes; {@code null} otherwise.
636 	 */
637 	private Address nextAddress = null;
638 
639 	/**
640 	 * <code>true</code> if execution position is within a BEGINFILE rule;
641 	 * <code>false</code> otherwise.
642 	 */
643 	private boolean withinBeginFileBlocks = false;
644 
645 	/**
646 	 * <code>true</code> if execution position is within an ENDFILE rule;
647 	 * <code>false</code> otherwise.
648 	 */
649 	private boolean withinEndFileBlocks = false;
650 
651 	/**
652 	 * <code>true</code> once the per-file main input loop has advanced to its
653 	 * first input file; <code>false</code> while still in the BEGIN blocks.
654 	 */
655 	private boolean inputFileLoopStarted = false;
656 
657 	/**
658 	 * <code>true</code> once the main input loop has started consuming input
659 	 * records; <code>false</code> while still in the BEGIN blocks (and, for
660 	 * per-file programs, while the first BEGINFILE rules run).
661 	 */
662 	private boolean mainInputLoopStarted = false;
663 
664 	/**
665 	 * <code>true</code> if execution position is within an END block;
666 	 * <code>false</code> otherwise.
667 	 */
668 	private boolean withinEndBlocks = false;
669 
670 	/**
671 	 * Exit code set by the <code>exit NN</code> command (0 by default)
672 	 */
673 	private int exitCode = 0;
674 
675 	/**
676 	 * Whether <code>exit</code> has been called and we should throw ExitException
677 	 */
678 	private boolean throwExitException = false;
679 
680 	/**
681 	 * Maps global variable names to their global array offsets.
682 	 * It is useful when passing variable assignments from the file-list
683 	 * portion of the command-line arguments.
684 	 */
685 	private Map<String, Integer> globalVariableOffsets;
686 	/**
687 	 * Indicates whether the variable, by name, is a scalar
688 	 * or not. If not, then it is an Associative Array.
689 	 */
690 	private Map<String, Boolean> globalVariableArrays;
691 	private Set<String> functionNames = Collections.emptySet();
692 	private Map<String, Integer> initializedEvalGlobalVariableOffsets;
693 	private Map<String, Boolean> initializedEvalGlobalVariableArrays;
694 
695 	/**
696 	 * Resets the interpreter to a fresh eval state and binds one text record as
697 	 * the current input.
698 	 *
699 	 * @param input text record to expose as {@code $0}
700 	 * @return {@code true} when a record was prepared, {@code false} when the
701 	 *         provided text represents no input
702 	 * @throws IOException if binding the input fails
703 	 */
704 	public boolean prepareForEval(String input) throws IOException {
705 		return prepareForEval(new SingleRecordInputSource(input), Collections.<String>emptyList(), null);
706 	}
707 
708 	/**
709 	 * Resets the interpreter to a fresh eval state and binds at most one record
710 	 * from the provided input source as the current input. Calling this method
711 	 * again on the same source advances to the next available record.
712 	 *
713 	 * @param inputSource source providing the record to bind
714 	 * @return {@code true} when a record was prepared, {@code false} when the
715 	 *         source is exhausted
716 	 * @throws IOException if reading the input fails
717 	 */
718 	public boolean prepareForEval(InputSource inputSource) throws IOException {
719 		return prepareForEval(inputSource, Collections.<String>emptyList(), null);
720 	}
721 
722 	private boolean prepareForEval(
723 			InputSource inputSource,
724 			List<String> runtimeArguments,
725 			Map<String, Object> variableOverrides)
726 			throws IOException {
727 		InputSource resolvedSource = Objects.requireNonNull(inputSource, "inputSource");
728 		resetRuntimeState(runtimeArguments, variableOverrides);
729 		rebindResolvedInputSource(resolvedSource);
730 
731 		jrt.jrtCloseAll();
732 		jrt.prepareForExecution(settings.getFieldSeparator(), settings.getDefaultRS());
733 		if (!executionSpecialVariables.isEmpty()) {
734 			jrt.applySpecialVariables(executionSpecialVariables);
735 		}
736 		return jrt.consumeInputForEval(resolvedInputSource);
737 	}
738 
739 	private void resetRuntimeState(List<String> runtimeArguments, Map<String, Object> variableOverrides) {
740 		resetTransientRuntimeState(runtimeArguments, variableOverrides);
741 		runtimeStack.clearGlobals();
742 	}
743 
744 	private void resetTransientRuntimeState(List<String> runtimeArguments, Map<String, Object> variableOverrides) {
745 		// Reset the AVM-owned state that must not leak across executions.
746 		clearOperandStack();
747 		elementArgumentReferences.clear();
748 		environOffset = NULL_OFFSET;
749 		argcOffset = NULL_OFFSET;
750 		argvOffset = NULL_OFFSET;
751 		symtabOffset = NULL_OFFSET;
752 		exitAddress = null;
753 		endFileAddress = null;
754 		nextFileAddress = null;
755 		nextAddress = null;
756 		withinBeginFileBlocks = false;
757 		withinEndFileBlocks = false;
758 		inputFileLoopStarted = false;
759 		mainInputLoopStarted = false;
760 		withinEndBlocks = false;
761 		exitCode = 0;
762 		throwExitException = false;
763 		globalVariableOffsets = null;
764 		globalVariableArrays = null;
765 		functionNames = Collections.emptySet();
766 		initializedEvalGlobalVariableOffsets = null;
767 		initializedEvalGlobalVariableArrays = null;
768 		installedEvalExpression = null;
769 		mergedGlobalLayoutActive = false;
770 		runtimeStack.resetTransientState();
771 		randomNumberGenerator.setSeed(1);
772 
773 		prepareExecutionInputs(runtimeArguments, variableOverrides);
774 	}
775 
776 	private void installExpressionMetadata(AwkExpression compiledExpression) {
777 		if (installedEvalExpression == compiledExpression) {
778 			return;
779 		}
780 		globalVariableOffsets = compiledExpression.getGlobalVariableOffsetMap();
781 		globalVariableArrays = compiledExpression.getGlobalVariableAarrayMap();
782 		functionNames = compiledExpression.getFunctionNameSet();
783 		installedEvalExpression = compiledExpression;
784 	}
785 
786 	private void installProgramMetadata(AwkProgram compiledProgram) {
787 		globalVariableOffsets = compiledProgram.getGlobalVariableOffsetMap();
788 		globalVariableArrays = compiledProgram.getGlobalVariableAarrayMap();
789 		functionNames = compiledProgram.getFunctionNameSet();
790 		sourceDescription = compiledProgram.getSourceDescription();
791 		exitAddress = compiledProgram.getExitAddress();
792 		endFileAddress = compiledProgram.getEndFileAddress();
793 		nextFileAddress = compiledProgram.getNextFileAddress();
794 		nextAddress = compiledProgram.getNextAddress();
795 	}
796 
797 	private void rebindResolvedInputSource(InputSource resolvedSource) {
798 		InputSource previousResolvedSource = resolvedInputSource;
799 		if (previousResolvedSource != null && previousResolvedSource != resolvedSource) {
800 			closeInputSource(previousResolvedSource);
801 		}
802 		resolvedInputSource = resolvedSource;
803 		// The /dev/stdin special filename follows the source bound to the run, so
804 		// that a reused AVM never reads the stream of a previous execution.
805 		jrt.bindStandardInput(resolvedSource);
806 	}
807 
808 	private boolean hasCompatibleEvalGlobalLayout(long numGlobals) {
809 		Object[] globals = runtimeStack.getNumGlobals();
810 		return globals != null
811 				&& globals.length == numGlobals
812 				&& Objects.equals(initializedEvalGlobalVariableOffsets, globalVariableOffsets)
813 				&& Objects.equals(initializedEvalGlobalVariableArrays, globalVariableArrays);
814 	}
815 
816 	/**
817 	 * Resets transient execution state, installs the new program metadata, and
818 	 * merges the previously retained user globals into the new compiled global
819 	 * layout.
820 	 *
821 	 * @param runtimeArguments name=value or filename entries for this execution
822 	 * @param variableOverrides per-call variable overrides for this execution
823 	 * @param compiledProgram program whose global layout should become active
824 	 */
825 	private void mergeRuntimeState(
826 			List<String> runtimeArguments,
827 			Map<String, Object> variableOverrides,
828 			AwkProgram compiledProgram) {
829 		Map<String, Object> carriedGlobals = collectPersistentGlobalValues();
830 		resetTransientRuntimeState(runtimeArguments, variableOverrides);
831 		installProgramMetadata(compiledProgram);
832 
833 		Map<String, Object> basePersistentSeeds = collectBasePersistentGlobalSeeds();
834 		Map<String, Object> executionUserSeeds = collectExecutionUserGlobalSeeds(variableOverrides);
835 		List<String> mergedGlobalNamesByOffset = buildMergedGlobalNamesByOffset(
836 				carriedGlobals,
837 				basePersistentSeeds,
838 				executionUserSeeds);
839 
840 		runtimeStack.rebindGlobals(mergedGlobalNamesByOffset);
841 		applyGlobalsToStack(carriedGlobals);
842 		applyGlobalsToStack(basePersistentSeeds);
843 		applyGlobalsToStack(executionUserSeeds);
844 		mergedGlobalLayoutActive = true;
845 	}
846 
847 	/**
848 	 * Returns whether the current runtime stack already contains a merged global
849 	 * layout for the compiled program about to execute.
850 	 * <p>
851 	 * Persistent execution may append previously retained globals after the
852 	 * compiled globals of the incoming program. The tuple stream only dereferences
853 	 * the prefix defined by {@code SET_NUM_GLOBALS}, so appended globals are valid
854 	 * as long as the compiled prefix still matches name-for-name and offset-for-offset.
855 	 *
856 	 * @param numGlobals number of globals compiled into the active program
857 	 * @return {@code true} when the merged layout is compatible with the active
858 	 *         program
859 	 */
860 	private boolean hasCompatiblePersistentGlobalLayout(long numGlobals) {
861 		Object[] globals = runtimeStack.getNumGlobals();
862 		if (!mergedGlobalLayoutActive
863 				|| globals == null
864 				|| globalVariableOffsets == null
865 				|| globals.length < numGlobals) {
866 			return false;
867 		}
868 		for (Map.Entry<String, Integer> entry : globalVariableOffsets.entrySet()) {
869 			int offset = entry.getValue().intValue();
870 			if (offset < 0 || offset >= globals.length || !entry.getKey().equals(runtimeStack.getGlobalName(offset))) {
871 				return false;
872 			}
873 		}
874 		return true;
875 	}
876 
877 	/**
878 	 * Applies execution-level initial variables to stack-managed globals.
879 	 * <p>
880 	 * Plain {@link #execute(AwkProgram, InputSource, List, Map)} calls apply all
881 	 * compatible globals here. Persistent executions only reapply non-persistent
882 	 * globals so carried user globals are not overwritten unless they were
883 	 * explicitly supplied for the current run.
884 	 *
885 	 * @param skipPersistentEligibleGlobals whether persistent user globals should
886 	 *        be skipped by this application pass
887 	 */
888 	private void applyExecutionInitialVariablesToGlobalSlots(boolean skipPersistentEligibleGlobals) {
889 		for (Map.Entry<String, Object> entry : executionInitialVariables.entrySet()) {
890 			String key = entry.getKey();
891 			if (skipPersistentEligibleGlobals && isPersistentEligibleGlobal(key)) {
892 				continue;
893 			}
894 			if (functionNames.contains(key)) {
895 				throw new IllegalArgumentException("Cannot assign a scalar to a function name (" + key + ").");
896 			}
897 			Integer offsetObj = globalVariableOffsets.get(key);
898 			Boolean arrayObj = globalVariableArrays.get(key);
899 			if (offsetObj != null) {
900 				Object obj = normalizeExternalVariableValue(entry.getValue());
901 				if (arrayObj.booleanValue()) {
902 					if (obj instanceof Map) {
903 						runtimeStack.setFilelistVariable(offsetObj.intValue(), obj);
904 					} else {
905 						throw new IllegalArgumentException(
906 								"Cannot assign a scalar to a non-scalar variable (" + key + ").");
907 					}
908 				} else {
909 					runtimeStack.setFilelistVariable(offsetObj.intValue(), obj);
910 				}
911 			}
912 		}
913 	}
914 
915 	/**
916 	 * Prepares the per-execution runtime arguments and variable overrides.
917 	 * <p>
918 	 * Base settings-level variables remain the default source. Per-call overrides
919 	 * are layered on top without mutating the base snapshot held by this AVM.
920 	 *
921 	 * @param runtimeArguments name=value or filename entries for this execution
922 	 * @param variableOverrides per-call variable overrides for this execution
923 	 */
924 	private void prepareExecutionInputs(
925 			List<String> runtimeArguments,
926 			Map<String, Object> variableOverrides) {
927 		this.arguments = runtimeArguments != null ? new ArrayList<>(runtimeArguments) : Collections.<String>emptyList();
928 
929 		if (variableOverrides == null || variableOverrides.isEmpty()) {
930 			executionInitialVariables = baseInitialVariables;
931 			executionSpecialVariables = baseSpecialVariables;
932 		} else {
933 			executionInitialVariables = new HashMap<>(baseInitialVariables);
934 			executionInitialVariables.putAll(variableOverrides);
935 
936 			Map<String, Object> specialOverrides = JRT.copySpecialVariables(variableOverrides);
937 			if (specialOverrides.isEmpty()) {
938 				executionSpecialVariables = baseSpecialVariables;
939 			} else {
940 				executionSpecialVariables = new HashMap<>(baseSpecialVariables);
941 				executionSpecialVariables.putAll(specialOverrides);
942 			}
943 		}
944 	}
945 
946 	/**
947 	 * Filters the given map to retain only entries whose keys are
948 	 * persistent-eligible globals.
949 	 *
950 	 * @param source source map to filter
951 	 * @return insertion-ordered map containing only persistent-eligible entries
952 	 */
953 	private Map<String, Object> filterToPersistentEligible(Map<String, Object> source) {
954 		Map<String, Object> result = new LinkedHashMap<>();
955 		for (Map.Entry<String, Object> entry : source.entrySet()) {
956 			if (isPersistentEligibleGlobal(entry.getKey())) {
957 				result.put(entry.getKey(), entry.getValue());
958 			}
959 		}
960 		return result;
961 	}
962 
963 	/**
964 	 * Collects the current user-defined globals retained in the runtime stack.
965 	 *
966 	 * @return retained user globals keyed by name, in current runtime order
967 	 */
968 	private Map<String, Object> collectPersistentGlobalValues() {
969 		return filterToPersistentEligible(runtimeStack.snapshotGlobalVariables());
970 	}
971 
972 	/**
973 	 * Collects the AVM-wide baseline variables that should be reapplied before
974 	 * each persistent execution.
975 	 *
976 	 * @return baseline user globals keyed by name
977 	 */
978 	private Map<String, Object> collectBasePersistentGlobalSeeds() {
979 		Map<String, Object> basePersistentSeeds = new LinkedHashMap<>();
980 		for (Map.Entry<String, Object> entry : baseInitialVariables.entrySet()) {
981 			String name = entry.getKey();
982 			if (isPersistentEligibleGlobal(name)) {
983 				validateSeededGlobalName(name);
984 				Object value = normalizeExternalVariableValue(entry.getValue());
985 				validateSeededGlobalValue(name, value);
986 				basePersistentSeeds.put(name, value);
987 			}
988 		}
989 		return basePersistentSeeds;
990 	}
991 
992 	/**
993 	 * Collects the user-defined variables that should override the retained
994 	 * global bank for the current persistent execution.
995 	 * <p>
996 	 * Only user globals are included here. JRT-managed special variables still
997 	 * flow through the normal execution setup.
998 	 *
999 	 * @param variableOverrides per-call variable overrides for this execution
1000 	 * @return insertion-ordered overriding seed values keyed by variable name
1001 	 */
1002 	private Map<String, Object> collectExecutionUserGlobalSeeds(Map<String, Object> variableOverrides) {
1003 		Map<String, Object> executionUserSeeds = new LinkedHashMap<>();
1004 		if (variableOverrides != null) {
1005 			for (Map.Entry<String, Object> entry : variableOverrides.entrySet()) {
1006 				String name = entry.getKey();
1007 				if (isPersistentEligibleGlobal(name)) {
1008 					validateSeededGlobalName(name);
1009 					Object value = normalizeExternalVariableValue(entry.getValue());
1010 					validateSeededGlobalValue(name, value);
1011 					executionUserSeeds.put(name, value);
1012 				}
1013 			}
1014 		}
1015 		return executionUserSeeds;
1016 	}
1017 
1018 	/**
1019 	 * Builds the slot order for the next persistent execution.
1020 	 * <p>
1021 	 * The compiled globals are always installed first in their compiled offset
1022 	 * order. Retained globals and seeded user globals that are not compiled by
1023 	 * the incoming program are appended afterwards so future runs can still reuse
1024 	 * them without changing the current program's compiled offsets.
1025 	 *
1026 	 * @param carriedGlobals retained user globals from the previous execution
1027 	 * @param basePersistentSeeds baseline user globals coming from the AVM settings
1028 	 * @param executionUserSeeds per-call user overrides for this execution
1029 	 * @return merged slot-to-name layout for the next persistent run
1030 	 */
1031 	private List<String> buildMergedGlobalNamesByOffset(
1032 			Map<String, Object> carriedGlobals,
1033 			Map<String, Object> basePersistentSeeds,
1034 			Map<String, Object> executionUserSeeds) {
1035 		LinkedHashSet<String> orderedNames = new LinkedHashSet<>();
1036 		List<Map.Entry<String, Integer>> compiledGlobals = new ArrayList<>(globalVariableOffsets.entrySet());
1037 		compiledGlobals.sort(java.util.Comparator.comparingInt(Map.Entry::getValue));
1038 		for (Map.Entry<String, Integer> entry : compiledGlobals) {
1039 			orderedNames.add(entry.getKey());
1040 		}
1041 		orderedNames.addAll(carriedGlobals.keySet());
1042 		orderedNames.addAll(basePersistentSeeds.keySet());
1043 		orderedNames.addAll(executionUserSeeds.keySet());
1044 		return new ArrayList<>(orderedNames);
1045 	}
1046 
1047 	/**
1048 	 * Writes each entry from {@code globals} into the corresponding named slot in
1049 	 * the runtime stack.
1050 	 *
1051 	 * @param globals map of variable name to value to apply
1052 	 */
1053 	private void applyGlobalsToStack(Map<String, Object> globals) {
1054 		for (Map.Entry<String, Object> entry : globals.entrySet()) {
1055 			runtimeStack.setGlobalVariable(entry.getKey(), entry.getValue());
1056 		}
1057 	}
1058 
1059 	/**
1060 	 * Returns whether the given global name should participate in persistent
1061 	 * memory.
1062 	 *
1063 	 * @param name global variable name
1064 	 * @return {@code true} when the variable should persist across runs
1065 	 */
1066 	private boolean isPersistentEligibleGlobal(String name) {
1067 		return name != null
1068 				&& !isManagedSpecialVariable(name)
1069 				&& !NON_PERSISTENT_GLOBALS.contains(name);
1070 	}
1071 
1072 	/**
1073 	 * Returns whether the named variable is a JRT-managed special variable in
1074 	 * the current execution mode. Most special variables (NR, FS, FILENAME,
1075 	 * ...) always are. The gawk-only ERRNO and ARGIND are special outside
1076 	 * POSIX mode only: with {@code --posix} they are ordinary global
1077 	 * variables, exactly like {@code gawk --posix} treats them.
1078 	 *
1079 	 * @param name variable name to inspect
1080 	 * @return {@code true} when reads and writes of the variable go through
1081 	 *         the JRT instead of a global slot
1082 	 */
1083 	private boolean isManagedSpecialVariable(String name) {
1084 		if (!JRT.isJrtManagedSpecialVariable(name)) {
1085 			return false;
1086 		}
1087 		if (JRT.isGawkOnlySpecialVariable(name)) {
1088 			return !settings.isPosix();
1089 		}
1090 		return true;
1091 	}
1092 
1093 	/**
1094 	 * Validates that a seeded global name is compatible with the compiled
1095 	 * metadata of the current program before any value normalization can mutate a
1096 	 * caller-provided object graph.
1097 	 *
1098 	 * @param name variable name to validate
1099 	 */
1100 	private void validateSeededGlobalName(String name) {
1101 		if (functionNames.contains(name)) {
1102 			throw new IllegalArgumentException("Cannot assign a value to a function name (" + name + ").");
1103 		}
1104 	}
1105 
1106 	/**
1107 	 * Validates that a normalized seeded global value is compatible with the
1108 	 * compiled metadata of the current program.
1109 	 *
1110 	 * @param name variable name to validate
1111 	 * @param value proposed seeded value after normalization
1112 	 */
1113 	private void validateSeededGlobalValue(String name, Object value) {
1114 		Boolean arrayObj = globalVariableArrays.get(name);
1115 		if (Boolean.TRUE.equals(arrayObj) && !(value instanceof Map)) {
1116 			throw new IllegalArgumentException("Cannot assign a scalar to a non-scalar variable (" + name + ").");
1117 		}
1118 	}
1119 
1120 	/**
1121 	 * Executes the tuple stream after the runtime has been fully prepared.
1122 	 *
1123 	 * @param position current position in the tuple stream
1124 	 * @throws ExitException when the AWK program executes {@code exit}
1125 	 * @throws IOException when runtime input operations fail
1126 	 */
1127 	private void executeTuples(PositionTracker position)
1128 			throws ExitException,
1129 			IOException {
1130 		Map<Long, ConditionPair> conditionPairs = null;
1131 		Opcode opcode = null;
1132 		long tupleStartNanos = 0L;
1133 		try {
1134 			while (!position.isEOF()) {
1135 				// System_out.println("--> "+position);
1136 				Tuple tuple = position.current();
1137 				opcode = tuple.getOpcode();
1138 				if (profiling) {
1139 					tupleStartNanos = beforeProfiledTuple(tuple, opcode);
1140 				}
1141 				// switch on OPCODE
1142 				switch (opcode) {
1143 				case PRINT: {
1144 					execPrint((CountTuple) tuple);
1145 					position.next();
1146 					break;
1147 				}
1148 				case PRINT_TO_FILE: {
1149 					execPrintToFile((CountAndAppendTuple) tuple);
1150 					position.next();
1151 					break;
1152 				}
1153 				case PRINT_TO_PIPE: {
1154 					execPrintToPipe((CountTuple) tuple);
1155 					position.next();
1156 					break;
1157 				}
1158 				case PRINTF: {
1159 					execPrintf((CountTuple) tuple);
1160 					position.next();
1161 					break;
1162 				}
1163 				case PRINTF_TO_FILE: {
1164 					execPrintfToFile((CountAndAppendTuple) tuple);
1165 					position.next();
1166 					break;
1167 				}
1168 				case PRINTF_TO_PIPE: {
1169 					execPrintfToPipe((CountTuple) tuple);
1170 					position.next();
1171 					break;
1172 				}
1173 				case SPRINTF: {
1174 					// arg[0] = # of sprintf arguments
1175 					// stack[0] = arg1 (format string)
1176 					// stack[1] = arg2
1177 					// etc.
1178 					CountTuple countTuple = (CountTuple) tuple;
1179 					long numArgs = countTuple.getCount();
1180 					push(sprintfFunction(numArgs));
1181 					position.next();
1182 					break;
1183 				}
1184 				case LENGTH: {
1185 					execLength((CountTuple) tuple);
1186 					position.next();
1187 					break;
1188 				}
1189 				case PUSH_LONG: {
1190 					// arg[0] = long constant to push onto the stack
1191 					PushLongTuple pushTuple = (PushLongTuple) tuple;
1192 					push(pushTuple.getValue());
1193 					position.next();
1194 					break;
1195 				}
1196 				case PUSH_DOUBLE: {
1197 					// arg[0] = double constant to push onto the stack
1198 					PushDoubleTuple pushTuple = (PushDoubleTuple) tuple;
1199 					push(pushTuple.getValue());
1200 					position.next();
1201 					break;
1202 				}
1203 				case PUSH_STRING: {
1204 					// arg[0] = string constant to push onto the stack
1205 					PushStringTuple pushTuple = (PushStringTuple) tuple;
1206 					push(pushTuple.getValue());
1207 					position.next();
1208 					break;
1209 				}
1210 				case POP: {
1211 					// stack[0] = item to pop from the stack
1212 					Object discarded = pop();
1213 					if (tuple instanceof ScalarPopTuple) {
1214 						checkScalar(discarded);
1215 					}
1216 					position.next();
1217 					break;
1218 				}
1219 				case IFFALSE: {
1220 					// arg[0] = address to jump to if top of stack is false
1221 					// stack[0] = item to check
1222 
1223 					// if int, then check for 0
1224 					// if double, then check for 0
1225 					// if String, then check for "" or double value of "0"
1226 					boolean jump = !jrt.toBoolean(pop());
1227 					if (jump) {
1228 						position.jump(tuple.getAddress());
1229 					} else {
1230 						position.next();
1231 					}
1232 					break;
1233 				}
1234 				case TO_NUMBER: {
1235 					// stack[0] = item to convert to a number
1236 
1237 					// if int, then check for 0
1238 					// if double, then check for 0
1239 					// if String, then check for "" or double value of "0"
1240 					boolean val = jrt.toBoolean(pop());
1241 					push(val ? ONE : ZERO);
1242 					position.next();
1243 					break;
1244 				}
1245 				case IFTRUE: {
1246 					// arg[0] = address to jump to if top of stack is true
1247 					// stack[0] = item to check
1248 
1249 					// if int, then check for 0
1250 					// if double, then check for 0
1251 					// if String, then check for "" or double value of "0"
1252 					boolean jump = jrt.toBoolean(pop());
1253 					if (jump) {
1254 						position.jump(tuple.getAddress());
1255 					} else {
1256 						position.next();
1257 					}
1258 					break;
1259 				}
1260 				case NOT: {
1261 					// stack[0] = item to logically negate
1262 
1263 					Object o = pop();
1264 
1265 					boolean result = jrt.toBoolean(o);
1266 
1267 					if (result) {
1268 						push(0);
1269 					} else {
1270 						push(1);
1271 					}
1272 					position.next();
1273 					break;
1274 				}
1275 				case NEGATE: {
1276 					// stack[0] = item to numerically negate
1277 
1278 					push(JRT.negate(pop()));
1279 					position.next();
1280 					break;
1281 				}
1282 				case UNARY_PLUS: {
1283 					// stack[0] = item to convert to a number
1284 					// A numeric scalar is already a number: pushing it back
1285 					// unchanged avoids both re-boxing and precision loss.
1286 					push(numericValueOf(pop()));
1287 					position.next();
1288 					break;
1289 				}
1290 				case GOTO: {
1291 					// arg[0] = address
1292 
1293 					position.jump(tuple.getAddress());
1294 					break;
1295 				}
1296 				case NOP: {
1297 					// do nothing, just advance the position
1298 					position.next();
1299 					break;
1300 				}
1301 				case CONCAT: {
1302 					// stack[0] = string1
1303 					// stack[1] = string2
1304 					String s2 = jrt.toAwkString(pop());
1305 					String s1 = jrt.toAwkString(pop());
1306 					String resultString = s1 + s2;
1307 					push(resultString);
1308 					position.next();
1309 					break;
1310 				}
1311 				case MULTI_CONCAT: {
1312 					// arg[0] = number of stack items to concatenate
1313 					// stack[0] = last concatenation operand
1314 					CountTuple countTuple = (CountTuple) tuple;
1315 					int count = (int) countTuple.getCount();
1316 					// Store String references so appends run left-to-right. Converting
1317 					// operands to char[] would copy them once before StringBuilder
1318 					// copies them again, and front-inserting would shift existing
1319 					// content on each operand.
1320 					String[] values = new String[count];
1321 					int resultLength = 0;
1322 					for (int i = count - 1; i >= 0; i--) {
1323 						values[i] = jrt.toAwkString(pop());
1324 						resultLength += values[i].length();
1325 					}
1326 					StringBuilder resultString = new StringBuilder(resultLength);
1327 					for (String value : values) {
1328 						resultString.append(value);
1329 					}
1330 					push(resultString.toString());
1331 					position.next();
1332 					break;
1333 				}
1334 				case ASSIGN:
1335 				case ASSIGN_NOPUSH: {
1336 					// arg[0] = offset
1337 					// arg[1] = isGlobal
1338 					// stack[0] = value
1339 					VariableTuple variableTuple = (VariableTuple) tuple;
1340 					Object value = pop();
1341 					assign(
1342 							variableTuple.getVariableOffset(),
1343 							value,
1344 							variableTuple.isGlobal(),
1345 							position,
1346 							opcode == Opcode.ASSIGN);
1347 					position.next();
1348 					break;
1349 				}
1350 				case ASSIGN_ARRAY: {
1351 					// arg[0] = offset
1352 					// arg[1] = isGlobal
1353 					// stack[0] = array index
1354 					// stack[1] = value
1355 					Object arrIdx = pop();
1356 					Object rhs = pop();
1357 					if (rhs == null) {
1358 						rhs = BLANK;
1359 					}
1360 					VariableTuple variableTuple = (VariableTuple) tuple;
1361 					long offset = variableTuple.getVariableOffset();
1362 					boolean isGlobal = variableTuple.isGlobal();
1363 					assignArray(offset, arrIdx, rhs, isGlobal);
1364 					position.next();
1365 					break;
1366 				}
1367 				case ASSIGN_MAP_ELEMENT: {
1368 					// stack[0] = array index
1369 					// stack[1] = associative array
1370 					// stack[2] = value
1371 					Object arrIdx = pop();
1372 					Map<Object, Object> array = toMap(pop());
1373 					Object rhs = pop();
1374 					if (rhs == null) {
1375 						rhs = BLANK;
1376 					}
1377 					assignMapElement(array, arrIdx, rhs);
1378 					position.next();
1379 					break;
1380 				}
1381 				case PLUS_EQ_ARRAY:
1382 				case MINUS_EQ_ARRAY:
1383 				case MULT_EQ_ARRAY:
1384 				case DIV_EQ_ARRAY:
1385 				case MOD_EQ_ARRAY:
1386 				case POW_EQ_ARRAY: {
1387 					execCompoundAssignArray(opcode, (VariableTuple) tuple);
1388 					position.next();
1389 					break;
1390 				}
1391 				case PLUS_EQ_MAP_ELEMENT:
1392 				case MINUS_EQ_MAP_ELEMENT:
1393 				case MULT_EQ_MAP_ELEMENT:
1394 				case DIV_EQ_MAP_ELEMENT:
1395 				case MOD_EQ_MAP_ELEMENT:
1396 				case POW_EQ_MAP_ELEMENT: {
1397 					execCompoundAssignMapElement(opcode);
1398 					position.next();
1399 					break;
1400 				}
1401 
1402 				case ASSIGN_AS_INPUT: {
1403 					// stack[0] = value
1404 					jrt.setInputLine(pop());
1405 					push(jrt.getInputLine());
1406 					position.next();
1407 					break;
1408 				}
1409 
1410 				case ASSIGN_AS_INPUT_FIELD: {
1411 					// stack[0] = field number
1412 					// stack[1] = value
1413 					Object fieldNumObj = pop();
1414 					long fieldNum = JRT.parseFieldNumber(fieldNumObj);
1415 					Object value = pop();
1416 					push(value); // leave the result on the stack
1417 					if (fieldNum == 0) {
1418 						jrt.setInputLine(value);
1419 						jrt.jrtParseFields();
1420 					} else {
1421 						jrt.jrtSetInputField(value, fieldNum);
1422 					}
1423 					position.next();
1424 					break;
1425 				}
1426 				case PLUS_EQ:
1427 				case MINUS_EQ:
1428 				case MULT_EQ:
1429 				case DIV_EQ:
1430 				case MOD_EQ:
1431 				case POW_EQ: {
1432 					execCompoundAssignVariable(opcode, (VariableTuple) tuple);
1433 					position.next();
1434 					break;
1435 				}
1436 				case PLUS_EQ_INPUT_FIELD:
1437 				case MINUS_EQ_INPUT_FIELD:
1438 				case MULT_EQ_INPUT_FIELD:
1439 				case DIV_EQ_INPUT_FIELD:
1440 				case MOD_EQ_INPUT_FIELD:
1441 				case POW_EQ_INPUT_FIELD: {
1442 					execCompoundAssignInputField(opcode);
1443 					position.next();
1444 					break;
1445 				}
1446 				case INC: {
1447 					// arg[0] = offset
1448 					// arg[1] = isGlobal
1449 					VariableTuple variableTuple = (VariableTuple) tuple;
1450 					inc(variableTuple.getVariableOffset(), variableTuple.isGlobal());
1451 					position.next();
1452 					break;
1453 				}
1454 				case DEC: {
1455 					// arg[0] = offset
1456 					// arg[1] = isGlobal
1457 					VariableTuple variableTuple = (VariableTuple) tuple;
1458 					dec(variableTuple.getVariableOffset(), variableTuple.isGlobal());
1459 					position.next();
1460 					break;
1461 				}
1462 				case POSTINC: {
1463 					// arg[0] = offset
1464 					// arg[1] = isGlobal
1465 					pop();
1466 					VariableTuple variableTuple = (VariableTuple) tuple;
1467 					push(inc(variableTuple.getVariableOffset(), variableTuple.isGlobal()));
1468 					position.next();
1469 					break;
1470 				}
1471 				case POSTDEC: {
1472 					// arg[0] = offset
1473 					// arg[1] = isGlobal
1474 					pop();
1475 					VariableTuple variableTuple = (VariableTuple) tuple;
1476 					push(dec(variableTuple.getVariableOffset(), variableTuple.isGlobal()));
1477 					position.next();
1478 					break;
1479 				}
1480 				case INC_ARRAY_REF: {
1481 					// arg[0] = offset
1482 					// arg[1] = isGlobal
1483 					// stack[0] = array index
1484 					VariableTuple variableTuple = (VariableTuple) tuple;
1485 					boolean isGlobal = variableTuple.isGlobal();
1486 					Map<Object, Object> aa = ensureMapVariable(variableTuple.getVariableOffset(), isGlobal);
1487 					Object key = pop();
1488 					checkScalar(key);
1489 					Object o = aa.get(key);
1490 					aa.put(key, JRT.inc(blankToZero(o)));
1491 					position.next();
1492 					break;
1493 				}
1494 				case DEC_ARRAY_REF: {
1495 					// arg[0] = offset
1496 					// arg[1] = isGlobal
1497 					// stack[0] = array index
1498 					VariableTuple variableTuple = (VariableTuple) tuple;
1499 					boolean isGlobal = variableTuple.isGlobal();
1500 					Map<Object, Object> aa = ensureMapVariable(variableTuple.getVariableOffset(), isGlobal);
1501 					Object key = pop();
1502 					checkScalar(key);
1503 					Object o = aa.get(key);
1504 					aa.put(key, JRT.dec(blankToZero(o)));
1505 					position.next();
1506 					break;
1507 				}
1508 				case INC_MAP_REF: {
1509 					// stack[0] = array index
1510 					// stack[1] = associative array
1511 					Object key = pop();
1512 					checkScalar(key);
1513 					Map<Object, Object> aa = toMap(pop());
1514 					Object o = aa.get(key);
1515 					aa.put(key, JRT.inc(blankToZero(o)));
1516 					position.next();
1517 					break;
1518 				}
1519 				case DEC_MAP_REF: {
1520 					// stack[0] = array index
1521 					// stack[1] = associative array
1522 					Object key = pop();
1523 					checkScalar(key);
1524 					Map<Object, Object> aa = toMap(pop());
1525 					Object o = aa.get(key);
1526 					aa.put(key, JRT.dec(blankToZero(o)));
1527 					position.next();
1528 					break;
1529 				}
1530 				case INC_DOLLAR_REF: {
1531 					// stack[0] = dollar index (field number)
1532 					long fieldnum = JRT.parseFieldNumber(pop());
1533 
1534 					Object numObj = blankToZero(jrt.jrtGetInputField(fieldnum));
1535 					Object original = numericValueOf(numObj);
1536 					setNumOnJRT(fieldnum, JRT.inc(original));
1537 
1538 					push(original);
1539 
1540 					position.next();
1541 					break;
1542 				}
1543 				case DEC_DOLLAR_REF: {
1544 					// stack[0] = dollar index (field number)
1545 					// same code as GET_INPUT_FIELD:
1546 					long fieldnum = JRT.parseFieldNumber(pop());
1547 
1548 					Object numObj = blankToZero(jrt.jrtGetInputField(fieldnum));
1549 					Object original = numericValueOf(numObj);
1550 					setNumOnJRT(fieldnum, JRT.dec(original));
1551 
1552 					push(original);
1553 
1554 					position.next();
1555 					break;
1556 				}
1557 				case DEREFERENCE: {
1558 					// arg[0] = offset
1559 					// arg[1] = isGlobal
1560 					DereferenceTuple dereferenceTuple = (DereferenceTuple) tuple;
1561 					boolean isGlobal = dereferenceTuple.isGlobal();
1562 					long offset = dereferenceTuple.getVariableOffset();
1563 					push(resolveVariable(offset, isGlobal, dereferenceTuple.isArray()));
1564 					position.next();
1565 					break;
1566 				}
1567 				case PEEK_DEREFERENCE: {
1568 					VariableTuple variableTuple = (VariableTuple) tuple;
1569 					Object value = runtimeStack
1570 							.getVariable(variableTuple.getVariableOffset(), variableTuple.isGlobal());
1571 					push(
1572 							value instanceof ArgumentReference ?
1573 									resolveRawArgumentReference((ArgumentReference) value) : value);
1574 					position.next();
1575 					break;
1576 				}
1577 				case PUSH_INDIRECT_ARGUMENT: {
1578 					VariableTuple variableTuple = (VariableTuple) tuple;
1579 					Object scalarValue = runtimeStack
1580 							.getVariable(variableTuple.getVariableOffset(), variableTuple.isGlobal());
1581 					// Typed values are passed as plain values; only untyped
1582 					// variables need a live link back to the caller's slot.
1583 					if (scalarValue instanceof ArgumentReference) {
1584 						push(resolveUserFunctionArgument(scalarValue));
1585 					} else if (isUntyped(scalarValue)) {
1586 						push(
1587 								new IndirectArgumentReference(
1588 										runtimeStack.getVariableFrame(variableTuple.isGlobal()),
1589 										variableTuple.getVariableOffset(),
1590 										scalarValue));
1591 					} else {
1592 						push(scalarValue);
1593 					}
1594 					position.next();
1595 					break;
1596 				}
1597 				case PUSH_INDIRECT_ARRAY_ARGUMENT: {
1598 					Object idx = pop();
1599 					checkScalar(idx);
1600 					Map<Object, Object> map = toMap(pop());
1601 					Object scalarValue = JRT.getAssocArrayValue(map, idx);
1602 					// Typed elements are passed as plain values; only untyped
1603 					// elements need a live link back to their containing array.
1604 					push(
1605 							isUntyped(scalarValue) ?
1606 									new IndirectArrayArgumentReference(map, idx, scalarValue) : scalarValue);
1607 					position.next();
1608 					break;
1609 				}
1610 				case DEREF_ARRAY: {
1611 					// stack[0] = array index
1612 					Object idx = pop(); // idx
1613 					checkScalar(idx);
1614 					Map<Object, Object> map = toMap(pop());
1615 					Object o = JRT.getAssocArrayValue(map, idx);
1616 					push(o);
1617 					position.next();
1618 					break;
1619 				}
1620 				case ENSURE_ARRAY_ELEMENT: {
1621 					// stack[0] = array index
1622 					// stack[1] = associative array
1623 					Object idx = pop();
1624 					Map<Object, Object> map = toMap(pop());
1625 					push(ensureArrayInArray(map, idx));
1626 					position.next();
1627 					break;
1628 				}
1629 				case PEEK_ARRAY_ELEMENT: {
1630 					// stack[0] = array index
1631 					Object idx = pop();
1632 					checkScalar(idx);
1633 					Map<Object, Object> map = toMap(pop());
1634 					if (map instanceof AssocArray && !JRT.containsAwkKey(map, idx)) {
1635 						push(AssocArray.UNTYPED);
1636 					} else {
1637 						Object value = map.get(idx);
1638 						push(value != null ? value : AssocArray.UNTYPED);
1639 					}
1640 					position.next();
1641 					break;
1642 				}
1643 				case SRAND: {
1644 					// arg[0] = numArgs (where 0 = no args, anything else = one argument)
1645 					// stack[0] = seed (only if numArgs != 0)
1646 					CountTuple countTuple = (CountTuple) tuple;
1647 					long numArgs = countTuple.getCount();
1648 					int seed;
1649 					if (numArgs == 0) {
1650 						// use the time of day for the seed
1651 						seed = JRT.timeSeed();
1652 					} else {
1653 						seed = (int) JRT.toDouble(pop());
1654 					}
1655 					int previousSeed = randomNumberGenerator.getSeed();
1656 					randomNumberGenerator.setSeed(seed);
1657 					push(previousSeed);
1658 					position.next();
1659 					break;
1660 				}
1661 				case RAND: {
1662 					push(randomNumberGenerator.nextDouble());
1663 					position.next();
1664 					break;
1665 				}
1666 				case INTFUNC: {
1667 					// stack[0] = arg to int() function
1668 					push(JRT.truncateToScalar(JRT.toDouble(pop())));
1669 					position.next();
1670 					break;
1671 				}
1672 				case SQRT: {
1673 					// stack[0] = arg to sqrt() function
1674 					push(Math.sqrt(JRT.toDouble(pop())));
1675 					position.next();
1676 					break;
1677 				}
1678 				case LOG: {
1679 					// stack[0] = arg to log() function
1680 					push(Math.log(JRT.toDouble(pop())));
1681 					position.next();
1682 					break;
1683 				}
1684 				case EXP: {
1685 					// stack[0] = arg to exp() function
1686 					push(Math.exp(JRT.toDouble(pop())));
1687 					position.next();
1688 					break;
1689 				}
1690 				case SIN: {
1691 					// stack[0] = arg to sin() function
1692 					push(Math.sin(JRT.toDouble(pop())));
1693 					position.next();
1694 					break;
1695 				}
1696 				case COS: {
1697 					// stack[0] = arg to cos() function
1698 					push(Math.cos(JRT.toDouble(pop())));
1699 					position.next();
1700 					break;
1701 				}
1702 				case ATAN2: {
1703 					// stack[0] = 2nd arg to atan2() function
1704 					// stack[1] = 1st arg to atan2() function
1705 					double d2 = JRT.toDouble(pop());
1706 					double d1 = JRT.toDouble(pop());
1707 					push(Math.atan2(d1, d2));
1708 					position.next();
1709 					break;
1710 				}
1711 				case MATCH: {
1712 					execMatch();
1713 					position.next();
1714 					break;
1715 				}
1716 				case INDEX: {
1717 					// stack[0] = 2nd arg to index() function
1718 					// stack[1] = 1st arg to index() function
1719 					String s2 = jrt.toAwkString(pop());
1720 					String s1 = jrt.toAwkString(pop());
1721 					push(jrt.index(s1, s2));
1722 					position.next();
1723 					break;
1724 				}
1725 				case SUB_FOR_DOLLAR_0: {
1726 					execSubForDollar0((BooleanTuple) tuple);
1727 					position.next();
1728 					break;
1729 				}
1730 				case SUB_FOR_DOLLAR_REFERENCE: {
1731 					execSubForDollarReference((BooleanTuple) tuple);
1732 					position.next();
1733 					break;
1734 				}
1735 				case SUB_FOR_VARIABLE: {
1736 					execSubForVariable((SubstitutionVariableTuple) tuple, position);
1737 					position.next();
1738 					break;
1739 				}
1740 				case SUB_FOR_ARRAY_REFERENCE: {
1741 					execSubForArrayReference((SubstitutionVariableTuple) tuple);
1742 					position.next();
1743 					break;
1744 				}
1745 				case SUB_FOR_MAP_REFERENCE: {
1746 					execSubForMapReference((BooleanTuple) tuple);
1747 					position.next();
1748 					break;
1749 				}
1750 				case SPLIT: {
1751 					execSplit((CountTuple) tuple, position);
1752 					position.next();
1753 					break;
1754 				}
1755 				case SUBSTR: {
1756 					execSubstr((CountTuple) tuple);
1757 					position.next();
1758 					break;
1759 				}
1760 				case TOLOWER: {
1761 					// stack[0] = string
1762 					push(jrt.toAwkString(pop()).toLowerCase());
1763 					position.next();
1764 					break;
1765 				}
1766 				case TOUPPER: {
1767 					// stack[0] = string
1768 					push(jrt.toAwkString(pop()).toUpperCase());
1769 					position.next();
1770 					break;
1771 				}
1772 				case SYSTEM: {
1773 					// stack[0] = command string
1774 					String s = jrt.toAwkString(pop());
1775 					push(jrt.jrtSystem(s));
1776 					position.next();
1777 					break;
1778 				}
1779 				case SWAP: {
1780 					// stack[0] = item1
1781 					// stack[1] = item2
1782 					Object o1 = pop();
1783 					Object o2 = pop();
1784 					push(o1);
1785 					push(o2);
1786 					position.next();
1787 					break;
1788 				}
1789 				case CMP_EQ: {
1790 					// stack[0] = item2
1791 					// stack[1] = item1
1792 					Object o2 = pop();
1793 					Object o1 = pop();
1794 					push(jrt.compare(o1, o2, 0) ? ONE : ZERO);
1795 					position.next();
1796 					break;
1797 				}
1798 				case CMP_LT: {
1799 					// stack[0] = item2
1800 					// stack[1] = item1
1801 					Object o2 = pop();
1802 					Object o1 = pop();
1803 					push(jrt.compare(o1, o2, -1) ? ONE : ZERO);
1804 					position.next();
1805 					break;
1806 				}
1807 				case CMP_GT: {
1808 					// stack[0] = item2
1809 					// stack[1] = item1
1810 					Object o2 = pop();
1811 					Object o1 = pop();
1812 					push(jrt.compare(o1, o2, 1) ? ONE : ZERO);
1813 					position.next();
1814 					break;
1815 				}
1816 				case MATCHES: {
1817 					// stack[0] = regexp (precompiled or dynamic)
1818 					// stack[1] = text
1819 					Object o2 = pop();
1820 					Object o1 = pop();
1821 					// The text operand must be converted with the AWK number-to-string rule, not with
1822 					// Object.toString(): a Double would otherwise be matched as "291.0" instead of "291".
1823 					push(jrt.matches(jrt.toAwkString(o1), o2) ? 1 : 0);
1824 					position.next();
1825 					break;
1826 				}
1827 				case ADD: {
1828 					// stack[0] = item2
1829 					// stack[1] = item1
1830 					Object o2 = pop();
1831 					Object o1 = pop();
1832 					push(JRT.add(o1, o2));
1833 					position.next();
1834 					break;
1835 				}
1836 				case SUBTRACT: {
1837 					// stack[0] = item2
1838 					// stack[1] = item1
1839 					Object o2 = pop();
1840 					Object o1 = pop();
1841 					push(JRT.subtract(o1, o2));
1842 					position.next();
1843 					break;
1844 				}
1845 				case MULTIPLY: {
1846 					// stack[0] = item2
1847 					// stack[1] = item1
1848 					Object o2 = pop();
1849 					Object o1 = pop();
1850 					push(JRT.multiply(o1, o2));
1851 					position.next();
1852 					break;
1853 				}
1854 				case DIVIDE: {
1855 					// stack[0] = item2
1856 					// stack[1] = item1
1857 					Object o2 = pop();
1858 					Object o1 = pop();
1859 					push(JRT.divide(o1, o2));
1860 					position.next();
1861 					break;
1862 				}
1863 				case MOD: {
1864 					// stack[0] = item2
1865 					// stack[1] = item1
1866 					Object o2 = pop();
1867 					Object o1 = pop();
1868 					push(JRT.mod(o1, o2));
1869 					position.next();
1870 					break;
1871 				}
1872 				case POW: {
1873 					// stack[0] = item2
1874 					// stack[1] = item1
1875 					Object o2 = pop();
1876 					Object o1 = pop();
1877 					push(JRT.pow(o1, o2));
1878 					position.next();
1879 					break;
1880 				}
1881 				case DUP: {
1882 					// stack[0] = top of stack item
1883 					Object o = pop();
1884 					push(o);
1885 					push(o);
1886 					position.next();
1887 					break;
1888 				}
1889 				case KEYLIST: {
1890 					Object o = pop();
1891 					if (isUntyped(o)) {
1892 						push(new ArrayDeque<>());
1893 						position.next();
1894 						break;
1895 					}
1896 					if (!(o instanceof Map)) {
1897 						throw new AwkRuntimeException("Attempting to use a scalar as an array.");
1898 					}
1899 					@SuppressWarnings("unchecked")
1900 					Map<Object, Object> map = (Map<Object, Object>) o;
1901 					push(new ArrayDeque<>(forInKeyOrder == null ? map.keySet() : forInKeyOrder.order(map)));
1902 					position.next();
1903 					break;
1904 				}
1905 				case IS_EMPTY_KEYLIST: {
1906 					// arg[0] = address
1907 					// stack[0] = Deque
1908 					Object o = pop();
1909 					if (o == null || !(o instanceof Deque)) {
1910 						throw new AwkRuntimeException(
1911 								position.lineNumber(),
1912 								"Cannot get a key list (via 'in') of a non associative array. arg = " + o.getClass() + ", " + o);
1913 					}
1914 					Deque<?> keylist = (Deque<?>) o;
1915 					if (keylist.isEmpty()) {
1916 						position.jump(tuple.getAddress());
1917 					} else {
1918 						position.next();
1919 					}
1920 					break;
1921 				}
1922 				case GET_FIRST_AND_REMOVE_FROM_KEYLIST: {
1923 					// stack[0] = Deque
1924 					Object o = pop();
1925 					if (o == null || !(o instanceof Deque)) {
1926 						throw new AwkRuntimeException(
1927 								position.lineNumber(),
1928 								"Cannot get a key list (via 'in') of a non associative array. arg = " + o.getClass() + ", " + o);
1929 					}
1930 					// pop off and return the head of the key set
1931 					Deque<?> keylist = (Deque<?>) o;
1932 					push(keylist.removeFirst());
1933 					position.next();
1934 					break;
1935 				}
1936 				case CHECK_CLASS: {
1937 					// arg[0] = class object
1938 					// stack[0] = item to check
1939 					ClassTuple checkTuple = (ClassTuple) tuple;
1940 					Object o = pop();
1941 					if (!checkTuple.getType().isInstance(o)) {
1942 						throw new AwkRuntimeException(
1943 								position.lineNumber(),
1944 								"Verification failed. Top-of-stack = " + o.getClass() + " isn't an instance of "
1945 										+ checkTuple.getType());
1946 					}
1947 					push(o);
1948 					position.next();
1949 					break;
1950 				}
1951 				case CONSUME_INPUT: {
1952 					// arg[0] = address
1953 					// store the next record into $0, $1, ...
1954 					mainInputLoopStarted = true;
1955 					if (jrt.consumeInput(resolvedInputSource)) {
1956 						position.next();
1957 					} else {
1958 						position.jump(tuple.getAddress());
1959 					}
1960 					break;
1961 				}
1962 				case CONSUME_FILE_INPUT: {
1963 					// arg[0] = address of the ENDFILE section
1964 					// store the next record of the current file into $0, $1, ...
1965 					mainInputLoopStarted = true;
1966 					withinBeginFileBlocks = false;
1967 					if (jrt.consumeCurrentFileInput(resolvedInputSource)) {
1968 						position.next();
1969 					} else {
1970 						withinEndFileBlocks = true;
1971 						position.jump(tuple.getAddress());
1972 					}
1973 					break;
1974 				}
1975 				case NEXT_FILE: {
1976 					// arg[0] = address to jump to when no input file remains
1977 					inputFileLoopStarted = true;
1978 					withinEndFileBlocks = false;
1979 					if (jrt.advanceToNextFile(resolvedInputSource)) {
1980 						withinBeginFileBlocks = true;
1981 						position.next();
1982 					} else {
1983 						position.jump(tuple.getAddress());
1984 					}
1985 					break;
1986 				}
1987 				case EXEC_NEXTFILE: {
1988 					executeNextfile(position);
1989 					break;
1990 				}
1991 				case EXEC_NEXT: {
1992 					executeNext(position);
1993 					break;
1994 				}
1995 
1996 				case GETLINE_INPUT: {
1997 					checkGetlineAllowed(position);
1998 					boolean consumed = isMainInputFileBounded() ?
1999 							jrt.consumeCurrentFileInput(resolvedInputSource) : jrt.consumeInput(resolvedInputSource);
2000 					push(consumed ? 1 : 0);
2001 					position.next();
2002 					break;
2003 				}
2004 				case GETLINE_INPUT_TO_TARGET: {
2005 					// arg[0] = address to jump to when no record was read
2006 					checkGetlineAllowed(position);
2007 					Object input = isMainInputFileBounded() ?
2008 							jrt.consumeCurrentFileInputToTarget(resolvedInputSource) : jrt.consumeInputToTarget(resolvedInputSource);
2009 					if (input != null) {
2010 						push(1);
2011 						push(input);
2012 						position.next();
2013 					} else {
2014 						push(0);
2015 						position.jump(tuple.getAddress());
2016 					}
2017 					break;
2018 				}
2019 				case USE_AS_FILE_INPUT: {
2020 					// stack[0] = filename
2021 					// arg[0] = address to jump to when no record was read
2022 					String s = jrt.toAwkString(pop());
2023 					Integer retcode = jrt.jrtConsumeFileInputForGetline(s);
2024 					push(retcode);
2025 					if (retcode.intValue() == 1) {
2026 						push(jrt.toInputScalar(jrt.jrtGetInputString()));
2027 						position.next();
2028 					} else {
2029 						position.jump(tuple.getAddress());
2030 					}
2031 					break;
2032 				}
2033 				case USE_AS_COMMAND_INPUT: {
2034 					// stack[0] = command line
2035 					// arg[0] = address to jump to when no record was read
2036 					String s = jrt.toAwkString(pop());
2037 					Integer retcode = jrt.jrtConsumeCommandInputForGetline(s);
2038 					push(retcode);
2039 					if (retcode.intValue() == 1) {
2040 						push(jrt.toInputScalar(jrt.jrtGetInputString()));
2041 						position.next();
2042 					} else {
2043 						position.jump(tuple.getAddress());
2044 					}
2045 					break;
2046 				}
2047 				case ENVIRON_OFFSET: {
2048 					// arg[0] = offset; already populated from SET_NUM_GLOBALS so
2049 					// beforeStart hooks observe the real value
2050 					populateEnviron(((LongTuple) tuple).getValue());
2051 					position.next();
2052 					break;
2053 				}
2054 				case ARGC_OFFSET: {
2055 					// arg[0] = offset; already populated from SET_NUM_GLOBALS
2056 					populateArgc(((LongTuple) tuple).getValue());
2057 					position.next();
2058 					break;
2059 				}
2060 				case ARGV_OFFSET: {
2061 					// arg[0] = offset; already populated from SET_NUM_GLOBALS
2062 					populateArgv(((LongTuple) tuple).getValue());
2063 					position.next();
2064 					break;
2065 				}
2066 				case GET_INPUT_FIELD: {
2067 					// stack[0] = field number
2068 					Object fieldNumber = pop();
2069 					push(jrt.jrtGetInputField(fieldNumber));
2070 					position.next();
2071 					break;
2072 				}
2073 				case GET_INPUT_FIELD_CONST: {
2074 					InputFieldTuple inputFieldTuple = (InputFieldTuple) tuple;
2075 					long fieldnum = inputFieldTuple.getFieldIndex();
2076 					push(jrt.jrtGetInputField(fieldnum));
2077 					position.next();
2078 					break;
2079 				}
2080 				case APPLY_RS: {
2081 					jrt.applyRS(jrt.getRSVar());
2082 					position.next();
2083 					break;
2084 				}
2085 				case CALL_FUNCTION: {
2086 					// arg[0] = function address
2087 					// arg[1] = function name
2088 					// arg[2] = # of formal parameters
2089 					// arg[3] = # of actual parameters
2090 					// stack[0] = last actual parameter
2091 					// stack[1] = before-last actual parameter
2092 					// ...
2093 					// stack[n-1] = first actual parameter
2094 					// etc.
2095 					CallFunctionTuple callTuple = (CallFunctionTuple) tuple;
2096 					Address funcAddr = callTuple.getAddress();
2097 					long numFormalParams = callTuple.getNumFormalParams();
2098 					long numActualParams = callTuple.getNumActualParams();
2099 					runtimeStack.pushFrame(numFormalParams, position.currentIndex());
2100 					// Arguments are stacked, so first in the stack is the last for the function
2101 					for (long i = numActualParams - 1; i >= 0; i--) {
2102 						Object argument = pop();
2103 						adoptElementArgumentReference(argument);
2104 						runtimeStack.setVariable(i, argument, false); // false = local
2105 					}
2106 					position.jump(funcAddr);
2107 					// position.next();
2108 					break;
2109 				}
2110 				case INDIRECT_CALL: {
2111 					execIndirectCall((IndirectCallTuple) tuple, position, tupleStartNanos);
2112 					break;
2113 				}
2114 				case FUNCTION: {
2115 					// important for compilation,
2116 					// not needed for interpretation
2117 					// arg[0] = function name
2118 					// arg[1] = # of formal parameters
2119 					position.next();
2120 					break;
2121 				}
2122 				case WARNING: {
2123 					jrt.printWarning(((Tuple.WarningTuple) tuple).getMessage());
2124 					position.next();
2125 					break;
2126 				}
2127 				case SET_RETURN_RESULT: {
2128 					// stack[0] = return result
2129 					runtimeStack.setReturnValue(pop());
2130 					position.next();
2131 					break;
2132 				}
2133 				case RETURN_FROM_FUNCTION: {
2134 					releaseElementArgumentReferences();
2135 					position.jump(runtimeStack.popFrame());
2136 					push(runtimeStack.getReturnValue());
2137 					position.next();
2138 					break;
2139 				}
2140 				case SET_NUM_GLOBALS: {
2141 					execSetNumGlobals((CountTuple) tuple);
2142 					position.next();
2143 					break;
2144 				}
2145 				case BEFORE_START_HOOKS: {
2146 					runBeforeStartHooks();
2147 					position.next();
2148 					break;
2149 				}
2150 				case UPDATE_SYMTAB: {
2151 					execUpdateSymtab(((LongTuple) tuple).getValue());
2152 					position.next();
2153 					break;
2154 				}
2155 				case UPDATE_FUNCTAB: {
2156 					execUpdateFunctab(((LongTuple) tuple).getValue());
2157 					position.next();
2158 					break;
2159 				}
2160 				case CLOSE: {
2161 					// stack[0] = file or command line to close
2162 					String s = jrt.toAwkString(pop());
2163 					push(jrt.jrtClose(s));
2164 					position.next();
2165 					break;
2166 				}
2167 				case APPLY_SUBSEP: {
2168 					execApplySubsep((CountTuple) tuple);
2169 					position.next();
2170 					break;
2171 				}
2172 				case APPLY_SUBSEP_UNDER_TOP: {
2173 					// stack[0] = value to keep on top (the "in" array operand)
2174 					// stack[1..count] = array indices to convert
2175 					Object top = pop();
2176 					execApplySubsep((CountTuple) tuple);
2177 					push(top);
2178 					position.next();
2179 					break;
2180 				}
2181 				case DELETE_ARRAY_ELEMENT: {
2182 					// arg[0] = offset
2183 					// arg[1] = isGlobal
2184 					// stack[0] = array index
2185 					VariableTuple variableTuple = (VariableTuple) tuple;
2186 					long offset = variableTuple.getVariableOffset();
2187 					boolean isGlobal = variableTuple.isGlobal();
2188 					Map<Object, Object> aa = getMapVariable(offset, isGlobal);
2189 					Object key = pop();
2190 					checkScalar(key);
2191 					if (aa != null) {
2192 						aa.remove(key);
2193 						detachMissingArrayArgumentReferences(aa);
2194 					}
2195 					position.next();
2196 					break;
2197 				}
2198 				case DELETE_MAP_ELEMENT: {
2199 					// stack[0] = array index
2200 					// stack[1] = associative array
2201 					Object key = pop();
2202 					checkScalar(key);
2203 					Map<Object, Object> aa = toMap(pop());
2204 					aa.remove(key);
2205 					detachMissingArrayArgumentReferences(aa);
2206 					position.next();
2207 					break;
2208 				}
2209 				case DELETE_ARRAY: {
2210 					// arg[0] = offset
2211 					// arg[1] = isGlobal
2212 					// (nothing on the stack)
2213 					VariableTuple variableTuple = (VariableTuple) tuple;
2214 					long offset = variableTuple.getVariableOffset();
2215 					boolean isGlobal = variableTuple.isGlobal();
2216 					Map<Object, Object> array = getMapVariable(offset, isGlobal);
2217 					if (array != null) {
2218 						array.clear();
2219 						detachMissingArrayArgumentReferences(array);
2220 					}
2221 					position.next();
2222 					break;
2223 				}
2224 				case SET_WITHIN_END_BLOCKS: {
2225 					// arg[0] = whether within the END blocks section
2226 					BooleanTuple endBlocksTuple = (BooleanTuple) tuple;
2227 					withinEndBlocks = endBlocksTuple.getValue();
2228 					position.next();
2229 					break;
2230 				}
2231 				case EXIT_WITHOUT_CODE:
2232 				case EXIT_WITH_CODE: {
2233 					if (opcode == Opcode.EXIT_WITH_CODE) {
2234 						// stack[0] = exit code
2235 						exitCode = (int) JRT.toDouble(pop());
2236 					}
2237 					throwExitException = true;
2238 					withinBeginFileBlocks = false;
2239 					withinEndFileBlocks = false;
2240 
2241 					// If in BEGIN or in a rule, jump to the END section
2242 					if (!withinEndBlocks && exitAddress != null) {
2243 						resetCallState();
2244 						position.jump(exitAddress);
2245 					} else {
2246 						// Exit immediately with ExitException
2247 						// clear operand stack
2248 						clearOperandStack();
2249 						throw new ExitException(exitCode, "The AWK script requested an exit");
2250 						// position.next();
2251 					}
2252 					break;
2253 				}
2254 				case REGEXP: {
2255 					// Literal regex tuples must provide a precompiled Pattern as arg[1]
2256 					RegexTuple regexTuple = (RegexTuple) tuple;
2257 					Pattern pattern = regexTuple.getPattern();
2258 					push(pattern);
2259 					position.next();
2260 					break;
2261 				}
2262 				case CONDITION_PAIR: {
2263 					// Legacy opcode kept for precompiled tuple streams
2264 					// stack[0] = End condition
2265 					// stack[1] = Start condition
2266 					if (conditionPairs == null) {
2267 						conditionPairs = new HashMap<Long, ConditionPair>();
2268 					}
2269 					long currentIndex = position.currentIndex();
2270 					ConditionPair cp = conditionPairs.get(currentIndex);
2271 					if (cp == null) {
2272 						cp = new ConditionPair();
2273 						conditionPairs.put(currentIndex, cp);
2274 					}
2275 					boolean end = jrt.toBoolean(pop());
2276 					boolean start = jrt.toBoolean(pop());
2277 					push(cp.update(start, end) ? ONE : ZERO);
2278 					position.next();
2279 					break;
2280 				}
2281 				case CONDITION_PAIR_IN_RANGE: {
2282 					// arg[0] = unique identifier of the range pattern
2283 					if (conditionPairs == null) {
2284 						conditionPairs = new HashMap<Long, ConditionPair>();
2285 					}
2286 					long id = ((LongTuple) tuple).getValue();
2287 					ConditionPair cp = conditionPairs.get(id);
2288 					if (cp == null) {
2289 						cp = new ConditionPair();
2290 						conditionPairs.put(id, cp);
2291 					}
2292 					push(cp.isWithin() ? ONE : ZERO);
2293 					position.next();
2294 					break;
2295 				}
2296 				case CONDITION_PAIR_ENTER: {
2297 					// arg[0] = unique identifier of the range pattern
2298 					// A CONDITION_PAIR_IN_RANGE tuple for the same identifier always
2299 					// executes first, so the pair is already registered
2300 					conditionPairs.get(((LongTuple) tuple).getValue()).enter();
2301 					position.next();
2302 					break;
2303 				}
2304 				case CONDITION_PAIR_LEAVE: {
2305 					// arg[0] = unique identifier of the range pattern
2306 					conditionPairs.get(((LongTuple) tuple).getValue()).leave();
2307 					position.next();
2308 					break;
2309 				}
2310 				case IS_IN: {
2311 					// stack[1] = key to check
2312 					Object arr = pop();
2313 					Object arg = pop();
2314 					checkScalar(arg);
2315 					if (isUntyped(arr)) {
2316 						push(ZERO);
2317 						position.next();
2318 						break;
2319 					}
2320 					if (!(arr instanceof Map)) {
2321 						throw new AwkRuntimeException("Attempting to use a scalar as an array.");
2322 					}
2323 					@SuppressWarnings("unchecked")
2324 					Map<Object, Object> aa = (Map<Object, Object>) arr;
2325 					boolean result = JRT.containsAwkKey(aa, arg);
2326 					push(result ? ONE : ZERO);
2327 					position.next();
2328 					break;
2329 				}
2330 				case THIS: {
2331 					// this is in preparation for a function
2332 					// call for the JVM-COMPILED script, only
2333 					// therefore, do NOTHING for the interpreted
2334 					// version
2335 					position.next();
2336 					break;
2337 				}
2338 				case EXTENSION: {
2339 					// arg[0] = extension function metadata
2340 					// arg[1] = # of args on the stack
2341 					// stack[0] = first actual parameter
2342 					// stack[1] = second actual parameter
2343 					// etc.
2344 					ExtensionTuple extensionTuple = (ExtensionTuple) tuple;
2345 					ExtensionFunction function = extensionTuple.getFunction();
2346 					long numArgs = extensionTuple.getArgCount();
2347 					push(
2348 							invokeExtension(
2349 									function,
2350 									popArguments(numArgs),
2351 									position.lineNumber()));
2352 
2353 					position.next();
2354 					break;
2355 				}
2356 				case ASSIGN_NF: {
2357 					Object v = pop();
2358 					jrt.setNF(v);
2359 					push(v);
2360 					position.next();
2361 					break;
2362 				}
2363 				case PUSH_NF: {
2364 					push(jrt.getNF());
2365 					position.next();
2366 					break;
2367 				}
2368 				case ASSIGN_NR: {
2369 					Object v = pop();
2370 					jrt.setNR(v);
2371 					push(v);
2372 					position.next();
2373 					break;
2374 				}
2375 				case PUSH_NR: {
2376 					push(jrt.getNR());
2377 					position.next();
2378 					break;
2379 				}
2380 				case ASSIGN_FNR: {
2381 					Object v = pop();
2382 					jrt.setFNR(v);
2383 					push(v);
2384 					position.next();
2385 					break;
2386 				}
2387 				case PUSH_FNR: {
2388 					push(jrt.getFNR());
2389 					position.next();
2390 					break;
2391 				}
2392 				case ASSIGN_FS: {
2393 					Object v = pop();
2394 					jrt.setFS(v);
2395 					push(v);
2396 					position.next();
2397 					break;
2398 				}
2399 				case ASSIGN_IGNORECASE: {
2400 					Object v = pop();
2401 					jrt.setIGNORECASE(v);
2402 					push(v);
2403 					position.next();
2404 					break;
2405 				}
2406 				case PUSH_IGNORECASE: {
2407 					push(jrt.getIGNORECASEVar());
2408 					position.next();
2409 					break;
2410 				}
2411 				case PUSH_FS: {
2412 					push(jrt.getFSVar());
2413 					position.next();
2414 					break;
2415 				}
2416 				case ASSIGN_RS: {
2417 					Object v = pop();
2418 					jrt.setRS(v);
2419 					push(v);
2420 					position.next();
2421 					break;
2422 				}
2423 				case PUSH_RS: {
2424 					push(jrt.getRSVar());
2425 					position.next();
2426 					break;
2427 				}
2428 				case ASSIGN_OFS: {
2429 					Object v = pop();
2430 					jrt.setOFS(v);
2431 					push(v);
2432 					position.next();
2433 					break;
2434 				}
2435 				case PUSH_OFS: {
2436 					push(jrt.getOFSVar());
2437 					position.next();
2438 					break;
2439 				}
2440 				case ASSIGN_ORS: {
2441 					Object v = pop();
2442 					jrt.setORS(v);
2443 					push(v);
2444 					position.next();
2445 					break;
2446 				}
2447 				case PUSH_ORS: {
2448 					push(jrt.getORSVar());
2449 					position.next();
2450 					break;
2451 				}
2452 				case ASSIGN_RSTART: {
2453 					Object v = pop();
2454 					jrt.setRSTART(v);
2455 					push(v);
2456 					position.next();
2457 					break;
2458 				}
2459 				case PUSH_RSTART: {
2460 					push(jrt.getRSTART());
2461 					position.next();
2462 					break;
2463 				}
2464 				case ASSIGN_RLENGTH: {
2465 					Object v = pop();
2466 					jrt.setRLENGTH(v);
2467 					push(v);
2468 					position.next();
2469 					break;
2470 				}
2471 				case PUSH_RLENGTH: {
2472 					push(jrt.getRLENGTH());
2473 					position.next();
2474 					break;
2475 				}
2476 				case ASSIGN_FILENAME: {
2477 					Object v = pop();
2478 					jrt.setFILENAMEViaJrt(v);
2479 					push(v == null ? "" : v);
2480 					position.next();
2481 					break;
2482 				}
2483 				case PUSH_FILENAME: {
2484 					push(jrt.getFILENAME());
2485 					position.next();
2486 					break;
2487 				}
2488 				case ASSIGN_ERRNO: {
2489 					Object v = pop();
2490 					jrt.setERRNO(v);
2491 					push(v == null ? "" : v);
2492 					position.next();
2493 					break;
2494 				}
2495 				case PUSH_ERRNO: {
2496 					push(jrt.getERRNO());
2497 					position.next();
2498 					break;
2499 				}
2500 				case ASSIGN_ARGIND: {
2501 					Object v = pop();
2502 					jrt.setARGIND(v);
2503 					push(v == null ? ZERO : v);
2504 					position.next();
2505 					break;
2506 				}
2507 				case PUSH_ARGIND: {
2508 					push(jrt.getARGIND());
2509 					position.next();
2510 					break;
2511 				}
2512 				case ASSIGN_SUBSEP: {
2513 					Object v = pop();
2514 					jrt.setSUBSEP(v);
2515 					push(v);
2516 					position.next();
2517 					break;
2518 				}
2519 				case PUSH_SUBSEP: {
2520 					push(jrt.getSUBSEPVar());
2521 					position.next();
2522 					break;
2523 				}
2524 				case ASSIGN_CONVFMT: {
2525 					Object v = pop();
2526 					jrt.setCONVFMT(v);
2527 					push(v);
2528 					position.next();
2529 					break;
2530 				}
2531 				case PUSH_CONVFMT: {
2532 					push(jrt.getCONVFMTVar());
2533 					position.next();
2534 					break;
2535 				}
2536 				case ASSIGN_OFMT: {
2537 					Object v = pop();
2538 					jrt.setOFMT(v);
2539 					push(v);
2540 					position.next();
2541 					break;
2542 				}
2543 				case PUSH_OFMT: {
2544 					push(getOFMT());
2545 					position.next();
2546 					break;
2547 				}
2548 				case ASSIGN_ARGC: {
2549 					Object v = pop();
2550 					if (argcOffset == NULL_OFFSET) {
2551 						throw new AwkRuntimeException("ARGC is read-only (not materialized).");
2552 					}
2553 					runtimeStack.setVariable(argcOffset, v, true);
2554 					push(v);
2555 					position.next();
2556 					break;
2557 				}
2558 				case PUSH_ARGC: {
2559 					if (argcOffset == NULL_OFFSET) {
2560 						push(getARGC());
2561 					} else {
2562 						push(runtimeStack.getVariable(argcOffset, true));
2563 					}
2564 					position.next();
2565 					break;
2566 				}
2567 				default:
2568 					throw new Error("invalid opcode: " + position.opcode());
2569 				}
2570 				if (profiling) {
2571 					afterProfiledTuple(opcode, tupleStartNanos);
2572 				}
2573 			}
2574 
2575 		} catch (ExitException ee) {
2576 			if (profiling && (opcode == Opcode.EXIT_WITH_CODE || opcode == Opcode.EXIT_WITHOUT_CODE)) {
2577 				afterProfiledTuple(opcode, tupleStartNanos);
2578 			}
2579 			throw ee;
2580 		} catch (IOException ioe) {
2581 			resetCallState();
2582 			throw ioe;
2583 		} catch (RuntimeException re) {
2584 			resetCallState();
2585 			if (re instanceof AwkSandboxException) {
2586 				throw re;
2587 			}
2588 			throw new AwkRuntimeException(position.lineNumber(), re.getMessage(), re);
2589 		} catch (AssertionError ae) {
2590 			resetCallState();
2591 			throw ae;
2592 		}
2593 
2594 		// If <code>exit</code> was called, throw an ExitException
2595 		if (throwExitException) {
2596 			throw new ExitException(exitCode, "The AWK script requested an exit");
2597 		}
2598 	}
2599 
2600 	/**
2601 	 * Clears all collected profiling statistics.
2602 	 */
2603 	public void resetProfiling() {
2604 		if (!profiling) {
2605 			return;
2606 		}
2607 		tupleProfilingStats.clear();
2608 		functionProfilingStats.clear();
2609 		activeProfilingFunctions.clear();
2610 	}
2611 
2612 	/**
2613 	 * Returns an immutable snapshot of the collected profiling statistics.
2614 	 *
2615 	 * @return profiling report snapshot
2616 	 */
2617 	public ProfilingReport getProfilingReport() {
2618 		if (!profiling) {
2619 			return ProfilingReport.empty();
2620 		}
2621 		return new ProfilingReport(tupleProfilingStats, functionProfilingStats);
2622 	}
2623 
2624 	// The exec* helpers below are extracted from executeTuples on purpose:
2625 	// that method must stay well under HotSpot's HugeMethodLimit (8000
2626 	// bytecodes) or the JIT never compiles the interpreter loop (see #562).
2627 
2628 	private void execCompoundAssignVariable(Opcode opcode, VariableTuple variableTuple) {
2629 		// arg[0] = offset
2630 		// arg[1] = isGlobal
2631 		// stack[0] = value
2632 		long offset = variableTuple.getVariableOffset();
2633 		boolean isGlobal = variableTuple.isGlobal();
2634 		Object o1 = blankToZero(resolveVariable(offset, isGlobal, false));
2635 		Object o2 = pop();
2636 		Object ans;
2637 		switch (opcode) {
2638 		case PLUS_EQ:
2639 			ans = JRT.add(o1, o2);
2640 			break;
2641 		case MINUS_EQ:
2642 			ans = JRT.subtract(o1, o2);
2643 			break;
2644 		case MULT_EQ:
2645 			ans = JRT.multiply(o1, o2);
2646 			break;
2647 		case DIV_EQ:
2648 			ans = JRT.divide(o1, o2);
2649 			break;
2650 		case MOD_EQ:
2651 			ans = JRT.mod(o1, o2);
2652 			break;
2653 		case POW_EQ:
2654 			ans = JRT.pow(o1, o2);
2655 			break;
2656 		default:
2657 			throw new Error("Invalid opcode here: " + opcode);
2658 		}
2659 		push(ans);
2660 		runtimeStack.setVariable(offset, ans, isGlobal);
2661 	}
2662 
2663 	private void execCompoundAssignArray(Opcode opcode, VariableTuple variableTuple) {
2664 		// arg[0] = offset
2665 		// arg[1] = isGlobal
2666 		// stack[0] = array index
2667 		// stack[1] = value
2668 		Object arrIdx = pop();
2669 		Object rhs = pop();
2670 		if (rhs == null) {
2671 			rhs = BLANK;
2672 		}
2673 		long offset = variableTuple.getVariableOffset();
2674 		boolean isGlobal = variableTuple.isGlobal();
2675 
2676 		Map<Object, Object> array = ensureMapVariable(offset, isGlobal);
2677 		checkScalar(arrIdx);
2678 		Object o = blankToZero(array.get(arrIdx));
2679 
2680 		Object newVal;
2681 
2682 		switch (opcode) {
2683 		case PLUS_EQ_ARRAY:
2684 			newVal = JRT.add(o, rhs);
2685 			break;
2686 		case MINUS_EQ_ARRAY:
2687 			newVal = JRT.subtract(o, rhs);
2688 			break;
2689 		case MULT_EQ_ARRAY:
2690 			newVal = JRT.multiply(o, rhs);
2691 			break;
2692 		case DIV_EQ_ARRAY:
2693 			newVal = JRT.divide(o, rhs);
2694 			break;
2695 		case MOD_EQ_ARRAY:
2696 			newVal = JRT.mod(o, rhs);
2697 			break;
2698 		case POW_EQ_ARRAY:
2699 			newVal = JRT.pow(o, rhs);
2700 			break;
2701 		default:
2702 			throw new Error("Invalid op code here: " + opcode);
2703 		}
2704 
2705 		assignArray(offset, arrIdx, newVal, isGlobal);
2706 	}
2707 
2708 	private void execCompoundAssignMapElement(Opcode opcode) {
2709 		// stack[0] = array index
2710 		// stack[1] = associative array
2711 		// stack[2] = value
2712 		Object arrIdx = pop();
2713 		Map<Object, Object> array = toMap(pop());
2714 		Object rhs = pop();
2715 		if (rhs == null) {
2716 			rhs = BLANK;
2717 		}
2718 
2719 		checkScalar(arrIdx);
2720 		Object o = blankToZero(array.get(arrIdx));
2721 		Object newVal;
2722 
2723 		switch (opcode) {
2724 		case PLUS_EQ_MAP_ELEMENT:
2725 			newVal = JRT.add(o, rhs);
2726 			break;
2727 		case MINUS_EQ_MAP_ELEMENT:
2728 			newVal = JRT.subtract(o, rhs);
2729 			break;
2730 		case MULT_EQ_MAP_ELEMENT:
2731 			newVal = JRT.multiply(o, rhs);
2732 			break;
2733 		case DIV_EQ_MAP_ELEMENT:
2734 			newVal = JRT.divide(o, rhs);
2735 			break;
2736 		case MOD_EQ_MAP_ELEMENT:
2737 			newVal = JRT.mod(o, rhs);
2738 			break;
2739 		case POW_EQ_MAP_ELEMENT:
2740 			newVal = JRT.pow(o, rhs);
2741 			break;
2742 		default:
2743 			throw new Error("Invalid op code here: " + opcode);
2744 		}
2745 
2746 		assignMapElement(array, arrIdx, newVal);
2747 	}
2748 
2749 	private void execCompoundAssignInputField(Opcode opcode) {
2750 		// stack[0] = dollar_fieldNumber
2751 		// stack[1] = inc value
2752 
2753 		// same code as GET_INPUT_FIELD:
2754 		long fieldnum = JRT.parseFieldNumber(pop());
2755 		Object incval = pop();
2756 
2757 		// except here, get the number, and add the incvalue
2758 		Object numObj = blankToZero(jrt.jrtGetInputField(fieldnum));
2759 		Object num;
2760 		switch (opcode) {
2761 		case PLUS_EQ_INPUT_FIELD:
2762 			num = JRT.add(numObj, incval);
2763 			break;
2764 		case MINUS_EQ_INPUT_FIELD:
2765 			num = JRT.subtract(numObj, incval);
2766 			break;
2767 		case MULT_EQ_INPUT_FIELD:
2768 			num = JRT.multiply(numObj, incval);
2769 			break;
2770 		case DIV_EQ_INPUT_FIELD:
2771 			num = JRT.divide(numObj, incval);
2772 			break;
2773 		case MOD_EQ_INPUT_FIELD:
2774 			num = JRT.mod(numObj, incval);
2775 			break;
2776 		case POW_EQ_INPUT_FIELD:
2777 			num = JRT.pow(numObj, incval);
2778 			break;
2779 		default:
2780 			throw new Error("Invalid opcode here: " + opcode);
2781 		}
2782 		setNumOnJRT(fieldnum, num);
2783 
2784 		// put the result value on the stack
2785 		push(num);
2786 	}
2787 
2788 	private void execIndirectCall(IndirectCallTuple callTuple, PositionTracker position, long tupleStartNanos) {
2789 		Object[] actualArguments = popArguments(callTuple.getNumActualParams());
2790 		String requestedName = jrt.toAwkString(pop());
2791 		String qualifiedName = normalizeIndirectFunctionName(requestedName);
2792 		IndirectFunctionTarget target = callTuple.getUserFunctions().get(qualifiedName);
2793 		if (target != null) {
2794 			long formalCount = target.getNumFormalParams();
2795 			if (actualArguments.length > formalCount) {
2796 				jrt
2797 						.printWarning(
2798 								"gawk: "
2799 										+ callTuple.getSourceName()
2800 										+ ":"
2801 										+ callTuple.getSourceLine()
2802 										+ ": warning: function `"
2803 										+ qualifiedName
2804 										+ "' called with more arguments than declared");
2805 			}
2806 			if (profiling) {
2807 				activeProfilingFunctions.push(new ActiveFunction(qualifiedName, tupleStartNanos));
2808 			}
2809 			runtimeStack.pushFrame(formalCount, position.currentIndex());
2810 			adoptElementArgumentReferences(actualArguments);
2811 			int copiedArgumentCount = Math.min(actualArguments.length, (int) formalCount);
2812 			for (int i = 0; i < copiedArgumentCount; i++) {
2813 				runtimeStack.setVariable(i, actualArguments[i], false);
2814 			}
2815 			position.jump(target.getAddress());
2816 			return;
2817 		}
2818 
2819 		String awkName = requestedName.startsWith("awk::") ?
2820 				requestedName.substring("awk::".length()) : requestedName;
2821 		BuiltinFunction builtin = BuiltinFunction.of(awkName);
2822 		if (builtin != null) {
2823 			resolveIndirectArguments(actualArguments, builtin);
2824 			push(invokeIndirectBuiltin(builtin, actualArguments, position.lineNumber()));
2825 			position.next();
2826 			return;
2827 		}
2828 		ExtensionFunction extensionFunction = callTuple.getExtensionFunctions().get(awkName);
2829 		if (extensionFunction != null) {
2830 			resolveIndirectArguments(actualArguments, extensionFunction);
2831 			if (profiling) {
2832 				activeProfilingFunctions.push(new ActiveFunction(awkName, tupleStartNanos));
2833 			}
2834 			try {
2835 				push(
2836 						invokeExtension(
2837 								extensionFunction,
2838 								actualArguments,
2839 								position.lineNumber()));
2840 			} finally {
2841 				if (profiling) {
2842 					recordFunctionExit(System.nanoTime());
2843 				}
2844 			}
2845 			position.next();
2846 			return;
2847 		}
2848 		throw new AwkRuntimeException(
2849 				position.lineNumber(),
2850 				"function `" + qualifiedName + "' is not defined");
2851 	}
2852 
2853 	private void execPrint(CountTuple tuple) throws IOException {
2854 		long numArgs = tuple.getCount();
2855 		jrt.printDefault(numArgs == 0 ? new Object[] { jrt.jrtGetInputField(0) } : popArguments(numArgs));
2856 	}
2857 
2858 	private void execPrintToFile(CountAndAppendTuple tuple) throws IOException {
2859 		String key = jrt.toAwkString(pop());
2860 		long numArgs = tuple.getCount();
2861 		jrt
2862 				.printToFile(
2863 						key,
2864 						tuple.isAppend(),
2865 						numArgs == 0 ? new Object[]
2866 						{ jrt.jrtGetInputField(0) } : popArguments(numArgs));
2867 	}
2868 
2869 	private void execPrintToPipe(CountTuple tuple) throws IOException {
2870 		String cmd = jrt.toAwkString(pop());
2871 		long numArgs = tuple.getCount();
2872 		jrt.printToProcess(cmd, numArgs == 0 ? new Object[] { jrt.jrtGetInputField(0) } : popArguments(numArgs));
2873 	}
2874 
2875 	private void execPrintf(CountTuple tuple) throws IOException {
2876 		long numArgs = tuple.getCount();
2877 		Object[] values = popArguments(numArgs - 1);
2878 		String format = jrt.toAwkString(pop());
2879 		jrt.printfDefault(format, values);
2880 	}
2881 
2882 	private void execPrintfToFile(CountAndAppendTuple tuple) throws IOException {
2883 		String key = jrt.toAwkString(pop());
2884 		long numArgs = tuple.getCount();
2885 		Object[] values = popArguments(numArgs - 1);
2886 		String format = jrt.toAwkString(pop());
2887 		jrt.printfToFile(key, tuple.isAppend(), format, values);
2888 	}
2889 
2890 	private void execPrintfToPipe(CountTuple tuple) throws IOException {
2891 		String cmd = jrt.toAwkString(pop());
2892 		long numArgs = tuple.getCount();
2893 		Object[] values = popArguments(numArgs - 1);
2894 		String format = jrt.toAwkString(pop());
2895 		jrt.printfToProcess(cmd, format, values);
2896 	}
2897 
2898 	private void execLength(CountTuple tuple) {
2899 		long num = tuple.getCount();
2900 		if (num == 0) {
2901 			push(jrt.jrtGetInputField(0).toString().length());
2902 			return;
2903 		}
2904 		Object value = pop();
2905 		if (value instanceof ArgumentReference) {
2906 			value = resolveLengthArgumentReference((ArgumentReference) value);
2907 		}
2908 		push(lengthOf(value));
2909 	}
2910 
2911 	private Object lengthOf(Object value) {
2912 		return value instanceof Map ?
2913 				Long.valueOf(((Map<?, ?>) value).size()) : Integer.valueOf(jrt.toAwkString(value).length());
2914 	}
2915 
2916 	private String normalizeIndirectFunctionName(String functionName) {
2917 		return functionName.startsWith("awk::") ? functionName.substring("awk::".length()) : functionName;
2918 	}
2919 
2920 	/**
2921 	 * Unwinds every call frame and clears the per-call runtime state, after an
2922 	 * {@code exit} or {@code nextfile} statement or an abandoned execution.
2923 	 * When profiling, the abandoned function calls are recorded as exited at
2924 	 * this point, so the timing report stays accurate and the active-function
2925 	 * stack does not leak entries (see #557).
2926 	 */
2927 	private void resetCallState() {
2928 		if (profiling) {
2929 			recordAllFunctionExits(System.nanoTime());
2930 		}
2931 		runtimeStack.popAllFrames();
2932 		elementArgumentReferences.clear();
2933 		clearOperandStack();
2934 	}
2935 
2936 	/**
2937 	 * Registers the untyped element references received by the call frame that
2938 	 * was just pushed, so array mutations occurring during the call can detach
2939 	 * them. References forwarded from an enclosing call keep their original
2940 	 * owner.
2941 	 */
2942 	private void adoptElementArgumentReferences(Object[] actualArguments) {
2943 		for (Object argument : actualArguments) {
2944 			adoptElementArgumentReference(argument);
2945 		}
2946 	}
2947 
2948 	private void adoptElementArgumentReference(Object argument) {
2949 		if (argument instanceof IndirectArrayArgumentReference) {
2950 			IndirectArrayArgumentReference reference = (IndirectArrayArgumentReference) argument;
2951 			if (reference.ownerFrame < 0) {
2952 				reference.ownerFrame = runtimeStack.frameCount();
2953 				elementArgumentReferences.push(reference);
2954 			}
2955 		}
2956 	}
2957 
2958 	/**
2959 	 * Drops the element references owned by the call frame that is about to be
2960 	 * popped. Frames are strictly nested, so the owned references always sit on
2961 	 * top of the tracking stack.
2962 	 */
2963 	private void releaseElementArgumentReferences() {
2964 		int depth = runtimeStack.frameCount();
2965 		while (!elementArgumentReferences.isEmpty()
2966 				&& elementArgumentReferences.peek().ownerFrame == depth) {
2967 			elementArgumentReferences.pop();
2968 		}
2969 	}
2970 
2971 	private Object resolveUserFunctionArgument(Object argument) {
2972 		if (!(argument instanceof ArgumentReference)) {
2973 			return argument;
2974 		}
2975 		ArgumentReference reference = (ArgumentReference) argument;
2976 		Object snapshot = reference.snapshot();
2977 		if (snapshot instanceof ArgumentReference) {
2978 			return resolveUserFunctionArgument(snapshot);
2979 		}
2980 		return isUntyped(snapshot) ? reference : snapshot;
2981 	}
2982 
2983 	private void resolveIndirectArguments(
2984 			Object[] actualArgumentsParam,
2985 			BuiltinFunction builtin) {
2986 		for (int index = 0; index < actualArgumentsParam.length; index++) {
2987 			boolean arrayArgument = builtin == BuiltinFunction.SPLIT && index == 1;
2988 			actualArgumentsParam[index] = resolveIndirectArgument(
2989 					actualArgumentsParam[index],
2990 					arrayArgument,
2991 					false);
2992 		}
2993 	}
2994 
2995 	private void resolveIndirectArguments(
2996 			Object[] actualArgumentsParam,
2997 			ExtensionFunction function) {
2998 		boolean[] arrayArguments = new boolean[actualArgumentsParam.length];
2999 		boolean[] rawValueArguments = new boolean[actualArgumentsParam.length];
3000 		for (int index : function.collectAssocArrayIndexes(actualArgumentsParam.length)) {
3001 			arrayArguments[index] = true;
3002 		}
3003 		for (int index : function.collectRawValueIndexes(actualArgumentsParam.length)) {
3004 			rawValueArguments[index] = true;
3005 		}
3006 		for (int index = 0; index < actualArgumentsParam.length; index++) {
3007 			actualArgumentsParam[index] = resolveIndirectArgument(
3008 					actualArgumentsParam[index],
3009 					arrayArguments[index],
3010 					rawValueArguments[index]);
3011 		}
3012 	}
3013 
3014 	private Object resolveIndirectArgument(
3015 			Object argument,
3016 			boolean arrayArgument,
3017 			boolean rawValueArgument) {
3018 		if (!(argument instanceof ArgumentReference)) {
3019 			return argument;
3020 		}
3021 		ArgumentReference reference = (ArgumentReference) argument;
3022 		if (rawValueArgument) {
3023 			return resolveRawArgumentReference(reference);
3024 		}
3025 		return resolveArgumentReference(reference, arrayArgument);
3026 	}
3027 
3028 	private Object invokeIndirectBuiltin(
3029 			BuiltinFunction builtin,
3030 			Object[] args,
3031 			int lineNumber) {
3032 		switch (builtin) {
3033 		case ATAN2:
3034 			requireIndirectArgumentCount(builtin, args, 2, 2, lineNumber);
3035 			return Math.atan2(JRT.toDouble(args[0]), JRT.toDouble(args[1]));
3036 		case CLOSE:
3037 			requireIndirectArgumentCount(builtin, args, 1, 1, lineNumber);
3038 			return jrt.jrtClose(jrt.toAwkString(args[0]));
3039 		case COS:
3040 			requireIndirectArgumentCount(builtin, args, 1, 1, lineNumber);
3041 			return Math.cos(JRT.toDouble(args[0]));
3042 		case EXP:
3043 			requireIndirectArgumentCount(builtin, args, 1, 1, lineNumber);
3044 			return Math.exp(JRT.toDouble(args[0]));
3045 		case GSUB:
3046 		case SUB:
3047 			requireIndirectArgumentCount(builtin, args, 2, 2, lineNumber);
3048 			return substituteInputLine(
3049 					builtin == BuiltinFunction.GSUB,
3050 					args[0],
3051 					args[1]);
3052 		case INDEX:
3053 			requireIndirectArgumentCount(builtin, args, 2, 2, lineNumber);
3054 			return jrt.index(jrt.toAwkString(args[0]), jrt.toAwkString(args[1]));
3055 		case INT:
3056 			requireIndirectArgumentCount(builtin, args, 1, 1, lineNumber);
3057 			return JRT.truncateToScalar(JRT.toDouble(args[0]));
3058 		case LENGTH:
3059 			requireIndirectArgumentCount(builtin, args, 0, 1, lineNumber);
3060 			return args.length == 0 ? Integer.valueOf(jrt.jrtGetInputField(0).toString().length()) : lengthOf(args[0]);
3061 		case LOG:
3062 			requireIndirectArgumentCount(builtin, args, 1, 1, lineNumber);
3063 			return Math.log(JRT.toDouble(args[0]));
3064 		case MATCH:
3065 			requireIndirectArgumentCount(builtin, args, 2, 2, lineNumber);
3066 			return jrt.matchPosition(jrt.toAwkString(args[0]), jrt.toAwkString(args[1]));
3067 		case RAND:
3068 			requireIndirectArgumentCount(builtin, args, 0, 0, lineNumber);
3069 			return randomNumberGenerator.nextDouble();
3070 		case SIN:
3071 			requireIndirectArgumentCount(builtin, args, 1, 1, lineNumber);
3072 			return Math.sin(JRT.toDouble(args[0]));
3073 		case SPLIT:
3074 			requireIndirectArgumentCount(builtin, args, 2, 3, lineNumber);
3075 			return splitIntoArray(
3076 					args[0],
3077 					args[1],
3078 					args.length == 3 ? args[2] : jrt.getFSVar(),
3079 					lineNumber);
3080 		case SPRINTF:
3081 			requireIndirectArgumentCount(builtin, args, 1, Integer.MAX_VALUE, lineNumber);
3082 			return jrt
3083 					.sprintf(
3084 							jrt.toAwkString(args[0]),
3085 							Arrays.copyOfRange(args, 1, args.length));
3086 		case SQRT:
3087 			requireIndirectArgumentCount(builtin, args, 1, 1, lineNumber);
3088 			return Math.sqrt(JRT.toDouble(args[0]));
3089 		case SRAND:
3090 			requireIndirectArgumentCount(builtin, args, 0, 1, lineNumber);
3091 			int seed = args.length == 0 ? JRT.timeSeed() : (int) JRT.toDouble(args[0]);
3092 			int previousSeed = randomNumberGenerator.getSeed();
3093 			randomNumberGenerator.setSeed(seed);
3094 			return Integer.valueOf(previousSeed);
3095 		case SUBSTR:
3096 			requireIndirectArgumentCount(builtin, args, 2, 3, lineNumber);
3097 			return substring(
3098 					args[0],
3099 					args[1],
3100 					args.length == 3 ? args[2] : null);
3101 		case SYSTEM:
3102 			requireIndirectArgumentCount(builtin, args, 1, 1, lineNumber);
3103 			return jrt.jrtSystem(jrt.toAwkString(args[0]));
3104 		case TOLOWER:
3105 			requireIndirectArgumentCount(builtin, args, 1, 1, lineNumber);
3106 			return jrt.toAwkString(args[0]).toLowerCase();
3107 		case TOUPPER:
3108 			requireIndirectArgumentCount(builtin, args, 1, 1, lineNumber);
3109 			return jrt.toAwkString(args[0]).toUpperCase();
3110 		default:
3111 			throw new AwkRuntimeException(
3112 					lineNumber,
3113 					"indirect calls are not implemented for builtin `" + builtin.getAwkName() + "'");
3114 		}
3115 	}
3116 
3117 	private void requireIndirectArgumentCount(
3118 			BuiltinFunction builtin,
3119 			Object[] args,
3120 			int minimum,
3121 			int maximum,
3122 			int lineNumber) {
3123 		if (args.length < minimum || args.length > maximum) {
3124 			String expected = minimum == maximum ? Integer.toString(minimum) : minimum + " to " + maximum;
3125 			throw new AwkRuntimeException(
3126 					lineNumber,
3127 					builtin.getAwkName() + " requires " + expected + " argument(s), not " + args.length);
3128 		}
3129 	}
3130 
3131 	private Object invokeExtension(
3132 			ExtensionFunction function,
3133 			Object[] args,
3134 			int lineNumber) {
3135 		// Let extensions report diagnostics at the call location.
3136 		currentLineNumber = lineNumber;
3137 		String extensionClassName = function.getExtensionClassName();
3138 		JawkExtension extension = extensionInstances.get(extensionClassName);
3139 		if (extension == null) {
3140 			throw new AwkRuntimeException(
3141 					lineNumber,
3142 					"Extension instance for class '" + extensionClassName + "' is not registered");
3143 		}
3144 		if (!(extension instanceof AbstractExtension)) {
3145 			throw new AwkRuntimeException(
3146 					lineNumber,
3147 					"Extension instance for class '" + extensionClassName
3148 							+ "' does not extend "
3149 							+ AbstractExtension.class.getName());
3150 		}
3151 		Map<IndirectArrayArgumentReference, Object> attachedValues = captureAttachedArrayArgumentValues();
3152 		Object result;
3153 		try {
3154 			result = function.invoke((AbstractExtension) extension, args);
3155 		} finally {
3156 			detachReplacedArrayArgumentReferences(attachedValues);
3157 		}
3158 		if (result == null) {
3159 			return "";
3160 		}
3161 		if (result instanceof Number
3162 				|| result instanceof String
3163 				|| result instanceof Map) {
3164 			return result;
3165 		}
3166 		return jrt.toAwkString(result);
3167 	}
3168 
3169 	private void execMatch() {
3170 		String ere = jrt.toAwkString(pop());
3171 		String s = jrt.toAwkString(pop());
3172 		push(jrt.matchPosition(s, ere));
3173 	}
3174 
3175 	private void execSubForDollar0(BooleanTuple tuple) {
3176 		Object replacement = pop();
3177 		Object ere = pop();
3178 		push(substituteInputLine(tuple.getValue(), ere, replacement));
3179 	}
3180 
3181 	private Object substituteInputLine(boolean global, Object ere, Object replacement) {
3182 		String orig = jrt.toAwkString(jrt.jrtGetInputField(0));
3183 		Object replacements = global ?
3184 				jrt.replaceAll(orig, jrt.toAwkString(replacement), jrt.toAwkString(ere)) :
3185 				jrt.replaceFirst(orig, jrt.toAwkString(replacement), jrt.toAwkString(ere));
3186 		jrt.setInputLine(jrt.getReplaceResult());
3187 		jrt.jrtParseFields();
3188 		return replacements;
3189 	}
3190 
3191 	private void execSubForDollarReference(BooleanTuple tuple) {
3192 		boolean isGsub = tuple.getValue();
3193 		long fieldNum = JRT.parseFieldNumber(pop());
3194 		String orig = jrt.toAwkString(pop());
3195 		String repl = jrt.toAwkString(pop());
3196 		String ere = jrt.toAwkString(pop());
3197 		push(isGsub ? jrt.replaceAll(orig, repl, ere) : jrt.replaceFirst(orig, repl, ere));
3198 		String newstring = jrt.getReplaceResult();
3199 		if (fieldNum == 0) {
3200 			jrt.setInputLine(newstring);
3201 			jrt.jrtParseFields();
3202 		} else {
3203 			jrt.jrtSetInputField(newstring, fieldNum);
3204 		}
3205 	}
3206 
3207 	private void execSubForVariable(SubstitutionVariableTuple tuple, PositionTracker position) {
3208 		String newString = execSubOrGSub(tuple.isGlobalSubstitution());
3209 		assign(tuple.getVariableOffset(), newString, tuple.isGlobal(), position, false);
3210 	}
3211 
3212 	private void execSubForArrayReference(SubstitutionVariableTuple tuple) {
3213 		Object arrIdx = pop();
3214 		String newString = execSubOrGSub(tuple.isGlobalSubstitution());
3215 		assignArray(tuple.getVariableOffset(), arrIdx, newString, tuple.isGlobal());
3216 		pop();
3217 	}
3218 
3219 	private void execSubForMapReference(BooleanTuple tuple) {
3220 		Object arrIdx = pop();
3221 		Map<Object, Object> array = toMap(pop());
3222 		String newString = execSubOrGSub(tuple.getValue());
3223 		assignMapElement(array, arrIdx, newString);
3224 		pop();
3225 	}
3226 
3227 	private void execSplit(CountTuple tuple, PositionTracker position) {
3228 		long numArgs = tuple.getCount();
3229 		Object fs;
3230 		if (numArgs == 2) {
3231 			fs = jrt.getFSVar();
3232 		} else if (numArgs == 3) {
3233 			// a regexp literal arrives precompiled and stays that way, so the
3234 			// tokenizer can honor IGNORECASE through the pattern-twin cache
3235 			fs = pop();
3236 		} else {
3237 			throw new Error("Invalid # of args. split() requires 2 or 3. Got: " + numArgs);
3238 		}
3239 		Object target = pop();
3240 		Object source = pop();
3241 		push(splitIntoArray(source, target, fs, position.lineNumber()));
3242 	}
3243 
3244 	private Object splitIntoArray(Object source, Object target, Object separator, int lineNumber) {
3245 		if (!(target instanceof Map)) {
3246 			throw new AwkRuntimeException(lineNumber, target + " is not an array.");
3247 		}
3248 		Enumeration<Object> tokenizer = jrt.splitTokenizer(jrt.toAwkString(source), separator);
3249 		@SuppressWarnings("unchecked")
3250 		Map<Object, Object> assocArray = (Map<Object, Object>) target;
3251 		assocArray.clear();
3252 		detachMissingArrayArgumentReferences(assocArray);
3253 		long cnt = 0;
3254 		while (tokenizer.hasMoreElements()) {
3255 			Object value = tokenizer.nextElement();
3256 			assocArray.put(++cnt, jrt.toInputScalar(value));
3257 		}
3258 		return Long.valueOf(cnt);
3259 	}
3260 
3261 	private void execSubstr(CountTuple tuple) {
3262 		long numArgs = tuple.getCount();
3263 		Object length = null;
3264 		if (numArgs == 3) {
3265 			length = pop();
3266 		} else if (numArgs != 2) {
3267 			throw new Error("numArgs for SUBSTR must be 2 or 3. It is " + numArgs);
3268 		}
3269 		Object start = pop();
3270 		Object value = pop();
3271 		push(substring(value, start, length));
3272 	}
3273 
3274 	private Object substring(Object value, Object start, Object requestedLength) {
3275 		String s = jrt.toAwkString(value);
3276 		int startPos = (int) JRT.toDouble(start);
3277 		if (startPos <= 0) {
3278 			startPos = 1;
3279 		}
3280 		if (startPos > s.length()) {
3281 			return BLANK;
3282 		}
3283 		int available = s.length() - startPos + 1;
3284 		// The requested length is kept as a long and clamped to what the string
3285 		// can supply: a length beyond the int range would otherwise wrap around
3286 		// and turn "the rest of the string" into an empty result.
3287 		long length = requestedLength == null ? available : JRT.toLong(requestedLength);
3288 		if (length <= 0) {
3289 			return BLANK;
3290 		}
3291 		if (length > available) {
3292 			length = available;
3293 		}
3294 		return s.substring(startPos - 1, startPos - 1 + (int) length);
3295 	}
3296 
3297 	private void execSetNumGlobals(CountTuple tuple) {
3298 		long numGlobals = tuple.getCount();
3299 		Object[] globals = runtimeStack.getNumGlobals();
3300 		if (mergedGlobalLayoutActive) {
3301 			if (!hasCompatiblePersistentGlobalLayout(numGlobals)) {
3302 				throw new IllegalStateException(
3303 						"AVM globals are already initialized for an incompatible persistent layout.");
3304 			}
3305 			applyExecutionInitialVariablesToGlobalSlots(true);
3306 		} else if (globals == null) {
3307 			runtimeStack.setNumGlobals(numGlobals, globalVariableOffsets);
3308 			initializedEvalGlobalVariableOffsets = globalVariableOffsets;
3309 			initializedEvalGlobalVariableArrays = globalVariableArrays;
3310 			applyExecutionInitialVariablesToGlobalSlots(false);
3311 		} else if (!hasCompatibleEvalGlobalLayout(numGlobals)) {
3312 			throw new IllegalStateException(
3313 					"AVM globals are already initialized for a different eval layout. Call prepareForEval(...) first.");
3314 		}
3315 	}
3316 
3317 	private void populateEnviron(long offset) {
3318 		environOffset = offset;
3319 		for (Map.Entry<String, String> var : System.getenv().entrySet()) {
3320 			assignArray(environOffset, var.getKey(), jrt.toInputScalar(var.getValue()), true);
3321 			pop(); // clean up the stack after the assignment
3322 		}
3323 	}
3324 
3325 	private void populateArgc(long offset) {
3326 		argcOffset = offset;
3327 		// +1 to include the "jawk" program name (ARGV[0])
3328 		runtimeStack.setVariable(argcOffset, Integer.valueOf(arguments.size() + 1), true);
3329 	}
3330 
3331 	private void populateArgv(long offset) {
3332 		argvOffset = offset;
3333 		// A host-supplied ARGV takes precedence over the operand list: leave
3334 		// it untouched instead of overwriting its entries.
3335 		Object existing = runtimeStack.getVariable(argvOffset, true);
3336 		if (existing instanceof Map && !((Map<?, ?>) existing).isEmpty()) {
3337 			return;
3338 		}
3339 		forEachArgvEntry((index, value) -> {
3340 			assignArray(argvOffset, index, value, true);
3341 			pop(); // clean up the stack after the assignment
3342 		});
3343 	}
3344 
3345 	/**
3346 	 * Supplies the ARGV entries to the given consumer, in index order:
3347 	 * ARGV[0] is the program name, ARGV[1..n] the command-line arguments.
3348 	 * Indexes are supplied as {@code Long}, the canonical AWK array key
3349 	 * form. The count comes straight from the argument list because ARGC
3350 	 * may not be materialized when the script does not reference it.
3351 	 *
3352 	 * @param consumer receives each (index, value) ARGV entry
3353 	 */
3354 	private void forEachArgvEntry(BiConsumer<Long, Object> consumer) {
3355 		consumer.accept(Long.valueOf(0L), "jawk");
3356 		for (int i = 1; i <= arguments.size(); i++) {
3357 			consumer.accept(Long.valueOf(i), jrt.toInputScalar(arguments.get(i - 1)));
3358 		}
3359 	}
3360 
3361 	/*
3362 	 * Extension initialization can be heavy, so the hooks run at most once per
3363 	 * AVM instance: a reused AVM (repeated executions, expression evaluations)
3364 	 * does not reinitialize its extensions.
3365 	 */
3366 	private void runBeforeStartHooks() {
3367 		if (beforeStartHooksExecuted || extensionInstances.isEmpty()) {
3368 			return;
3369 		}
3370 		beforeStartHooksExecuted = true;
3371 		Set<JawkExtension> started = new LinkedHashSet<JawkExtension>();
3372 		for (JawkExtension extension : extensionInstances.values()) {
3373 			if (started.add(extension)) {
3374 				extension.beforeStart(this, jrt);
3375 			}
3376 		}
3377 	}
3378 
3379 	/*
3380 	 * SYMTAB is a gawk extension mirroring the symbol table: the script's
3381 	 * globals, the JRT-managed specials, and host-supplied variables that have
3382 	 * no compiled slot. The parser emits UPDATE_SYMTAB only when the script
3383 	 * references SYMTAB outside POSIX mode. Declared globals and managed special
3384 	 * variables remain linked to their runtime values.
3385 	 */
3386 	private void execUpdateSymtab(long offset) {
3387 		symtabOffset = offset;
3388 		if (runtimeStack.getVariable(offset, true) != null) {
3389 			// a host-supplied SYMTAB value wins
3390 			return;
3391 		}
3392 		SymtabArray symtab = new SymtabArray();
3393 		for (String name : executionInitialVariables.keySet()) {
3394 			symtab.put(name, getVariable(name));
3395 		}
3396 		for (String name : getGlobalVariableNames()) {
3397 			// gawk keeps the meta tables out of the symbol table snapshot
3398 			if ("SYMTAB".equals(name) || "FUNCTAB".equals(name)) {
3399 				continue;
3400 			}
3401 			symtab.put(name, getVariable(name));
3402 		}
3403 		// specials last: their accessors are authoritative even when the name
3404 		// also has a (possibly not yet materialized) global slot
3405 		for (String name : getSpecialVariableNames()) {
3406 			symtab.put(name, getVariable(name));
3407 		}
3408 		symtab.activate();
3409 		runtimeStack.setVariable(offset, symtab, true);
3410 	}
3411 
3412 	private final class SymtabArray extends java.util.AbstractMap<Object, Object> implements AssocArray {
3413 		private final Map<Object, Object> entries = newAwkArray();
3414 		private final Set<String> assignableNames = new HashSet<String>();
3415 		private boolean active;
3416 
3417 		private void activate() {
3418 			active = true;
3419 		}
3420 
3421 		/** {@inheritDoc} */
3422 		@Override
3423 		public Object get(Object key) {
3424 			String name = key == null ? "" : key.toString();
3425 			if (active && !isMetaTableName(name) && isLiveSpecialVariable(name)) {
3426 				return getVariable(name);
3427 			}
3428 			Integer offset = globalVariableOffsets == null ? null : globalVariableOffsets.get(name);
3429 			if (active && !isMetaTableName(name) && offset != null) {
3430 				return runtimeStack.getVariable(offset.intValue(), true);
3431 			}
3432 			return entries.get(key);
3433 		}
3434 
3435 		/** {@inheritDoc} */
3436 		@Override
3437 		public boolean containsKey(Object key) {
3438 			String name = key == null ? "" : key.toString();
3439 			return active
3440 					&& !isMetaTableName(name)
3441 					&& (isLiveSpecialVariable(name)
3442 							|| (globalVariableOffsets != null && globalVariableOffsets.containsKey(name)))
3443 					|| entries.containsKey(key);
3444 		}
3445 
3446 		/** {@inheritDoc} */
3447 		@Override
3448 		public Object put(Object key, Object value) {
3449 			String name = key == null ? "" : key.toString();
3450 			if (!active) {
3451 				if (key != null) {
3452 					assignableNames.add(name);
3453 				}
3454 				return entries.put(key, value);
3455 			}
3456 			if (!assignableNames.contains(name)) {
3457 				throw new AwkRuntimeException("Cannot assign to an arbitrary element of SYMTAB.");
3458 			}
3459 			validateGlobalType(name, value);
3460 			Object previous = entries.put(key, value);
3461 			if (isMetaTableName(name)) {
3462 				return previous;
3463 			}
3464 			if (applyLiveSpecialVariable(name, value)) {
3465 				return previous;
3466 			}
3467 			Integer offset = globalVariableOffsets == null ? null : globalVariableOffsets.get(name);
3468 			if (offset != null) {
3469 				runtimeStack.setVariable(offset.intValue(), value, true);
3470 			}
3471 			return previous;
3472 		}
3473 
3474 		private Object putRuntimeVariable(String name, Object value) {
3475 			assignableNames.add(name);
3476 			return put(name, value);
3477 		}
3478 
3479 		private boolean applyLiveSpecialVariable(String name, Object value) {
3480 			if ("ARGC".equals(name)) {
3481 				if (argcOffset == NULL_OFFSET) {
3482 					throw new AwkRuntimeException("ARGC is read-only (not materialized).");
3483 				}
3484 				runtimeStack.setVariable(argcOffset, value, true);
3485 				return true;
3486 			}
3487 			if ("RSTART".equals(name)) {
3488 				jrt.setRSTART(value);
3489 				return true;
3490 			}
3491 			if ("RLENGTH".equals(name)) {
3492 				jrt.setRLENGTH(value);
3493 				return true;
3494 			}
3495 			return isManagedSpecialVariable(name) && jrt.applySpecialVariable(name, value);
3496 		}
3497 
3498 		private boolean isLiveSpecialVariable(String name) {
3499 			return isManagedSpecialVariable(name)
3500 					|| "RSTART".equals(name)
3501 					|| "RLENGTH".equals(name);
3502 		}
3503 
3504 		/** {@inheritDoc} */
3505 		@Override
3506 		public Object remove(Object key) {
3507 			if (active) {
3508 				throw new AwkRuntimeException("Cannot delete an element from SYMTAB.");
3509 			}
3510 			return entries.remove(key);
3511 		}
3512 
3513 		/** {@inheritDoc} */
3514 		@Override
3515 		public void clear() {
3516 			if (active) {
3517 				throw new AwkRuntimeException("Cannot delete SYMTAB.");
3518 			}
3519 			entries.clear();
3520 		}
3521 
3522 		private void validateGlobalType(String name, Object value) {
3523 			Boolean array = globalVariableArrays == null ? null : globalVariableArrays.get(name);
3524 			if (Boolean.TRUE.equals(array) && !(value instanceof Map)) {
3525 				throw new AwkRuntimeException(
3526 						"Attempting to use array `" + name + "' in a scalar context.");
3527 			}
3528 			if (Boolean.FALSE.equals(array) && value instanceof Map) {
3529 				throw new AwkRuntimeException(
3530 						"Attempting to use scalar `" + name + "' as an array.");
3531 			}
3532 		}
3533 
3534 		/** {@inheritDoc} */
3535 		@Override
3536 		public Set<Map.Entry<Object, Object>> entrySet() {
3537 			return new AbstractSet<Map.Entry<Object, Object>>() {
3538 
3539 				@Override
3540 				public Iterator<Map.Entry<Object, Object>> iterator() {
3541 					final Iterator<Map.Entry<Object, Object>> iterator = entries.entrySet().iterator();
3542 					return new Iterator<Map.Entry<Object, Object>>() {
3543 
3544 						@Override
3545 						public boolean hasNext() {
3546 							return iterator.hasNext();
3547 						}
3548 
3549 						@Override
3550 						public Map.Entry<Object, Object> next() {
3551 							return new LiveSymtabEntry(iterator.next().getKey());
3552 						}
3553 
3554 						@Override
3555 						public void remove() {
3556 							if (active) {
3557 								throw new AwkRuntimeException("Cannot delete an element from SYMTAB.");
3558 							}
3559 							iterator.remove();
3560 						}
3561 					};
3562 				}
3563 
3564 				@Override
3565 				public int size() {
3566 					return entries.size();
3567 				}
3568 			};
3569 		}
3570 
3571 		private boolean isMetaTableName(String name) {
3572 			return "SYMTAB".equals(name) || "FUNCTAB".equals(name);
3573 		}
3574 
3575 		private final class LiveSymtabEntry implements Map.Entry<Object, Object> {
3576 
3577 			private final Object key;
3578 
3579 			private LiveSymtabEntry(Object key) {
3580 				this.key = key;
3581 			}
3582 
3583 			@Override
3584 			public Object getKey() {
3585 				return key;
3586 			}
3587 
3588 			@Override
3589 			public Object getValue() {
3590 				return SymtabArray.this.get(key);
3591 			}
3592 
3593 			@Override
3594 			public Object setValue(Object value) {
3595 				return SymtabArray.this.put(key, value);
3596 			}
3597 
3598 			@Override
3599 			public boolean equals(Object obj) {
3600 				if (!(obj instanceof Map.Entry)) {
3601 					return false;
3602 				}
3603 				Map.Entry<?, ?> other = (Map.Entry<?, ?>) obj;
3604 				return Objects.equals(key, other.getKey()) && Objects.equals(getValue(), other.getValue());
3605 			}
3606 
3607 			@Override
3608 			public int hashCode() {
3609 				return Objects.hashCode(key) ^ Objects.hashCode(getValue());
3610 			}
3611 		}
3612 	}
3613 
3614 	/*
3615 	 * FUNCTAB is a gawk extension listing the names of the standard built-in
3616 	 * functions, the program's user-defined functions, and the loaded
3617 	 * extensions' function keywords. The parser emits UPDATE_FUNCTAB only when
3618 	 * the script references FUNCTAB outside POSIX mode.
3619 	 */
3620 	private void execUpdateFunctab(long offset) {
3621 		if (runtimeStack.getVariable(offset, true) != null) {
3622 			return;
3623 		}
3624 		Map<Object, Object> functab = newAwkArray();
3625 		for (String name : BuiltinFunction.names()) {
3626 			functab.put(name, name);
3627 		}
3628 		for (String name : functionNames) {
3629 			functab.put(name, name);
3630 		}
3631 		Set<JawkExtension> seen = new LinkedHashSet<JawkExtension>();
3632 		for (JawkExtension extension : extensionInstances.values()) {
3633 			if (seen.add(extension)) {
3634 				for (String keyword : extension.getExtensionFunctions().keySet()) {
3635 					functab.put(keyword, keyword);
3636 				}
3637 			}
3638 		}
3639 		runtimeStack.setVariable(offset, new ReadOnlyArray("FUNCTAB", functab), true);
3640 	}
3641 
3642 	private static final class ReadOnlyArray extends java.util.AbstractMap<Object, Object> implements AssocArray {
3643 		private final String name;
3644 		private final Map<Object, Object> entries;
3645 
3646 		private ReadOnlyArray(String nameParam, Map<Object, Object> entriesParam) {
3647 			name = nameParam;
3648 			entries = entriesParam;
3649 		}
3650 
3651 		/** {@inheritDoc} */
3652 		@Override
3653 		public Object get(Object key) {
3654 			return JRT.containsAwkKey(entries, key) ? entries.get(key) : BLANK;
3655 		}
3656 
3657 		/** {@inheritDoc} */
3658 		@Override
3659 		public boolean containsKey(Object key) {
3660 			return JRT.containsAwkKey(entries, key);
3661 		}
3662 
3663 		/** {@inheritDoc} */
3664 		@Override
3665 		public Set<Map.Entry<Object, Object>> entrySet() {
3666 			return Collections.unmodifiableMap(entries).entrySet();
3667 		}
3668 
3669 		/** {@inheritDoc} */
3670 		@Override
3671 		public Object put(Object key, Object value) {
3672 			throw readOnlyError();
3673 		}
3674 
3675 		/** {@inheritDoc} */
3676 		@Override
3677 		public Object remove(Object key) {
3678 			throw readOnlyError();
3679 		}
3680 
3681 		/** {@inheritDoc} */
3682 		@Override
3683 		public void clear() {
3684 			throw readOnlyError();
3685 		}
3686 
3687 		private AwkRuntimeException readOnlyError() {
3688 			return new AwkRuntimeException(name + " is read-only.");
3689 		}
3690 	}
3691 
3692 	/** Reflects a command-line variable assignment into a materialized SYMTAB. */
3693 	private void updateSymtabEntry(String name, Object value) {
3694 		if (symtabOffset == NULL_OFFSET) {
3695 			return;
3696 		}
3697 		Object symtab = runtimeStack.getVariable(symtabOffset, true);
3698 		if (symtab instanceof SymtabArray) {
3699 			((SymtabArray) symtab).putRuntimeVariable(name, value);
3700 		} else if (symtab instanceof Map) {
3701 			@SuppressWarnings("unchecked")
3702 			Map<Object, Object> symtabMap = (Map<Object, Object>) symtab;
3703 			symtabMap.put(name, value);
3704 		}
3705 	}
3706 
3707 	private void execApplySubsep(CountTuple tuple) {
3708 		long count = tuple.getCount();
3709 		if (count == 1) {
3710 			Object value = pop();
3711 			checkScalar(value);
3712 			// An integral number is already in the form the array
3713 			// implementations canonicalize to a Long key, so it skips the
3714 			// CONVFMT string round-trip; every other scalar converts to a
3715 			// string with the CONVFMT currently in effect, fixing the key
3716 			// from that point on.
3717 			push(AssocArray.isIntegralNumberKey(value) ? value : jrt.toAwkString(value));
3718 			return;
3719 		}
3720 		StringBuilder sb = new StringBuilder();
3721 		Object value = pop();
3722 		checkScalar(value);
3723 		sb.append(jrt.toAwkString(value));
3724 		String subsep = jrt.toAwkString(jrt.getSUBSEPVar());
3725 		for (int i = 1; i < count; i++) {
3726 			sb.insert(0, subsep);
3727 			value = pop();
3728 			checkScalar(value);
3729 			sb.insert(0, jrt.toAwkString(value));
3730 		}
3731 		push(sb.toString());
3732 	}
3733 
3734 	private long beforeProfiledTuple(Tuple tuple, Opcode opcode) {
3735 		long now = System.nanoTime();
3736 		if (opcode == Opcode.CALL_FUNCTION) {
3737 			CallFunctionTuple callTuple = (CallFunctionTuple) tuple;
3738 			activeProfilingFunctions.push(new ActiveFunction(callTuple.getFunctionName(), now));
3739 		} else if (opcode == Opcode.EXTENSION) {
3740 			ExtensionTuple extensionTuple = (ExtensionTuple) tuple;
3741 			ExtensionFunction function = extensionTuple.getFunction();
3742 			activeProfilingFunctions.push(new ActiveFunction(function.getKeyword(), now));
3743 		}
3744 		return now;
3745 	}
3746 
3747 	private void afterProfiledTuple(Opcode opcode, long tupleStartNanos) {
3748 		long now = System.nanoTime();
3749 		statisticsFor(tupleProfilingStats, opcode).add(now - tupleStartNanos);
3750 		if (opcode == Opcode.EXIT_WITH_CODE || opcode == Opcode.EXIT_WITHOUT_CODE) {
3751 			recordAllFunctionExits(now);
3752 		} else if (opcode == Opcode.EXTENSION || opcode == Opcode.RETURN_FROM_FUNCTION) {
3753 			recordFunctionExit(now);
3754 		}
3755 	}
3756 
3757 	private static <K> ProfilingReport.Accumulator statisticsFor(
3758 			Map<K, ProfilingReport.Accumulator> stats,
3759 			K key) {
3760 		ProfilingReport.Accumulator accumulator = stats.get(key);
3761 		if (accumulator == null) {
3762 			accumulator = new ProfilingReport.Accumulator();
3763 			stats.put(key, accumulator);
3764 		}
3765 		return accumulator;
3766 	}
3767 
3768 	private void recordFunctionExit(long now) {
3769 		if (activeProfilingFunctions.isEmpty()) {
3770 			return;
3771 		}
3772 		ActiveFunction function = activeProfilingFunctions.pop();
3773 		statisticsFor(functionProfilingStats, function.name).add(now - function.startNanos);
3774 	}
3775 
3776 	private void recordAllFunctionExits(long now) {
3777 		while (!activeProfilingFunctions.isEmpty()) {
3778 			recordFunctionExit(now);
3779 		}
3780 	}
3781 
3782 	private static final class ActiveFunction {
3783 		private final String name;
3784 		private final long startNanos;
3785 
3786 		private ActiveFunction(String name, long startNanos) {
3787 			this.name = name;
3788 			this.startNanos = startNanos;
3789 		}
3790 	}
3791 
3792 	/**
3793 	 * Releases any prepared input source and runtime I/O resources owned by this
3794 	 * AVM.
3795 	 * <p>
3796 	 * Call this when you are done with an AVM obtained through expert-level
3797 	 * integration, or after direct {@link #eval(AwkExpression, InputSource)} /
3798 	 * {@link #execute(AwkProgram, InputSource)} usage.
3799 	 * The AVM may be prepared again afterwards, but callers should treat a closed
3800 	 * instance as end-of-use unless they intentionally reinitialize it.
3801 	 * </p>
3802 	 */
3803 	@Override
3804 	public void close() throws IOException {
3805 		jrt.jrtCloseAll();
3806 		closeResolvedInputSource();
3807 		resolvedInputSource = null;
3808 	}
3809 
3810 	/**
3811 	 * Close the resolved {@link InputSource} if it implements {@link Closeable}.
3812 	 * This is used by {@link #close()} and by explicit rebind operations such as
3813 	 * {@link #prepareForEval(InputSource)} when the AVM switches to a different
3814 	 * source instance.
3815 	 */
3816 	private void closeResolvedInputSource() {
3817 		closeInputSource(resolvedInputSource);
3818 	}
3819 
3820 	private void closeInputSource(InputSource inputSource) {
3821 		if (!(inputSource instanceof Closeable)) {
3822 			return;
3823 		}
3824 		try {
3825 			((Closeable) inputSource).close();
3826 		} catch (IOException ignored) {
3827 			// Best-effort close.
3828 		}
3829 	}
3830 
3831 	private Object[] popArguments(long numArgs) {
3832 		int count = (int) numArgs;
3833 		Object[] args = new Object[count];
3834 		operandStackSize -= count;
3835 		System.arraycopy(operandStack, operandStackSize, args, 0, count);
3836 		Arrays.fill(operandStack, operandStackSize, operandStackSize + count, null);
3837 		return args;
3838 	}
3839 
3840 	/**
3841 	 * sprintf() functionality
3842 	 */
3843 	private String sprintfFunction(long numArgs) {
3844 		Object[] argArray = popArguments(numArgs - 1);
3845 		String fmt = jrt.toAwkString(pop());
3846 		return jrt.sprintf(fmt, argArray);
3847 	}
3848 
3849 	private void setNumOnJRT(long fieldNum, Object num) {
3850 		// same code as ASSIGN_AS_INPUT_FIELD: a field retains the numeric
3851 		// scalar itself (so an exact integer survives repeated arithmetic),
3852 		// while $0 is record text and converts with CONVFMT immediately
3853 		if (fieldNum == 0) {
3854 			jrt.setInputLine(jrt.toAwkString(num));
3855 			jrt.jrtParseFields();
3856 		} else {
3857 			jrt.jrtSetInputField(num, fieldNum);
3858 		}
3859 	}
3860 
3861 	private String execSubOrGSub(boolean isGsub) {
3862 		String newString;
3863 
3864 		// stack[0] = original field value
3865 		// stack[1] = replacement string
3866 		// stack[2] = ere
3867 		String orig = jrt.toAwkString(pop());
3868 		String repl = jrt.toAwkString(pop());
3869 		String ere = jrt.toAwkString(pop());
3870 		push(isGsub ? jrt.replaceAll(orig, repl, ere) : jrt.replaceFirst(orig, repl, ere));
3871 		newString = jrt.getReplaceResult();
3872 
3873 		return newString;
3874 	}
3875 
3876 	/**
3877 	 * Awk variable assignment functionality.
3878 	 */
3879 	private void assign(long l, Object value, boolean isGlobal, PositionTracker position, boolean push) {
3880 		value = JRT.untypedToBlank(value);
3881 		checkScalar(value);
3882 		// check if curr value already refers to an array
3883 		if (resolveVariable(l, isGlobal, false) instanceof Map) {
3884 			throw new AwkRuntimeException(position.lineNumber(), "Attempting to use an array in a scalar context.");
3885 		}
3886 		if (push) {
3887 			push(value);
3888 		}
3889 		runtimeStack.setVariable(l, value, isGlobal);
3890 		// When specials are compiled correctly, they use ASSIGN_* and skip this path.
3891 	}
3892 
3893 	/**
3894 	 * Awk array element assignment functionality.
3895 	 */
3896 	private void assignArray(long offset, Object arrIdx, Object rhs, boolean isGlobal) {
3897 		assignMapElement(ensureMapVariable(offset, isGlobal), arrIdx, rhs);
3898 	}
3899 
3900 	private void assignMapElement(Map<Object, Object> array, Object arrIdx, Object rhs) {
3901 		checkScalar(arrIdx);
3902 		rhs = JRT.untypedToBlank(rhs);
3903 		checkScalar(rhs);
3904 		if (JRT.containsAwkKey(array, arrIdx)
3905 				&& JRT.getAssocArrayValue(array, arrIdx) instanceof Map) {
3906 			throw new AwkRuntimeException("Attempting to use an array in a scalar context.");
3907 		}
3908 		array.put(arrIdx, rhs);
3909 		push(rhs);
3910 	}
3911 
3912 	/**
3913 	 * Numerically increases an Awk variable by one; the result
3914 	 * is placed back into that variable.
3915 	 */
3916 	/**
3917 	 * Replaces a missing or uninitialized array element by numeric zero, so
3918 	 * that {@code ++}/{@code --} on it starts from an exact integer, the same
3919 	 * way {@link #inc(long, boolean)} treats an uninitialized variable.
3920 	 */
3921 	private static Object blankToZero(Object o) {
3922 		return o == null || o instanceof UninitializedObject ? ZERO : o;
3923 	}
3924 
3925 	/**
3926 	 * Coerces a scalar to its numeric value, keeping an already numeric
3927 	 * scalar unchanged so an exact integer is not rounded through a double.
3928 	 */
3929 	private static Object numericValueOf(Object o) {
3930 		if (o instanceof Long || o instanceof Integer || o instanceof Double) {
3931 			return o;
3932 		}
3933 		return JRT.toDouble(o);
3934 	}
3935 
3936 	private Object inc(long l, boolean isGlobal) {
3937 		Object o = resolveVariable(l, isGlobal, false);
3938 		if (o instanceof UninitializedObject) {
3939 			o = ZERO;
3940 			runtimeStack.setVariable(l, o, isGlobal);
3941 		}
3942 		Object updated = JRT.inc(o);
3943 		runtimeStack.setVariable(l, updated, isGlobal);
3944 		return o;
3945 	}
3946 
3947 	/**
3948 	 * Numerically decreases an Awk variable by one; the result
3949 	 * is placed back into that variable.
3950 	 */
3951 	private Object dec(long l, boolean isGlobal) {
3952 		Object o = resolveVariable(l, isGlobal, false);
3953 		if (o instanceof UninitializedObject) {
3954 			o = ZERO;
3955 			runtimeStack.setVariable(l, o, isGlobal);
3956 		}
3957 		Object updated = JRT.dec(o);
3958 		runtimeStack.setVariable(l, updated, isGlobal);
3959 		return o;
3960 	}
3961 
3962 	/** {@inheritDoc} */
3963 	@Override
3964 	public final Object getRS() {
3965 		return jrt.getRSVar();
3966 	}
3967 
3968 	/** {@inheritDoc} */
3969 	@Override
3970 	public final Object getOFS() {
3971 		return jrt.getOFSVar();
3972 	}
3973 
3974 	/** {@inheritDoc} */
3975 	@Override
3976 	public final Object getORS() {
3977 		return jrt.getORSVar();
3978 	}
3979 
3980 	/** {@inheritDoc} */
3981 	@Override
3982 	public final Object getSUBSEP() {
3983 		return jrt.getSUBSEPVar();
3984 	}
3985 
3986 	/**
3987 	 * Returns the names of the global variables declared by the compiled
3988 	 * program.
3989 	 *
3990 	 * @return unmodifiable set of global variable names, empty when no program
3991 	 *         metadata is installed
3992 	 */
3993 	public Set<String> getGlobalVariableNames() {
3994 		return globalVariableOffsets == null ?
3995 				Collections.<String>emptySet() : Collections.unmodifiableSet(globalVariableOffsets.keySet());
3996 	}
3997 
3998 	/**
3999 	 * Returns the names of the user-defined functions of the compiled program.
4000 	 *
4001 	 * @return unmodifiable set of function names, empty when no program metadata
4002 	 *         is installed
4003 	 */
4004 	public Set<String> getFunctionNames() {
4005 		return functionNames == null ? Collections.<String>emptySet() : Collections.unmodifiableSet(functionNames);
4006 	}
4007 
4008 	/**
4009 	 * The names of the special variables the interpreter answers by name.
4010 	 * Must stay in sync with the switch in {@link #getVariable(String)}; a
4011 	 * unit test verifies that every listed name is answered.
4012 	 */
4013 	private static final Set<String> SPECIAL_VARIABLE_NAMES = Collections
4014 			.unmodifiableSet(
4015 					new LinkedHashSet<String>(
4016 							Arrays
4017 									.asList(
4018 											"FS",
4019 											"RS",
4020 											"OFS",
4021 											"ORS",
4022 											"FILENAME",
4023 											"SUBSEP",
4024 											"CONVFMT",
4025 											"OFMT",
4026 											"NF",
4027 											"NR",
4028 											"FNR",
4029 											"RSTART",
4030 											"RLENGTH",
4031 											"IGNORECASE",
4032 											"ERRNO",
4033 											"ARGIND",
4034 											"ARGC",
4035 											"ARGV")));
4036 
4037 	/**
4038 	 * Returns the names of the special variables that
4039 	 * {@link #getVariable(String)} answers directly.
4040 	 *
4041 	 * @return unmodifiable set of special variable names
4042 	 */
4043 	public Set<String> getSpecialVariableNames() {
4044 		return SPECIAL_VARIABLE_NAMES;
4045 	}
4046 
4047 	/** {@inheritDoc} */
4048 	@Override
4049 	public final Object getVariable(String name) {
4050 		if (name == null) {
4051 			return null;
4052 		}
4053 		switch (name) {
4054 		case "FS":
4055 			return getFS();
4056 		case "RS":
4057 			return getRS();
4058 		case "OFS":
4059 			return getOFS();
4060 		case "ORS":
4061 			return getORS();
4062 		case "FILENAME":
4063 			return jrt.getFILENAME();
4064 		case "SUBSEP":
4065 			return getSUBSEP();
4066 		case "CONVFMT":
4067 			return getCONVFMT();
4068 		case "OFMT":
4069 			return jrt.getOFMTString();
4070 		case "NF":
4071 			return jrt.getNF();
4072 		case "NR":
4073 			return jrt.getNR();
4074 		case "FNR":
4075 			return jrt.getFNR();
4076 		case "RSTART":
4077 			return jrt.getRSTART();
4078 		case "RLENGTH":
4079 			return jrt.getRLENGTH();
4080 		case "IGNORECASE":
4081 			return jrt.getIGNORECASEVar();
4082 		case "ERRNO":
4083 			if (isManagedSpecialVariable(name)) {
4084 				return jrt.getERRNO();
4085 			}
4086 			// POSIX mode: ordinary global, answered by the slot lookup below
4087 			break;
4088 		case "ARGIND":
4089 			if (isManagedSpecialVariable(name)) {
4090 				return jrt.getARGIND();
4091 			}
4092 			// POSIX mode: ordinary global, answered by the slot lookup below
4093 			break;
4094 		// lazily-materialized globals answered through their synthetic accessors
4095 		case "ARGC":
4096 			return getARGC();
4097 		case "ARGV":
4098 			return getARGV();
4099 		default:
4100 			break;
4101 		}
4102 		if (globalVariableOffsets == null) {
4103 			return executionInitialVariables.get(name);
4104 		}
4105 		Integer offsetObj = globalVariableOffsets.get(name);
4106 		if (offsetObj != null) {
4107 			return runtimeStack.getVariable(offsetObj.intValue(), true);
4108 		}
4109 		// Variables supplied through -v or the Java API but never referenced in
4110 		// the script have no compiled offset; they are still observable (e.g.
4111 		// IGNORECASE read by the gawk extension).
4112 		return executionInitialVariables == null ? null : executionInitialVariables.get(name);
4113 	}
4114 
4115 	/**
4116 	 * Returns the description of the primary script source (typically its file
4117 	 * name), for extension-emitted diagnostics.
4118 	 *
4119 	 * @return script source description, or {@code null} when unknown
4120 	 */
4121 	public String getSourceDescription() {
4122 		return sourceDescription;
4123 	}
4124 
4125 	/**
4126 	 * Returns the script line of the extension call currently being dispatched,
4127 	 * for extension-emitted diagnostics.
4128 	 *
4129 	 * @return current script line number
4130 	 */
4131 	public int getCurrentLineNumber() {
4132 		return currentLineNumber;
4133 	}
4134 
4135 	/** {@inheritDoc} */
4136 	@Override
4137 	public final void assignVariable(String name, Object obj) {
4138 		// When offsets are not available yet, treat the assignment as part of this
4139 		// AVM's baseline initial-variable snapshot.
4140 		if (globalVariableOffsets == null || globalVariableArrays == null) {
4141 			Object normalized = normalizeExternalVariableValue(obj);
4142 			baseInitialVariables.put(name, normalized);
4143 			if (isManagedSpecialVariable(name)) {
4144 				baseSpecialVariables.put(name, normalized);
4145 			}
4146 			return;
4147 		}
4148 
4149 		// make sure we're not receiving funcname=value assignments
4150 		if (functionNames.contains(name)) {
4151 			throw new IllegalArgumentException("Cannot assign a scalar to a function name (" + name + ").");
4152 		}
4153 
4154 		Object normalized = normalizeExternalVariableValue(obj);
4155 		// Runtime assignments to JRT-managed specials (e.g. IGNORECASE=1 or
4156 		// FS=: between input files) must reach the JRT, not a global slot.
4157 		// ARGC is excluded: JRT.setARGC delegates back to this method, and its
4158 		// authoritative storage is the compiled slot below.
4159 		if (!"ARGC".equals(name) && isManagedSpecialVariable(name)) {
4160 			jrt.applySpecialVariable(name, normalized);
4161 			updateSymtabEntry(name, normalized);
4162 			return;
4163 		}
4164 
4165 		Integer offsetObj = globalVariableOffsets.get(name);
4166 		Boolean arrayObj = globalVariableArrays.get(name);
4167 
4168 		if (offsetObj != null) {
4169 			if (arrayObj.booleanValue() && !(normalized instanceof Map)) {
4170 				throw new IllegalArgumentException(
4171 						"Cannot assign a scalar to a non-scalar variable (" + name + ").");
4172 			}
4173 			runtimeStack.setFilelistVariable(offsetObj.intValue(), normalized);
4174 		} else if (runtimeStack.hasGlobalVariable(name)) {
4175 			runtimeStack.setGlobalVariable(name, normalized);
4176 		}
4177 		// names without a compiled slot are still symbols: keep SYMTAB current
4178 		updateSymtabEntry(name, normalized);
4179 	}
4180 
4181 	/**
4182 	 * Executes the {@code nextfile} statement: abandons the current input file
4183 	 * and resumes the per-file input loop at the appropriate point. The
4184 	 * runtime and operand stacks are cleared, so {@code nextfile} unwinds
4185 	 * user-defined function calls, mirroring {@code exit}.
4186 	 * <p>
4187 	 * A {@code nextfile} written directly inside a BEGIN, END, or ENDFILE
4188 	 * rule is already rejected at compile time by the parser. The checks
4189 	 * below cover the uses reached through user-defined functions, where the
4190 	 * calling rule cannot be known statically (the same function may be
4191 	 * called from both an ordinary rule and an END rule); gawk performs the
4192 	 * same checks at runtime.
4193 	 * </p>
4194 	 *
4195 	 * @param position the tuple position tracker to redirect
4196 	 */
4197 	private void executeNextfile(PositionTracker position) {
4198 		if (nextFileAddress == null || !inputFileLoopStarted) {
4199 			throw new AwkRuntimeException(
4200 					position.lineNumber(),
4201 					"`nextfile' cannot be called from a BEGIN rule");
4202 		}
4203 		if (withinEndBlocks) {
4204 			throw new AwkRuntimeException(
4205 					position.lineNumber(),
4206 					"`nextfile' cannot be called from an END rule");
4207 		}
4208 		if (withinEndFileBlocks) {
4209 			throw new AwkRuntimeException(
4210 					position.lineNumber(),
4211 					"`nextfile' cannot be called from an ENDFILE rule");
4212 		}
4213 		// nextfile can be invoked from user-defined functions: unwind them.
4214 		resetCallState();
4215 		if (endFileAddress == null
4216 				|| withinBeginFileBlocks && jrt.hasPendingInputFileError(resolvedInputSource)) {
4217 			// No ENDFILE rules to run, or the file could not be opened: skip
4218 			// the ENDFILE section (gawk BEGINFILE error handling) and go
4219 			// straight to the next file.
4220 			withinBeginFileBlocks = false;
4221 			position.jump(nextFileAddress);
4222 		} else {
4223 			withinBeginFileBlocks = false;
4224 			withinEndFileBlocks = true;
4225 			position.jump(endFileAddress);
4226 		}
4227 	}
4228 
4229 	/**
4230 	 * Executes the {@code next} statement when it is reached through a
4231 	 * user-defined function call: abandons the current input record and
4232 	 * resumes the main input loop. The runtime and operand stacks are
4233 	 * cleared, so {@code next} unwinds user-defined function calls, mirroring
4234 	 * {@code nextfile}.
4235 	 * <p>
4236 	 * A {@code next} written directly inside an input rule compiles to a
4237 	 * plain jump, and direct uses inside BEGIN, END, BEGINFILE, or ENDFILE
4238 	 * rules are rejected at compile time by the parser. The checks below
4239 	 * cover the uses reached through user-defined functions, where the
4240 	 * calling rule cannot be known statically (the same function may be
4241 	 * called from both an input rule and a special rule); gawk performs the
4242 	 * same checks at runtime.
4243 	 * </p>
4244 	 *
4245 	 * @param position the tuple position tracker to redirect
4246 	 */
4247 	private void executeNext(PositionTracker position) {
4248 		if (withinEndBlocks) {
4249 			throw new AwkRuntimeException(
4250 					position.lineNumber(),
4251 					"`next' cannot be called from a `END' rule");
4252 		}
4253 		if (withinBeginFileBlocks) {
4254 			throw new AwkRuntimeException(
4255 					position.lineNumber(),
4256 					"`next' cannot be called from a `BEGINFILE' rule");
4257 		}
4258 		if (withinEndFileBlocks) {
4259 			throw new AwkRuntimeException(
4260 					position.lineNumber(),
4261 					"`next' cannot be called from a `ENDFILE' rule");
4262 		}
4263 		if (nextAddress == null || !mainInputLoopStarted) {
4264 			throw new AwkRuntimeException(
4265 					position.lineNumber(),
4266 					"`next' cannot be called from a `BEGIN' rule");
4267 		}
4268 		// next can be invoked from user-defined functions: unwind them.
4269 		resetCallState();
4270 		position.jump(nextAddress);
4271 	}
4272 
4273 	/**
4274 	 * Returns whether a non-redirected {@code getline} must be confined to
4275 	 * the current input file. While the per-file main input loop of a program
4276 	 * with BEGINFILE/ENDFILE rules is running, only the loop itself may cross
4277 	 * file boundaries, so that no file's hooks are ever skipped; a
4278 	 * {@code getline} in an action therefore reports end-of-input at the end
4279 	 * of the current file. In BEGIN and END rules — before the loop starts or
4280 	 * after it ends — {@code getline} keeps streaming across the remaining
4281 	 * input.
4282 	 *
4283 	 * @return {@code true} when getline must not advance to the next file
4284 	 */
4285 	private boolean isMainInputFileBounded() {
4286 		return endFileAddress != null && inputFileLoopStarted && !withinEndBlocks;
4287 	}
4288 
4289 	/**
4290 	 * Raises the gawk-compatible fatal error when a non-redirected
4291 	 * {@code getline} executes inside a BEGINFILE or ENDFILE rule.
4292 	 * <p>
4293 	 * A non-redirected {@code getline} written directly inside a
4294 	 * BEGINFILE/ENDFILE rule is already rejected at compile time by the
4295 	 * parser. This runtime check covers the uses reached through
4296 	 * user-defined functions, where the calling rule cannot be known
4297 	 * statically; it lives here rather than in {@link JRT} because the
4298 	 * current-rule flags are interpreter execution state.
4299 	 * </p>
4300 	 *
4301 	 * @param position the tuple position tracker, for error reporting
4302 	 */
4303 	private void checkGetlineAllowed(PositionTracker position) {
4304 		if (withinBeginFileBlocks || withinEndFileBlocks) {
4305 			throw new AwkRuntimeException(
4306 					position.lineNumber(),
4307 					"non-redirected `getline' invalid inside `"
4308 							+ (withinBeginFileBlocks ? "BEGINFILE" : "ENDFILE")
4309 							+ "' rule");
4310 		}
4311 	}
4312 
4313 	/** {@inheritDoc} */
4314 	@Override
4315 	public Object getFS() {
4316 		return jrt.getFSVar();
4317 	}
4318 
4319 	/** {@inheritDoc} */
4320 	@Override
4321 	public Object getCONVFMT() {
4322 		return jrt.getCONVFMTString();
4323 	}
4324 
4325 	/** {@inheritDoc} */
4326 	@Override
4327 	public void resetFNR() {
4328 		jrt.setFNR(0);
4329 	}
4330 
4331 	/** {@inheritDoc} */
4332 	@Override
4333 	public void incFNR() {
4334 		long v = jrt.getFNR();
4335 		jrt.setFNR(v + 1);
4336 	}
4337 
4338 	/** {@inheritDoc} */
4339 	@Override
4340 	public void incNR() {
4341 		long v = jrt.getNR();
4342 		jrt.setNR(v + 1);
4343 	}
4344 
4345 	/** {@inheritDoc} */
4346 	@Override
4347 	public void setNF(Integer newNf) {
4348 		jrt.setNF(newNf);
4349 	}
4350 
4351 	/** {@inheritDoc} */
4352 	@Override
4353 	public void setFILENAME(String filename) {
4354 		jrt.setFILENAMEViaJrt(jrt.toInputScalar(filename));
4355 	}
4356 
4357 	/** {@inheritDoc} */
4358 	@Override
4359 	public Object getARGV() {
4360 		if (argvOffset == NULL_OFFSET) {
4361 			Map<Object, Object> argv = newAwkArray();
4362 			forEachArgvEntry(argv::put);
4363 			return argv;
4364 		}
4365 		return runtimeStack.getVariable(argvOffset, true);
4366 	}
4367 
4368 	/** {@inheritDoc} */
4369 	@Override
4370 	public Object getARGC() {
4371 		if (argcOffset == NULL_OFFSET) {
4372 			return Long.valueOf(arguments.size() + 1);
4373 		}
4374 		return runtimeStack.getVariable(argcOffset, true);
4375 	}
4376 
4377 	private String getOFMT() {
4378 		return jrt.getOFMTString();
4379 	}
4380 
4381 	private Map<Object, Object> newAwkArray() {
4382 		return JRT.createAwkMap(sortedArrayKeys);
4383 	}
4384 
4385 	private Map<Object, Object> ensureMapVariable(long offset, boolean isGlobal) {
4386 		return toMap(resolveVariable(offset, isGlobal, true));
4387 	}
4388 
4389 	private Map<Object, Object> getMapVariable(long offset, boolean isGlobal) {
4390 		return toMap(resolveVariable(offset, isGlobal, true));
4391 	}
4392 
4393 	private Object resolveVariable(long offset, boolean isGlobal, boolean arrayContext) {
4394 		Object value = runtimeStack.getVariable(offset, isGlobal);
4395 		// Argument references are only ever stored in local parameter slots,
4396 		// so global reads skip the reference check entirely.
4397 		if (!isGlobal && value instanceof ArgumentReference) {
4398 			value = resolveArgumentReference((ArgumentReference) value, arrayContext);
4399 			runtimeStack.setVariable(offset, value, isGlobal);
4400 			return value;
4401 		}
4402 		if (!isUntyped(value)) {
4403 			return value;
4404 		}
4405 		value = arrayContext ? newAwkArray() : BLANK;
4406 		runtimeStack.setVariable(offset, value, isGlobal);
4407 		return value;
4408 	}
4409 
4410 	private Object resolveRawArgumentReference(ArgumentReference reference) {
4411 		Object currentValue = readCurrentArgumentValue(reference);
4412 		if (currentValue instanceof Map
4413 				|| currentValue instanceof UninitializedObject
4414 						&& !(currentValue instanceof UntypedObject)) {
4415 			return currentValue;
4416 		}
4417 		Object value = reference.snapshot();
4418 		return value instanceof ArgumentReference ?
4419 				resolveRawArgumentReference((ArgumentReference) value) : value;
4420 	}
4421 
4422 	private Object readCurrentArgumentValue(ArgumentReference reference) {
4423 		Object value = reference.currentValue();
4424 		return value instanceof ArgumentReference ?
4425 				readCurrentArgumentValue((ArgumentReference) value) : value;
4426 	}
4427 
4428 	private Object resolveArgumentReference(ArgumentReference reference, boolean arrayContext) {
4429 		if (!arrayContext) {
4430 			checkScalar(readCurrentArgumentValue(reference));
4431 		}
4432 		Object value = arrayContext ? reference.currentValue() : reference.snapshot();
4433 		if (value instanceof ArgumentReference) {
4434 			value = resolveArgumentReference((ArgumentReference) value, arrayContext);
4435 			if (arrayContext) {
4436 				reference.setValue(value);
4437 			} else {
4438 				reference.setScalarValue(value);
4439 			}
4440 			return value;
4441 		}
4442 		if (!isUntyped(value)) {
4443 			return value;
4444 		}
4445 		value = arrayContext ? newAwkArray() : BLANK;
4446 		if (arrayContext) {
4447 			reference.setValue(value);
4448 		} else {
4449 			reference.setScalarValue(value);
4450 		}
4451 		return value;
4452 	}
4453 
4454 	private Object resolveLengthArgumentReference(ArgumentReference reference) {
4455 		Object currentValue = readCurrentArgumentValue(reference);
4456 		return currentValue instanceof Map ? currentValue : resolveArgumentReference(reference, false);
4457 	}
4458 
4459 	private Map<IndirectArrayArgumentReference, Object> captureAttachedArrayArgumentValues() {
4460 		if (elementArgumentReferences.isEmpty()) {
4461 			return Collections.emptyMap();
4462 		}
4463 		Map<IndirectArrayArgumentReference, Object> values = new IdentityHashMap<IndirectArrayArgumentReference, Object>();
4464 		for (IndirectArrayArgumentReference reference : elementArgumentReferences) {
4465 			if (reference.isAttached()) {
4466 				values.put(reference, reference.currentValue());
4467 			}
4468 		}
4469 		return values;
4470 	}
4471 
4472 	private void detachReplacedArrayArgumentReferences(
4473 			Map<IndirectArrayArgumentReference, Object> attachedValues) {
4474 		for (Map.Entry<IndirectArrayArgumentReference, Object> entry : attachedValues.entrySet()) {
4475 			entry.getKey().detachIfReplaced(entry.getValue());
4476 		}
4477 	}
4478 
4479 	private void detachMissingArrayArgumentReferences(Map<Object, Object> map) {
4480 		if (elementArgumentReferences.isEmpty()) {
4481 			return;
4482 		}
4483 		for (IndirectArrayArgumentReference reference : elementArgumentReferences) {
4484 			reference.detachIfMissing(map);
4485 		}
4486 	}
4487 
4488 	private static boolean isUntyped(Object value) {
4489 		return value == null || value instanceof UntypedObject;
4490 	}
4491 
4492 	/**
4493 	 * Casts an AWK value to an associative array.
4494 	 *
4495 	 * @param value value to validate
4496 	 * @return the associative array value
4497 	 * @throws AwkRuntimeException when {@code value} is scalar
4498 	 */
4499 	private Map<Object, Object> toMap(Object value) {
4500 		if (!(value instanceof Map)) {
4501 			throw new AwkRuntimeException("Attempting to use a scalar as an array.");
4502 		}
4503 		@SuppressWarnings("unchecked")
4504 		Map<Object, Object> map = (Map<Object, Object>) value;
4505 		return map;
4506 	}
4507 
4508 	/**
4509 	 * Ensures a value is scalar before using it in a scalar-only context such as
4510 	 * a subscript component.
4511 	 *
4512 	 * @param value value to validate
4513 	 * @throws AwkRuntimeException when {@code value} is an array
4514 	 */
4515 	private void checkScalar(Object value) {
4516 		if (value instanceof Map) {
4517 			throw new AwkRuntimeException("Attempting to use an array in a scalar context.");
4518 		}
4519 	}
4520 
4521 	/**
4522 	 * Returns the nested associative array stored in {@code map[key]}, creating it
4523 	 * when the key is undefined.
4524 	 *
4525 	 * @param map containing array
4526 	 * @param key nested-array key
4527 	 * @return the nested associative array stored at {@code key}
4528 	 * @throws AwkRuntimeException when {@code key} is scalar-incompatible or when
4529 	 *         the existing slot contains a scalar
4530 	 */
4531 	private Map<Object, Object> ensureArrayInArray(Map<Object, Object> map, Object key) {
4532 		checkScalar(key);
4533 		boolean existingKey = JRT.containsAwkKey(map, key);
4534 		Object value = JRT.getAssocArrayValue(map, key);
4535 		if (!existingKey || value == null || value instanceof UntypedObject) {
4536 			Map<Object, Object> nested = newAwkArray();
4537 			map.put(key, nested);
4538 			return nested;
4539 		}
4540 		if (!(value instanceof Map)) {
4541 			throw new AwkRuntimeException("Attempting to use a scalar as an array.");
4542 		}
4543 		@SuppressWarnings("unchecked")
4544 		Map<Object, Object> nested = (Map<Object, Object>) value;
4545 		return nested;
4546 	}
4547 
4548 	private Object normalizeExternalVariableValue(Object value) {
4549 		if (value instanceof String) {
4550 			return jrt.toInputScalar(value);
4551 		}
4552 		if (!(value instanceof Map) && !(value instanceof List)) {
4553 			return value;
4554 		}
4555 		return AssocArray.normalizeValue(value, sortedArrayKeys);
4556 	}
4557 
4558 	private static final UninitializedObject BLANK = new UninitializedObject();
4559 
4560 	private interface ArgumentReference {
4561 
4562 		Object snapshot();
4563 
4564 		Object currentValue();
4565 
4566 		void setValue(Object value);
4567 
4568 		void setScalarValue(Object value);
4569 	}
4570 
4571 	private static final class IndirectArgumentReference implements ArgumentReference {
4572 		private final Object[] frame;
4573 		private final long offset;
4574 		private final Object scalarValue;
4575 
4576 		private IndirectArgumentReference(Object[] frameParam, long offsetParam, Object scalarValueParam) {
4577 			frame = frameParam;
4578 			offset = offsetParam;
4579 			scalarValue = scalarValueParam;
4580 		}
4581 
4582 		@Override
4583 		public Object snapshot() {
4584 			return scalarValue;
4585 		}
4586 
4587 		@Override
4588 		public Object currentValue() {
4589 			return frame[(int) offset];
4590 		}
4591 
4592 		@Override
4593 		public void setValue(Object value) {
4594 			frame[(int) offset] = value;
4595 		}
4596 
4597 		@Override
4598 		public void setScalarValue(Object value) {
4599 			if (isUntyped(currentValue())) {
4600 				setValue(value);
4601 			}
4602 		}
4603 	}
4604 
4605 	private static final class IndirectArrayArgumentReference implements ArgumentReference {
4606 		private final Map<Object, Object> map;
4607 		private final Object key;
4608 		private Object detachedValue;
4609 		private boolean detached;
4610 		// Depth of the call frame that received this reference as an
4611 		// argument; negative until the call is entered.
4612 		private int ownerFrame = -1;
4613 
4614 		private IndirectArrayArgumentReference(
4615 				Map<Object, Object> mapParam,
4616 				Object keyParam,
4617 				Object scalarValueParam) {
4618 			map = mapParam;
4619 			key = keyParam;
4620 			detachedValue = scalarValueParam;
4621 			detached = !JRT.containsAwkKey(map, key);
4622 		}
4623 
4624 		@Override
4625 		public Object snapshot() {
4626 			return detachedValue;
4627 		}
4628 
4629 		@Override
4630 		public Object currentValue() {
4631 			return !detached && JRT.containsAwkKey(map, key) ?
4632 					JRT.getAssocArrayValue(map, key) : detachedValue;
4633 		}
4634 
4635 		@Override
4636 		public void setValue(Object value) {
4637 			if (!detached && JRT.containsAwkKey(map, key)) {
4638 				map.put(key, value);
4639 			} else {
4640 				detachedValue = value;
4641 			}
4642 		}
4643 
4644 		@Override
4645 		public void setScalarValue(Object value) {
4646 			if (detached || !JRT.containsAwkKey(map, key)) {
4647 				detachedValue = value;
4648 			} else if (isUntyped(JRT.getAssocArrayValue(map, key))) {
4649 				map.put(key, value);
4650 			}
4651 		}
4652 
4653 		private void detachIfMissing(Map<Object, Object> candidateMap) {
4654 			if (!detached && map == candidateMap && !JRT.containsAwkKey(map, key)) {
4655 				detached = true;
4656 			}
4657 		}
4658 
4659 		private boolean isAttached() {
4660 			return !detached && JRT.containsAwkKey(map, key);
4661 		}
4662 
4663 		private void detachIfReplaced(Object previousValue) {
4664 			if (!detached
4665 					&& (!JRT.containsAwkKey(map, key)
4666 							|| JRT.getAssocArrayValue(map, key) != previousValue)) {
4667 				detached = true;
4668 			}
4669 		}
4670 	}
4671 
4672 	/**
4673 	 * Global names that must not participate in persistent memory even though they
4674 	 * are technically user-visible variables.
4675 	 */
4676 	private static final Set<String> NON_PERSISTENT_GLOBALS = new HashSet<>(
4677 			Arrays
4678 					.asList(
4679 							"ARGV",
4680 							"ARGC",
4681 							"ENVIRON",
4682 							"RSTART",
4683 							"RLENGTH",
4684 							"IGNORECASE",
4685 							"SYMTAB",
4686 							"FUNCTAB"));
4687 
4688 	private static final class SingleRecordInputSource implements InputSource {
4689 
4690 		private final String record;
4691 		private boolean consumed;
4692 
4693 		private SingleRecordInputSource(String record) {
4694 			this.record = record;
4695 		}
4696 
4697 		@Override
4698 		public boolean nextRecord() {
4699 			if (consumed || record == null) {
4700 				return false;
4701 			}
4702 			consumed = true;
4703 			return true;
4704 		}
4705 
4706 		@Override
4707 		public String getRecordText() {
4708 			return consumed ? record : null;
4709 		}
4710 
4711 		@Override
4712 		public List<String> getFields() {
4713 			return null;
4714 		}
4715 
4716 		@Override
4717 		public boolean isFromFilenameList() {
4718 			return false;
4719 		}
4720 	}
4721 
4722 	/**
4723 	 * The value of an address which is not yet assigned a tuple index.
4724 	 */
4725 	public static final int NULL_OFFSET = -1;
4726 
4727 }