View Javadoc
1   package io.jawk;
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.ByteArrayInputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.OutputStream;
29  import java.io.PrintStream;
30  import java.io.Reader;
31  import java.io.StringReader;
32  import java.nio.charset.StandardCharsets;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.Collection;
36  import java.util.Collections;
37  import java.util.LinkedHashMap;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Objects;
41  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
42  import io.jawk.backend.AVM;
43  import io.jawk.ext.ExtensionFunction;
44  import io.jawk.ext.ExtensionRegistry;
45  import io.jawk.ext.GawkExtension;
46  import io.jawk.ext.JawkExtension;
47  import io.jawk.frontend.AwkParser;
48  import io.jawk.frontend.AstNode;
49  import io.jawk.jrt.AppendableAwkSink;
50  import io.jawk.jrt.AwkSink;
51  import io.jawk.jrt.InputSource;
52  import io.jawk.jrt.OutputStreamAwkSink;
53  import io.jawk.jrt.StreamInputSource;
54  import io.jawk.util.AwkSettings;
55  import io.jawk.util.ScriptSource;
56  
57  /**
58   * Entry point into the parsing, analysis, and execution
59   * of a Jawk script.
60   * This entry point is used both when Jawk is executed as a library and when
61   * invoked from the command line.
62   * <p>
63   * The overall process to execute a Jawk script is as follows:
64   * <ul>
65   * <li>Parse the Jawk script, producing an abstract syntax tree.
66   * <li>Traverse the abstract syntax tree, producing a list of
67   * instruction tuples for the interpreter.
68   * <li>Traverse the list of tuples, providing a runtime which
69   * ultimately executes the Jawk script, <strong>or</strong>
70   * Command-line parameters dictate which action is to take place.
71   * </ul>
72   * Two additional semantic checks on the syntax tree are employed
73   * (both to resolve function calls for defined functions).
74   * As a result, the syntax tree is traversed three times.
75   * And the number of times tuples are traversed is depends
76   * on whether interpretation or compilation takes place.
77   * <p>
78   * The engine does not enable any extensions automatically. Extensions can be
79   * provided programmatically via the {@link Awk#Awk(Collection)} constructors or
80   * via the command line when using the CLI entry point.
81   *
82   * @see io.jawk.backend.AVM
83   * @author Danny Daglas
84   */
85  public class Awk {
86  
87  	/** POSIX default field separator ({@code " "}). */
88  	public static final String DEFAULT_FS = " ";
89  
90  	/** POSIX default record separator ({@code "\n"}). */
91  	public static final String DEFAULT_RS = "\n";
92  
93  	/** POSIX default output field separator ({@code " "}). */
94  	public static final String DEFAULT_OFS = " ";
95  
96  	/** POSIX default output record separator ({@code "\n"}). */
97  	public static final String DEFAULT_ORS = "\n";
98  
99  	/** POSIX default number-to-string conversion format ({@code "%.6g"}). */
100 	public static final String DEFAULT_CONVFMT = "%.6g";
101 
102 	/** POSIX default output number format ({@code "%.6g"}). */
103 	public static final String DEFAULT_OFMT = "%.6g";
104 
105 	/** POSIX default subscript separator ({@code "\034"}). */
106 	public static final String DEFAULT_SUBSEP = String.valueOf((char) 28);
107 
108 	private final Map<String, ExtensionFunction> extensionFunctions;
109 
110 	private final Map<String, JawkExtension> extensionInstances;
111 
112 	/**
113 	 * The behavioral settings used by this engine instance.
114 	 */
115 	private final AwkSettings settings;
116 
117 	/**
118 	 * The last parsed {@link AstNode} produced during compilation.
119 	 */
120 	private AstNode lastAst;
121 
122 	/**
123 	 * Create a new instance of Awk with default extensions.
124 	 */
125 	public Awk() {
126 		this(new AwkSettings());
127 	}
128 
129 	/**
130 	 * Create a new instance of Awk with the specified settings.
131 	 *
132 	 * @param settings behavioral configuration for this engine
133 	 */
134 	public Awk(AwkSettings settings) {
135 		this(ExtensionSetup.createDefault(), settings);
136 	}
137 
138 	/**
139 	 * Create a new instance of Awk with the specified extension instances.
140 	 *
141 	 * @param extensions extension instances implementing {@link JawkExtension}
142 	 */
143 	public Awk(Collection<? extends JawkExtension> extensions) {
144 		this(createExtensionSetup(extensions));
145 	}
146 
147 	/**
148 	 * Create a new instance of Awk with the specified extension instances
149 	 * and settings.
150 	 *
151 	 * @param extensions extension instances implementing {@link JawkExtension}
152 	 * @param settings behavioral configuration for this engine
153 	 */
154 	public Awk(Collection<? extends JawkExtension> extensions, AwkSettings settings) {
155 		this(createExtensionSetup(extensions), settings);
156 	}
157 
158 	/**
159 	 * Create a new instance of Awk with the specified extension instances.
160 	 *
161 	 * @param extensions extension instances implementing {@link JawkExtension}
162 	 */
163 	@SafeVarargs
164 	public Awk(JawkExtension... extensions) {
165 		this(createExtensionSetup(Arrays.asList(extensions)));
166 	}
167 
168 	/**
169 	 * Creates an engine from an already resolved extension set, with default
170 	 * settings.
171 	 * <p>
172 	 * The public constructors that take no settings delegate here. The extension
173 	 * set is a private type, so this constructor is private too: a subclass could
174 	 * not name the parameter type anyway, and selects its extensions through the
175 	 * public constructors instead.
176 	 * </p>
177 	 *
178 	 * @param setup extension functions and instances to bind to this engine
179 	 */
180 	private Awk(ExtensionSetup setup) {
181 		this(setup, new AwkSettings());
182 	}
183 
184 	/**
185 	 * Creates an engine from an already resolved extension set and the given
186 	 * settings. Every other constructor ends up here.
187 	 *
188 	 * @param setup extension functions and instances to bind to this engine
189 	 * @param settings behavioral configuration for this engine
190 	 * @throws NullPointerException if {@code settings} is {@code null}
191 	 */
192 	private Awk(ExtensionSetup setup, AwkSettings settings) {
193 		this.extensionFunctions = setup.functions;
194 		this.extensionInstances = setup.instances;
195 		this.settings = Objects.requireNonNull(settings, "settings");
196 	}
197 
198 	/**
199 	 * Returns the extension functions bound to this engine, keyed by the Awk
200 	 * function name that calls them.
201 	 *
202 	 * @return an unmodifiable map of Awk function name to implementation
203 	 */
204 	protected Map<String, ExtensionFunction> getExtensionFunctions() {
205 		return extensionFunctions;
206 	}
207 
208 	/**
209 	 * Returns the extension instances bound to this engine, keyed by the fully
210 	 * qualified name of their class, which is how the interpreter looks them up
211 	 * when a tuple invokes an extension function.
212 	 * <p>
213 	 * Subclasses pass this map on when they build their own interpreter, as
214 	 * {@link SandboxedAwk} does.
215 	 * </p>
216 	 *
217 	 * @return an unmodifiable map of extension class name to extension instance
218 	 */
219 	protected Map<String, JawkExtension> getExtensionInstances() {
220 		return extensionInstances;
221 	}
222 
223 	/**
224 	 * Returns the behavioral settings associated with this engine instance.
225 	 *
226 	 * @return the {@link AwkSettings} used by this instance, never {@code null}
227 	 */
228 	@SuppressFBWarnings("EI_EXPOSE_REP")
229 	public AwkSettings getSettings() {
230 		return settings;
231 	}
232 
233 	static Map<String, ExtensionFunction> createExtensionFunctionMap(Collection<? extends JawkExtension> extensions) {
234 		return createExtensionSetup(extensions).functions;
235 	}
236 
237 	static Map<String, JawkExtension> createExtensionInstanceMap(Collection<? extends JawkExtension> extensions) {
238 		return createExtensionSetup(extensions).instances;
239 	}
240 
241 	static Map<String, ExtensionFunction> createExtensionFunctionMap(JawkExtension... extensions) {
242 		return createExtensionFunctionMap(
243 				extensions == null ? Collections.<JawkExtension>emptyList() : Arrays.asList(extensions));
244 	}
245 
246 	static Map<String, JawkExtension> createExtensionInstanceMap(JawkExtension... extensions) {
247 		return createExtensionInstanceMap(
248 				extensions == null ? Collections.<JawkExtension>emptyList() : Arrays.asList(extensions));
249 	}
250 
251 	/*
252 	 * An explicit extension list is honored verbatim, including an empty one:
253 	 * a caller that passes no extensions gets none, which is the only way to
254 	 * reclaim names such as gensub or typeof. The default set is installed only
255 	 * by the no-argument constructors, which route through createDefault().
256 	 */
257 	private static ExtensionSetup createExtensionSetup(Collection<? extends JawkExtension> extensions) {
258 		if (extensions == null || extensions.isEmpty()) {
259 			return ExtensionSetup.EMPTY;
260 		}
261 		Map<String, ExtensionFunction> keywordMap = new LinkedHashMap<String, ExtensionFunction>();
262 		Map<String, JawkExtension> instanceMap = new LinkedHashMap<String, JawkExtension>();
263 		for (JawkExtension extension : extensions) {
264 			if (extension == null) {
265 				throw new IllegalArgumentException("Extension instance must not be null");
266 			}
267 			String className = extension.getClass().getName();
268 			JawkExtension previousInstance = instanceMap.putIfAbsent(className, extension);
269 			if (previousInstance != null) {
270 				throw new IllegalArgumentException(
271 						"Extension class '" + className + "' was provided multiple times");
272 			}
273 			for (Map.Entry<String, ExtensionFunction> entry : extension.getExtensionFunctions().entrySet()) {
274 				String keyword = entry.getKey();
275 				ExtensionFunction previous = keywordMap.putIfAbsent(keyword, entry.getValue());
276 				if (previous != null) {
277 					throw new IllegalArgumentException(
278 							"Keyword '" + keyword + "' already provided by another extension");
279 				}
280 			}
281 		}
282 		return new ExtensionSetup(
283 				Collections.unmodifiableMap(keywordMap),
284 				Collections.unmodifiableMap(instanceMap));
285 	}
286 
287 	private static final class ExtensionSetup {
288 
289 		private static final ExtensionSetup EMPTY = new ExtensionSetup(
290 				Collections.<String, ExtensionFunction>emptyMap(),
291 				Collections.<String, JawkExtension>emptyMap());
292 
293 		/*
294 		 * Extensions keep per-engine runtime state (VariableManager, JRT), so the
295 		 * default set must be a fresh instance per Awk engine, never a shared
296 		 * singleton: two engines sharing one GawkExtension would clobber each
297 		 * other's runtime bindings.
298 		 */
299 		private static ExtensionSetup createDefault() {
300 			return createExtensionSetup(Collections.singletonList(new GawkExtension()));
301 		}
302 
303 		private final Map<String, ExtensionFunction> functions;
304 		private final Map<String, JawkExtension> instances;
305 
306 		private ExtensionSetup(Map<String, ExtensionFunction> functionsParam,
307 				Map<String, JawkExtension> instancesParam) {
308 			this.functions = functionsParam;
309 			this.instances = instancesParam;
310 		}
311 	}
312 
313 	/**
314 	 * Returns the last parsed AST produced by the most recent program compilation.
315 	 *
316 	 * @return the last {@link AstNode}, or {@code null} if no compilation occurred
317 	 */
318 	@SuppressFBWarnings("EI_EXPOSE_REP")
319 	public AstNode getLastAst() {
320 		return lastAst;
321 	}
322 
323 	/**
324 	 * Final empty finalizer to mitigate finalizer attacks flagged by SpotBugs.
325 	 * This prevents subclasses from introducing a finalizer that could run on a
326 	 * partially constructed instance if a constructor throws.
327 	 */
328 	@SuppressWarnings("deprecation")
329 	@Override
330 	protected final void finalize() { /* no-op */ }
331 
332 	/**
333 	 * Compiles a full AWK program.
334 	 *
335 	 * @param script AWK program source
336 	 * @return compiled immutable program
337 	 * @throws IOException if compilation fails
338 	 */
339 	public AwkProgram compile(String script) throws IOException {
340 		return compile(script, false);
341 	}
342 
343 	/**
344 	 * Compiles a full AWK program.
345 	 *
346 	 * @param script AWK program source
347 	 * @return compiled immutable program
348 	 * @throws IOException if compilation fails
349 	 */
350 	public AwkProgram compile(Reader script) throws IOException {
351 		return compile(script, false);
352 	}
353 
354 	/**
355 	 * Creates a reusable runtime backed by one {@link AVM} instance.
356 	 *
357 	 * @return reusable AVM
358 	 */
359 	public AVM createAvm() {
360 		return createAvm(this.settings);
361 	}
362 
363 	/**
364 	 * Creates a reusable runtime backed by one {@link AVM} instance, optionally
365 	 * collecting runtime profiling statistics.
366 	 *
367 	 * @param profilingEnabled whether runtime profiling should be enabled
368 	 * @return reusable AVM
369 	 */
370 	public AVM createAvm(boolean profilingEnabled) {
371 		return createAvm(this.settings, profilingEnabled);
372 	}
373 
374 	/**
375 	 * Starts building a run request for a compiled AWK program.
376 	 * <p>
377 	 * Use the returned {@link AwkRunBuilder} to configure input, arguments,
378 	 * variables, and output, then call one of the terminal methods to execute.
379 	 * </p>
380 	 *
381 	 * <pre>{@code
382 	 * awk.script(program).input(stream).execute(mySink);
383 	 * String out = awk.script(program).input("hello").execute();
384 	 * }</pre>
385 	 *
386 	 * @param program compiled program to execute
387 	 * @return a builder for configuring and executing the run
388 	 */
389 	public AwkRunBuilder script(AwkProgram program) {
390 		return new AwkRunBuilder(Objects.requireNonNull(program, "program"));
391 	}
392 
393 	/**
394 	 * Starts building a run request from an AWK script string.
395 	 * <p>
396 	 * The script is compiled and executed when a terminal method is called.
397 	 * Additional scripts can be appended by calling {@link AwkRunBuilder#script(String)}
398 	 * on the returned builder.
399 	 * </p>
400 	 *
401 	 * <pre>{@code
402 	 * String result = awk.script("{ print toupper($0) }").input("hello").execute();
403 	 * }</pre>
404 	 *
405 	 * @param scriptText AWK program source
406 	 * @return a builder for configuring and executing the run
407 	 */
408 	public AwkRunBuilder script(String scriptText) {
409 		return new AwkRunBuilder().script(Objects.requireNonNull(scriptText, "script"));
410 	}
411 
412 	/**
413 	 * Evaluates a compiled expression using a fresh isolated runtime.
414 	 *
415 	 * @param expression compiled expression
416 	 * @return evaluated value
417 	 * @throws IOException if evaluation fails
418 	 */
419 	public Object eval(AwkExpression expression) throws IOException {
420 		AwkExpression compiledExpression = Objects.requireNonNull(expression, "expression");
421 		try (AVM activeEvalAvm = createAvm(settings)) {
422 			return activeEvalAvm.eval(compiledExpression, new SingleRecordInputSource(null));
423 		}
424 	}
425 
426 	/**
427 	 * Evaluates a compiled expression against one text record using a fresh
428 	 * isolated runtime.
429 	 *
430 	 * @param expression compiled expression
431 	 * @param input record exposed as {@code $0}
432 	 * @return evaluated value
433 	 * @throws IOException if evaluation fails
434 	 */
435 	public Object eval(AwkExpression expression, String input) throws IOException {
436 		AwkExpression compiledExpression = Objects.requireNonNull(expression, "expression");
437 		try (AVM activeEvalAvm = createAvm(settings)) {
438 			return activeEvalAvm.eval(compiledExpression, new SingleRecordInputSource(input));
439 		}
440 	}
441 
442 	/**
443 	 * Evaluates a compiled expression against one structured record source using a
444 	 * fresh isolated runtime.
445 	 *
446 	 * @param expression compiled expression
447 	 * @param source structured record source
448 	 * @return evaluated value
449 	 * @throws IOException if evaluation fails
450 	 */
451 	public Object eval(AwkExpression expression, InputSource source) throws IOException {
452 		AwkExpression compiledExpression = Objects.requireNonNull(expression, "expression");
453 		InputSource resolvedSource = Objects.requireNonNull(source, "source");
454 		try (AVM activeEvalAvm = createAvm(settings)) {
455 			return activeEvalAvm.eval(compiledExpression, resolvedSource);
456 		}
457 	}
458 
459 	/**
460 	 * Compiles the specified AWK script and returns an immutable AWK program.
461 	 *
462 	 * @param script AWK script to compile
463 	 * @param disableOptimizeParam {@code true} to skip tuple optimization
464 	 * @return compiled immutable program
465 	 * @throws IOException if an I/O error occurs during compilation
466 	 */
467 	AwkProgram compile(String script, boolean disableOptimizeParam) throws IOException {
468 		ScriptSource source = new ScriptSource(
469 				ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
470 				new StringReader(script));
471 		return compile(Collections.singletonList(source), disableOptimizeParam);
472 	}
473 
474 	/**
475 	 * Compiles the specified AWK script and returns an immutable AWK program.
476 	 *
477 	 * @param script AWK script to compile (as a {@link Reader})
478 	 * @param disableOptimizeParam {@code true} to skip tuple optimization
479 	 * @return compiled immutable program
480 	 * @throws IOException if an I/O error occurs during compilation
481 	 */
482 	AwkProgram compile(Reader script, boolean disableOptimizeParam) throws IOException {
483 		ScriptSource source = new ScriptSource(
484 				ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
485 				script);
486 		return compile(Collections.singletonList(source), disableOptimizeParam);
487 	}
488 
489 	/**
490 	 * Compiles a list of script sources into an immutable AWK program that can be
491 	 * executed by the {@link AVM} runtime.
492 	 *
493 	 * @param scripts script sources to compile
494 	 * @return compiled immutable program
495 	 * @throws IOException if an I/O error occurs while reading the
496 	 *         scripts
497 	 */
498 	public AwkProgram compile(List<ScriptSource> scripts)
499 			throws IOException {
500 		return compile(scripts, false);
501 	}
502 
503 	/**
504 	 * Compiles a list of script sources into an immutable AWK program that can be
505 	 * executed by the {@link AVM} runtime.
506 	 *
507 	 * @param scripts script sources to compile
508 	 * @param disableOptimizeParam {@code true} to skip tuple optimization
509 	 * @return compiled immutable program
510 	 * @throws IOException if an I/O error occurs while reading the
511 	 *         scripts
512 	 */
513 	public AwkProgram compile(List<ScriptSource> scripts, boolean disableOptimizeParam)
514 			throws IOException {
515 		return compileProgram(scripts, disableOptimizeParam, new AwkProgram());
516 	}
517 
518 	/**
519 	 * Compiles a full AWK program into the supplied tuple implementation.
520 	 *
521 	 * @param scripts script sources to compile
522 	 * @param disableOptimizeParam {@code true} to skip tuple optimization
523 	 * @param tuples destination tuple implementation
524 	 * @param <T> concrete tuple type to populate
525 	 * @return the populated compiled program
526 	 * @throws IOException if reading script sources fails
527 	 */
528 	protected final <T extends AwkProgram> T compileProgram(
529 			List<ScriptSource> scripts,
530 			boolean disableOptimizeParam,
531 			T tuples)
532 			throws IOException {
533 		lastAst = null;
534 		if (!scripts.isEmpty()) {
535 			// Parse all script sources into a single AST
536 			AwkParser parser = new AwkParser(
537 					this.extensionFunctions,
538 					settings.isPosix(),
539 					isSourceIncludeAllowed());
540 			AstNode ast = parser.parse(scripts);
541 			lastAst = ast;
542 			if (ast != null) {
543 				// Perform semantic checks twice to resolve forward references
544 				ast.semanticAnalysis();
545 				ast.semanticAnalysis();
546 				// Record the primary source description for runtime diagnostics
547 				tuples.setSourceDescription(scripts.get(0).getDescription());
548 				// Build tuples from the AST
549 				ast.populateTuples(tuples);
550 				// Assign addresses and prepare tuples for interpretation
551 				tuples.postProcess();
552 				if (!disableOptimizeParam) {
553 					tuples.optimize();
554 				}
555 				// Record global variable offset mappings for the interpreter
556 				parser.populateGlobalVariableNameToOffsetMappings(tuples);
557 			}
558 		}
559 		tuples.freezeMetadata();
560 
561 		return tuples;
562 	}
563 
564 	/**
565 	 * Returns whether scripts compiled by this engine may use {@code @include}.
566 	 *
567 	 * @return {@code true} for the standard engine
568 	 */
569 	protected boolean isSourceIncludeAllowed() {
570 		return true;
571 	}
572 
573 	/**
574 	 * Compile an expression to evaluate (not a full script).
575 	 *
576 	 * @param expression AWK expression to compile
577 	 * @return compiled immutable expression
578 	 * @throws IOException if anything goes wrong with the compilation
579 	 */
580 	public AwkExpression compileExpression(String expression) throws IOException {
581 		return compileExpression(expression, false);
582 	}
583 
584 	/**
585 	 * Compile an expression to evaluate (not a full script).
586 	 *
587 	 * @param expression AWK expression to compile
588 	 * @param disableOptimizeParam {@code true} to skip tuple optimization
589 	 * @return compiled immutable expression
590 	 * @throws IOException if anything goes wrong with the compilation
591 	 */
592 	public AwkExpression compileExpression(String expression, boolean disableOptimizeParam) throws IOException {
593 		return compileExpression(expression, disableOptimizeParam, new AwkExpression());
594 	}
595 
596 	/**
597 	 * Compiles an AWK expression into the supplied tuple implementation.
598 	 *
599 	 * @param expression expression source to compile
600 	 * @param disableOptimizeParam {@code true} to skip tuple optimization
601 	 * @param tuples destination tuple implementation
602 	 * @param <T> concrete tuple type to populate
603 	 * @return the populated compiled expression
604 	 * @throws IOException if reading the expression fails
605 	 */
606 	protected final <T extends AwkExpression> T compileExpression(
607 			String expression,
608 			boolean disableOptimizeParam,
609 			T tuples)
610 			throws IOException {
611 		// Create a ScriptSource
612 		ScriptSource expressionSource = new ScriptSource(
613 				ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
614 				new StringReader(expression));
615 
616 		// Parse the expression
617 		AwkParser parser = new AwkParser(this.extensionFunctions, settings.isPosix());
618 		AstNode ast = parser.parseExpression(expressionSource);
619 
620 		// Attempt to traverse the syntax tree and build
621 		// the intermediate code
622 		if (ast != null) {
623 			// 1st pass to tie actual parameters to back-referenced formal parameters
624 			ast.semanticAnalysis();
625 			// 2nd pass to tie actual parameters to forward-referenced formal parameters
626 			ast.semanticAnalysis();
627 			// build tuples
628 			ast.populateTuples(tuples);
629 			// Calls touch(...) per Tuple so that addresses can be normalized/assigned/allocated
630 			tuples.postProcess();
631 			if (!disableOptimizeParam) {
632 				tuples.optimize();
633 			}
634 			// record global_var -> offset mapping into the tuples
635 			// so that the interpreter can assign variables
636 			parser.populateGlobalVariableNameToOffsetMappings(tuples);
637 		}
638 		tuples.freezeMetadata();
639 
640 		return tuples;
641 	}
642 
643 	/**
644 	 * Evaluates the specified AWK expression (not a full script, just an expression)
645 	 * and returns the value of this expression.
646 	 *
647 	 * @param expression Expression to evaluate (e.g. <code>2+3</code>)
648 	 * @return the value of the specified expression
649 	 * @throws IOException if anything goes wrong with the evaluation
650 	 */
651 	public Object eval(String expression) throws IOException {
652 		return eval(compileExpression(expression));
653 	}
654 
655 	/**
656 	 * Evaluates the specified AWK expression (not a full script, just an expression)
657 	 * and returns the value of this expression.
658 	 *
659 	 * @param expression Expression to evaluate (e.g. <code>2+3</code> or <code>$2 "-" $3</code>
660 	 * @param input Optional text input (that will be available as $0, and tokenized as $1, $2, etc.)
661 	 * @return the value of the specified expression
662 	 * @throws IOException if anything goes wrong with the evaluation
663 	 */
664 	public Object eval(String expression, String input) throws IOException {
665 		return eval(compileExpression(expression), input);
666 	}
667 
668 	/**
669 	 * Evaluates the specified AWK expression using a structured {@link InputSource}
670 	 * to populate {@code $0}, {@code $1}, etc.
671 	 *
672 	 * @param expression Expression to evaluate (e.g. {@code $2 "-" $3})
673 	 * @param source structured input source providing the current record
674 	 * @return the value of the specified expression
675 	 * @throws IOException if anything goes wrong with the evaluation
676 	 */
677 	public Object eval(String expression, InputSource source) throws IOException {
678 		return eval(compileExpression(expression), source);
679 	}
680 
681 	/**
682 	 * Prepares one text record for repeated expression evaluation and returns the
683 	 * mutable {@link AVM} that will execute those expressions.
684 	 * <p>
685 	 * The returned {@link AVM} is created using the current runtime
686 	 * configuration of this {@link Awk} instance and binds the provided record
687 	 * once. Later calls to
688 	 * {@link AVM#eval(AwkExpression)} reuse the same AVM state without resetting it
689 	 * between expressions, so mutations intentionally leak across evaluations.
690 	 * This is the high-level convenience wrapper around direct
691 	 * {@link AVM#prepareForEval(String)} and {@link AVM#eval(AwkExpression)} usage.
692 	 * </p>
693 	 *
694 	 * @param input non-null text record to expose as {@code $0}
695 	 *        Call {@link AVM#close()} when you are done with the returned interpreter.
696 	 * @return prepared AVM ready for repeated {@link AVM#eval(AwkExpression)} calls
697 	 * @throws IOException if binding the record fails
698 	 */
699 	public AVM prepareEval(String input) throws IOException {
700 		String resolvedInput = Objects.requireNonNull(input, "input");
701 		AVM evalAvm = createAvm(settings);
702 		try {
703 			evalAvm.prepareForEval(resolvedInput);
704 			return evalAvm;
705 		} catch (IOException | RuntimeException e) {
706 			try {
707 				evalAvm.close();
708 			} catch (IOException closeException) {
709 				e.addSuppressed(closeException);
710 			}
711 			throw e;
712 		}
713 	}
714 
715 	/**
716 	 * Prepares the first available record from a structured {@link InputSource}
717 	 * for repeated expression evaluation and returns the mutable {@link AVM}
718 	 * that will execute those expressions.
719 	 * <p>
720 	 * The returned AVM remains attached to the provided source, so later
721 	 * {@code getline} operations and repeated {@link AVM#prepareForEval(InputSource)}
722 	 * calls continue from that source's current position. Later
723 	 * {@link AVM#eval(AwkExpression)} calls reuse the same AVM state without
724 	 * resetting it between expressions, so mutations intentionally leak across
725 	 * evaluations. Close the returned AVM when you are done with it to release
726 	 * any bound input or runtime I/O resources.
727 	 * </p>
728 	 *
729 	 * @param source structured source providing the record to bind
730 	 * @return prepared AVM ready for repeated {@link AVM#eval(AwkExpression)} calls
731 	 * @throws IOException if reading the record fails or the source is exhausted
732 	 */
733 	public AVM prepareEval(InputSource source) throws IOException {
734 		InputSource resolvedSource = Objects.requireNonNull(source, "source");
735 		AVM evalAvm = createAvm(settings);
736 		try {
737 			if (!evalAvm.prepareForEval(resolvedSource)) {
738 				throw new IOException("No record available from source.");
739 			}
740 			return evalAvm;
741 		} catch (IOException | RuntimeException e) {
742 			try {
743 				evalAvm.close();
744 			} catch (IOException closeException) {
745 				e.addSuppressed(closeException);
746 			}
747 			throw e;
748 		}
749 	}
750 
751 	/**
752 	 * Creates an {@link AVM} using the provided runtime settings.
753 	 *
754 	 * @param settingsParam runtime settings to apply
755 	 * @return reusable AVM
756 	 */
757 	protected AVM createAvm(AwkSettings settingsParam) {
758 		return createAvm(settingsParam, false);
759 	}
760 
761 	/**
762 	 * Creates an {@link AVM} using the provided runtime settings and profiling
763 	 * mode.
764 	 *
765 	 * @param settingsParam runtime settings to apply
766 	 * @param profilingEnabled whether runtime profiling should be enabled
767 	 * @return reusable AVM
768 	 */
769 	protected AVM createAvm(AwkSettings settingsParam, boolean profilingEnabled) {
770 		return new AVM(settingsParam, this.extensionInstances, profilingEnabled);
771 	}
772 
773 	/**
774 	 * Converts a text input into an {@link InputStream} using UTF-8 encoding.
775 	 */
776 	private static InputStream toInputStream(String input) {
777 		if (input == null) {
778 			return new ByteArrayInputStream(new byte[0]);
779 		}
780 		return new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
781 	}
782 
783 	/**
784 	 * Fluent builder for configuring and executing an AWK script or program.
785 	 * <p>
786 	 * Obtain an instance through {@link Awk#script(String)} or
787 	 * {@link Awk#script(AwkProgram)}, configure input, arguments, and
788 	 * variables, then call one of the terminal methods to execute.
789 	 * </p>
790 	 *
791 	 * <pre>{@code
792 	 * // Execute and capture printed output as a String
793 	 * String result = awk.script("{ print toupper($0) }").input("hello").execute();
794 	 *
795 	 * // Execute to a specific stream
796 	 * awk.script(program).input(stream).execute(outputStream);
797 	 *
798 	 * // Execute with a custom sink
799 	 * awk.script("{ print $1 }").input(source).execute(mySink);
800 	 *
801 	 * // Execute to an appendable
802 	 * awk.script("{ print $1 }").input(source).execute(appendable);
803 	 * }</pre>
804 	 */
805 	public final class AwkRunBuilder {
806 
807 		private AwkProgram compiledProgram;
808 		private List<String> scripts;
809 		private InputStream inputStream;
810 		private InputSource inputSource;
811 		private List<String> arguments;
812 		private Map<String, Object> variableOverrides;
813 		private PrintStream errorStream;
814 
815 		AwkRunBuilder() {}
816 
817 		AwkRunBuilder(AwkProgram program) {
818 			this.compiledProgram = program;
819 		}
820 
821 		/**
822 		 * Appends an additional AWK script to compile and execute.
823 		 * Multiple scripts are concatenated, like multiple {@code -f} options
824 		 * in the CLI.
825 		 *
826 		 * @param scriptText AWK program source
827 		 * @return this builder
828 		 * @throws IllegalStateException if a precompiled program was already set
829 		 */
830 		public AwkRunBuilder script(String scriptText) {
831 			if (compiledProgram != null) {
832 				throw new IllegalStateException("Cannot add scripts when a precompiled program is set");
833 			}
834 			if (scripts == null) {
835 				scripts = new ArrayList<String>();
836 			}
837 			scripts.add(Objects.requireNonNull(scriptText, "script"));
838 			return this;
839 		}
840 
841 		/**
842 		 * Sets the text input to process.
843 		 *
844 		 * @param input text input (encoded as UTF-8 internally)
845 		 * @return this builder
846 		 */
847 		public AwkRunBuilder input(String input) {
848 			this.inputStream = toInputStream(input);
849 			return this;
850 		}
851 
852 		/**
853 		 * Sets the byte-stream input to process.
854 		 *
855 		 * @param input byte stream, or {@code null} for no input
856 		 * @return this builder
857 		 */
858 		public AwkRunBuilder input(InputStream input) {
859 			this.inputStream = input;
860 			return this;
861 		}
862 
863 		/**
864 		 * Sets a structured {@link InputSource} to process.
865 		 *
866 		 * @param source structured record source
867 		 * @return this builder
868 		 */
869 		public AwkRunBuilder input(InputSource source) {
870 			this.inputSource = source;
871 			return this;
872 		}
873 
874 		/**
875 		 * Sets runtime arguments visible through {@code ARGC}/{@code ARGV}.
876 		 *
877 		 * @param args runtime arguments
878 		 * @return this builder
879 		 */
880 		@SuppressFBWarnings("EI_EXPOSE_REP2")
881 		public AwkRunBuilder arguments(List<String> args) {
882 			this.arguments = args;
883 			return this;
884 		}
885 
886 		/**
887 		 * Sets runtime arguments visible through {@code ARGC}/{@code ARGV}.
888 		 *
889 		 * @param args runtime arguments
890 		 * @return this builder
891 		 */
892 		public AwkRunBuilder arguments(String... args) {
893 			this.arguments = Arrays.asList(args);
894 			return this;
895 		}
896 
897 		/**
898 		 * Adds a single runtime argument visible through {@code ARGC}/{@code ARGV}.
899 		 *
900 		 * @param arg runtime argument
901 		 * @return this builder
902 		 */
903 		public AwkRunBuilder argument(String arg) {
904 			if (this.arguments == null) {
905 				this.arguments = new ArrayList<String>();
906 			}
907 			this.arguments.add(Objects.requireNonNull(arg, "arg"));
908 			return this;
909 		}
910 
911 		/**
912 		 * Sets the stream used for the stderr output of spawned processes
913 		 * (e.g.&nbsp;{@code system("...")}).
914 		 * <p>
915 		 * When not set, process stderr is merged into the main output sink.
916 		 * The CLI sets this explicitly to {@code System.err} so that command
917 		 * errors appear on the console rather than being mixed with normal output.
918 		 *
919 		 * @param stream stream to receive process stderr
920 		 * @return this builder
921 		 */
922 		public AwkRunBuilder errorStream(PrintStream stream) {
923 			this.errorStream = Objects.requireNonNull(stream, "errorStream");
924 			return this;
925 		}
926 
927 		/**
928 		 * Sets per-call variable overrides applied on top of the settings-level
929 		 * variables.
930 		 *
931 		 * @param overrides variable assignments (may be {@code null})
932 		 * @return this builder
933 		 */
934 		@SuppressFBWarnings("EI_EXPOSE_REP2")
935 		public AwkRunBuilder variables(Map<String, Object> overrides) {
936 			this.variableOverrides = overrides;
937 			return this;
938 		}
939 
940 		/**
941 		 * Sets a single per-call variable override.
942 		 *
943 		 * @param name variable name
944 		 * @param value variable value
945 		 * @return this builder
946 		 */
947 		public AwkRunBuilder variable(String name, Object value) {
948 			if (this.variableOverrides == null) {
949 				this.variableOverrides = new LinkedHashMap<String, Object>();
950 			}
951 			this.variableOverrides
952 					.put(
953 							Objects.requireNonNull(name, "name"),
954 							value);
955 			return this;
956 		}
957 
958 		/**
959 		 * Executes the script and returns the printed output as a {@link String}.
960 		 *
961 		 * @return printed output
962 		 * @throws IOException if compilation or execution fails
963 		 * @throws ExitException if the script terminates with a non-zero exit code
964 		 */
965 		public String execute() throws IOException, ExitException {
966 			StringBuilder output = new StringBuilder();
967 			doExecute(new AppendableAwkSink(output, settings.getLocale()));
968 			return output.toString();
969 		}
970 
971 		/**
972 		 * Executes the script, sending output to the specified {@link AwkSink}.
973 		 *
974 		 * @param sink output sink
975 		 * @throws IOException if compilation or execution fails
976 		 * @throws ExitException if the script terminates with a non-zero exit code
977 		 */
978 		public void execute(AwkSink sink) throws IOException, ExitException {
979 			doExecute(Objects.requireNonNull(sink, "sink"));
980 		}
981 
982 		/**
983 		 * Executes the script, sending output to the specified {@link PrintStream}.
984 		 *
985 		 * @param out print stream (e.g. {@code System.out})
986 		 * @throws IOException if compilation or execution fails
987 		 * @throws ExitException if the script terminates with a non-zero exit code
988 		 */
989 		public void execute(PrintStream out) throws IOException, ExitException {
990 			Objects.requireNonNull(out, "out");
991 			doExecute(new OutputStreamAwkSink(out, settings.getLocale()));
992 		}
993 
994 		/**
995 		 * Executes the script, sending output to the specified {@link OutputStream}.
996 		 *
997 		 * @param out output stream
998 		 * @throws IOException if compilation or execution fails
999 		 * @throws ExitException if the script terminates with a non-zero exit code
1000 		 */
1001 		public void execute(OutputStream out) throws IOException, ExitException {
1002 			doExecute(new OutputStreamAwkSink(toPrintStream(out), settings.getLocale()));
1003 		}
1004 
1005 		/**
1006 		 * Executes the script, sending output to the specified {@link Appendable}
1007 		 * (such as {@link StringBuilder} or {@link java.io.StringWriter}).
1008 		 *
1009 		 * @param appendable output destination
1010 		 * @throws IOException if compilation or execution fails
1011 		 * @throws ExitException if the script terminates with a non-zero exit code
1012 		 */
1013 		public void execute(Appendable appendable) throws IOException, ExitException {
1014 			doExecute(
1015 					new AppendableAwkSink(
1016 							Objects.requireNonNull(appendable, "appendable"),
1017 							settings.getLocale()));
1018 		}
1019 
1020 		private void doExecute(AwkSink sink) throws IOException, ExitException {
1021 			AwkProgram program = resolveProgram();
1022 			List<String> resolvedArguments = arguments == null ? Collections.<String>emptyList() : arguments;
1023 			try (AVM avm = createAvm(settings)) {
1024 				avm.setAwkSink(sink);
1025 				if (errorStream != null) {
1026 					avm.setErrorStream(errorStream);
1027 					avm.setWarningStream(errorStream);
1028 				} else {
1029 					// process stderr keeps its historical sink fallback, but
1030 					// warnings stay on System.err so they can never leak into
1031 					// the script output a host captures
1032 					avm.setErrorStream(sink.getPrintStream());
1033 				}
1034 				try {
1035 					InputSource resolvedSource;
1036 					if (inputSource != null) {
1037 						resolvedSource = inputSource;
1038 					} else {
1039 						InputStream in = inputStream != null ? inputStream : new ByteArrayInputStream(new byte[0]);
1040 						resolvedSource = new StreamInputSource(in, avm, avm.getJrt());
1041 					}
1042 					avm.execute(program, resolvedSource, resolvedArguments, variableOverrides);
1043 				} catch (ExitException e) {
1044 					if (e.getCode() != 0) {
1045 						throw e;
1046 					}
1047 				} finally {
1048 					sink.flush();
1049 				}
1050 			}
1051 		}
1052 
1053 		private AwkProgram resolveProgram() throws IOException {
1054 			if (compiledProgram != null) {
1055 				return compiledProgram;
1056 			}
1057 			if (scripts == null || scripts.isEmpty()) {
1058 				throw new IllegalStateException("No script or program specified");
1059 			}
1060 			if (scripts.size() == 1) {
1061 				return compile(scripts.get(0));
1062 			}
1063 			List<ScriptSource> sources = new ArrayList<ScriptSource>(scripts.size());
1064 			for (int i = 0; i < scripts.size(); i++) {
1065 				sources
1066 						.add(
1067 								new ScriptSource(
1068 										ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
1069 										new StringReader(scripts.get(i))));
1070 			}
1071 			return compile(sources);
1072 		}
1073 	}
1074 
1075 	private static PrintStream toPrintStream(OutputStream out) {
1076 		Objects.requireNonNull(out, "outputStream");
1077 		if (out instanceof PrintStream) {
1078 			return (PrintStream) out;
1079 		}
1080 		try {
1081 			return new PrintStream(out, false, "UTF-8");
1082 		} catch (java.io.UnsupportedEncodingException e) {
1083 			throw new IllegalStateException(e);
1084 		}
1085 	}
1086 
1087 	/**
1088 	 * Lists metadata for the {@link JawkExtension} implementations discovered on
1089 	 * the class path.
1090 	 *
1091 	 * @return list of discovered extension descriptors
1092 	 */
1093 	public static Map<String, JawkExtension> listAvailableExtensions() {
1094 		return ExtensionRegistry.listExtensions();
1095 	}
1096 
1097 	private static final class SingleRecordInputSource implements InputSource {
1098 
1099 		private final String record;
1100 
1101 		private boolean consumed;
1102 
1103 		private SingleRecordInputSource(String record) {
1104 			this.record = record;
1105 		}
1106 
1107 		@Override
1108 		public boolean nextRecord() {
1109 			if (consumed || record == null) {
1110 				return false;
1111 			}
1112 			consumed = true;
1113 			return true;
1114 		}
1115 
1116 		@Override
1117 		public String getRecordText() {
1118 			return consumed ? record : null;
1119 		}
1120 
1121 		@Override
1122 		public List<String> getFields() {
1123 			return null;
1124 		}
1125 
1126 		@Override
1127 		public boolean isFromFilenameList() {
1128 			return false;
1129 		}
1130 	}
1131 
1132 }