View Javadoc
1   package org.metricshub.jawk;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * Jawk
6    * ჻჻჻჻჻჻
7    * Copyright (C) 2006 - 2025 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.ByteArrayOutputStream;
27  import java.io.File;
28  import java.io.FileInputStream;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.io.OutputStream;
32  import java.io.PrintStream;
33  import java.io.Reader;
34  import java.io.StringReader;
35  import java.nio.charset.StandardCharsets;
36  import java.util.Arrays;
37  import java.util.Collection;
38  import java.util.Collections;
39  import java.util.LinkedHashMap;
40  import java.util.List;
41  import java.util.Map;
42  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
43  import org.metricshub.jawk.backend.AVM;
44  import org.metricshub.jawk.ext.ExtensionFunction;
45  import org.metricshub.jawk.ext.ExtensionRegistry;
46  import org.metricshub.jawk.ext.JawkExtension;
47  import org.metricshub.jawk.frontend.AwkParser;
48  import org.metricshub.jawk.frontend.AstNode;
49  import org.metricshub.jawk.intermediate.AwkTuples;
50  import org.metricshub.jawk.util.AwkSettings;
51  import org.metricshub.jawk.util.ScriptSource;
52  
53  /**
54   * Entry point into the parsing, analysis, and execution
55   * of a Jawk script.
56   * This entry point is used both when Jawk is executed as a library and when
57   * invoked from the command line.
58   * <p>
59   * The overall process to execute a Jawk script is as follows:
60   * <ul>
61   * <li>Parse the Jawk script, producing an abstract syntax tree.
62   * <li>Traverse the abstract syntax tree, producing a list of
63   * instruction tuples for the interpreter.
64   * <li>Traverse the list of tuples, providing a runtime which
65   * ultimately executes the Jawk script, <strong>or</strong>
66   * Command-line parameters dictate which action is to take place.
67   * </ul>
68   * Two additional semantic checks on the syntax tree are employed
69   * (both to resolve function calls for defined functions).
70   * As a result, the syntax tree is traversed three times.
71   * And the number of times tuples are traversed is depends
72   * on whether interpretation or compilation takes place.
73   * <p>
74   * The engine does not enable any extensions automatically. Extensions can be
75   * provided programmatically via the {@link Awk#Awk(Collection)} constructors or
76   * via the command line when using the CLI entry point.
77   *
78   * @see org.metricshub.jawk.backend.AVM
79   * @author Danny Daglas
80   */
81  public class Awk {
82  
83  	private final Map<String, ExtensionFunction> extensionFunctions;
84  
85  	private final Map<String, JawkExtension> extensionInstances;
86  
87  	/**
88  	 * The last parsed {@link AstNode} produced during compilation.
89  	 */
90  	private AstNode lastAst;
91  
92  	/**
93  	 * Create a new instance of Awk without extensions
94  	 */
95  	public Awk() {
96  		this(ExtensionSetup.EMPTY);
97  	}
98  
99  	/**
100 	 * Create a new instance of Awk with the specified extension instances.
101 	 *
102 	 * @param extensions extension instances implementing {@link JawkExtension}
103 	 */
104 	public Awk(Collection<? extends JawkExtension> extensions) {
105 		this(createExtensionSetup(extensions));
106 	}
107 
108 	/**
109 	 * Create a new instance of Awk with the specified extension instances.
110 	 *
111 	 * @param extensions extension instances implementing {@link JawkExtension}
112 	 */
113 	@SafeVarargs
114 	public Awk(JawkExtension... extensions) {
115 		this(createExtensionSetup(Arrays.asList(extensions)));
116 	}
117 
118 	protected Awk(ExtensionSetup setup) {
119 		this.extensionFunctions = setup.functions;
120 		this.extensionInstances = setup.instances;
121 	}
122 
123 	protected Map<String, ExtensionFunction> getExtensionFunctions() {
124 		return extensionFunctions;
125 	}
126 
127 	protected Map<String, JawkExtension> getExtensionInstances() {
128 		return extensionInstances;
129 	}
130 
131 	static Map<String, ExtensionFunction> createExtensionFunctionMap(Collection<? extends JawkExtension> extensions) {
132 		return createExtensionSetup(extensions).functions;
133 	}
134 
135 	static Map<String, JawkExtension> createExtensionInstanceMap(Collection<? extends JawkExtension> extensions) {
136 		return createExtensionSetup(extensions).instances;
137 	}
138 
139 	static Map<String, ExtensionFunction> createExtensionFunctionMap(JawkExtension... extensions) {
140 		if (extensions == null || extensions.length == 0) {
141 			return ExtensionSetup.EMPTY.functions;
142 		}
143 		return createExtensionFunctionMap(Arrays.asList(extensions));
144 	}
145 
146 	static Map<String, JawkExtension> createExtensionInstanceMap(JawkExtension... extensions) {
147 		if (extensions == null || extensions.length == 0) {
148 			return ExtensionSetup.EMPTY.instances;
149 		}
150 		return createExtensionInstanceMap(Arrays.asList(extensions));
151 	}
152 
153 	private static ExtensionSetup createExtensionSetup(Collection<? extends JawkExtension> extensions) {
154 		if (extensions == null || extensions.isEmpty()) {
155 			return ExtensionSetup.EMPTY;
156 		}
157 		Map<String, ExtensionFunction> keywordMap = new LinkedHashMap<String, ExtensionFunction>();
158 		Map<String, JawkExtension> instanceMap = new LinkedHashMap<String, JawkExtension>();
159 		for (JawkExtension extension : extensions) {
160 			if (extension == null) {
161 				throw new IllegalArgumentException("Extension instance must not be null");
162 			}
163 			String className = extension.getClass().getName();
164 			JawkExtension previousInstance = instanceMap.putIfAbsent(className, extension);
165 			if (previousInstance != null) {
166 				throw new IllegalArgumentException(
167 						"Extension class '" + className + "' was provided multiple times");
168 			}
169 			for (Map.Entry<String, ExtensionFunction> entry : extension.getExtensionFunctions().entrySet()) {
170 				String keyword = entry.getKey();
171 				ExtensionFunction previous = keywordMap.putIfAbsent(keyword, entry.getValue());
172 				if (previous != null) {
173 					throw new IllegalArgumentException(
174 							"Keyword '" + keyword + "' already provided by another extension");
175 				}
176 			}
177 		}
178 		return new ExtensionSetup(
179 				Collections.unmodifiableMap(keywordMap),
180 				Collections.unmodifiableMap(instanceMap));
181 	}
182 
183 	private static final class ExtensionSetup {
184 
185 		private static final ExtensionSetup EMPTY = new ExtensionSetup(
186 				Collections.<String, ExtensionFunction>emptyMap(),
187 				Collections.<String, JawkExtension>emptyMap());
188 
189 		private final Map<String, ExtensionFunction> functions;
190 		private final Map<String, JawkExtension> instances;
191 
192 		private ExtensionSetup(Map<String, ExtensionFunction> functionsParam,
193 				Map<String, JawkExtension> instancesParam) {
194 			this.functions = functionsParam;
195 			this.instances = instancesParam;
196 		}
197 	}
198 
199 	/**
200 	 * Returns the last parsed AST produced by {@link #compile(List)}.
201 	 *
202 	 * @return the last {@link AstNode}, or {@code null} if no compilation occurred
203 	 */
204 	@SuppressFBWarnings("EI_EXPOSE_REP")
205 	public AstNode getLastAst() {
206 		return lastAst;
207 	}
208 
209 	/**
210 	 * <p>
211 	 * invoke.
212 	 * </p>
213 	 *
214 	 * @param settings This tells AWK what to do
215 	 *        (where to get input from, where to write it to, in what mode to run,
216 	 *        ...)
217 	 * @throws java.io.IOException upon an IO error.
218 	 * @throws java.lang.ClassNotFoundException if intermediate code is specified
219 	 *         but deserialization fails to load in the JVM
220 	 * @throws org.metricshub.jawk.ExitException if interpretation is requested,
221 	 *         and a specific exit code is requested.
222 	 */
223 	public void invoke(String script, AwkSettings settings)
224 			throws IOException,
225 			ClassNotFoundException,
226 			ExitException {
227 		invoke(
228 				new ScriptSource(
229 						ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
230 						new StringReader(script)),
231 				settings);
232 	}
233 
234 	/**
235 	 * Compiles and invokes a single {@link ScriptSource} using the provided
236 	 * {@link AwkSettings}. This is a convenience overload for callers who have
237 	 * a single script to execute.
238 	 *
239 	 * @param script script source to compile and run
240 	 * @param settings runtime settings such as input and output streams
241 	 * @throws IOException if an I/O error occurs during compilation or
242 	 *         execution
243 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
244 	 * @throws ExitException if the script terminates with a non-zero exit
245 	 *         code
246 	 */
247 	public void invoke(ScriptSource script, AwkSettings settings)
248 			throws IOException,
249 			ClassNotFoundException,
250 			ExitException {
251 		// Delegate to the List-based overload for the actual work
252 		invoke(Collections.singletonList(script), settings);
253 	}
254 
255 	/**
256 	 * Compiles and invokes the specified list of {@link ScriptSource}s using the
257 	 * provided {@link AwkSettings}.
258 	 *
259 	 * @param scripts list of script sources to compile and run
260 	 * @param settings runtime settings such as input and output streams
261 	 * @throws IOException if an I/O error occurs during compilation or
262 	 *         execution
263 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
264 	 * @throws ExitException if the script terminates with a non-zero exit
265 	 *         code
266 	 */
267 	public void invoke(List<ScriptSource> scripts, AwkSettings settings)
268 			throws IOException,
269 			ClassNotFoundException,
270 			ExitException {
271 		// Compile the scripts into tuples then execute them
272 		AwkTuples tuples = compile(scripts);
273 		invoke(tuples, settings);
274 	}
275 
276 	/**
277 	 * Interprets the specified precompiled {@link AwkTuples} using the provided
278 	 * {@link AwkSettings}.
279 	 *
280 	 * @param tuples precompiled tuples to interpret
281 	 * @param settings runtime settings
282 	 * @throws IOException upon an IO error
283 	 * @throws ExitException if interpretation is requested, and a specific exit
284 	 *         code is requested
285 	 */
286 	public void invoke(AwkTuples tuples, AwkSettings settings)
287 			throws IOException,
288 			ExitException {
289 		if (tuples == null) {
290 			return;
291 		}
292 
293 		AVM avm = null;
294 		try {
295 // interpret!
296 			avm = createAvm(settings);
297 			avm.interpret(tuples);
298 		} finally {
299 			if (avm != null) {
300 				avm.waitForIO();
301 			}
302 		}
303 	}
304 
305 	/**
306 	 * Executes the specified AWK script against the given input and returns the
307 	 * printed output as a {@link String}.
308 	 *
309 	 * @param script AWK script to execute
310 	 * @param input text to process
311 	 * @return result of the execution as a String
312 	 * @throws IOException if an I/O error occurs
313 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
314 	 * @throws ExitException if the script terminates with a non-zero exit code
315 	 */
316 	public String run(String script, String input)
317 			throws IOException,
318 			ClassNotFoundException,
319 			ExitException {
320 		ByteArrayOutputStream out = new ByteArrayOutputStream();
321 		run(script, input, out);
322 		return out.toString(StandardCharsets.UTF_8.name());
323 	}
324 
325 	/**
326 	 * Executes the specified AWK script against the given input and writes the
327 	 * result to the provided {@link OutputStream}.
328 	 *
329 	 * @param script AWK script to execute
330 	 * @param input text to process
331 	 * @param output destination for the printed output
332 	 * @throws IOException if an I/O error occurs
333 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
334 	 * @throws ExitException if the script terminates with a non-zero exit code
335 	 */
336 	public void run(String script, String input, OutputStream output)
337 			throws IOException,
338 			ClassNotFoundException,
339 			ExitException {
340 		run(new StringReader(script), toInputStream(input), output, true);
341 	}
342 
343 	/**
344 	 * Executes the specified AWK script against the given input and returns the
345 	 * printed output as a {@link String}.
346 	 *
347 	 * @param script AWK script to execute (as a {@link Reader})
348 	 * @param input text to process
349 	 * @return result of the execution as a String
350 	 * @throws IOException if an I/O error occurs
351 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
352 	 * @throws ExitException if the script terminates with a non-zero exit code
353 	 */
354 	public String run(Reader script, String input)
355 			throws IOException,
356 			ClassNotFoundException,
357 			ExitException {
358 		ByteArrayOutputStream out = new ByteArrayOutputStream();
359 		run(script, input, out);
360 		return out.toString(StandardCharsets.UTF_8.name());
361 	}
362 
363 	/**
364 	 * Executes the specified AWK script against the given input and writes the
365 	 * result to the provided {@link OutputStream}.
366 	 *
367 	 * @param script AWK script to execute (as a {@link Reader})
368 	 * @param input text to process
369 	 * @param output destination for the printed output
370 	 * @throws IOException if an I/O error occurs
371 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
372 	 * @throws ExitException if the script terminates with a non-zero exit code
373 	 */
374 	public void run(Reader script, String input, OutputStream output)
375 			throws IOException,
376 			ClassNotFoundException,
377 			ExitException {
378 		run(script, toInputStream(input), output, true);
379 	}
380 
381 	/**
382 	 * Executes the specified AWK script against the given input and returns the
383 	 * printed output as a {@link String}.
384 	 *
385 	 * @param script AWK script to execute
386 	 * @param input text reader to process
387 	 * @return result of the execution as a String
388 	 * @throws IOException if an I/O error occurs
389 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
390 	 * @throws ExitException if the script terminates with a non-zero exit code
391 	 */
392 	public String run(String script, Reader input)
393 			throws IOException,
394 			ClassNotFoundException,
395 			ExitException {
396 		ByteArrayOutputStream out = new ByteArrayOutputStream();
397 		run(script, input, out);
398 		return out.toString(StandardCharsets.UTF_8.name());
399 	}
400 
401 	/**
402 	 * Executes the specified AWK script against the given input and writes the
403 	 * result to the provided {@link OutputStream}.
404 	 *
405 	 * @param script AWK script to execute
406 	 * @param input text reader to process
407 	 * @param output destination for the printed output
408 	 * @throws IOException if an I/O error occurs
409 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
410 	 * @throws ExitException if the script terminates with a non-zero exit code
411 	 */
412 	public void run(String script, Reader input, OutputStream output)
413 			throws IOException,
414 			ClassNotFoundException,
415 			ExitException {
416 		run(new StringReader(script), toInputStream(input), output, true);
417 	}
418 
419 	/**
420 	 * Executes the specified AWK script against the given input and returns the
421 	 * printed output as a {@link String}.
422 	 *
423 	 * @param script AWK script to execute (as a {@link Reader})
424 	 * @param input text reader to process
425 	 * @return result of the execution as a String
426 	 * @throws IOException if an I/O error occurs
427 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
428 	 * @throws ExitException if the script terminates with a non-zero exit code
429 	 */
430 	public String run(Reader script, Reader input)
431 			throws IOException,
432 			ClassNotFoundException,
433 			ExitException {
434 		ByteArrayOutputStream out = new ByteArrayOutputStream();
435 		run(script, input, out);
436 		return out.toString(StandardCharsets.UTF_8.name());
437 	}
438 
439 	/**
440 	 * Executes the specified AWK script against the given input and writes the
441 	 * result to the provided {@link OutputStream}.
442 	 *
443 	 * @param script AWK script to execute (as a {@link Reader})
444 	 * @param input text reader to process
445 	 * @param output destination for the printed output
446 	 * @throws IOException if an I/O error occurs
447 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
448 	 * @throws ExitException if the script terminates with a non-zero exit code
449 	 */
450 	public void run(Reader script, Reader input, OutputStream output)
451 			throws IOException,
452 			ClassNotFoundException,
453 			ExitException {
454 		run(script, toInputStream(input), output, true);
455 	}
456 
457 	/**
458 	 * Executes the specified AWK script against the given input file and returns
459 	 * the printed output as a {@link String}.
460 	 *
461 	 * @param script AWK script to execute
462 	 * @param input file containing text to process
463 	 * @return result of the execution as a String
464 	 * @throws IOException if an I/O error occurs
465 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
466 	 * @throws ExitException if the script terminates with a non-zero exit code
467 	 */
468 	public String run(String script, File input)
469 			throws IOException,
470 			ClassNotFoundException,
471 			ExitException {
472 		try (InputStream in = new FileInputStream(input)) {
473 			return run(script, in);
474 		}
475 	}
476 
477 	/**
478 	 * Executes the specified AWK script against the given input file and writes
479 	 * the printed output to the provided {@link OutputStream}.
480 	 *
481 	 * @param script AWK script to execute
482 	 * @param input file containing text to process
483 	 * @param output destination for the printed output
484 	 * @throws IOException if an I/O error occurs
485 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
486 	 * @throws ExitException if the script terminates with a non-zero exit code
487 	 */
488 	public void run(String script, File input, OutputStream output)
489 			throws IOException,
490 			ClassNotFoundException,
491 			ExitException {
492 		try (InputStream in = new FileInputStream(input)) {
493 			run(script, in, output);
494 		}
495 	}
496 
497 	/**
498 	 * Executes the specified AWK script against the given input file and returns
499 	 * the printed output as a {@link String}.
500 	 *
501 	 * @param script AWK script to execute (as a {@link Reader})
502 	 * @param input file containing text to process
503 	 * @return result of the execution as a String
504 	 * @throws IOException if an I/O error occurs
505 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
506 	 * @throws ExitException if the script terminates with a non-zero exit code
507 	 */
508 	public String run(Reader script, File input)
509 			throws IOException,
510 			ClassNotFoundException,
511 			ExitException {
512 		try (InputStream in = new FileInputStream(input)) {
513 			return run(script, in);
514 		}
515 	}
516 
517 	/**
518 	 * Executes the specified AWK script against the given input file and writes
519 	 * the printed output to the provided {@link OutputStream}.
520 	 *
521 	 * @param script AWK script to execute (as a {@link Reader})
522 	 * @param input file containing text to process
523 	 * @param output destination for the printed output
524 	 * @throws IOException if an I/O error occurs
525 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
526 	 * @throws ExitException if the script terminates with a non-zero exit code
527 	 */
528 	public void run(Reader script, File input, OutputStream output)
529 			throws IOException,
530 			ClassNotFoundException,
531 			ExitException {
532 		try (InputStream in = new FileInputStream(input)) {
533 			run(script, in, output);
534 		}
535 	}
536 
537 	/**
538 	 * Executes the specified AWK script against the provided input stream and
539 	 * returns the printed output as a {@link String}.
540 	 *
541 	 * @param script AWK script to execute
542 	 * @param input stream to process
543 	 * @return result of the execution as a String
544 	 * @throws IOException if an I/O error occurs
545 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
546 	 * @throws ExitException if the script terminates with a non-zero exit code
547 	 */
548 	public String run(String script, InputStream input)
549 			throws IOException,
550 			ClassNotFoundException,
551 			ExitException {
552 		ByteArrayOutputStream out = new ByteArrayOutputStream();
553 		run(script, input, out);
554 		return out.toString(StandardCharsets.UTF_8.name());
555 	}
556 
557 	/**
558 	 * Executes the specified AWK script against the provided input stream and
559 	 * writes the result to the given {@link OutputStream}.
560 	 *
561 	 * @param script AWK script to execute
562 	 * @param input stream to process
563 	 * @param output destination for the printed output
564 	 * @throws IOException if an I/O error occurs
565 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
566 	 * @throws ExitException if the script terminates with a non-zero exit code
567 	 */
568 	public void run(String script, InputStream input, OutputStream output)
569 			throws IOException,
570 			ClassNotFoundException,
571 			ExitException {
572 		run(new StringReader(script), input, output, false);
573 	}
574 
575 	/**
576 	 * Executes the specified AWK script against the provided input stream and
577 	 * returns the printed output as a {@link String}.
578 	 *
579 	 * @param script AWK script to execute (as a {@link Reader})
580 	 * @param input stream to process
581 	 * @return result of the execution as a String
582 	 * @throws IOException if an I/O error occurs
583 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
584 	 * @throws ExitException if the script terminates with a non-zero exit code
585 	 */
586 	public String run(Reader script, InputStream input)
587 			throws IOException,
588 			ClassNotFoundException,
589 			ExitException {
590 		ByteArrayOutputStream out = new ByteArrayOutputStream();
591 		run(script, input, out);
592 		return out.toString(StandardCharsets.UTF_8.name());
593 	}
594 
595 	/**
596 	 * Executes the specified AWK script against the provided input stream and
597 	 * writes the result to the given {@link OutputStream}.
598 	 *
599 	 * @param script AWK script to execute (as a {@link Reader})
600 	 * @param input stream to process
601 	 * @param output destination for the printed output
602 	 * @throws IOException if an I/O error occurs
603 	 * @throws ClassNotFoundException if intermediate code cannot be loaded
604 	 * @throws ExitException if the script terminates with a non-zero exit code
605 	 */
606 	public void run(Reader script, InputStream input, OutputStream output)
607 			throws IOException,
608 			ClassNotFoundException,
609 			ExitException {
610 		run(script, input, output, false);
611 	}
612 
613 	/**
614 	 * Internal method that configures default {@link AwkSettings} and executes
615 	 * the AWK script.
616 	 */
617 	private void run(
618 			Reader scriptReader,
619 			InputStream inputStream,
620 			OutputStream outputStream,
621 			boolean textInput)
622 			throws IOException,
623 			ClassNotFoundException,
624 			ExitException {
625 
626 		AwkSettings settings = new AwkSettings();
627 		if (inputStream != null) {
628 			settings.setInput(inputStream);
629 		}
630 
631 		if (textInput) {
632 			settings.setDefaultRS("\n");
633 			settings.setDefaultORS("\n");
634 		}
635 
636 		settings
637 				.setOutputStream(
638 						new PrintStream(
639 								outputStream,
640 								false,
641 								StandardCharsets.UTF_8.name()));
642 
643 		ScriptSource script = new ScriptSource(
644 				ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
645 				scriptReader);
646 
647 		try {
648 			invoke(script, settings);
649 		} catch (ExitException e) {
650 			if (e.getCode() != 0) {
651 				throw e;
652 			}
653 		}
654 	}
655 
656 	/**
657 	 * Compiles the specified AWK script and returns the intermediate representation
658 	 * as {@link AwkTuples}.
659 	 *
660 	 * @param script AWK script to compile
661 	 * @return compiled {@link AwkTuples}
662 	 * @throws IOException if an I/O error occurs during compilation
663 	 */
664 	public AwkTuples compile(String script) throws IOException {
665 		ScriptSource source = new ScriptSource(
666 				ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
667 				new StringReader(script));
668 		return compile(Collections.singletonList(source));
669 	}
670 
671 	/**
672 	 * Compiles the specified AWK script and returns the intermediate representation
673 	 * as {@link AwkTuples}.
674 	 *
675 	 * @param script AWK script to compile (as a {@link Reader})
676 	 * @return compiled {@link AwkTuples}
677 	 * @throws IOException if an I/O error occurs during compilation
678 	 */
679 	public AwkTuples compile(Reader script) throws IOException {
680 		ScriptSource source = new ScriptSource(
681 				ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
682 				script);
683 		return compile(Collections.singletonList(source));
684 	}
685 
686 	/**
687 	 * Compiles a list of script sources into {@link AwkTuples} that can be
688 	 * interpreted by the {@link AVM} runtime.
689 	 *
690 	 * @param scripts script sources to compile
691 	 * @return compiled {@link AwkTuples}
692 	 * @throws IOException if an I/O error occurs while reading the
693 	 *         scripts
694 	 */
695 	public AwkTuples compile(List<ScriptSource> scripts)
696 			throws IOException {
697 
698 		lastAst = null;
699 		AwkTuples tuples = createTuples();
700 		if (!scripts.isEmpty()) {
701 			// Parse all script sources into a single AST
702 			AwkParser parser = new AwkParser(this.extensionFunctions);
703 			AstNode ast = parser.parse(scripts);
704 			lastAst = ast;
705 			if (ast != null) {
706 				// Perform semantic checks twice to resolve forward references
707 				ast.semanticAnalysis();
708 				ast.semanticAnalysis();
709 				// Build tuples from the AST
710 				int result = ast.populateTuples(tuples);
711 				assert result == 0;
712 				// Assign addresses and prepare tuples for interpretation
713 				tuples.postProcess();
714 				// Record global variable offset mappings for the interpreter
715 				parser.populateGlobalVariableNameToOffsetMappings(tuples);
716 			}
717 		}
718 
719 		return tuples;
720 	}
721 
722 	/**
723 	 * Compile an expression to evaluate (not a full script).
724 	 *
725 	 * @param expression AWK expression to compile to AwkTuples
726 	 * @return AwkTuples to be interpreted by AVM
727 	 * @throws IOException if anything goes wrong with the compilation
728 	 */
729 	public AwkTuples compileForEval(String expression) throws IOException {
730 
731 		// Create a ScriptSource
732 		ScriptSource expressionSource = new ScriptSource(
733 				ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT,
734 				new StringReader(expression));
735 
736 		// Parse the expression
737 		AwkParser parser = new AwkParser(this.extensionFunctions);
738 		AstNode ast = parser.parseExpression(expressionSource);
739 
740 		// Create the tuples that we will return
741 		AwkTuples tuples = createTuples();
742 
743 		// Attempt to traverse the syntax tree and build
744 		// the intermediate code
745 		if (ast != null) {
746 			// 1st pass to tie actual parameters to back-referenced formal parameters
747 			ast.semanticAnalysis();
748 			// 2nd pass to tie actual parameters to forward-referenced formal parameters
749 			ast.semanticAnalysis();
750 			// build tuples
751 			ast.populateTuples(tuples);
752 			// Calls touch(...) per Tuple so that addresses can be normalized/assigned/allocated
753 			tuples.postProcess();
754 			// record global_var -> offset mapping into the tuples
755 			// so that the interpreter can assign variables
756 			parser.populateGlobalVariableNameToOffsetMappings(tuples);
757 		}
758 
759 		return tuples;
760 	}
761 
762 	/**
763 	 * Evaluates the specified AWK expression (not a full script, just an expression)
764 	 * and returns the value of this expression.
765 	 *
766 	 * @param expression Expression to evaluate (e.g. <code>2+3</code>)
767 	 * @return the value of the specified expression
768 	 * @throws IOException if anything goes wrong with the evaluation
769 	 */
770 	public Object eval(String expression) throws IOException {
771 		return eval(expression, null, null);
772 	}
773 
774 	/**
775 	 * Evaluates the specified AWK expression (not a full script, just an expression)
776 	 * and returns the value of this expression.
777 	 *
778 	 * @param expression Expression to evaluate (e.g. <code>2+3</code> or <code>$2 "-" $3</code>
779 	 * @param input Optional text input (that will be available as $0, and tokenized as $1, $2, etc.)
780 	 * @return the value of the specified expression
781 	 * @throws IOException if anything goes wrong with the evaluation
782 	 */
783 	public Object eval(String expression, String input) throws IOException {
784 		return eval(expression, input, null);
785 	}
786 
787 	/**
788 	 * Evaluates the specified AWK expression (not a full script, just an expression)
789 	 * and returns the value of this expression.
790 	 *
791 	 * @param expression Expression to evaluate (e.g. <code>2+3</code> or <code>$2 "-" $3</code>
792 	 * @param input Optional text input (that will be available as $0, and tokenized as $1, $2, etc.)
793 	 * @param fieldSeparator Value of the FS global variable used for parsing the input
794 	 * @return the value of the specified expression
795 	 * @throws IOException if anything goes wrong with the evaluation
796 	 */
797 	public Object eval(String expression, String input, String fieldSeparator) throws IOException {
798 		return eval(compileForEval(expression), input, fieldSeparator);
799 	}
800 
801 	/**
802 	 * Evaluates the specified AWK tuples, i.e. the result of the execution of the
803 	 * TERNARY_EXPRESSION AST (the value that has been pushed in the stack).
804 	 *
805 	 * @param tuples Tuples returned by {@link Awk#compileForEval(String)}
806 	 * @param input Optional text input (that will be available as $0, and tokenized as $1, $2, etc.)
807 	 * @param fieldSeparator Value of the FS global variable used for parsing the input
808 	 * @return the value of the specified expression
809 	 * @throws IOException if anything goes wrong with the evaluation
810 	 */
811 	public Object eval(AwkTuples tuples, String input, String fieldSeparator) throws IOException {
812 
813 		AwkSettings settings = new AwkSettings();
814 		if (input != null) {
815 			settings.setInput(toInputStream(input));
816 		} else {
817 			settings.setInput(toInputStream(""));
818 		}
819 
820 		settings.setDefaultRS("\n");
821 		settings.setDefaultORS("\n");
822 		settings.setFieldSeparator(fieldSeparator);
823 
824 		settings
825 				.setOutputStream(
826 						new PrintStream(new ByteArrayOutputStream(), false, StandardCharsets.UTF_8.name()));
827 
828 		AVM avm = createAvm(settings);
829 		return avm.eval(tuples, input);
830 	}
831 
832 	protected AwkTuples createTuples() {
833 		return new AwkTuples();
834 	}
835 
836 	protected AVM createAvm(AwkSettings settings) {
837 		return new AVM(settings, this.extensionInstances, this.extensionFunctions);
838 	}
839 
840 	/**
841 	 * Converts a text input into an {@link InputStream} using UTF-8 encoding.
842 	 */
843 	private static InputStream toInputStream(String input) {
844 		if (input == null) {
845 			return new ByteArrayInputStream(new byte[0]);
846 		}
847 		return new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
848 	}
849 
850 	/**
851 	 * Reads all characters from the supplied {@link Reader} and returns an
852 	 * {@link InputStream} containing the same data using UTF-8 encoding.
853 	 */
854 	private static InputStream toInputStream(Reader reader) throws IOException {
855 		if (reader == null) {
856 			return new ByteArrayInputStream(new byte[0]);
857 		}
858 		StringBuilder sb = new StringBuilder();
859 		char[] buf = new char[4096];
860 		int len;
861 		while ((len = reader.read(buf)) != -1) {
862 			sb.append(buf, 0, len);
863 		}
864 		return new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8));
865 	}
866 
867 	/**
868 	 * Lists metadata for the {@link JawkExtension} implementations discovered on
869 	 * the class path.
870 	 *
871 	 * @return list of discovered extension descriptors
872 	 */
873 	public static Map<String, JawkExtension> listAvailableExtensions() {
874 		return ExtensionRegistry.listExtensions();
875 	}
876 
877 }