View Javadoc
1   package io.jawk.frontend;
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.File;
26  import java.io.IOException;
27  import java.io.LineNumberReader;
28  import java.io.PrintStream;
29  import java.nio.charset.StandardCharsets;
30  import java.nio.file.Files;
31  import java.nio.file.InvalidPathException;
32  import java.nio.file.Path;
33  import java.nio.file.Paths;
34  import java.util.ArrayDeque;
35  import java.util.ArrayList;
36  import java.util.Collections;
37  import java.util.Deque;
38  import java.util.EnumSet;
39  import java.util.HashMap;
40  import java.util.HashSet;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.Set;
44  import java.util.function.Supplier;
45  import io.jawk.AwkSandboxException;
46  import io.jawk.NotImplementedError;
47  import io.jawk.backend.AVM;
48  import io.jawk.ext.ExtensionFunction;
49  import io.jawk.intermediate.Address;
50  import io.jawk.intermediate.AwkTuples;
51  import io.jawk.intermediate.BuiltinFunction;
52  import io.jawk.jrt.JRT;
53  import io.jawk.intermediate.Tuple;
54  import io.jawk.util.ScriptFileSource;
55  import io.jawk.util.ScriptSource;
56  import io.jawk.frontend.ast.LexerException;
57  import io.jawk.frontend.ast.ParserException;
58  
59  /**
60   * Converts the AWK script into a syntax tree,
61   * which is useful the backend that either compiles or interprets the script.
62   * <p>
63   * It contains the internal state of the parser and the lexer.
64   *
65   * @author Danny Daglas
66   */
67  public class AwkParser {
68  
69  	/**
70  	 * Flags that describe special behaviours of AST nodes. These replace the
71  	 * previous marker interfaces such as {@code Breakable} and
72  	 * {@code NonStatementAst}.
73  	 */
74  	private enum AstFlag {
75  		BREAKABLE,
76  		NEXTABLE,
77  		CONTINUEABLE,
78  		RETURNABLE,
79  		NON_STATEMENT
80  	}
81  
82  	/** Lexer token values. */
83  	enum Token {
84  		EOF,
85  		NEWLINE,
86  		SEMICOLON,
87  		ID,
88  		FUNC_ID,
89  		INTEGER,
90  		DOUBLE,
91  		STRING,
92  
93  		EQUALS,
94  
95  		AND,
96  		OR,
97  
98  		EQ,
99  		GT,
100 		GE,
101 		LT,
102 		LE,
103 		NE,
104 		NOT,
105 		PIPE,
106 		QUESTION_MARK,
107 		COLON,
108 		APPEND,
109 
110 		PLUS,
111 		MINUS,
112 		MULT,
113 		DIVIDE,
114 		MOD,
115 		POW,
116 		COMMA,
117 		MATCHES,
118 		NOT_MATCHES,
119 		DOLLAR,
120 
121 		INC,
122 		DEC,
123 
124 		PLUS_EQ,
125 		MINUS_EQ,
126 		MULT_EQ,
127 		DIV_EQ,
128 		MOD_EQ,
129 		POW_EQ,
130 
131 		OPEN_PAREN,
132 		CLOSE_PAREN,
133 		OPEN_BRACE,
134 		CLOSE_BRACE,
135 		OPEN_BRACKET,
136 		CLOSE_BRACKET,
137 
138 		BUILTIN_FUNC_NAME,
139 
140 		EXTENSION,
141 		TYPED_REGEXP,
142 		INDIRECT,
143 		DIRECTIVE_INCLUDE,
144 		DIRECTIVE_NAMESPACE,
145 		DIRECTIVE_UNSUPPORTED,
146 
147 		KW_FUNCTION,
148 		KW_BEGIN,
149 		KW_END,
150 		KW_BEGINFILE,
151 		KW_ENDFILE,
152 		KW_IN,
153 		KW_IF,
154 		KW_ELSE,
155 		KW_WHILE,
156 		KW_FOR,
157 		KW_DO,
158 		KW_RETURN,
159 		KW_EXIT,
160 		KW_NEXT,
161 		KW_NEXTFILE,
162 		KW_CONTINUE,
163 		KW_DELETE,
164 		KW_BREAK,
165 		KW_PRINT,
166 		KW_PRINTF,
167 		KW_GETLINE
168 	}
169 
170 	/**
171 	 * Contains a mapping of Jawk keywords to their
172 	 * token values.
173 	 * They closely correspond to AWK keywords, but with
174 	 * a few added extensions.
175 	 * <p>
176 	 * Keys are the keywords themselves, and values are the
177 	 * token values (equivalent to yytok values in lex/yacc).
178 	 * <p>
179 	 * <strong>Note:</strong> whether built-in AWK function names
180 	 * and special AWK variable names are formally keywords or not,
181 	 * they are not stored in this map. They are separated
182 	 * into other maps.
183 	 */
184 	private static final Map<String, Token> KEYWORDS = new HashMap<String, Token>();
185 
186 	static {
187 		// special keywords
188 		KEYWORDS.put("function", Token.KW_FUNCTION);
189 		KEYWORDS.put("BEGIN", Token.KW_BEGIN);
190 		KEYWORDS.put("END", Token.KW_END);
191 		KEYWORDS.put("BEGINFILE", Token.KW_BEGINFILE);
192 		KEYWORDS.put("ENDFILE", Token.KW_ENDFILE);
193 		KEYWORDS.put("in", Token.KW_IN);
194 
195 		// statements
196 		KEYWORDS.put("if", Token.KW_IF);
197 		KEYWORDS.put("else", Token.KW_ELSE);
198 		KEYWORDS.put("while", Token.KW_WHILE);
199 		KEYWORDS.put("for", Token.KW_FOR);
200 		KEYWORDS.put("do", Token.KW_DO);
201 		KEYWORDS.put("return", Token.KW_RETURN);
202 		KEYWORDS.put("exit", Token.KW_EXIT);
203 		KEYWORDS.put("next", Token.KW_NEXT);
204 		KEYWORDS.put("nextfile", Token.KW_NEXTFILE);
205 		KEYWORDS.put("continue", Token.KW_CONTINUE);
206 		KEYWORDS.put("delete", Token.KW_DELETE);
207 		KEYWORDS.put("break", Token.KW_BREAK);
208 
209 		// special-form functions
210 		KEYWORDS.put("print", Token.KW_PRINT);
211 		KEYWORDS.put("printf", Token.KW_PRINTF);
212 		KEYWORDS.put("getline", Token.KW_GETLINE);
213 	}
214 
215 	private static final int SP_IDX = 257;
216 	/**
217 	 * Contains a mapping of Jawk special variables to their
218 	 * variable token values.
219 	 * As of this writing, they correspond exactly to
220 	 * standard AWK variables, no more, no less.
221 	 * <p>
222 	 * Keys are the variable names themselves, and values are the
223 	 * variable token values.
224 	 */
225 	private static final Map<String, Integer> SPECIAL_VAR_NAMES = new HashMap<String, Integer>();
226 
227 	static {
228 		SPECIAL_VAR_NAMES.put("NR", SP_IDX);
229 		SPECIAL_VAR_NAMES.put("FNR", SP_IDX);
230 		SPECIAL_VAR_NAMES.put("NF", SP_IDX);
231 		SPECIAL_VAR_NAMES.put("FS", SP_IDX);
232 		SPECIAL_VAR_NAMES.put("RS", SP_IDX);
233 		SPECIAL_VAR_NAMES.put("OFS", SP_IDX);
234 		SPECIAL_VAR_NAMES.put("ORS", SP_IDX);
235 		SPECIAL_VAR_NAMES.put("RSTART", SP_IDX);
236 		SPECIAL_VAR_NAMES.put("RLENGTH", SP_IDX);
237 		SPECIAL_VAR_NAMES.put("FILENAME", SP_IDX);
238 		SPECIAL_VAR_NAMES.put("SUBSEP", SP_IDX);
239 		SPECIAL_VAR_NAMES.put("CONVFMT", SP_IDX);
240 		SPECIAL_VAR_NAMES.put("OFMT", SP_IDX);
241 		SPECIAL_VAR_NAMES.put("ENVIRON", SP_IDX);
242 		SPECIAL_VAR_NAMES.put("ARGC", SP_IDX);
243 		SPECIAL_VAR_NAMES.put("ARGV", SP_IDX);
244 		SPECIAL_VAR_NAMES.put("IGNORECASE", SP_IDX);
245 		SPECIAL_VAR_NAMES.put("ERRNO", SP_IDX);
246 		SPECIAL_VAR_NAMES.put("ARGIND", SP_IDX);
247 	}
248 
249 	/**
250 	 * Defined as concrete implementation class (not an
251 	 * interface reference) as to not clutter the interface
252 	 * with methods appropriate for private access, only.
253 	 */
254 	private final AwkSymbolTableImpl symbolTable = new AwkSymbolTableImpl();
255 
256 	private final Map<String, ExtensionFunction> extensions;
257 
258 	/** POSIX compile-time mode: rejects gawk syntax such as arrays of arrays and typed regexps. */
259 	private final boolean posix;
260 
261 	/** Whether the compiling engine permits source inclusion from the filesystem. */
262 	private final boolean sourceIncludeAllowed;
263 
264 	/**
265 	 * <p>
266 	 * Constructor for AwkParser.
267 	 * </p>
268 	 *
269 	 * @param extensions a {@link java.util.Map} object
270 	 * @param posix {@code true} to enforce POSIX compile-time behavior
271 	 */
272 	public AwkParser(Map<String, ExtensionFunction> extensions, boolean posix) {
273 		this(extensions, posix, true);
274 	}
275 
276 	/**
277 	 * Creates a parser with an explicit source-inclusion policy.
278 	 *
279 	 * @param extensions extension functions available during parsing
280 	 * @param posix {@code true} to enforce POSIX compile-time behavior
281 	 * @param sourceIncludeAllowed {@code true} to permit {@code @include}
282 	 */
283 	public AwkParser(
284 			Map<String, ExtensionFunction> extensions,
285 			boolean posix,
286 			boolean sourceIncludeAllowed) {
287 		this.extensions = extensions == null ?
288 				Collections.emptyMap() : Collections.unmodifiableMap(new HashMap<>(extensions));
289 		this.posix = posix;
290 		this.sourceIncludeAllowed = sourceIncludeAllowed;
291 	}
292 
293 	/**
294 	 * Returns whether the keyword is disabled in the current compile-time
295 	 * mode. BEGINFILE and ENDFILE are gawk extensions: in POSIX mode they are
296 	 * not special and lex as plain identifiers, exactly like
297 	 * {@code gawk --posix}.
298 	 *
299 	 * @param keywordToken the keyword token to inspect
300 	 * @return {@code true} when the keyword must be treated as an identifier
301 	 */
302 	private boolean isDisabledKeyword(Token keywordToken) {
303 		return posix && (keywordToken == Token.KW_BEGINFILE || keywordToken == Token.KW_ENDFILE);
304 	}
305 
306 	private boolean isAwkNamespaceIdentifier(String identifier) {
307 		int separator = identifier.indexOf("::");
308 		if (separator >= 0) {
309 			return "awk".equals(identifier.substring(0, separator));
310 		}
311 		return "awk".equals(currentNamespace);
312 	}
313 
314 	private String awkNamespaceComponent(String identifier) {
315 		return identifier.startsWith("awk::") ? identifier.substring("awk::".length()) : identifier;
316 	}
317 
318 	private String qualifyGlobalIdentifier(String identifier) {
319 		int separator = identifier.indexOf("::");
320 		if (separator >= 0) {
321 			return "awk".equals(identifier.substring(0, separator)) ? identifier.substring(separator + 2) : identifier;
322 		}
323 		if ("awk".equals(currentNamespace) || isAllUppercaseIdentifier(identifier)) {
324 			return identifier;
325 		}
326 		return currentNamespace + "::" + identifier;
327 	}
328 
329 	private boolean isAllUppercaseIdentifier(String identifier) {
330 		if (identifier.isEmpty()) {
331 			return false;
332 		}
333 		for (int i = 0; i < identifier.length(); i++) {
334 			char ch = identifier.charAt(i);
335 			if (ch < 'A' || ch > 'Z') {
336 				return false;
337 			}
338 		}
339 		return true;
340 	}
341 
342 	private List<ScriptSource> scriptSources;
343 	private int scriptSourcesCurrentIndex;
344 	private ScriptSource currentScriptSource;
345 	private LineNumberReader reader;
346 	private int c;
347 	private Token token;
348 	private String pendingIndirectIdentifier;
349 	private boolean pendingColon;
350 	private String currentNamespace = "awk";
351 	private long conditionPairCount;
352 	private final Deque<SourceState> includedSourceStack = new ArrayDeque<SourceState>();
353 	private final Set<Path> includedSourcePaths = new HashSet<Path>();
354 	private final Set<Path> topLevelSourcePaths = new HashSet<Path>();
355 
356 	private StringBuffer text = new StringBuffer();
357 	private StringBuffer string = new StringBuffer();
358 	private StringBuffer regexp = new StringBuffer();
359 
360 	private static final class SourceState {
361 		private final ScriptSource scriptSource;
362 		private final LineNumberReader reader;
363 		private final int currentCharacter;
364 		private final String namespace;
365 
366 		private SourceState(
367 				ScriptSource scriptSourceParam,
368 				LineNumberReader readerParam,
369 				int currentCharacterParam,
370 				String namespaceParam) {
371 			scriptSource = scriptSourceParam;
372 			reader = readerParam;
373 			currentCharacter = currentCharacterParam;
374 			namespace = namespaceParam;
375 		}
376 	}
377 
378 	private void read() throws IOException {
379 		text.append((char) c);
380 		c = reader.read();
381 		// completely bypass \r's
382 		while (c == '\r') {
383 			c = reader.read();
384 		}
385 	}
386 
387 	/**
388 	 * Advances to the next readable source at a token boundary when the current
389 	 * reader has reached end-of-file. Deferring this transition until the current
390 	 * token is complete preserves the source and namespace used to classify its
391 	 * final token. Included files are unwound first in LIFO order, restoring the
392 	 * including reader, its unread character, and its namespace. Once the include
393 	 * stack is empty, parsing continues with the next top-level script source,
394 	 * whose namespace starts at {@code awk}.
395 	 *
396 	 * @throws IOException if a source cannot be closed or read
397 	 */
398 	private void advancePastEndOfSource() throws IOException {
399 		while (c < 0) {
400 			if (!includedSourceStack.isEmpty()) {
401 				reader.close();
402 				SourceState previous = includedSourceStack.pop();
403 				currentScriptSource = previous.scriptSource;
404 				reader = previous.reader;
405 				c = previous.currentCharacter;
406 				currentNamespace = previous.namespace;
407 			} else if ((scriptSourcesCurrentIndex + 1) < scriptSources.size()) {
408 				scriptSourcesCurrentIndex++;
409 				currentScriptSource = scriptSources.get(scriptSourcesCurrentIndex);
410 				reader = new LineNumberReader(currentScriptSource.getReader());
411 				currentNamespace = "awk";
412 				c = reader.read();
413 				while (c == '\r') {
414 					c = reader.read();
415 				}
416 			} else {
417 				return;
418 			}
419 		}
420 	}
421 
422 	/**
423 	 * Skip all whitespaces and comments
424 	 *
425 	 * @throws IOException
426 	 */
427 	private void skipWhitespaces() throws IOException {
428 		while (c == ' ' || c == '\t' || c == '#' || c == '\n') {
429 			if (c == '#') {
430 				while (c >= 0 && c != '\n') {
431 					read();
432 				}
433 			}
434 			read();
435 		}
436 	}
437 
438 	/**
439 	 * Parse the script streamed by script_reader. Build and return the
440 	 * root of the abstract syntax tree which represents the Jawk script.
441 	 *
442 	 * @param localScriptSources List of script sources
443 	 * @return The abstract syntax tree of this script.
444 	 * @throws java.io.IOException upon an IO error.
445 	 */
446 	public AstNode parse(List<ScriptSource> localScriptSources) throws IOException {
447 		if (localScriptSources == null || localScriptSources.isEmpty()) {
448 			throw new IOException("No script sources supplied");
449 		}
450 		this.scriptSources = Collections.unmodifiableList(new ArrayList<>(localScriptSources));
451 		scriptSourcesCurrentIndex = 0;
452 		currentScriptSource = this.scriptSources.get(scriptSourcesCurrentIndex);
453 		reader = new LineNumberReader(currentScriptSource.getReader());
454 		currentNamespace = "awk";
455 		includedSourceStack.clear();
456 		resetIncludedSourcePaths();
457 		pendingIndirectIdentifier = null;
458 		pendingColon = false;
459 		read();
460 		lexer();
461 		return SCRIPT();
462 	}
463 
464 	/**
465 	 * Parse a single AWK expression and return the corresponding AST.
466 	 *
467 	 * @param expressionSource The expression to parse (not a statement or rule, just an expression)
468 	 * @return tuples representing the expression
469 	 * @throws IOException upon an IO error or parsing error
470 	 */
471 	public AstNode parseExpression(ScriptSource expressionSource) throws IOException {
472 
473 		// Sanity check
474 		if (expressionSource == null) {
475 			throw new IOException("No source supplied");
476 		}
477 
478 		// Reader of the expression
479 		this.scriptSources = Collections.singletonList(expressionSource);
480 		scriptSourcesCurrentIndex = 0;
481 		currentScriptSource = expressionSource;
482 		reader = new LineNumberReader(currentScriptSource.getReader());
483 		currentNamespace = "awk";
484 		includedSourceStack.clear();
485 		resetIncludedSourcePaths();
486 		pendingIndirectIdentifier = null;
487 		pendingColon = false;
488 
489 		// Initialize the lexer
490 		read();
491 		lexer();
492 
493 		// An expression is a TERNARY_EXPRESSION
494 		return EXPRESSION_TO_EVALUATE();
495 	}
496 
497 	private void resetIncludedSourcePaths() throws IOException {
498 		includedSourcePaths.clear();
499 		topLevelSourcePaths.clear();
500 		for (ScriptSource source : scriptSources) {
501 			if (source instanceof ScriptFileSource) {
502 				String filePath = ((ScriptFileSource) source).getFilePath();
503 				Path sourcePath = Paths.get(filePath).toRealPath();
504 				includedSourcePaths.add(sourcePath);
505 				topLevelSourcePaths.add(sourcePath);
506 			}
507 		}
508 	}
509 
510 	private LexerException lexerException(String msg) {
511 		return new LexerException(
512 				msg,
513 				currentScriptSource.getDescription(),
514 				reader.getLineNumber());
515 	}
516 
517 	/**
518 	 * Returns the current 1-based source line number to stamp onto AST nodes that
519 	 * will later emit tuple line markers for runtime error reporting.
520 	 *
521 	 * @return current source line number using 1-based counting
522 	 */
523 	private int currentSourceLineNumber() {
524 		return reader.getLineNumber() + 1;
525 	}
526 
527 	/**
528 	 * Reads the string and handle all escape codes.
529 	 *
530 	 * @throws IOException
531 	 */
532 	private void readString() throws IOException {
533 		string.setLength(0);
534 
535 		while (token != Token.EOF && c > 0 && c != '"' && c != '\n') {
536 			if (c == '\\') {
537 				read();
538 				switch (c) {
539 				case 'n':
540 					string.append('\n');
541 					break;
542 				case 't':
543 					string.append('\t');
544 					break;
545 				case 'r':
546 					string.append('\r');
547 					break;
548 				case 'a':
549 					string.append('\007');
550 					break; // BEL 0x07
551 				case 'b':
552 					string.append('\010');
553 					break; // BS 0x08
554 				case 'f':
555 					string.append('\014');
556 					break; // FF 0x0C
557 				case 'v':
558 					string.append('\013');
559 					break; // VT 0x0B
560 				// Octal notation: \N \NN \NNN
561 				case '0':
562 				case '1':
563 				case '2':
564 				case '3':
565 				case '4':
566 				case '5':
567 				case '6':
568 				case '7': {
569 					int octalChar = c - '0';
570 					read();
571 					if (c >= '0' && c <= '7') {
572 						octalChar = (octalChar << 3) + c - '0';
573 						read();
574 						if (c >= '0' && c <= '7') {
575 							octalChar = (octalChar << 3) + c - '0';
576 							read();
577 						}
578 					}
579 					string.append((char) octalChar);
580 					continue;
581 				}
582 				// Hexadecimal notation: \xN \xNN
583 				case 'x': {
584 					int hexChar = 0;
585 					read();
586 					if (c >= '0' && c <= '9') {
587 						hexChar = c - '0';
588 					} else if (c >= 'A' && c <= 'F') {
589 						hexChar = c - 'A' + 10;
590 					} else if (c >= 'a' && c <= 'f') {
591 						hexChar = c - 'a' + 10;
592 					} else {
593 						string.append('x');
594 						continue;
595 					}
596 					read();
597 					if (c >= '0' && c <= '9') {
598 						hexChar = (hexChar << 4) + c - '0';
599 					} else if (c >= 'A' && c <= 'F') {
600 						hexChar = (hexChar << 4) + c - 'A' + 10;
601 					} else if (c >= 'a' && c <= 'f') {
602 						hexChar = (hexChar << 4) + c - 'a' + 10;
603 					} else {
604 						// Append what we already have, and continue directly, because we already have read the next char
605 						string.append((char) hexChar);
606 						continue;
607 					}
608 					string.append((char) hexChar);
609 					break;
610 				}
611 				default:
612 					string.append((char) c);
613 					break; // Remove the backslash
614 				}
615 			} else {
616 				string.append((char) c);
617 			}
618 			read();
619 		}
620 		if (token == Token.EOF || c == '\n' || c <= 0) {
621 			throw lexerException("Unterminated string: " + text);
622 		}
623 		read();
624 	}
625 
626 	/**
627 	 * Reads the regular expression (between slashes '/') and handle '\/'.
628 	 * A slash within a bracket expression (e.g. {@code /[/]/}) does not
629 	 * terminate the regular expression, per POSIX ERE bracket semantics.
630 	 *
631 	 * @throws IOException
632 	 */
633 	private void readRegexp() throws IOException {
634 		regexp.setLength(0);
635 
636 		boolean inBracket = false;
637 		while (token != Token.EOF && c > 0 && (c != '/' || inBracket) && c != '\n') {
638 			if (c == '\\') {
639 				read();
640 				if (c != '/') {
641 					regexp.append('\\');
642 				}
643 				regexp.append((char) c);
644 				read();
645 				continue;
646 			}
647 			if (!inBracket && c == '[') {
648 				inBracket = true;
649 				regexp.append((char) c);
650 				read();
651 				// a ']' right after '[' (or after '[^') is a literal ']'
652 				if (c == '^') {
653 					regexp.append((char) c);
654 					read();
655 				}
656 				if (c == ']') {
657 					regexp.append((char) c);
658 					read();
659 				}
660 				continue;
661 			}
662 			if (inBracket && c == '[') {
663 				regexp.append((char) c);
664 				read();
665 				// POSIX character class, collating element, or equivalence
666 				// class ([:alpha:], [.x.], [=e=]): its closing ']' does not
667 				// end the outer bracket expression.
668 				if (c == ':' || c == '.' || c == '=') {
669 					int delimiter = c;
670 					boolean closed = false;
671 					while (token != Token.EOF && c > 0 && c != '\n' && !closed) {
672 						int previous = c;
673 						regexp.append((char) c);
674 						read();
675 						if (previous == delimiter && c == ']') {
676 							regexp.append((char) c);
677 							read();
678 							closed = true;
679 						}
680 					}
681 				}
682 				continue;
683 			}
684 			if (inBracket && c == ']') {
685 				inBracket = false;
686 			}
687 			regexp.append((char) c);
688 			read();
689 		}
690 		if (token == Token.EOF || c == '\n' || c <= 0) {
691 			throw lexerException("Unterminated string: " + text);
692 		}
693 		read();
694 	}
695 
696 	private Token lexer(Token expectedToken) throws IOException {
697 		if (token != expectedToken) {
698 			throw parserException(
699 					"Expecting " + expectedToken.name() + ". Found: " + token.name() + " (" + text + ")");
700 		}
701 		return lexer();
702 	}
703 
704 	private Token lexer() throws IOException {
705 		// clear whitespace
706 		while (true) {
707 			advancePastEndOfSource();
708 			if (c < 0 || c != ' ' && c != '\t' && c != '#' && c != '\\') {
709 				break;
710 			}
711 			if (c == '\\') {
712 				read();
713 				if (c == '\n') {
714 					read();
715 				}
716 				continue;
717 			}
718 			if (c == '#') {
719 				// kill comment
720 				while (c >= 0 && c != '\n') {
721 					read();
722 				}
723 			} else {
724 				read();
725 			}
726 		}
727 		text.setLength(0);
728 		if (pendingColon) {
729 			pendingColon = false;
730 			token = Token.COLON;
731 			return token;
732 		}
733 		if (pendingIndirectIdentifier != null) {
734 			text.append(pendingIndirectIdentifier);
735 			pendingIndirectIdentifier = null;
736 			token = Token.ID;
737 			return token;
738 		}
739 		if (c < 0) {
740 			token = Token.EOF;
741 			return token;
742 		}
743 		if (c == ',') {
744 			read();
745 			skipWhitespaces();
746 			token = Token.COMMA;
747 			return token;
748 		}
749 		if (c == '(') {
750 			read();
751 			token = Token.OPEN_PAREN;
752 			return token;
753 		}
754 		if (c == ')') {
755 			read();
756 			token = Token.CLOSE_PAREN;
757 			return token;
758 		}
759 		if (c == '{') {
760 			read();
761 			skipWhitespaces();
762 			token = Token.OPEN_BRACE;
763 			return token;
764 		}
765 		if (c == '}') {
766 			read();
767 			token = Token.CLOSE_BRACE;
768 			return token;
769 		}
770 		if (c == '[') {
771 			read();
772 			token = Token.OPEN_BRACKET;
773 			return token;
774 		}
775 		if (c == ']') {
776 			read();
777 			token = Token.CLOSE_BRACKET;
778 			return token;
779 		}
780 		if (c == '$') {
781 			read();
782 			token = Token.DOLLAR;
783 			return token;
784 		}
785 		if (c == '@') {
786 			if (posix) {
787 				throw lexerException("gawk @ syntax is not supported in POSIX mode.");
788 			}
789 			read();
790 			if (c == '/') {
791 				read();
792 				readRegexp();
793 				token = Token.TYPED_REGEXP;
794 				return token;
795 			}
796 			if (Character.isJavaIdentifierStart(c)) {
797 				while (Character.isJavaIdentifierPart(c)) {
798 					read();
799 				}
800 				if (c == ':') {
801 					read();
802 					if (c != ':') {
803 						throw lexerException("Namespace separator must be two colons (::).");
804 					}
805 					read();
806 					if (!Character.isJavaIdentifierStart(c)) {
807 						throw lexerException("A namespace-qualified name requires an identifier after ::.");
808 					}
809 					read();
810 					while (Character.isJavaIdentifierPart(c)) {
811 						read();
812 					}
813 				}
814 				String atWord = text.toString();
815 				if ("@include".equals(atWord)) {
816 					token = Token.DIRECTIVE_INCLUDE;
817 					return token;
818 				}
819 				if ("@namespace".equals(atWord)) {
820 					token = Token.DIRECTIVE_NAMESPACE;
821 					return token;
822 				}
823 				if ("@load".equals(atWord)) {
824 					token = Token.DIRECTIVE_UNSUPPORTED;
825 					return token;
826 				}
827 				pendingIndirectIdentifier = atWord.substring(1);
828 				validateIndirectIdentifier(pendingIndirectIdentifier);
829 				token = Token.INDIRECT;
830 				return token;
831 			}
832 			token = Token.INDIRECT;
833 			return token;
834 		}
835 		if (c == '~') {
836 			read();
837 			token = Token.MATCHES;
838 			return token;
839 		}
840 		if (c == '?') {
841 			read();
842 			skipWhitespaces();
843 			token = Token.QUESTION_MARK;
844 			return token;
845 		}
846 		if (c == ':') {
847 			read();
848 			skipWhitespaces();
849 			token = Token.COLON;
850 			return token;
851 		}
852 		if (c == '&') {
853 			read();
854 			if (c == '&') {
855 				read();
856 				skipWhitespaces();
857 				token = Token.AND;
858 				return token;
859 			}
860 			throw lexerException("use && for logical and");
861 		}
862 		if (c == '|') {
863 			read();
864 			if (c == '|') {
865 				read();
866 				skipWhitespaces();
867 				token = Token.OR;
868 				return token;
869 			}
870 			token = Token.PIPE;
871 			return token;
872 		}
873 		if (c == '=') {
874 			read();
875 			if (c == '=') {
876 				read();
877 				token = Token.EQ;
878 				return token;
879 			}
880 			token = Token.EQUALS;
881 			return token;
882 		}
883 		if (c == '+') {
884 			read();
885 			if (c == '=') {
886 				read();
887 				token = Token.PLUS_EQ;
888 				return token;
889 			} else if (c == '+') {
890 				read();
891 				token = Token.INC;
892 				return token;
893 			}
894 			token = Token.PLUS;
895 			return token;
896 		}
897 		if (c == '-') {
898 			read();
899 			if (c == '=') {
900 				read();
901 				token = Token.MINUS_EQ;
902 				return token;
903 			} else if (c == '-') {
904 				read();
905 				token = Token.DEC;
906 				return token;
907 			}
908 			token = Token.MINUS;
909 			return token;
910 		}
911 		if (c == '*') {
912 			read();
913 			if (c == '=') {
914 				read();
915 				token = Token.MULT_EQ;
916 				return token;
917 			} else if (c == '*') {
918 				read();
919 				if (c == '=') {
920 					read();
921 					token = Token.POW_EQ;
922 					return token;
923 				}
924 				token = Token.POW;
925 				return token;
926 			}
927 			token = Token.MULT;
928 			return token;
929 		}
930 		if (c == '/') {
931 			read();
932 			if (c == '=') {
933 				read();
934 				token = Token.DIV_EQ;
935 				return token;
936 			}
937 			token = Token.DIVIDE;
938 			return token;
939 		}
940 		if (c == '%') {
941 			read();
942 			if (c == '=') {
943 				read();
944 				token = Token.MOD_EQ;
945 				return token;
946 			}
947 			token = Token.MOD;
948 			return token;
949 		}
950 		if (c == '^') {
951 			read();
952 			if (c == '=') {
953 				read();
954 				token = Token.POW_EQ;
955 				return token;
956 			}
957 			token = Token.POW;
958 			return token;
959 		}
960 		if (c == '>') {
961 			read();
962 			if (c == '=') {
963 				read();
964 				token = Token.GE;
965 				return token;
966 			} else if (c == '>') {
967 				read();
968 				token = Token.APPEND;
969 				return token;
970 			}
971 			token = Token.GT;
972 			return token;
973 		}
974 		if (c == '<') {
975 			read();
976 			if (c == '=') {
977 				read();
978 				token = Token.LE;
979 				return token;
980 			}
981 			token = Token.LT;
982 			return token;
983 		}
984 		if (c == '!') {
985 			read();
986 			if (c == '=') {
987 				read();
988 				token = Token.NE;
989 				return token;
990 			} else if (c == '~') {
991 				read();
992 				token = Token.NOT_MATCHES;
993 				return token;
994 			}
995 			token = Token.NOT;
996 			return token;
997 		}
998 
999 		if (c == '.') {
1000 			// double!
1001 			read();
1002 			boolean hit = false;
1003 			while (c > 0 && Character.isDigit(c)) {
1004 				hit = true;
1005 				read();
1006 			}
1007 			if (!hit) {
1008 				throw lexerException("Decimal point encountered with no values on either side.");
1009 			}
1010 			token = Token.DOUBLE;
1011 			return token;
1012 		}
1013 
1014 		if (Character.isDigit(c)) {
1015 			// integer or double.
1016 			read();
1017 			while (c > 0) {
1018 				if (c == '.') {
1019 					// double!
1020 					read();
1021 					while (c > 0 && Character.isDigit(c)) {
1022 						read();
1023 					}
1024 					token = Token.DOUBLE;
1025 					return token;
1026 				} else if (Character.isDigit(c)) {
1027 					// integer or double.
1028 					read();
1029 				} else {
1030 					break;
1031 				}
1032 			}
1033 			// integer, only
1034 			token = Token.INTEGER;
1035 			return token;
1036 		}
1037 
1038 		if (Character.isJavaIdentifierStart(c)) {
1039 			read();
1040 			while (Character.isJavaIdentifierPart(c)) {
1041 				read();
1042 			}
1043 			if (c == ':') {
1044 				read();
1045 				if (c != ':') {
1046 					text.setLength(text.length() - 1);
1047 					pendingColon = true;
1048 				} else {
1049 					if (posix) {
1050 						throw lexerException("gawk namespace syntax is not supported in POSIX mode.");
1051 					}
1052 					read();
1053 					if (!Character.isJavaIdentifierStart(c)) {
1054 						throw lexerException("A namespace-qualified name requires an identifier after ::.");
1055 					}
1056 					read();
1057 					while (Character.isJavaIdentifierPart(c)) {
1058 						read();
1059 					}
1060 					if (c == ':') {
1061 						read();
1062 						if (c == ':') {
1063 							throw lexerException("A namespace-qualified name may contain only one :: separator.");
1064 						}
1065 						text.setLength(text.length() - 1);
1066 						pendingColon = true;
1067 					}
1068 				}
1069 			}
1070 			// check for certain keywords
1071 			// extensions override built-in stuff
1072 			String sourceIdentifier = text.toString();
1073 			int namespaceSeparator = sourceIdentifier.indexOf("::");
1074 			if (namespaceSeparator >= 0) {
1075 				String namespaceComponent = sourceIdentifier.substring(namespaceSeparator + 2);
1076 				if (KEYWORDS.containsKey(namespaceComponent)
1077 						|| BuiltinFunction.of(namespaceComponent) != null) {
1078 					throw lexerException(
1079 							"Reserved word cannot be used after a namespace separator: "
1080 									+ sourceIdentifier);
1081 				}
1082 			}
1083 			String lookupIdentifier = awkNamespaceComponent(sourceIdentifier);
1084 			boolean awkNamespaceIdentifier = isAwkNamespaceIdentifier(sourceIdentifier);
1085 			boolean unqualifiedIdentifier = namespaceSeparator < 0;
1086 			if (awkNamespaceIdentifier && extensions.get(lookupIdentifier) != null) {
1087 				text.setLength(0);
1088 				text.append(lookupIdentifier);
1089 				token = Token.EXTENSION;
1090 				return token;
1091 			}
1092 			Token kwToken = KEYWORDS.get(sourceIdentifier);
1093 			if (kwToken != null && !isDisabledKeyword(kwToken)) {
1094 				token = kwToken;
1095 				return token;
1096 			}
1097 			if ((unqualifiedIdentifier || awkNamespaceIdentifier)
1098 					&& BuiltinFunction.of(lookupIdentifier) != null) {
1099 				text.setLength(0);
1100 				text.append(lookupIdentifier);
1101 				token = Token.BUILTIN_FUNC_NAME;
1102 				return token;
1103 			}
1104 			if (c == '(' && !pendingColon) {
1105 				token = Token.FUNC_ID;
1106 				return token;
1107 			} else {
1108 				token = Token.ID;
1109 				return token;
1110 			}
1111 		}
1112 
1113 		if (c == ';') {
1114 			read();
1115 			while (c == ' ' || c == '\t' || c == '\n' || c == '#') {
1116 				if (c == '\n') {
1117 					break;
1118 				}
1119 				if (c == '#') {
1120 					while (c >= 0 && c != '\n') {
1121 						read();
1122 					}
1123 					if (c == '\n') {
1124 						read();
1125 					}
1126 				} else {
1127 					read();
1128 				}
1129 			}
1130 			token = Token.SEMICOLON;
1131 			return token;
1132 		}
1133 
1134 		if (c == '\n') {
1135 			read();
1136 			while (c == ' ' || c == '\t' || c == '#' || c == '\n') {
1137 				if (c == '#') {
1138 					while (c >= 0 && c != '\n') {
1139 						read();
1140 					}
1141 				}
1142 				read();
1143 			}
1144 			token = Token.NEWLINE;
1145 			return token;
1146 		}
1147 
1148 		if (c == '"') {
1149 			// string
1150 			read();
1151 			readString();
1152 			token = Token.STRING;
1153 			return token;
1154 		}
1155 
1156 		/*
1157 		 * if (c == '\\') {
1158 		 * c = reader.read();
1159 		 * // completely bypass \r's
1160 		 * while(c == '\r') c = reader.read();
1161 		 * if (c<0)
1162 		 * chr=0; // eof
1163 		 * else
1164 		 * chr=c;
1165 		 * }
1166 		 */
1167 
1168 		throw lexerException("Invalid character (" + c + "): " + ((char) c));
1169 	}
1170 
1171 	// SUPPORTING FUNCTIONS/METHODS
1172 	private void terminator() throws IOException {
1173 		// like optTerminator, except error if no terminator was found
1174 		if (!optTerminator()) {
1175 			throw parserException("Expecting statement terminator. Got " + token.name() + ": " + text);
1176 		}
1177 	}
1178 
1179 	private boolean optTerminator() throws IOException {
1180 		if (optNewline()) {
1181 			return true;
1182 		} else if (token == Token.EOF || token == Token.CLOSE_BRACE) {
1183 			return true; // do nothing
1184 		} else if (token == Token.SEMICOLON) {
1185 			lexer();
1186 			return true;
1187 		} else {
1188 			// no terminator consumed
1189 			return false;
1190 		}
1191 	}
1192 
1193 	private boolean optNewline() throws IOException {
1194 		if (token == Token.NEWLINE) {
1195 			lexer();
1196 			return true;
1197 		} else {
1198 			return false;
1199 		}
1200 	}
1201 
1202 	// RECURSIVE DECENT PARSER:
1203 	// CHECKSTYLE.OFF: MethodName
1204 	// SCRIPT : \n [RULE_LIST] Token.EOF
1205 	AST SCRIPT() throws IOException {
1206 		AST rl;
1207 		if (token != Token.EOF) {
1208 			rl = RULE_LIST();
1209 		} else {
1210 			rl = null;
1211 		}
1212 		lexer(Token.EOF);
1213 		return rl;
1214 	}
1215 
1216 	// EXPRESSION_TO_EVALUATE: [TERNARY_EXPRESSION] Token.EOF
1217 	// Used to parse simple expressions to evaluate instead of full scripts
1218 	AST EXPRESSION_TO_EVALUATE() throws IOException {
1219 		AST exprAst = token != Token.EOF ? TERNARY_EXPRESSION(null, true, false, true) : null;
1220 		lexer(Token.EOF);
1221 		return new ExpressionToEvaluateAst(exprAst);
1222 	}
1223 
1224 	// RULE_LIST : \n [ ( RULE | FUNCTION terminator ) optTerminator RULE_LIST ]
1225 	AST RULE_LIST() throws IOException {
1226 		optNewline();
1227 		AST ruleOrFunction = null;
1228 		if (token == Token.DIRECTIVE_INCLUDE) {
1229 			INCLUDE_DIRECTIVE();
1230 			return RULE_LIST();
1231 		} else if (token == Token.DIRECTIVE_NAMESPACE) {
1232 			NAMESPACE_DIRECTIVE();
1233 			return RULE_LIST();
1234 		} else if (token == Token.DIRECTIVE_UNSUPPORTED) {
1235 			throw parserException("Unsupported gawk directive: " + text);
1236 		} else if (token == Token.KW_FUNCTION) {
1237 			ruleOrFunction = FUNCTION();
1238 		} else if (token != Token.EOF) {
1239 			ruleOrFunction = RULE();
1240 		} else {
1241 			return null;
1242 		}
1243 		optTerminator(); // newline or ; (maybe)
1244 		return new RuleListAst(ruleOrFunction, RULE_LIST());
1245 	}
1246 
1247 	private void NAMESPACE_DIRECTIVE() throws IOException {
1248 		lexer();
1249 		if (token != Token.STRING) {
1250 			throw parserException("@namespace requires a quoted namespace name.");
1251 		}
1252 		String namespace = string.toString();
1253 		validateNamespace(namespace);
1254 		currentNamespace = namespace;
1255 		lexer();
1256 		terminator();
1257 	}
1258 
1259 	private void INCLUDE_DIRECTIVE() throws IOException {
1260 		lexer();
1261 		if (token != Token.STRING) {
1262 			throw parserException("@include requires a quoted file name.");
1263 		}
1264 		if (!sourceIncludeAllowed) {
1265 			throw new AwkSandboxException("@include is disabled in sandbox mode");
1266 		}
1267 		String includeName = string.toString();
1268 		boolean includeTerminatedByEndOfSource = validateIncludeTerminator();
1269 		Path includePath = resolveIncludePath(includeName);
1270 		if (topLevelSourcePaths.contains(includePath)) {
1271 			throw parserException(
1272 					"Cannot include a top-level program source: " + includeName);
1273 		}
1274 		if (!includedSourcePaths.add(includePath)) {
1275 			lexer();
1276 			if (!includeTerminatedByEndOfSource) {
1277 				terminator();
1278 			}
1279 			return;
1280 		}
1281 		includedSourceStack.push(new SourceState(currentScriptSource, reader, c, currentNamespace));
1282 		currentScriptSource = new ScriptSource(
1283 				includePath.toString(),
1284 				Files.newBufferedReader(includePath, StandardCharsets.UTF_8));
1285 		reader = new LineNumberReader(currentScriptSource.getReader());
1286 		currentNamespace = "awk";
1287 		c = reader.read();
1288 		while (c == '\r') {
1289 			c = reader.read();
1290 		}
1291 		advancePastEndOfSource();
1292 		lexer();
1293 	}
1294 
1295 	private boolean validateIncludeTerminator() throws IOException {
1296 		while (c == ' ' || c == '\t') {
1297 			read();
1298 		}
1299 		if (c == '#') {
1300 			while (c >= 0 && c != '\n') {
1301 				read();
1302 			}
1303 		}
1304 		if (c >= 0 && c != '\n' && c != ';') {
1305 			throw parserException("@include must be followed by a newline, semicolon, or end of file.");
1306 		}
1307 		return c < 0;
1308 	}
1309 
1310 	private void validateNamespace(String namespace) {
1311 		if (namespace == null
1312 				|| namespace.isEmpty()
1313 				|| !Character.isJavaIdentifierStart(namespace.charAt(0))) {
1314 			throw parserException("Invalid gawk namespace name: " + namespace);
1315 		}
1316 		for (int i = 1; i < namespace.length(); i++) {
1317 			if (!Character.isJavaIdentifierPart(namespace.charAt(i))) {
1318 				throw parserException("Invalid gawk namespace name: " + namespace);
1319 			}
1320 		}
1321 		if (KEYWORDS.containsKey(namespace)
1322 				|| BuiltinFunction.of(namespace) != null
1323 				|| extensions.containsKey(namespace)) {
1324 			throw parserException("Reserved identifier cannot be used as a gawk namespace: " + namespace);
1325 		}
1326 	}
1327 
1328 	private void validateIndirectIdentifier(String identifier) throws LexerException {
1329 		int separator = identifier.indexOf("::");
1330 		String namespace = separator < 0 ? "awk" : identifier.substring(0, separator);
1331 		String component = separator < 0 ? identifier : identifier.substring(separator + 2);
1332 		if (KEYWORDS.containsKey(component)
1333 				|| BuiltinFunction.of(component) != null
1334 				|| ("awk".equals(namespace) && extensions.containsKey(component))) {
1335 			throw lexerException("Reserved identifier cannot be used as an indirect-call selector: " + identifier);
1336 		}
1337 	}
1338 
1339 	private Path resolveIncludePath(String includeName) {
1340 		Path requested = Paths.get(includeName);
1341 		List<Path> candidates = new ArrayList<Path>();
1342 		if (requested.isAbsolute()) {
1343 			candidates.add(requested);
1344 		} else {
1345 			if (!ScriptSource.DESCRIPTION_COMMAND_LINE_SCRIPT.equals(currentScriptSource.getDescription())) {
1346 				try {
1347 					Path sourcePath = Paths.get(currentScriptSource.getDescription());
1348 					Path parent = sourcePath.toAbsolutePath().normalize().getParent();
1349 					if (parent != null) {
1350 						candidates.add(parent.resolve(requested));
1351 					}
1352 				} catch (InvalidPathException ignored) {
1353 					// Reader-backed ScriptSource values may use a descriptive
1354 					// label rather than a file path.
1355 				}
1356 			}
1357 			String awkPath = System.getenv("AWKPATH");
1358 			if (awkPath != null) {
1359 				for (String entry : awkPath.split(java.util.regex.Pattern.quote(File.pathSeparator), -1)) {
1360 					candidates.add(Paths.get(entry.isEmpty() ? "." : entry).resolve(requested));
1361 				}
1362 			}
1363 			candidates.add(requested);
1364 		}
1365 		for (Path candidate : candidates) {
1366 			Path normalized = candidate.toAbsolutePath().normalize();
1367 			if (Files.isRegularFile(normalized)) {
1368 				try {
1369 					return normalized.toRealPath();
1370 				} catch (IOException ignored) {
1371 					// The candidate may have disappeared between the existence
1372 					// check and canonicalization; continue searching AWKPATH.
1373 				}
1374 			}
1375 		}
1376 		throw parserException("Cannot find @include file: " + includeName);
1377 	}
1378 
1379 	// FUNCTION: function functionName( [FORMAL_PARAM_LIST] ) STATEMENT_LIST
1380 	AST FUNCTION() throws IOException {
1381 		expectKeyword("function");
1382 		String functionName;
1383 		if (token == Token.FUNC_ID || token == Token.ID) {
1384 			functionName = qualifyGlobalIdentifier(text.toString());
1385 			lexer();
1386 		} else {
1387 			throw parserException("Expecting function name. Got " + token.name() + ": " + text);
1388 		}
1389 		symbolTable.setFunctionName(functionName);
1390 		lexer(Token.OPEN_PAREN);
1391 		AST formalParamList;
1392 		if (token == Token.CLOSE_PAREN) {
1393 			formalParamList = null;
1394 		} else {
1395 			formalParamList = FORMAL_PARAM_LIST(functionName);
1396 		}
1397 		lexer(Token.CLOSE_PAREN);
1398 		optNewline();
1399 
1400 		lexer(Token.OPEN_BRACE);
1401 		AST functionBlock = STATEMENT_LIST();
1402 		lexer(Token.CLOSE_BRACE);
1403 		symbolTable.clearFunctionName(functionName);
1404 		return symbolTable.addFunctionDef(functionName, formalParamList, functionBlock);
1405 	}
1406 
1407 	// FORMAT_PARAM_LIST:
1408 	AST FORMAL_PARAM_LIST(String functionName) throws IOException {
1409 		if (token == Token.ID) {
1410 			String id = text.toString();
1411 			symbolTable.addFunctionParameter(functionName, id);
1412 			lexer();
1413 			if (token == Token.COMMA) {
1414 				lexer();
1415 				optNewline();
1416 				AST rest = FORMAL_PARAM_LIST(functionName);
1417 				if (rest == null) {
1418 					throw parserException("Cannot terminate a formal parameter list with a comma.");
1419 				} else {
1420 					return new FunctionDefParamListAst(id, rest);
1421 				}
1422 			} else {
1423 				return new FunctionDefParamListAst(id, null);
1424 			}
1425 		} else {
1426 			return null;
1427 		}
1428 	}
1429 
1430 	// RULE : [ASSIGNMENT_EXPRESSION] [ { STATEMENT_LIST } ]
1431 	AST RULE() throws IOException {
1432 		AST optExpr;
1433 		AST optStmts;
1434 		if (token == Token.KW_BEGIN) {
1435 			lexer();
1436 			optExpr = symbolTable.addBEGIN();
1437 		} else if (token == Token.KW_END) {
1438 			lexer();
1439 			optExpr = symbolTable.addEND();
1440 		} else if (token == Token.KW_BEGINFILE) {
1441 			lexer();
1442 			optExpr = symbolTable.addBEGINFILE();
1443 		} else if (token == Token.KW_ENDFILE) {
1444 			lexer();
1445 			optExpr = symbolTable.addENDFILE();
1446 		} else if (token != Token.OPEN_BRACE && token != Token.SEMICOLON && token != Token.NEWLINE && token != Token.EOF) {
1447 			// true = allow comparators, allow IN keyword, do Token.NOT allow multidim indices expressions
1448 			optExpr = ASSIGNMENT_EXPRESSION(null, true, true, false);
1449 			// for ranges, like conditionStart, conditionEnd
1450 			if (token == Token.COMMA) {
1451 				lexer();
1452 				optNewline();
1453 				// true = allow comparators, allow IN keyword, do Token.NOT allow multidim indices expressions
1454 				optExpr = new ConditionPairAst(
1455 						optExpr,
1456 						ASSIGNMENT_EXPRESSION(null, true, true, false));
1457 			}
1458 		} else {
1459 			optExpr = null;
1460 		}
1461 		if (token == Token.OPEN_BRACE) {
1462 			lexer();
1463 			optStmts = STATEMENT_LIST();
1464 			lexer(Token.CLOSE_BRACE);
1465 		} else {
1466 			optStmts = null;
1467 		}
1468 		return new RuleAst(optExpr, optStmts);
1469 	}
1470 
1471 	// STATEMENT_LIST : [ STATEMENT_BLOCK|STATEMENT STATEMENT_LIST ]
1472 	private AST STATEMENT_LIST() throws IOException {
1473 		// statement lists can only live within curly brackets (braces)
1474 		optNewline();
1475 		if (token == Token.CLOSE_BRACE || token == Token.EOF) {
1476 			return null;
1477 		}
1478 		AST stmt;
1479 		if (token == Token.OPEN_BRACE) {
1480 			lexer();
1481 			stmt = STATEMENT_LIST();
1482 			lexer(Token.CLOSE_BRACE);
1483 		} else {
1484 			if (token == Token.SEMICOLON) {
1485 				// an empty statement (;)
1486 				// do not polute the syntax tree with nulls in this case
1487 				// just return the next statement (recursively)
1488 				lexer();
1489 				return STATEMENT_LIST();
1490 			} else {
1491 				stmt = STATEMENT();
1492 			}
1493 		}
1494 
1495 		AST rest = STATEMENT_LIST();
1496 		if (rest == null) {
1497 			return stmt;
1498 		} else if (stmt == null) {
1499 			return rest;
1500 		} else {
1501 			return new StatementListAst(stmt, rest);
1502 		}
1503 	}
1504 
1505 	/**
1506 	 * Parse a (possibly comma-separated) list of ASSIGNMENT_EXPRESSIONs.
1507 	 *
1508 	 * @param allowComparisons
1509 	 *        – true ⇒ treat ‘>’ and ‘<’ as comparison operators
1510 	 *        – false ⇒ treat ‘>’ and ‘<’ as redirection tokens (break out)
1511 	 * @param allowInKeyword
1512 	 *        – true ⇒ allow the “in” keyword inside expressions
1513 	 *        – false ⇒ disallow “in”
1514 	 */
1515 	AST EXPRESSION_LIST(boolean allowComparisons, boolean allowInKeyword) throws IOException {
1516 		// 1) Parse exactly one assignment expression.
1517 		// Passing `allowComparisons` will decide if ‘>’/’<’ become comparisons or redirectors.
1518 		AST expr = ASSIGNMENT_EXPRESSION(null, allowComparisons, allowInKeyword, /* allowMultidim= */ false);
1519 
1520 		// 2) If the next token is a comma, consume it and build the rest of the list.
1521 		// This supports both regular function calls and print/printf argument lists.
1522 		if (token == Token.COMMA) {
1523 			lexer(); // consume ','
1524 			optNewline(); // allow newline after comma (AWK style)
1525 
1526 			AST rest = EXPRESSION_LIST(allowComparisons, allowInKeyword);
1527 			return new FunctionCallParamListAst(expr, rest);
1528 		}
1529 
1530 		// 3) No comma ⇒ this single expression is a one‐element list.
1531 		return new FunctionCallParamListAst(expr, null);
1532 	}
1533 
1534 	private AST ASSIGNMENT_EXPRESSION(
1535 			AST left,
1536 			boolean allowComparison,
1537 			boolean allowInKeyword,
1538 			boolean allowMultidimIndices)
1539 			throws IOException {
1540 		AST ternaryExpression = TERNARY_EXPRESSION(left, allowComparison, allowInKeyword, allowMultidimIndices);
1541 		AST result = ternaryExpression;
1542 		if (token == Token.EQUALS
1543 				|| token == Token.PLUS_EQ
1544 				|| token == Token.MINUS_EQ
1545 				|| token == Token.MULT_EQ
1546 				|| token == Token.DIV_EQ
1547 				|| token == Token.MOD_EQ
1548 				|| token == Token.POW_EQ) {
1549 			Token op = token;
1550 			String txt = text.toString();
1551 			lexer();
1552 			// An assignment RHS is a single expression: a following comma belongs to
1553 			// the enclosing grouping, as in ((y=1, 2) in a), where the group elements
1554 			// are (y=1) and (2)
1555 			AST assignmentExpression = ASSIGNMENT_EXPRESSION(
1556 					null,
1557 					allowComparison,
1558 					allowInKeyword,
1559 					false);
1560 			result = new AssignmentExpressionAst(ternaryExpression, op, txt, assignmentExpression);
1561 		}
1562 		// ASSIGNMENT_EXPRESSION [, ASSIGNMENT_EXPRESSION] !!!ONLY IF!!! allowMultidimIndices is true
1563 		// allowMultidimIndices is set to true when we need (1,2,3,4) expressions to collapse into an array index
1564 		// expression (converts 1,2,3,4 to 1 SUBSEP 2 SUBSEP 3 SUBSEP 4) after an open parenthesis (grouping)
1565 		// expression starter
1566 		if (allowMultidimIndices && token == Token.COMMA) {
1567 			lexer();
1568 			optNewline();
1569 			AST rest = ASSIGNMENT_EXPRESSION(null, allowComparison, allowInKeyword, true);
1570 			if (rest instanceof ArrayIndexAst) {
1571 				return new ArrayIndexAst(result, rest);
1572 			}
1573 			return new ArrayIndexAst(result, new ArrayIndexAst(rest, null));
1574 		}
1575 		return result;
1576 	}
1577 
1578 	// TERNARY_EXPRESSION = LOGICAL_OR_EXPRESSION [ ? TERNARY_EXPRESSION : TERNARY_EXPRESSION ]
1579 	private AST TERNARY_EXPRESSION(
1580 			AST left,
1581 			boolean allowComparison,
1582 			boolean allowInKeyword,
1583 			boolean allowMultidimIndices)
1584 			throws IOException {
1585 		AST condition = LOGICAL_OR_EXPRESSION(left, allowComparison, allowInKeyword, allowMultidimIndices);
1586 		if (token == Token.QUESTION_MARK) {
1587 			lexer();
1588 			AST trueBlock = TERNARY_EXPRESSION(null, allowComparison, allowInKeyword, allowMultidimIndices);
1589 			lexer(Token.COLON);
1590 			AST falseBlock = TERNARY_EXPRESSION(null, allowComparison, allowInKeyword, allowMultidimIndices);
1591 			return new TernaryExpressionAst(condition, trueBlock, falseBlock);
1592 		}
1593 		return condition;
1594 	}
1595 
1596 	// LOGICAL_OR_EXPRESSION = LOGICAL_AND_EXPRESSION [ || LOGICAL_OR_EXPRESSION ]
1597 	private AST LOGICAL_OR_EXPRESSION(
1598 			AST left,
1599 			boolean allowComparison,
1600 			boolean allowInKeyword,
1601 			boolean allowMultidimIndices)
1602 			throws IOException {
1603 		AST result = LOGICAL_AND_EXPRESSION(left, allowComparison, allowInKeyword, allowMultidimIndices);
1604 		while (token == Token.OR) {
1605 			Token op = token;
1606 			String txt = text.toString();
1607 			lexer();
1608 			AST rhs = LOGICAL_OR_EXPRESSION(null, allowComparison, allowInKeyword, allowMultidimIndices);
1609 			result = new LogicalExpressionAst(result, op, txt, rhs);
1610 		}
1611 		return result;
1612 	}
1613 
1614 	// LOGICAL_AND_EXPRESSION = IN_EXPRESSION [ && LOGICAL_AND_EXPRESSION ]
1615 	private AST LOGICAL_AND_EXPRESSION(
1616 			AST left,
1617 			boolean allowComparison,
1618 			boolean allowInKeyword,
1619 			boolean allowMultidimIndices)
1620 			throws IOException {
1621 		AST result = IN_EXPRESSION(left, allowComparison, allowInKeyword, allowMultidimIndices);
1622 		while (token == Token.AND) {
1623 			Token op = token;
1624 			String txt = text.toString();
1625 			lexer();
1626 			AST rhs = LOGICAL_AND_EXPRESSION(null, allowComparison, allowInKeyword, allowMultidimIndices);
1627 			result = new LogicalExpressionAst(result, op, txt, rhs);
1628 		}
1629 		return result;
1630 	}
1631 
1632 	// IN_EXPRESSION = MATCHING_EXPRESSION [ IN_EXPRESSION ]
1633 	// allowInKeyword is set false while parsing the first expression within
1634 	// a for() statement (because it could be "for (key in arr)", and this
1635 	// production will consume and the for statement will never have a chance
1636 	// of processing it
1637 	// all other times, it is true
1638 	private AST IN_EXPRESSION(
1639 			AST left,
1640 			boolean allowComparison,
1641 			boolean allowInKeyword,
1642 			boolean allowMultidimIndices)
1643 			throws IOException {
1644 		AST result = MATCHING_EXPRESSION(left, allowComparison, allowInKeyword, allowMultidimIndices);
1645 		if (allowInKeyword && token == Token.KW_IN) {
1646 			lexer();
1647 			result = new InExpressionAst(
1648 					result,
1649 					IN_EXPRESSION(null, allowComparison, allowInKeyword, allowMultidimIndices));
1650 		}
1651 		return result;
1652 	}
1653 
1654 	// MATCHING_EXPRESSION = COMPARISON_EXPRESSION [ (~,!~) MATCHING_EXPRESSION ]
1655 	private AST MATCHING_EXPRESSION(
1656 			AST left,
1657 			boolean allowComparison,
1658 			boolean allowInKeyword,
1659 			boolean allowMultidimIndices)
1660 			throws IOException {
1661 		AST result = COMPARISON_EXPRESSION(left, allowComparison, allowInKeyword, allowMultidimIndices);
1662 		while (token == Token.MATCHES || token == Token.NOT_MATCHES) {
1663 			Token op = token;
1664 			String txt = text.toString();
1665 			lexer();
1666 			AST rhs = MATCHING_EXPRESSION(null, allowComparison, allowInKeyword, allowMultidimIndices);
1667 			result = new ComparisonExpressionAst(result, op, txt, rhs);
1668 		}
1669 		return result;
1670 	}
1671 
1672 	// COMPARISON_EXPRESSION = CONCAT_EXPRESSION [ (==,>,>=,<,<=,!=,|) COMPARISON_EXPRESSION ]
1673 	// allowComparison is set false when within a print/printf statement;
1674 	// all other times it is set true
1675 	private AST COMPARISON_EXPRESSION(
1676 			AST left,
1677 			boolean allowComparison,
1678 			boolean allowInKeyword,
1679 			boolean allowMultidimIndices)
1680 			throws IOException {
1681 		AST result = CONCAT_EXPRESSION(left, allowComparison, allowInKeyword, allowMultidimIndices);
1682 		if (token == Token.EQ
1683 				|| token == Token.GE
1684 				|| token == Token.LT
1685 				|| token == Token.LE
1686 				|| token == Token.NE
1687 				|| (token == Token.GT && allowComparison)) {
1688 			Token op = token;
1689 			String txt = text.toString();
1690 			lexer();
1691 			AST rhs = COMPARISON_EXPRESSION(null, allowComparison, allowInKeyword, allowMultidimIndices);
1692 			return new ComparisonExpressionAst(result, op, txt, rhs);
1693 		} else if (allowComparison && token == Token.PIPE) {
1694 			lexer();
1695 			return GETLINE_EXPRESSION(result, allowComparison, allowInKeyword);
1696 		}
1697 
1698 		return result;
1699 	}
1700 
1701 	// CONCAT_EXPRESSION = EXPRESSION [ CONCAT_EXPRESSION ]
1702 	private AST CONCAT_EXPRESSION(
1703 			AST left,
1704 			boolean allowComparison,
1705 			boolean allowInKeyword,
1706 			boolean allowMultidimIndices)
1707 			throws IOException {
1708 		AST result = EXPRESSION(left, allowComparison, allowInKeyword, allowMultidimIndices);
1709 		if (token == Token.INTEGER
1710 				|| token == Token.DOUBLE
1711 				|| token == Token.OPEN_PAREN
1712 				|| token == Token.FUNC_ID
1713 				|| token == Token.INC
1714 				|| token == Token.DEC
1715 				|| token == Token.ID
1716 				|| token == Token.STRING
1717 				|| token == Token.DOLLAR
1718 				|| token == Token.BUILTIN_FUNC_NAME
1719 				|| token == Token.EXTENSION) {
1720 			return new ConcatExpressionAst(
1721 					result,
1722 					CONCAT_EXPRESSION(null, allowComparison, allowInKeyword, allowMultidimIndices));
1723 		}
1724 		return result;
1725 	}
1726 
1727 	// EXPRESSION : TERM [ (+|-) EXPRESSION ]
1728 	private AST EXPRESSION(
1729 			AST left,
1730 			boolean allowComparison,
1731 			boolean allowInKeyword,
1732 			boolean allowMultidimIndices)
1733 			throws IOException {
1734 		AST result = TERM(left, allowComparison, allowInKeyword, allowMultidimIndices);
1735 		while (token == Token.PLUS || token == Token.MINUS) {
1736 			Token op = token;
1737 			String txt = text.toString();
1738 			lexer();
1739 			AST nextTerm = TERM(null, allowComparison, allowInKeyword, allowMultidimIndices);
1740 			result = new BinaryExpressionAst(result, op, txt, nextTerm);
1741 		}
1742 		return result;
1743 	}
1744 
1745 	// TERM : UNARY_FACTOR [ (*|/|%) TERM ]
1746 	private AST TERM(
1747 			AST left,
1748 			boolean allowComparison,
1749 			boolean allowInKeyword,
1750 			boolean allowMultidimIndices)
1751 			throws IOException {
1752 		AST result = (left == null) ? UNARY_FACTOR(allowComparison, allowInKeyword, allowMultidimIndices) : left;
1753 		while (token == Token.MULT || token == Token.DIVIDE || token == Token.MOD) {
1754 			Token op = token;
1755 			String txt = text.toString();
1756 			lexer();
1757 			AST nextUnaryFactor = UNARY_FACTOR(allowComparison, allowInKeyword, allowMultidimIndices);
1758 			result = new BinaryExpressionAst(result, op, txt, nextUnaryFactor);
1759 		}
1760 		return result;
1761 	}
1762 
1763 	// UNARY_FACTOR : [ ! | - | + ] POWER_FACTOR
1764 	AST UNARY_FACTOR(boolean allowComparison, boolean allowInKeyword, boolean allowMultidimIndices)
1765 			throws IOException {
1766 		if (token == Token.NOT) {
1767 			lexer();
1768 			return new NotExpressionAst(POWER_FACTOR(null, allowComparison, allowInKeyword, allowMultidimIndices));
1769 		} else if (token == Token.MINUS) {
1770 			lexer();
1771 			return new NegativeExpressionAst(
1772 					POWER_FACTOR(null, allowComparison, allowInKeyword, allowMultidimIndices));
1773 		} else if (token == Token.PLUS) {
1774 			lexer();
1775 			return new UnaryPlusExpressionAst(
1776 					POWER_FACTOR(null, allowComparison, allowInKeyword, allowMultidimIndices));
1777 		} else {
1778 			return POWER_FACTOR(null, allowComparison, allowInKeyword, allowMultidimIndices);
1779 		}
1780 	}
1781 
1782 	// POWER_FACTOR : FACTOR_FOR_INCDEC [ ^ POWER_FACTOR ]
1783 	private AST POWER_FACTOR(
1784 			AST left,
1785 			boolean allowComparison,
1786 			boolean allowInKeyword,
1787 			boolean allowMultidimIndices)
1788 			throws IOException {
1789 		AST result = (left == null) ? FACTOR_FOR_INCDEC(allowComparison, allowInKeyword, allowMultidimIndices) : left;
1790 		if (token == Token.POW) {
1791 			Token op = token;
1792 			String txt = text.toString();
1793 			lexer();
1794 			AST rhs = POWER_FACTOR(null, allowComparison, allowInKeyword, allowMultidimIndices);
1795 			return new BinaryExpressionAst(result, op, txt, rhs);
1796 		}
1797 		return result;
1798 	}
1799 
1800 	// according to the spec, pre/post inc can occur
1801 	// only on lvalues, which are NAMES (IDs), array,
1802 	// or field references
1803 	private boolean isLvalue(AST ast) {
1804 		return (ast instanceof IDAst) || (ast instanceof ArrayReferenceAst) || (ast instanceof DollarExpressionAst);
1805 	}
1806 
1807 	AST FACTOR_FOR_INCDEC(boolean allowComparison, boolean allowInKeyword, boolean allowMultidimIndices)
1808 			throws IOException {
1809 		boolean preInc = false;
1810 		boolean preDec = false;
1811 		boolean postInc = false;
1812 		boolean postDec = false;
1813 		if (token == Token.INC) {
1814 			preInc = true;
1815 			lexer();
1816 		} else if (token == Token.DEC) {
1817 			preDec = true;
1818 			lexer();
1819 		}
1820 
1821 		AST factorAst = FACTOR(allowComparison, allowInKeyword, allowMultidimIndices);
1822 
1823 		if ((preInc || preDec) && !isLvalue(factorAst)) {
1824 			throw parserException("Cannot pre inc/dec a non-lvalue");
1825 		}
1826 
1827 		// only do post ops if:
1828 		// - factorAst is an lvalue
1829 		// - pre ops were not encountered
1830 		if (isLvalue(factorAst) && !preInc && !preDec) {
1831 			if (token == Token.INC) {
1832 				postInc = true;
1833 				lexer();
1834 			} else if (token == Token.DEC) {
1835 				postDec = true;
1836 				lexer();
1837 			}
1838 		}
1839 
1840 		if ((preInc || preDec) && (postInc || postDec)) {
1841 			throw parserException("Cannot do pre inc/dec Token.AND post inc/dec.");
1842 		}
1843 
1844 		if (preInc) {
1845 			return new PreIncAst(factorAst);
1846 		} else if (preDec) {
1847 			return new PreDecAst(factorAst);
1848 		} else if (postInc) {
1849 			return new PostIncAst(factorAst);
1850 		} else if (postDec) {
1851 			return new PostDecAst(factorAst);
1852 		} else {
1853 			return factorAst;
1854 		}
1855 	}
1856 
1857 	// FACTOR : '(' ASSIGNMENT_EXPRESSION ')' | Token.INTEGER | Token.DOUBLE | Token.STRING | GETLINE
1858 	// [Token.ID-or-array-or-$val] | /[=].../
1859 	// | [++|--] SYMBOL [++|--]
1860 	// AST FACTOR(boolean allowComparison, boolean allowInKeyword, boolean allow_post_incdec_operators)
1861 	AST FACTOR(boolean allowComparison, boolean allowInKeyword, boolean allowMultidimIndices) throws IOException {
1862 		if (token == Token.OPEN_PAREN) {
1863 			lexer();
1864 			// true = allow multi-dimensional array indices (i.e., commas for 1,2,3,4)
1865 			AST assignmentExpression = ASSIGNMENT_EXPRESSION(null, true, allowInKeyword, true);
1866 			lexer(Token.CLOSE_PAREN);
1867 			if (assignmentExpression instanceof ArrayIndexAst && !(allowInKeyword && token == Token.KW_IN)) {
1868 				// (expr, expr, ...) is a grouping, not an expression: it is only valid
1869 				// immediately before "in", as in ((i, j) in array). This also rejects
1870 				// nested groupings such as (1, (2, 3)), whose inner group is followed
1871 				// by ')' rather than "in".
1872 				throw parserException("A parenthesized expression list is only valid before 'in'.");
1873 			}
1874 			return assignmentExpression;
1875 		} else if (token == Token.INTEGER) {
1876 			AST integer = symbolTable.addINTEGER(text.toString());
1877 			lexer();
1878 			return integer;
1879 		} else if (token == Token.DOUBLE) {
1880 			AST dbl = symbolTable.addDOUBLE(text.toString());
1881 			lexer();
1882 			return dbl;
1883 		} else if (token == Token.STRING) {
1884 			AST str = symbolTable.addSTRING(string.toString());
1885 			lexer();
1886 			return str;
1887 		} else if (token == Token.INDIRECT) {
1888 			return INDIRECT_FUNCTION_CALL(allowInKeyword);
1889 		} else if (token == Token.TYPED_REGEXP) {
1890 			AST regexpAst = symbolTable.addTYPED_REGEXP(regexp.toString());
1891 			lexer();
1892 			return regexpAst;
1893 		} else if (token == Token.KW_GETLINE) {
1894 			return GETLINE_EXPRESSION(null, allowComparison, allowInKeyword);
1895 		} else if (token == Token.DIVIDE || token == Token.DIV_EQ) {
1896 			readRegexp();
1897 			if (token == Token.DIV_EQ) {
1898 				regexp.insert(0, '=');
1899 			}
1900 			AST regexpAst = symbolTable.addREGEXP(regexp.toString());
1901 			lexer();
1902 			return regexpAst;
1903 		} else {
1904 			if (token == Token.DOLLAR) {
1905 				lexer();
1906 				if (token == Token.INC || token == Token.DEC) {
1907 					return new DollarExpressionAst(
1908 							FACTOR_FOR_INCDEC(allowComparison, allowInKeyword, allowMultidimIndices));
1909 				}
1910 				if (token == Token.NOT || token == Token.MINUS || token == Token.PLUS) {
1911 					return new DollarExpressionAst(UNARY_FACTOR(allowComparison, allowInKeyword, allowMultidimIndices));
1912 				}
1913 				return new DollarExpressionAst(FACTOR(allowComparison, allowInKeyword, allowMultidimIndices));
1914 			}
1915 			return SYMBOL(allowComparison, allowInKeyword);
1916 		}
1917 	}
1918 
1919 	private AST INDIRECT_FUNCTION_CALL(boolean allowInKeyword) throws IOException {
1920 		lexer();
1921 		if (token != Token.ID) {
1922 			throw parserException("An indirect function call requires a variable name after @.");
1923 		}
1924 		AST functionNameAst = symbolTable.getID(text.toString());
1925 		lexer();
1926 		lexer(Token.OPEN_PAREN);
1927 		AST params = token == Token.CLOSE_PAREN ? null : EXPRESSION_LIST(true, allowInKeyword);
1928 		lexer(Token.CLOSE_PAREN);
1929 		return new IndirectFunctionCallAst(functionNameAst, params);
1930 	}
1931 
1932 	// SYMBOL : Token.ID [ '(' params ')' | '[' ASSIGNMENT_EXPRESSION ']' ]
1933 	AST SYMBOL(boolean allowComparison, boolean allowInKeyword) throws IOException {
1934 		if (token != Token.ID && token != Token.FUNC_ID && token != Token.BUILTIN_FUNC_NAME && token != Token.EXTENSION) {
1935 			throw parserException("Expecting an Token.ID. Got " + token.name() + ": " + text);
1936 		}
1937 		Token idToken = token;
1938 		String id = text.toString();
1939 		lexer();
1940 
1941 		if (idToken == Token.EXTENSION) {
1942 			String extensionKeyword = id;
1943 			ExtensionFunction function = extensions.get(extensionKeyword);
1944 			if (function == null) {
1945 				throw parserException("Unknown extension keyword: " + extensionKeyword);
1946 			}
1947 			AST params;
1948 
1949 			/*
1950 			 * if (extension.requiresParen()) {
1951 			 * lexer(Token.OPEN_PAREN);
1952 			 * if (token == Token.CLOSE_PAREN)
1953 			 * params = null;
1954 			 * else
1955 			 * params = EXPRESSION_LIST(allowComparison, allowInKeyword);
1956 			 * lexer(Token.CLOSE_PAREN);
1957 			 * } else {
1958 			 * boolean parens = c == '(';
1959 			 * //expectKeyword("delete");
1960 			 * if (parens) {
1961 			 * assert token == Token.OPEN_PAREN;
1962 			 * lexer();
1963 			 * }
1964 			 * //AST symbolAst = SYMBOL(true,true); // allow comparators
1965 			 * params = EXPRESSION_LIST(allowComparison, allowInKeyword);
1966 			 * if (parens)
1967 			 * lexer(Token.CLOSE_PAREN);
1968 			 * }
1969 			 */
1970 
1971 			// like the built-in functions (and gawk's own builtins, which is
1972 			// what these keywords stand in for), an extension call accepts
1973 			// whitespace between the keyword and its argument list
1974 			if (token == Token.OPEN_PAREN) {
1975 				lexer();
1976 				if (token == Token.CLOSE_PAREN) {
1977 					params = null;
1978 				} else { // comparators allowed, allow “in” inside the extension call
1979 					params = EXPRESSION_LIST(true, allowInKeyword);
1980 				}
1981 				lexer(Token.CLOSE_PAREN);
1982 			} else {
1983 				/*
1984 				 * if (token == Token.NEWLINE || token == Token.SEMICOLON || token == Token.CLOSE_BRACE || token ==
1985 				 * Token.CLOSE_PAREN
1986 				 * || (token == Token.GT || token == Token.APPEND || token == Token.PIPE) )
1987 				 * params = null;
1988 				 * else
1989 				 * params = EXPRESSION_LIST(false,true);
1990 				 */
1991 				params = null;
1992 			}
1993 
1994 			return new ExtensionAst(function, params, extensionCallLineNumber(params));
1995 		} else if (idToken == Token.FUNC_ID || idToken == Token.BUILTIN_FUNC_NAME) {
1996 			AST params;
1997 			// length can take on the special form of no parens
1998 			if (id.equals("length")) {
1999 				if (token == Token.OPEN_PAREN) {
2000 					lexer();
2001 					if (token == Token.CLOSE_PAREN) {
2002 						params = null;
2003 					} else {
2004 						params = EXPRESSION_LIST(true, allowInKeyword);
2005 					}
2006 					lexer(Token.CLOSE_PAREN);
2007 				} else {
2008 					params = null;
2009 				}
2010 			} else {
2011 				lexer(Token.OPEN_PAREN);
2012 				if (token == Token.CLOSE_PAREN) {
2013 					params = null;
2014 				} else {
2015 					params = EXPRESSION_LIST(true, allowInKeyword);
2016 				}
2017 				lexer(Token.CLOSE_PAREN);
2018 			}
2019 			if (idToken == Token.BUILTIN_FUNC_NAME) {
2020 				return new BuiltinFunctionCallAst(id, params);
2021 			} else {
2022 				return symbolTable.addFunctionCall(id, params);
2023 			}
2024 		}
2025 		if (token == Token.OPEN_BRACKET) {
2026 			int arrayReferenceLineNo = currentSourceLineNumber();
2027 			lexer();
2028 			AST idxAst = ARRAY_INDEX(true, allowInKeyword);
2029 			lexer(Token.CLOSE_BRACKET);
2030 			AST arrayReference = symbolTable.addArrayReference(id, idxAst, arrayReferenceLineNo);
2031 			if (posix && token == Token.OPEN_BRACKET) {
2032 				throw parserException("Use [a,b,c,...] instead of [a][b][c]... for multi-dimensional arrays.");
2033 			}
2034 			while (!posix && token == Token.OPEN_BRACKET) {
2035 				int nestedArrayReferenceLineNo = currentSourceLineNumber();
2036 				lexer();
2037 				idxAst = ARRAY_INDEX(true, allowInKeyword);
2038 				lexer(Token.CLOSE_BRACKET);
2039 				arrayReference = new ArrayReferenceAst(nestedArrayReferenceLineNo, arrayReference, idxAst);
2040 			}
2041 			return arrayReference;
2042 		}
2043 		return symbolTable.addID(id);
2044 	}
2045 
2046 	// ARRAY_INDEX : ASSIGNMENT_EXPRESSION [, ARRAY_INDEX]
2047 	AST ARRAY_INDEX(boolean allowComparison, boolean allowInKeyword) throws IOException {
2048 		AST exprAst = ASSIGNMENT_EXPRESSION(null, allowComparison, allowInKeyword, false);
2049 		if (token == Token.COMMA) {
2050 			optNewline();
2051 			lexer();
2052 			return new ArrayIndexAst(exprAst, ARRAY_INDEX(allowComparison, allowInKeyword));
2053 		} else {
2054 			return new ArrayIndexAst(exprAst, null);
2055 		}
2056 	}
2057 
2058 	// STATEMENT :
2059 	// IF_STATEMENT
2060 	// | WHILE_STATEMENT
2061 	// | FOR_STATEMENT
2062 	// | DO_STATEMENT
2063 	// | RETURN_STATEMENT
2064 	// | ASSIGNMENT_EXPRESSION
2065 	AST STATEMENT() throws IOException {
2066 		if (token == Token.OPEN_BRACE) {
2067 			lexer();
2068 			AST lst = STATEMENT_LIST();
2069 			lexer(Token.CLOSE_BRACE);
2070 			return lst;
2071 		}
2072 		AST stmt;
2073 		if (token == Token.KW_IF) {
2074 			stmt = IF_STATEMENT();
2075 		} else if (token == Token.KW_WHILE) {
2076 			stmt = WHILE_STATEMENT();
2077 		} else if (token == Token.KW_FOR) {
2078 			stmt = FOR_STATEMENT();
2079 		} else {
2080 			if (token == Token.KW_DO) {
2081 				stmt = DO_STATEMENT();
2082 			} else if (token == Token.KW_RETURN) {
2083 				stmt = RETURN_STATEMENT();
2084 			} else if (token == Token.KW_EXIT) {
2085 				stmt = EXIT_STATEMENT();
2086 			} else if (token == Token.KW_DELETE) {
2087 				stmt = DELETE_STATEMENT();
2088 			} else if (token == Token.KW_PRINT) {
2089 				stmt = PRINT_STATEMENT();
2090 			} else if (token == Token.KW_PRINTF) {
2091 				stmt = PRINTF_STATEMENT();
2092 			} else if (token == Token.KW_NEXT) {
2093 				stmt = NEXT_STATEMENT();
2094 			} else if (token == Token.KW_NEXTFILE) {
2095 				stmt = NEXTFILE_STATEMENT();
2096 			} else if (token == Token.KW_CONTINUE) {
2097 				stmt = CONTINUE_STATEMENT();
2098 			} else if (token == Token.KW_BREAK) {
2099 				stmt = BREAK_STATEMENT();
2100 			} else {
2101 				stmt = EXPRESSION_STATEMENT(true);
2102 			}
2103 			terminator();
2104 			return stmt;
2105 		}
2106 		// NO TERMINATOR FOR IF, WHILE, Token.AND FOR
2107 		// (leave it for absorption by the callee)
2108 		return stmt;
2109 	}
2110 
2111 	AST EXPRESSION_STATEMENT(boolean allowInKeyword) throws IOException {
2112 		// true = allow comparators
2113 		// false = do Token.NOT allow multi-dimensional array indices
2114 		// return new ExpressionStatementAst(ASSIGNMENT_EXPRESSION(true, allowInKeyword, false));
2115 
2116 		AST exprAst = ASSIGNMENT_EXPRESSION(null, true, allowInKeyword, false);
2117 		return new ExpressionStatementAst(exprAst);
2118 	}
2119 
2120 	AST IF_STATEMENT() throws IOException {
2121 		expectKeyword("if");
2122 		lexer(Token.OPEN_PAREN);
2123 		AST expr = ASSIGNMENT_EXPRESSION(null, true, true, false); // allow comparators, allow in keyword, do Token.NOT
2124 																																// allow
2125 		// multidim
2126 		// indices expressions
2127 		lexer(Token.CLOSE_PAREN);
2128 
2129 		//// Was:
2130 		//// AST b1 = BLOCK_OR_STMT();
2131 		//// But it didn't handle
2132 		//// if ; else ...
2133 		//// properly
2134 		optNewline();
2135 		AST b1;
2136 		if (token == Token.SEMICOLON) {
2137 			lexer();
2138 			// consume the newline after the semicolon
2139 			optNewline();
2140 			b1 = null;
2141 		} else {
2142 			b1 = BLOCK_OR_STMT();
2143 		}
2144 
2145 		// The OPT_NEWLINE() above causes issues with the following form:
2146 		// if (...) {
2147 		// }
2148 		// else { ... }
2149 		// The \n before the else disassociates subsequent statements
2150 		// if an "else" does not immediately follow.
2151 		// To accommodate, the ifStatement will continue to manage
2152 		// statements, causing the original OPT_STATEMENT_LIST to relinquish
2153 		// processing statements to this OPT_STATEMENT_LIST.
2154 
2155 		optNewline();
2156 		if (token == Token.KW_ELSE) {
2157 			lexer();
2158 			optNewline();
2159 			AST b2 = BLOCK_OR_STMT();
2160 			return new IfStatementAst(expr, b1, b2);
2161 		} else {
2162 			AST ifAst = new IfStatementAst(expr, b1, null);
2163 			return ifAst;
2164 		}
2165 	}
2166 
2167 	AST BREAK_STATEMENT() throws IOException {
2168 		expectKeyword("break");
2169 		return new BreakStatementAst();
2170 	}
2171 
2172 	AST BLOCK_OR_STMT() throws IOException {
2173 		// default case, does Token.NOT consume (require) a terminator
2174 		return BLOCK_OR_STMT(false);
2175 	}
2176 
2177 	AST BLOCK_OR_STMT(boolean requireTerminator) throws IOException {
2178 		optNewline();
2179 		AST block;
2180 		// HIJACK BRACES HERE SINCE WE MAY Token.NOT HAVE A TERMINATOR AFTER THE CLOSING BRACE
2181 		if (token == Token.OPEN_BRACE) {
2182 			lexer();
2183 			block = STATEMENT_LIST();
2184 			lexer(Token.CLOSE_BRACE);
2185 			return block;
2186 		} else if (token == Token.SEMICOLON) {
2187 			block = null;
2188 		} else {
2189 			block = STATEMENT();
2190 			// NO TERMINATOR HERE!
2191 		}
2192 		if (requireTerminator) {
2193 			terminator();
2194 		}
2195 		return block;
2196 	}
2197 
2198 	AST WHILE_STATEMENT() throws IOException {
2199 		expectKeyword("while");
2200 		lexer(Token.OPEN_PAREN);
2201 		AST expr = ASSIGNMENT_EXPRESSION(null, true, true, false); // allow comparators, allow IN keyword, do Token.NOT
2202 																																// allow
2203 		// multidim
2204 		// indices expressions
2205 		lexer(Token.CLOSE_PAREN);
2206 		AST block = BLOCK_OR_STMT();
2207 		return new WhileStatementAst(expr, block);
2208 	}
2209 
2210 	AST FOR_STATEMENT() throws IOException {
2211 		expectKeyword("for");
2212 		AST expr1 = null;
2213 		AST expr2 = null;
2214 		AST expr3 = null;
2215 		lexer(Token.OPEN_PAREN);
2216 		expr1 = OPT_SIMPLE_STATEMENT(false); // false = "no in keyword allowed"
2217 
2218 		// branch here if we expect a for(... in ...) statement
2219 		if (token == Token.KW_IN) {
2220 			if (expr1.ast1 == null || expr1.ast2 != null) {
2221 				throw parserException("Invalid expression prior to 'in' statement. Got : " + expr1);
2222 			}
2223 			expr1 = expr1.ast1;
2224 			// analyze expr1 to make sure it's a singleton IDAst
2225 			if (!(expr1 instanceof IDAst)) {
2226 				throw parserException("Expecting an Token.ID for 'in' statement. Got : " + expr1);
2227 			}
2228 			// in
2229 			lexer();
2230 			if (token != Token.ID) {
2231 				throw parserException(
2232 						"Expecting an array or subarray for 'in' statement. Got " + token.name() + ": " + text);
2233 			}
2234 			AST arrayAst = SYMBOL(true, true);
2235 			// close paren ...
2236 			lexer(Token.CLOSE_PAREN);
2237 			AST block = BLOCK_OR_STMT();
2238 			return new ForInStatementAst(expr1, arrayAst, block);
2239 		}
2240 
2241 		if (token == Token.SEMICOLON) {
2242 			lexer();
2243 			optNewline();
2244 		} else {
2245 			throw parserException("Expecting ;. Got " + token.name() + ": " + text);
2246 		}
2247 		if (token != Token.SEMICOLON) {
2248 			expr2 = ASSIGNMENT_EXPRESSION(null, true, true, false); // allow comparators, allow IN keyword, do Token.NOT allow
2249 			// multidim
2250 			// indices expressions
2251 		}
2252 		if (token == Token.SEMICOLON) {
2253 			lexer();
2254 			optNewline();
2255 		} else {
2256 			throw parserException("Expecting ;. Got " + token.name() + ": " + text);
2257 		}
2258 		if (token != Token.CLOSE_PAREN) {
2259 			expr3 = OPT_SIMPLE_STATEMENT(true); // true = "allow the in keyword"
2260 		}
2261 		lexer(Token.CLOSE_PAREN);
2262 		AST block = BLOCK_OR_STMT();
2263 		return new ForStatementAst(expr1, expr2, expr3, block);
2264 	}
2265 
2266 	AST OPT_SIMPLE_STATEMENT(boolean allowInKeyword) throws IOException {
2267 		if (token == Token.SEMICOLON) {
2268 			return null;
2269 		} else if (token == Token.KW_DELETE) {
2270 			return DELETE_STATEMENT();
2271 		} else if (token == Token.KW_PRINT) {
2272 			return PRINT_STATEMENT();
2273 		} else if (token == Token.KW_PRINTF) {
2274 			return PRINTF_STATEMENT();
2275 		} else {
2276 			// allow non-statement ASTs
2277 			return EXPRESSION_STATEMENT(allowInKeyword);
2278 		}
2279 	}
2280 
2281 	AST DELETE_STATEMENT() throws IOException {
2282 		boolean parens = c == '(';
2283 		expectKeyword("delete");
2284 		if (parens) {
2285 			lexer();
2286 		}
2287 		AST symbolAst = SYMBOL(true, true); // allow comparators
2288 		if (parens) {
2289 			lexer(Token.CLOSE_PAREN);
2290 		}
2291 
2292 		return new DeleteStatementAst(symbolAst);
2293 	}
2294 
2295 	private static final class ParsedPrintStatement {
2296 
2297 		private final AST funcParams;
2298 		private final Token outputToken;
2299 		private final AST outputExpr;
2300 		private final boolean parenthesized;
2301 
2302 		ParsedPrintStatement(AST funcParams, Token outputToken, AST outputExpr, boolean parenthesized) {
2303 			this.funcParams = funcParams;
2304 			this.outputToken = outputToken;
2305 			this.outputExpr = outputExpr;
2306 			this.parenthesized = parenthesized;
2307 		}
2308 
2309 		public AST getFuncParams() {
2310 			return funcParams;
2311 		}
2312 
2313 		public Token getOutputToken() {
2314 			return outputToken;
2315 		}
2316 
2317 		public AST getOutputExpr() {
2318 			return outputExpr;
2319 		}
2320 
2321 		public boolean isParenthesized() {
2322 			return parenthesized;
2323 		}
2324 	}
2325 
2326 	private ParsedPrintStatement parsePrintStatement() throws IOException {
2327 		AST funcParams;
2328 		Token outputToken;
2329 		AST outputExpr;
2330 		boolean parenthesized = false;
2331 
2332 		if (token == Token.OPEN_PAREN) {
2333 			parenthesized = true;
2334 			funcParams = parseParenthesizedPrintArguments();
2335 		} else if (endsPrintArgumentList(token)) {
2336 			funcParams = null;
2337 		} else {
2338 			funcParams = EXPRESSION_LIST(false, true); // no comparisons allowed, but allow “in”
2339 		}
2340 
2341 		if (token == Token.GT || token == Token.APPEND || token == Token.PIPE) {
2342 			outputToken = token;
2343 			lexer();
2344 			outputExpr = ASSIGNMENT_EXPRESSION(null, true, true, false); // allow comparisons, “in”; no multidim indices
2345 		} else {
2346 			outputToken = null;
2347 			outputExpr = null;
2348 		}
2349 
2350 		return new ParsedPrintStatement(funcParams, outputToken, outputExpr, parenthesized);
2351 	}
2352 
2353 	private boolean endsPrintArgumentList(Token candidate) {
2354 		return candidate == Token.NEWLINE
2355 				|| candidate == Token.SEMICOLON
2356 				|| candidate == Token.CLOSE_BRACE
2357 				|| candidate == Token.CLOSE_PAREN
2358 				|| candidate == Token.GT
2359 				|| candidate == Token.APPEND
2360 				|| candidate == Token.PIPE
2361 				|| candidate == Token.EOF;
2362 	}
2363 
2364 	private AST parseParenthesizedPrintArguments() throws IOException {
2365 		lexer(); // consume '('
2366 		if (token == Token.CLOSE_PAREN) {
2367 			lexer(); // consume ')'
2368 			return null;
2369 		}
2370 
2371 		AST params = EXPRESSION_LIST(true, true); // allow comparisons and “in” within parentheses
2372 		lexer(Token.CLOSE_PAREN);
2373 
2374 		if (params instanceof FunctionCallParamListAst) {
2375 			FunctionCallParamListAst paramList = (FunctionCallParamListAst) params;
2376 			boolean singleExpression = paramList.getAst2() == null;
2377 			// A parenthesized group followed by "in" is a membership test whose key is
2378 			// the whole group, e.g.: print (1,2) in a
2379 			boolean membershipKey = !singleExpression && token == Token.KW_IN;
2380 			if ((singleExpression || membershipKey) && !endsPrintArgumentList(token)) {
2381 				AST continuedExpression = ASSIGNMENT_EXPRESSION(
2382 						singleExpression ? paramList.getAst1() : toMultidimIndex(paramList),
2383 						false,
2384 						true,
2385 						false);
2386 				if (token == Token.COMMA) {
2387 					// A single parenthesized expression followed by a comma continues the
2388 					// output expression list, e.g.: print (i==0), (i=="")
2389 					lexer(); // consume ','
2390 					optNewline(); // allow newline after comma (AWK style)
2391 					return new FunctionCallParamListAst(continuedExpression, EXPRESSION_LIST(false, true));
2392 				}
2393 				return new FunctionCallParamListAst(continuedExpression, null);
2394 			}
2395 		}
2396 
2397 		return params;
2398 	}
2399 
2400 	// Converts a print argument list back into the multi-dimensional array index
2401 	// it turned out to be, when the parenthesized group is followed by "in"
2402 	private AST toMultidimIndex(FunctionCallParamListAst list) {
2403 		AST rest = list.getAst2() == null ? null : toMultidimIndex((FunctionCallParamListAst) list.getAst2());
2404 		return new ArrayIndexAst(list.getAst1(), rest);
2405 	}
2406 
2407 	AST PRINT_STATEMENT() throws IOException {
2408 		expectKeyword("print");
2409 		ParsedPrintStatement parsedPrintStatement = parsePrintStatement();
2410 
2411 		AST params = parsedPrintStatement.getFuncParams();
2412 		if (parsedPrintStatement.isParenthesized()
2413 				&& token == Token.QUESTION_MARK
2414 				&& params instanceof FunctionCallParamListAst
2415 				&& ((FunctionCallParamListAst) params).getAst2() == null) {
2416 			AST condExpr = ((FunctionCallParamListAst) params).getAst1();
2417 			lexer();
2418 			AST trueBlock = TERNARY_EXPRESSION(null, true, true, true);
2419 			lexer(Token.COLON);
2420 			AST falseBlock = TERNARY_EXPRESSION(null, true, true, true);
2421 			params = new FunctionCallParamListAst(
2422 					new TernaryExpressionAst(condExpr, trueBlock, falseBlock),
2423 					null);
2424 		}
2425 
2426 		return new PrintAst(
2427 				params,
2428 				parsedPrintStatement.getOutputToken(),
2429 				parsedPrintStatement.getOutputExpr(),
2430 				parsedPrintStatement.isParenthesized());
2431 	}
2432 
2433 	AST PRINTF_STATEMENT() throws IOException {
2434 		expectKeyword("printf");
2435 		ParsedPrintStatement parsedPrintStatement = parsePrintStatement();
2436 
2437 		AST params = parsedPrintStatement.getFuncParams();
2438 		if (parsedPrintStatement.isParenthesized()
2439 				&& token == Token.QUESTION_MARK
2440 				&& params instanceof FunctionCallParamListAst
2441 				&& ((FunctionCallParamListAst) params).getAst2() == null) {
2442 			AST condExpr = ((FunctionCallParamListAst) params).getAst1();
2443 			lexer();
2444 			AST trueBlock = TERNARY_EXPRESSION(null, true, true, true);
2445 			lexer(Token.COLON);
2446 			AST falseBlock = TERNARY_EXPRESSION(null, true, true, true);
2447 			params = new FunctionCallParamListAst(
2448 					new TernaryExpressionAst(condExpr, trueBlock, falseBlock),
2449 					null);
2450 		}
2451 
2452 		return new PrintfAst(
2453 				params,
2454 				parsedPrintStatement.getOutputToken(),
2455 				parsedPrintStatement.getOutputExpr());
2456 	}
2457 
2458 	AST GETLINE_EXPRESSION(AST pipeExpr, boolean allowComparison, boolean allowInKeyword) throws IOException {
2459 		expectKeyword("getline");
2460 		AST lvalue = LVALUE(allowComparison, allowInKeyword);
2461 		if (token == Token.LT) {
2462 			lexer();
2463 			AST assignmentExpr = ASSIGNMENT_EXPRESSION(null, allowComparison, allowInKeyword, false); // do Token.NOT allow
2464 																																																// multidim
2465 			// indices expressions
2466 			return pipeExpr == null ?
2467 					new GetlineAst(null, lvalue, assignmentExpr) : new GetlineAst(pipeExpr, lvalue, assignmentExpr);
2468 		} else {
2469 			return pipeExpr == null ? new GetlineAst(null, lvalue, null) : new GetlineAst(pipeExpr, lvalue, null);
2470 		}
2471 	}
2472 
2473 	AST LVALUE(boolean allowComparison, boolean allowInKeyword) throws IOException {
2474 		// false = do Token.NOT allow multi dimension indices expressions
2475 		if (token == Token.DOLLAR) {
2476 			return FACTOR(allowComparison, allowInKeyword, false);
2477 		}
2478 		if (token == Token.ID) {
2479 			return FACTOR(allowComparison, allowInKeyword, false);
2480 		}
2481 		return null;
2482 	}
2483 
2484 	AST DO_STATEMENT() throws IOException {
2485 		expectKeyword("do");
2486 		optNewline();
2487 		AST block = BLOCK_OR_STMT();
2488 		if (token == Token.SEMICOLON) {
2489 			lexer();
2490 		}
2491 		optNewline();
2492 		expectKeyword("while");
2493 		lexer(Token.OPEN_PAREN);
2494 		AST expr = ASSIGNMENT_EXPRESSION(null, true, true, false); // true = allow comparators, allow IN keyword, do
2495 																																// Token.NOT
2496 		// allow
2497 		// multidim indices expressions
2498 		lexer(Token.CLOSE_PAREN);
2499 		return new DoStatementAst(block, expr);
2500 	}
2501 
2502 	AST RETURN_STATEMENT() throws IOException {
2503 		expectKeyword("return");
2504 		if (token == Token.SEMICOLON || token == Token.NEWLINE || token == Token.CLOSE_BRACE) {
2505 			return new ReturnStatementAst(null);
2506 		} else {
2507 			return new ReturnStatementAst(ASSIGNMENT_EXPRESSION(null, true, true, false)); // true = allow comparators, allow
2508 																																											// IN
2509 			// keyword, do Token.NOT allow multidim
2510 			// indices expressions
2511 		}
2512 	}
2513 
2514 	AST EXIT_STATEMENT() throws IOException {
2515 		expectKeyword("exit");
2516 		if (token == Token.SEMICOLON || token == Token.NEWLINE || token == Token.CLOSE_BRACE) {
2517 			return new ExitStatementAst(null);
2518 		} else {
2519 			return new ExitStatementAst(ASSIGNMENT_EXPRESSION(null, true, true, false)); // true = allow comparators, allow IN
2520 			// keyword, do Token.NOT allow multidim
2521 			// indices
2522 			// expressions
2523 		}
2524 	}
2525 
2526 	AST NEXT_STATEMENT() throws IOException {
2527 		expectKeyword("next");
2528 		return new NextStatementAst();
2529 	}
2530 
2531 	AST NEXTFILE_STATEMENT() throws IOException {
2532 		expectKeyword("nextfile");
2533 		nextfileEncountered = true;
2534 		return new NextfileStatementAst();
2535 	}
2536 
2537 	AST CONTINUE_STATEMENT() throws IOException {
2538 		expectKeyword("continue");
2539 		return new ContinueStatementAst();
2540 	}
2541 
2542 	// CHECKSTYLE.ON MethodName
2543 
2544 	private void expectKeyword(String keyword) throws IOException {
2545 		if (token == KEYWORDS.get(keyword)) {
2546 			lexer();
2547 		} else {
2548 			throw parserException("Expecting " + keyword + ". Got " + token.name() + ": " + text);
2549 		}
2550 	}
2551 
2552 	private void populateArrayOperandTuples(
2553 			AST arrayAst,
2554 			AwkTuples tuples,
2555 			boolean createIfMissing,
2556 			String errorMessage) {
2557 		if (arrayAst instanceof IDAst) {
2558 			IDAst idAst = (IDAst) arrayAst;
2559 			idAst.setArray(true);
2560 			if (isJrtManagedSpecialName(idAst.id)) {
2561 				idAst.populateTuples(tuples);
2562 			} else {
2563 				tuples.dereference(idAst.offset, true, idAst.isGlobal);
2564 			}
2565 			return;
2566 		}
2567 		if (arrayAst instanceof ArrayReferenceAst) {
2568 			if (posix) {
2569 				arrayAst.throwSemanticException(errorMessage);
2570 			}
2571 			((ArrayReferenceAst) arrayAst).populateArrayValueTuples(tuples, createIfMissing);
2572 			return;
2573 		}
2574 		arrayAst.throwSemanticException(errorMessage);
2575 	}
2576 
2577 	private int populateActualParameters(
2578 			AwkTuples tuples,
2579 			FunctionCallParamListAst params,
2580 			Set<Integer> arrayParameterIndexes,
2581 			Set<Integer> rawValueParameterIndexes,
2582 			Set<Integer> literalRegexpIndexes,
2583 			int parameterIndex) {
2584 		if (params == null) {
2585 			return 0;
2586 		}
2587 		if (arrayParameterIndexes.contains(Integer.valueOf(parameterIndex))) {
2588 			populateArrayOperandTuples(
2589 					params.getAst1(),
2590 					tuples,
2591 					true,
2592 					"Parameter position " + (parameterIndex + 1) + " must be an array or subarray.");
2593 		} else if (literalRegexpIndexes.contains(Integer.valueOf(parameterIndex))) {
2594 			populateRawRegexpParameterTuples(params.getAst1(), tuples);
2595 		} else if (rawValueParameterIndexes.contains(Integer.valueOf(parameterIndex))) {
2596 			populateRawValueTuples(params.getAst1(), tuples);
2597 		} else {
2598 			params.getAst1().populateTuples(tuples);
2599 		}
2600 		if (params.getAst2() == null) {
2601 			return 1;
2602 		}
2603 		return 1 + populateActualParameters(
2604 				tuples,
2605 				(FunctionCallParamListAst) params.getAst2(),
2606 				arrayParameterIndexes,
2607 				rawValueParameterIndexes,
2608 				literalRegexpIndexes,
2609 				parameterIndex + 1);
2610 	}
2611 
2612 	private int populateActualParameters(
2613 			AwkTuples tuples,
2614 			FunctionCallParamListAst params,
2615 			int... literalRegexpIndexesParam) {
2616 		Set<Integer> literalRegexpIndexes = new HashSet<Integer>();
2617 		for (int idx : literalRegexpIndexesParam) {
2618 			literalRegexpIndexes.add(Integer.valueOf(idx));
2619 		}
2620 		return populateActualParameters(
2621 				tuples,
2622 				params,
2623 				Collections.<Integer>emptySet(),
2624 				Collections.<Integer>emptySet(),
2625 				literalRegexpIndexes,
2626 				0);
2627 	}
2628 
2629 	private int populateActualParametersUpTo(
2630 			AwkTuples tuples,
2631 			FunctionCallParamListAst params,
2632 			int parameterIndex,
2633 			int maxParameterCount) {
2634 		/*
2635 		 * Gawk accepts extra user-function arguments with a runtime warning: the
2636 		 * callee has no local slots for them, but their expressions are still
2637 		 * evaluated for their side effects. Extra arguments are therefore emitted
2638 		 * followed by a POP, and only the formal parameter prefix is counted.
2639 		 */
2640 		if (params == null) {
2641 			return 0;
2642 		}
2643 		if (parameterIndex >= maxParameterCount) {
2644 			// Raw-value evaluation runs the expression's side effects but merely
2645 			// peeks at bare variables, so an untyped variable passed as an extra
2646 			// argument is not autovivified into an assigned scalar.
2647 			populateRawValueTuples(params.getAst1(), tuples);
2648 			tuples.pop();
2649 			populateActualParametersUpTo(
2650 					tuples,
2651 					(FunctionCallParamListAst) params.getAst2(),
2652 					parameterIndex + 1,
2653 					maxParameterCount);
2654 			return 0;
2655 		}
2656 		AST argument = params.getAst1();
2657 		if (argument instanceof IDAst
2658 				&& !isJrtManagedSpecialName(((IDAst) argument).id)) {
2659 			IDAst idAst = (IDAst) argument;
2660 			tuples.pushIndirectArgument(idAst.offset, idAst.isGlobal);
2661 		} else if (argument instanceof ArrayReferenceAst) {
2662 			((ArrayReferenceAst) argument).populateTargetReferenceTuples(tuples);
2663 			tuples.pushIndirectArrayArgument();
2664 		} else {
2665 			argument.populateTuples(tuples);
2666 		}
2667 		if (params.getAst2() == null) {
2668 			return 1;
2669 		}
2670 		return 1 + populateActualParametersUpTo(
2671 				tuples,
2672 				(FunctionCallParamListAst) params.getAst2(),
2673 				parameterIndex + 1,
2674 				maxParameterCount);
2675 	}
2676 
2677 	private void populateRawValueTuples(AST valueAst, AwkTuples tuples) {
2678 		/*
2679 		 * typeof() and isarray() need to inspect an lvalue's current state. A
2680 		 * normal scalar dereference autoconverts an untyped variable into AWK's
2681 		 * assigned blank scalar, which would erase the distinction gawk exposes,
2682 		 * so scalar variables use a non-assigning peek instead.
2683 		 * Array elements need no special opcode: a normal element read already
2684 		 * returns the raw untyped marker for a missing element, and, as in gawk,
2685 		 * brings that element into existence so a later `in`, delete, or for-in
2686 		 * observes it.
2687 		 */
2688 		if (valueAst instanceof IDAst) {
2689 			IDAst idAst = (IDAst) valueAst;
2690 			if (isJrtManagedSpecialName(idAst.id)) {
2691 				idAst.populateTuples(tuples);
2692 			} else {
2693 				tuples.peekDereference(idAst.offset, idAst.isGlobal);
2694 			}
2695 			return;
2696 		}
2697 		valueAst.populateTuples(tuples);
2698 	}
2699 
2700 	private void populateRawRegexpParameterTuples(AST valueAst, AwkTuples tuples) {
2701 		if (valueAst instanceof RegexpAst) {
2702 			((RegexpAst) valueAst).populateRawRegexpTuples(tuples);
2703 			return;
2704 		}
2705 		valueAst.populateTuples(tuples);
2706 	}
2707 
2708 	private int populateIndirectActualParameters(
2709 			AwkTuples tuples,
2710 			FunctionCallParamListAst params) {
2711 		if (params == null) {
2712 			return 0;
2713 		}
2714 		AST argument = params.getAst1();
2715 		if (argument instanceof IDAst
2716 				&& !isJrtManagedSpecialName(((IDAst) argument).id)) {
2717 			IDAst idAst = (IDAst) argument;
2718 			tuples.pushIndirectArgument(idAst.offset, idAst.isGlobal);
2719 		} else if (argument instanceof ArrayReferenceAst) {
2720 			((ArrayReferenceAst) argument).populateTargetReferenceTuples(tuples);
2721 			tuples.pushIndirectArrayArgument();
2722 		} else {
2723 			argument.populateTuples(tuples);
2724 		}
2725 		return 1 + populateIndirectActualParameters(
2726 				tuples,
2727 				(FunctionCallParamListAst) params.getAst2());
2728 	}
2729 
2730 	private Set<Integer> collectArrayParameterIndexes(FunctionDefAst functionDefAst) {
2731 		Set<Integer> arrayIndexes = new HashSet<Integer>();
2732 		FunctionDefParamListAst fPtr = (FunctionDefParamListAst) functionDefAst.getAst1();
2733 		int index = 0;
2734 		while (fPtr != null) {
2735 			IDAst fparam = symbolTable.getFunctionParameterIDAST(functionDefAst.id, fPtr.id);
2736 			if (fparam.isArray()) {
2737 				arrayIndexes.add(Integer.valueOf(index));
2738 			}
2739 			fPtr = (FunctionDefParamListAst) fPtr.getAst1();
2740 			index++;
2741 		}
2742 		return arrayIndexes;
2743 	}
2744 
2745 	// parser
2746 	// ===============================================================================
2747 	// AST class defs
2748 	private abstract class AST extends AstNode {
2749 
2750 		private final String sourceDescription = currentScriptSource.getDescription();
2751 		// PositionTracker consumes these tuple-emitted source lines at runtime, but
2752 		// AST nodes have to capture them here during parsing before tuples exist.
2753 		private final int lineNo;
2754 		private AST parent;
2755 		private AST ast1, ast2, ast3, ast4;
2756 		private final EnumSet<AstFlag> flags = EnumSet.noneOf(AstFlag.class);
2757 
2758 		protected final void addFlag(AstFlag flag) {
2759 			flags.add(flag);
2760 		}
2761 
2762 		protected final boolean hasFlag(AstFlag flag) {
2763 			return flags.contains(flag);
2764 		}
2765 
2766 		protected Address breakAddress() {
2767 			return null;
2768 		}
2769 
2770 		protected Address continueAddress() {
2771 			return null;
2772 		}
2773 
2774 		protected Address nextAddress() {
2775 			return null;
2776 		}
2777 
2778 		protected Address returnAddress() {
2779 			return null;
2780 		}
2781 
2782 		protected final AST getParent() {
2783 			return parent;
2784 		}
2785 
2786 		@SuppressWarnings("unused")
2787 		protected final void setParent(AST p) {
2788 			parent = p;
2789 		}
2790 
2791 		protected final AST getAst1() {
2792 			return ast1;
2793 		}
2794 
2795 		@SuppressWarnings("unused")
2796 		protected final void setAst1(AST a1) {
2797 			ast1 = a1;
2798 		}
2799 
2800 		protected final AST getAst2() {
2801 			return ast2;
2802 		}
2803 
2804 		@SuppressWarnings("unused")
2805 		protected final void setAst2(AST a2) {
2806 			ast2 = a2;
2807 		}
2808 
2809 		protected final AST getAst3() {
2810 			return ast3;
2811 		}
2812 
2813 		@SuppressWarnings("unused")
2814 		protected final void setAst3(AST a3) {
2815 			ast3 = a3;
2816 		}
2817 
2818 		protected final AST getAst4() {
2819 			return ast4;
2820 		}
2821 
2822 		@SuppressWarnings("unused")
2823 		protected final void setAst4(AST a4) {
2824 			ast4 = a4;
2825 		}
2826 
2827 		protected final AST searchFor(AstFlag flag) {
2828 			AST ptr = this;
2829 			while (ptr != null) {
2830 				if (ptr.hasFlag(flag)) {
2831 					return ptr;
2832 				}
2833 				ptr = ptr.parent;
2834 			}
2835 			return null;
2836 		}
2837 
2838 		protected AST() {
2839 			this(currentSourceLineNumber());
2840 		}
2841 
2842 		protected AST(int lineNo) {
2843 			this.lineNo = lineNo;
2844 		}
2845 
2846 		protected int getLineNo() {
2847 			return lineNo;
2848 		}
2849 
2850 		protected String getSourceDescription() {
2851 			return sourceDescription;
2852 		}
2853 
2854 		protected String sourceBasename() {
2855 			// File.getName is a pure string operation and, unlike java.nio.Path,
2856 			// never rejects non-path descriptions such as <command-line-supplied-script>
2857 			return new File(getSourceDescription()).getName();
2858 		}
2859 
2860 		protected AST(AST ast1) {
2861 			this(currentSourceLineNumber(), ast1);
2862 		}
2863 
2864 		protected AST(int lineNo, AST ast1) {
2865 			this(lineNo);
2866 			this.ast1 = ast1;
2867 
2868 			if (ast1 != null) {
2869 				ast1.parent = this;
2870 			}
2871 		}
2872 
2873 		protected AST(AST ast1, AST ast2) {
2874 			this(currentSourceLineNumber(), ast1, ast2);
2875 		}
2876 
2877 		protected AST(int lineNo, AST ast1, AST ast2) {
2878 			this(lineNo);
2879 			this.ast1 = ast1;
2880 			this.ast2 = ast2;
2881 
2882 			if (ast1 != null) {
2883 				ast1.parent = this;
2884 			}
2885 			if (ast2 != null) {
2886 				ast2.parent = this;
2887 			}
2888 		}
2889 
2890 		protected AST(AST ast1, AST ast2, AST ast3) {
2891 			this(currentSourceLineNumber(), ast1, ast2, ast3);
2892 		}
2893 
2894 		protected AST(int lineNo, AST ast1, AST ast2, AST ast3) {
2895 			this(lineNo);
2896 			this.ast1 = ast1;
2897 			this.ast2 = ast2;
2898 			this.ast3 = ast3;
2899 
2900 			if (ast1 != null) {
2901 				ast1.parent = this;
2902 			}
2903 			if (ast2 != null) {
2904 				ast2.parent = this;
2905 			}
2906 			if (ast3 != null) {
2907 				ast3.parent = this;
2908 			}
2909 		}
2910 
2911 		protected AST(AST ast1, AST ast2, AST ast3, AST ast4) {
2912 			this(currentSourceLineNumber(), ast1, ast2, ast3, ast4);
2913 		}
2914 
2915 		protected AST(int lineNo, AST ast1, AST ast2, AST ast3, AST ast4) {
2916 			this(lineNo);
2917 			this.ast1 = ast1;
2918 			this.ast2 = ast2;
2919 			this.ast3 = ast3;
2920 			this.ast4 = ast4;
2921 
2922 			if (ast1 != null) {
2923 				ast1.parent = this;
2924 			}
2925 			if (ast2 != null) {
2926 				ast2.parent = this;
2927 			}
2928 			if (ast3 != null) {
2929 				ast3.parent = this;
2930 			}
2931 			if (ast4 != null) {
2932 				ast4.parent = this;
2933 			}
2934 		}
2935 
2936 		/**
2937 		 * Dump a meaningful text representation of this
2938 		 * abstract syntax tree node to the output (print)
2939 		 * stream. Either it is called directly by the
2940 		 * application program, or it is called by the
2941 		 * parent node of this tree node.
2942 		 *
2943 		 * @param ps The print stream to dump the text
2944 		 *        representation.
2945 		 */
2946 		@Override
2947 		public void dump(PrintStream ps) {
2948 			dump(ps, 0);
2949 		}
2950 
2951 		private void dump(PrintStream ps, int lvl) {
2952 			StringBuffer spaces = new StringBuffer();
2953 			for (int i = 0; i < lvl; i++) {
2954 				spaces.append(' ');
2955 			}
2956 			ps.println(spaces + toString());
2957 			if (ast1 != null) {
2958 				ast1.dump(ps, lvl + 1);
2959 			}
2960 			if (ast2 != null) {
2961 				ast2.dump(ps, lvl + 1);
2962 			}
2963 			if (ast3 != null) {
2964 				ast3.dump(ps, lvl + 1);
2965 			}
2966 			if (ast4 != null) {
2967 				ast4.dump(ps, lvl + 1);
2968 			}
2969 		}
2970 
2971 		/**
2972 		 * Apply semantic checks to this node. The default
2973 		 * implementation is to simply call semanticAnalysis()
2974 		 * on all the children of this abstract syntax tree node.
2975 		 * Therefore, this method must be overridden to provide
2976 		 * meaningful semantic analysis / checks.
2977 		 *
2978 		 * @throws SemanticException upon a semantic error.
2979 		 */
2980 		@Override
2981 		public void semanticAnalysis() {
2982 			if (ast1 != null) {
2983 				ast1.semanticAnalysis();
2984 			}
2985 			if (ast2 != null) {
2986 				ast2.semanticAnalysis();
2987 			}
2988 			if (ast3 != null) {
2989 				ast3.semanticAnalysis();
2990 			}
2991 			if (ast4 != null) {
2992 				ast4.semanticAnalysis();
2993 			}
2994 		}
2995 
2996 		/**
2997 		 * Appends tuples to the AwkTuples list
2998 		 * for this abstract syntax tree node. Subclasses
2999 		 * must implement this method.
3000 		 * <p>
3001 		 * This is called either by the main program to generate a full
3002 		 * list of tuples for the abstract syntax tree, or it is called
3003 		 * by other abstract syntax tree nodes in response to their
3004 		 * attempt at populating tuples.
3005 		 *
3006 		 * @param tuples The tuples to populate.
3007 		 * @return The number of items left on the stack after
3008 		 *         these tuples have executed.
3009 		 */
3010 		@Override
3011 		public abstract int populateTuples(AwkTuples tuples);
3012 
3013 		protected final void pushSourceLineNumber(AwkTuples tuples) {
3014 			tuples.pushSourceLineNumber(lineNo);
3015 		}
3016 
3017 		protected final void popSourceLineNumber(AwkTuples tuples) {
3018 			tuples.popSourceLineNumber(lineNo);
3019 		}
3020 
3021 		private boolean isBegin = isBegin();
3022 
3023 		@SuppressWarnings("unused")
3024 		protected final boolean isBeginFlag() {
3025 			return isBegin;
3026 		}
3027 
3028 		protected final void setBeginFlag(boolean flag) {
3029 			isBegin = flag;
3030 		}
3031 
3032 		private boolean isBegin() {
3033 			boolean result = isBegin;
3034 			if (!result && ast1 != null) {
3035 				result = ast1.isBegin();
3036 			}
3037 			if (!result && ast2 != null) {
3038 				result = ast2.isBegin();
3039 			}
3040 			if (!result && ast3 != null) {
3041 				result = ast3.isBegin();
3042 			}
3043 			if (!result && ast4 != null) {
3044 				result = ast4.isBegin();
3045 			}
3046 			return result;
3047 		}
3048 
3049 		private boolean isEnd = isEnd();
3050 
3051 		@SuppressWarnings("unused")
3052 		protected final boolean isEndFlag() {
3053 			return isEnd;
3054 		}
3055 
3056 		protected final void setEndFlag(boolean flag) {
3057 			isEnd = flag;
3058 		}
3059 
3060 		private boolean isEnd() {
3061 			boolean result = isEnd;
3062 			if (!result && ast1 != null) {
3063 				result = ast1.isEnd();
3064 			}
3065 			if (!result && ast2 != null) {
3066 				result = ast2.isEnd();
3067 			}
3068 			if (!result && ast3 != null) {
3069 				result = ast3.isEnd();
3070 			}
3071 			if (!result && getAst4() != null) {
3072 				result = getAst4().isEnd();
3073 			}
3074 			return result;
3075 		}
3076 
3077 		private boolean isBeginFile = isBeginFile();
3078 
3079 		protected final void setBeginFileFlag(boolean flag) {
3080 			isBeginFile = flag;
3081 		}
3082 
3083 		private boolean isBeginFile() {
3084 			boolean result = isBeginFile;
3085 			if (!result && ast1 != null) {
3086 				result = ast1.isBeginFile();
3087 			}
3088 			if (!result && ast2 != null) {
3089 				result = ast2.isBeginFile();
3090 			}
3091 			if (!result && ast3 != null) {
3092 				result = ast3.isBeginFile();
3093 			}
3094 			if (!result && ast4 != null) {
3095 				result = ast4.isBeginFile();
3096 			}
3097 			return result;
3098 		}
3099 
3100 		private boolean isEndFile = isEndFile();
3101 
3102 		protected final void setEndFileFlag(boolean flag) {
3103 			isEndFile = flag;
3104 		}
3105 
3106 		private boolean isEndFile() {
3107 			boolean result = isEndFile;
3108 			if (!result && ast1 != null) {
3109 				result = ast1.isEndFile();
3110 			}
3111 			if (!result && ast2 != null) {
3112 				result = ast2.isEndFile();
3113 			}
3114 			if (!result && ast3 != null) {
3115 				result = ast3.isEndFile();
3116 			}
3117 			if (!result && ast4 != null) {
3118 				result = ast4.isEndFile();
3119 			}
3120 			return result;
3121 		}
3122 
3123 		private boolean isFunction = isFunction();
3124 
3125 		@SuppressWarnings("unused")
3126 		protected final boolean isFunctionFlag() {
3127 			return isFunction;
3128 		}
3129 
3130 		protected final void setFunctionFlag(boolean flag) {
3131 			isFunction = flag;
3132 		}
3133 
3134 		private boolean isFunction() {
3135 			boolean result = isFunction;
3136 			if (!result && getAst1() != null) {
3137 				result = getAst1().isFunction();
3138 			}
3139 			if (!result && getAst2() != null) {
3140 				result = getAst2().isFunction();
3141 			}
3142 			if (!result && getAst3() != null) {
3143 				result = getAst3().isFunction();
3144 			}
3145 			if (!result && getAst4() != null) {
3146 				result = getAst4().isFunction();
3147 			}
3148 			return result;
3149 		}
3150 
3151 		public boolean isArray() {
3152 			return false;
3153 		}
3154 
3155 		public boolean isScalar() {
3156 			return false;
3157 		}
3158 
3159 		/**
3160 		 * Made protected so that subclasses can access it.
3161 		 * Package-level access was not necessary.
3162 		 */
3163 		protected class SemanticException extends RuntimeException {
3164 
3165 			private static final long serialVersionUID = 1L;
3166 
3167 			SemanticException(String msg) {
3168 				super(msg + " (" + sourceDescription + ":" + lineNo + ")");
3169 			}
3170 		}
3171 
3172 		protected final void throwSemanticException(String msg) {
3173 			throw new SemanticException(msg);
3174 		}
3175 
3176 		@Override
3177 		public String toString() {
3178 			return getClass().getName().replaceFirst(".*[$.]", "");
3179 		}
3180 	}
3181 
3182 	private abstract class ScalarExpressionAst extends AST {
3183 
3184 		protected ScalarExpressionAst() {
3185 			super();
3186 		}
3187 
3188 		protected ScalarExpressionAst(int lineNo) {
3189 			super(lineNo);
3190 		}
3191 
3192 		protected ScalarExpressionAst(AST a1) {
3193 			super(a1);
3194 		}
3195 
3196 		protected ScalarExpressionAst(int lineNo, AST a1) {
3197 			super(lineNo, a1);
3198 		}
3199 
3200 		protected ScalarExpressionAst(AST a1, AST a2) {
3201 			super(a1, a2);
3202 		}
3203 
3204 		protected ScalarExpressionAst(int lineNo, AST a1, AST a2) {
3205 			super(lineNo, a1, a2);
3206 		}
3207 
3208 		protected ScalarExpressionAst(AST a1, AST a2, AST a3) {
3209 			super(a1, a2, a3);
3210 		}
3211 
3212 		protected ScalarExpressionAst(int lineNo, AST a1, AST a2, AST a3) {
3213 			super(lineNo, a1, a2, a3);
3214 		}
3215 
3216 		@Override
3217 		public boolean isArray() {
3218 			return false;
3219 		}
3220 
3221 		@Override
3222 		public boolean isScalar() {
3223 			return true;
3224 		}
3225 	}
3226 
3227 	private static boolean isRule(AST ast) {
3228 		return ast != null
3229 				&& !ast.isBegin()
3230 				&& !ast.isEnd()
3231 				&& !ast.isBeginFile()
3232 				&& !ast.isEndFile()
3233 				&& !ast.isFunction();
3234 	}
3235 
3236 	/**
3237 	 * Inspects the action rule condition whether it contains
3238 	 * extensions. It does a superficial check of
3239 	 * the abstract syntax tree of the action rule.
3240 	 * In other words, it will not examine whether user-defined
3241 	 * functions within the action rule contain extensions.
3242 	 *
3243 	 * @param ast The action rule expression to examine.
3244 	 * @return true if the action rule condition contains
3245 	 *         an extension; false otherwise.
3246 	 */
3247 	@SuppressWarnings("unused")
3248 	private static boolean isExtensionConditionRule(AST ast) {
3249 		if (!isRule(ast)) {
3250 			return false;
3251 		}
3252 		if (ast.getAst1() == null) {
3253 			return false;
3254 		}
3255 
3256 		if (!containsASTType(ast.getAst1(), ExtensionAst.class)) {
3257 			return false;
3258 		}
3259 
3260 		if (containsASTType(ast.getAst1(), new Class[] { FunctionCallAst.class, DollarExpressionAst.class })) {
3261 			return false;
3262 		}
3263 
3264 		return true;
3265 	}
3266 
3267 	private static boolean containsASTType(AST ast, Class<?> cls) {
3268 		return containsASTType(ast, new Class[] { cls });
3269 	}
3270 
3271 	private static boolean containsASTType(AST ast, Class<?>[] clsArray) {
3272 		if (ast == null) {
3273 			return false;
3274 		}
3275 		for (Class<?> cls : clsArray) {
3276 			if (cls.isInstance(ast)) {
3277 				return true;
3278 			}
3279 		}
3280 		// prettier-ignore
3281 		return containsASTType(ast.getAst1(), clsArray)
3282 				|| containsASTType(ast.getAst2(), clsArray)
3283 				|| containsASTType(ast.getAst3(), clsArray)
3284 				|| containsASTType(ast.getAst4(), clsArray);
3285 	}
3286 
3287 	private Address nextAddress;
3288 
3289 	/**
3290 	 * Whether the program contains at least one {@code nextfile} statement,
3291 	 * anywhere (rules or user-defined functions). When set, the main input
3292 	 * loop is compiled with per-file stepping so the runtime can jump to the
3293 	 * ENDFILE section and advance to the next input file.
3294 	 */
3295 	private boolean nextfileEncountered;
3296 
3297 	private final class RuleListAst extends AST {
3298 
3299 		private RuleListAst(AST rule, AST rest) {
3300 			super(rule, rest);
3301 		}
3302 
3303 		@Override
3304 		public int populateTuples(AwkTuples tuples) {
3305 
3306 			pushSourceLineNumber(tuples);
3307 
3308 			nextAddress = tuples.createAddress("nextAddress");
3309 
3310 			// goto start address
3311 			Address startAddress = tuples.createAddress("start address");
3312 			tuples.gotoAddress(startAddress);
3313 
3314 			AST ptr;
3315 
3316 			// compile functions
3317 			ptr = this;
3318 			while (ptr != null) {
3319 				if (ptr.getAst1() != null && ptr.getAst1().isFunction()) {
3320 					ptr.getAst1().populateTuples(tuples);
3321 				}
3322 
3323 				ptr = ptr.getAst2();
3324 			}
3325 
3326 			// START OF MAIN BLOCK
3327 			tuples.address(startAddress);
3328 
3329 			// initialize runtime-managed special variables via JRT defaults in AVM
3330 			symbolTable.getID("NR");
3331 			symbolTable.getID("FNR");
3332 			symbolTable.getID("NF");
3333 			symbolTable.getID("FS");
3334 			symbolTable.getID("RS");
3335 			symbolTable.getID("OFS");
3336 			symbolTable.getID("ORS");
3337 			symbolTable.getID("RSTART");
3338 			symbolTable.getID("RLENGTH");
3339 			symbolTable.getID("FILENAME");
3340 			symbolTable.getID("SUBSEP");
3341 			symbolTable.getID("CONVFMT");
3342 			symbolTable.getID("OFMT");
3343 			IDAst environAst = symbolTable.getID("ENVIRON");
3344 			IDAst argcAst = symbolTable.getID("ARGC");
3345 			IDAst argvAst = symbolTable.getID("ARGV");
3346 
3347 			// MUST BE DONE AFTER FUNCTIONS ARE COMPILED,
3348 			// and after special variables are made known to the symbol table
3349 			// (see above)!
3350 			tuples.setNumGlobals(symbolTable.numGlobals());
3351 
3352 			// Only ENVIRON/ARGC/ARGV remain regular globals. ENVIRON and ARGV
3353 			// are materialized only when the script references them, or when
3354 			// SYMTAB is active (its snapshot exposes them); unreferenced ones
3355 			// are answered by the synthetic accessors. ARGC is always
3356 			// materialized (a single cheap assignment): its slot must stay
3357 			// authoritative so ARGC=n command-line operand assignments affect
3358 			// input traversal.
3359 			boolean symtabActive = !posix && symbolTable.isGlobalReferenced("SYMTAB");
3360 			if (environAst.isReferenced() || symtabActive) {
3361 				tuples.environOffset(environAst.offset);
3362 			}
3363 			tuples.argcOffset(argcAst.offset);
3364 			if (argvAst.isReferenced() || symtabActive) {
3365 				tuples.argvOffset(argvAst.offset);
3366 			}
3367 			// SYMTAB and FUNCTAB are gawk extensions, not POSIX
3368 			if (symtabActive) {
3369 				tuples.updateSymtab(symbolTable.getID("SYMTAB").offset);
3370 			}
3371 			if (!posix && symbolTable.isGlobalReferenced("FUNCTAB")) {
3372 				tuples.updateFunctab(symbolTable.getID("FUNCTAB").offset);
3373 			}
3374 			tuples.beforeStartHooks();
3375 
3376 			Address exitAddr = tuples.createAddress("end blocks start address");
3377 			tuples.setExitAddress(exitAddr);
3378 
3379 			// Does the program use BEGINFILE/ENDFILE rules or nextfile? If so,
3380 			// the main input loop must step through the input one file at a
3381 			// time (per-file scaffolding) instead of streaming across files.
3382 			boolean hasBeginFileRules = false;
3383 			boolean hasEndFileRules = false;
3384 			ptr = this;
3385 			while (ptr != null) {
3386 				if (ptr.getAst1() != null && ptr.getAst1().isBeginFile()) {
3387 					hasBeginFileRules = true;
3388 				}
3389 				if (ptr.getAst1() != null && ptr.getAst1().isEndFile()) {
3390 					hasEndFileRules = true;
3391 				}
3392 				ptr = ptr.getAst2();
3393 			}
3394 
3395 			// Do we have rules? (apart from BEGIN)
3396 			// If we have rules, END, BEGINFILE, or ENDFILE, we need to parse
3397 			// the input
3398 			boolean reqInput = hasBeginFileRules || hasEndFileRules;
3399 
3400 			// Check for "normal" rules
3401 			ptr = this;
3402 			while (!reqInput && (ptr != null)) {
3403 				if (isRule(ptr.getAst1())) {
3404 					reqInput = true;
3405 				}
3406 				ptr = ptr.getAst2();
3407 			}
3408 
3409 			// Now check for "END" rules
3410 			ptr = this;
3411 			while (!reqInput && (ptr != null)) {
3412 				if (ptr.getAst1() != null && ptr.getAst1().isEnd()) {
3413 					reqInput = true;
3414 				}
3415 				ptr = ptr.getAst2();
3416 			}
3417 
3418 			boolean perFileScaffolding = reqInput
3419 					&& (hasBeginFileRules || hasEndFileRules || nextfileEncountered);
3420 
3421 			// The per-file addresses are carried as properties of the tuple
3422 			// stream, so a nextfile executed from a user-defined function can
3423 			// resolve them at runtime. The begin_file address labels the
3424 			// NEXT_FILE tuple that opens each input file.
3425 			Address beginFileAddress = null;
3426 			Address endFileAddress = null;
3427 			if (perFileScaffolding) {
3428 				beginFileAddress = tuples.createAddress("begin_file");
3429 				endFileAddress = tuples.createAddress("end_file");
3430 				// The ENDFILE address is registered only when BEGINFILE or
3431 				// ENDFILE rules exist: its presence also confines a
3432 				// non-redirected getline to the current input file, which
3433 				// only matters when there are per-file hooks to protect.
3434 				if (hasBeginFileRules || hasEndFileRules) {
3435 					tuples.setEndFileAddress(endFileAddress);
3436 				}
3437 				tuples.setNextFileAddress(beginFileAddress);
3438 			}
3439 
3440 			// grab all BEGINs
3441 			ptr = this;
3442 			// ptr.getAst1() == blank rule condition (i.e.: { print })
3443 			while (ptr != null) {
3444 				if (ptr.getAst1() != null && ptr.getAst1().isBegin()) {
3445 					ptr.getAst1().populateTuples(tuples);
3446 				}
3447 
3448 				ptr = ptr.getAst2();
3449 			}
3450 
3451 			if (reqInput) {
3452 				Address inputLoopAddress = tuples.createAddress("input_loop_address");
3453 				Address noMoreInput = tuples.createAddress("no_more_input");
3454 
3455 				if (perFileScaffolding) {
3456 					// Advance to the next input file so the BEGINFILE rules
3457 					// observe its FILENAME, FNR, ARGIND, and ERRNO. The
3458 					// ENDFILE section loops back here for the following files.
3459 					tuples.address(beginFileAddress);
3460 					tuples.nextFile(noMoreInput);
3461 
3462 					// BEGINFILE rules, in the order they were read
3463 					ptr = this;
3464 					while (ptr != null) {
3465 						if (ptr.getAst1() != null && ptr.getAst1().isBeginFile()) {
3466 							ptr.getAst1().populateTuples(tuples);
3467 						}
3468 						ptr = ptr.getAst2();
3469 					}
3470 
3471 					// per-file input loop: at end of the current file, run the
3472 					// ENDFILE rules instead of silently opening the next file
3473 					tuples.address(inputLoopAddress);
3474 					tuples.consumeFileInput(endFileAddress);
3475 				} else {
3476 					tuples.address(inputLoopAddress);
3477 					tuples.consumeInput(noMoreInput);
3478 				}
3479 
3480 				// grab all INPUT RULES
3481 				ptr = this;
3482 				while (ptr != null) {
3483 					// the first one of these is an input rule
3484 					if (isRule(ptr.getAst1())) {
3485 						ptr.getAst1().populateTuples(tuples);
3486 					}
3487 					ptr = ptr.getAst2();
3488 				}
3489 				tuples.address(nextAddress);
3490 
3491 				tuples.gotoAddress(inputLoopAddress);
3492 
3493 				if (perFileScaffolding) {
3494 					// ENDFILE rules, in the order they were read
3495 					tuples.address(endFileAddress);
3496 					ptr = this;
3497 					while (ptr != null) {
3498 						if (ptr.getAst1() != null && ptr.getAst1().isEndFile()) {
3499 							ptr.getAst1().populateTuples(tuples);
3500 						}
3501 						ptr = ptr.getAst2();
3502 					}
3503 
3504 					// then loop back to the NEXT_FILE tuple at begin_file,
3505 					// which falls through to the END rules once the input is
3506 					// exhausted
3507 					tuples.gotoAddress(beginFileAddress);
3508 				}
3509 
3510 				tuples.address(noMoreInput);
3511 				// compiler has issue with missing nop here
3512 				tuples.nop();
3513 			}
3514 
3515 			// indicate where the first end block resides
3516 			// in the event of an exit statement
3517 			tuples.address(exitAddr);
3518 			tuples.setWithinEndBlocks(true);
3519 
3520 			// grab all ENDs
3521 			ptr = this;
3522 			while (ptr != null) {
3523 				if (ptr.getAst1() != null && ptr.getAst1().isEnd()) {
3524 					ptr.getAst1().populateTuples(tuples);
3525 				}
3526 				ptr = ptr.getAst2();
3527 			}
3528 
3529 			// force a nop here to resolve any addresses that haven't been resolved yet
3530 			// (i.e., no_more_input wouldn't be resolved if there are no END{} blocks)
3531 			tuples.nop();
3532 
3533 			popSourceLineNumber(tuples);
3534 			return 0;
3535 		}
3536 	}
3537 
3538 	private final class ExpressionToEvaluateAst extends AST {
3539 
3540 		private ExpressionToEvaluateAst(AST expr) {
3541 			super(expr);
3542 		}
3543 
3544 		@Override
3545 		public int populateTuples(AwkTuples tuples) {
3546 
3547 			pushSourceLineNumber(tuples);
3548 
3549 			// initialize runtime-managed special variables via JRT defaults in AVM
3550 			symbolTable.getID("NR");
3551 			symbolTable.getID("FNR");
3552 			symbolTable.getID("NF");
3553 			symbolTable.getID("FS");
3554 			symbolTable.getID("RS");
3555 			symbolTable.getID("SUBSEP");
3556 			symbolTable.getID("CONVFMT");
3557 			IDAst environAst = symbolTable.getID("ENVIRON");
3558 
3559 			// MUST BE DONE AFTER FUNCTIONS ARE COMPILED,
3560 			// and after special variables are made known to the symbol table
3561 			// (see above)!
3562 			tuples.markEvalTupleStream();
3563 			tuples.setNumGlobals(symbolTable.numGlobals());
3564 
3565 			boolean evalSymtabActive = !posix && symbolTable.isGlobalReferenced("SYMTAB");
3566 			if (environAst.isReferenced() || evalSymtabActive) {
3567 				tuples.environOffset(environAst.offset);
3568 			}
3569 			if (evalSymtabActive) {
3570 				tuples.updateSymtab(symbolTable.getID("SYMTAB").offset);
3571 			}
3572 			if (!posix && symbolTable.isGlobalReferenced("FUNCTAB")) {
3573 				tuples.updateFunctab(symbolTable.getID("FUNCTAB").offset);
3574 			}
3575 			tuples.beforeStartHooks();
3576 
3577 			if (getAst1() != null) {
3578 				getAst1().populateTuples(tuples);
3579 			}
3580 			// Some expression forms (for example ternaries) still need a concrete
3581 			// terminal tuple to resolve branch targets during post-processing.
3582 			tuples.nop();
3583 
3584 			popSourceLineNumber(tuples);
3585 			return 0;
3586 		}
3587 	}
3588 
3589 	// made non-static to access the "nextAddress" field of the frontend
3590 	private final class RuleAst extends AST {
3591 
3592 		private RuleAst(AST optExpression, AST optRule) {
3593 			super(optExpression, optRule);
3594 			addFlag(AstFlag.NEXTABLE);
3595 		}
3596 
3597 		@Override
3598 		public int populateTuples(AwkTuples tuples) {
3599 			pushSourceLineNumber(tuples);
3600 			boolean unconditionalRule = getAst1() == null
3601 					|| getAst1().isBegin()
3602 					|| getAst1().isEnd()
3603 					|| getAst1().isBeginFile()
3604 					|| getAst1().isEndFile();
3605 			if (!unconditionalRule) {
3606 				getAst1().populateTuples(tuples);
3607 				// result of whether to execute or not is on the stack
3608 				Address bypassRule = tuples.createAddress("bypassRule");
3609 				tuples.ifFalse(bypassRule);
3610 				populateRuleBody(tuples);
3611 				tuples.address(bypassRule).nop();
3612 			} else {
3613 				populateRuleBody(tuples);
3614 			}
3615 			popSourceLineNumber(tuples);
3616 			return 0;
3617 		}
3618 
3619 		private void populateRuleBody(AwkTuples tuples) {
3620 			// execute the optRule here!
3621 			if (getAst2() == null) {
3622 				if (isRule(this)) {
3623 					// display $0
3624 					tuples.print(0);
3625 				}
3626 				// else, don't populate it with anything
3627 				// (i.e., blank BEGIN/END/BEGINFILE/ENDFILE rule)
3628 			} else {
3629 				// execute it, and leave nothing on the stack
3630 				getAst2().populateTuples(tuples);
3631 			}
3632 		}
3633 
3634 		@Override
3635 		public Address nextAddress() {
3636 			if (!isRule(this)) {
3637 				throw new SemanticException(
3638 						"`next' cannot be called from a `" + specialRuleName() + "' rule.");
3639 			}
3640 			if (nextAddress == null) {
3641 				throw new SemanticException("Cannot call next here.");
3642 			}
3643 			return nextAddress;
3644 		}
3645 
3646 		/**
3647 		 * Names the special (non-input) rule this AST represents, for
3648 		 * gawk-compatible diagnostics.
3649 		 */
3650 		private String specialRuleName() {
3651 			AST pattern = getAst1();
3652 			if (pattern != null && pattern.isBegin()) {
3653 				return "BEGIN";
3654 			}
3655 			if (pattern != null && pattern.isEnd()) {
3656 				return "END";
3657 			}
3658 			if (pattern != null && pattern.isBeginFile()) {
3659 				return "BEGINFILE";
3660 			}
3661 			if (pattern != null && pattern.isEndFile()) {
3662 				return "ENDFILE";
3663 			}
3664 			return "special";
3665 		}
3666 	}
3667 
3668 	private final class IfStatementAst extends AST {
3669 
3670 		private IfStatementAst(AST expr, AST b1, AST b2) {
3671 			super(expr, b1, b2);
3672 		}
3673 
3674 		@Override
3675 		public int populateTuples(AwkTuples tuples) {
3676 			pushSourceLineNumber(tuples);
3677 
3678 			Address elseblock = tuples.createAddress("elseblock");
3679 
3680 			getAst1().populateTuples(tuples);
3681 			tuples.ifFalse(elseblock);
3682 			if (getAst2() != null) {
3683 				getAst2().populateTuples(tuples);
3684 			}
3685 			if (getAst3() == null) {
3686 				tuples.address(elseblock);
3687 			} else {
3688 				Address end = tuples.createAddress("end");
3689 				tuples.gotoAddress(end);
3690 				tuples.address(elseblock);
3691 				getAst3().populateTuples(tuples);
3692 				tuples.address(end);
3693 			}
3694 			popSourceLineNumber(tuples);
3695 			return 0;
3696 		}
3697 	}
3698 
3699 	private final class TernaryExpressionAst extends ScalarExpressionAst {
3700 
3701 		private TernaryExpressionAst(AST a1, AST a2, AST a3) {
3702 			super(a1, a2, a3);
3703 		}
3704 
3705 		@Override
3706 		public int populateTuples(AwkTuples tuples) {
3707 			pushSourceLineNumber(tuples);
3708 
3709 			Address elseexpr = tuples.createAddress("elseexpr");
3710 			Address endTertiary = tuples.createAddress("endTertiary");
3711 
3712 			getAst1().populateTuples(tuples);
3713 			tuples.ifFalse(elseexpr);
3714 			getAst2().populateTuples(tuples);
3715 			tuples.gotoAddress(endTertiary);
3716 
3717 			tuples.address(elseexpr);
3718 			getAst3().populateTuples(tuples);
3719 			tuples.address(endTertiary);
3720 
3721 			popSourceLineNumber(tuples);
3722 			return 1;
3723 		}
3724 	}
3725 
3726 	private final class WhileStatementAst extends AST {
3727 
3728 		private Address breakAddress;
3729 		private Address continueAddress;
3730 
3731 		private WhileStatementAst(AST expr, AST block) {
3732 			super(expr, block);
3733 			addFlag(AstFlag.BREAKABLE);
3734 			addFlag(AstFlag.CONTINUEABLE);
3735 		}
3736 
3737 		@Override
3738 		public Address breakAddress() {
3739 			return breakAddress;
3740 		}
3741 
3742 		@Override
3743 		public Address continueAddress() {
3744 			return continueAddress;
3745 		}
3746 
3747 		@Override
3748 		public int populateTuples(AwkTuples tuples) {
3749 			pushSourceLineNumber(tuples);
3750 
3751 			breakAddress = tuples.createAddress("breakAddress");
3752 
3753 			// LOOP
3754 			Address loop = tuples.createAddress("loop");
3755 			tuples.address(loop);
3756 
3757 			// for while statements, the start-of-loop is the continue jump address
3758 			continueAddress = loop;
3759 
3760 			// condition
3761 			getAst1().populateTuples(tuples);
3762 			tuples.ifFalse(breakAddress);
3763 
3764 			if (getAst2() != null) {
3765 				getAst2().populateTuples(tuples);
3766 			}
3767 
3768 			tuples.gotoAddress(loop);
3769 
3770 			tuples.address(breakAddress);
3771 
3772 			popSourceLineNumber(tuples);
3773 			return 0;
3774 		}
3775 	}
3776 
3777 	private final class DoStatementAst extends AST {
3778 
3779 		private Address breakAddress;
3780 		private Address continueAddress;
3781 
3782 		private DoStatementAst(AST block, AST expr) {
3783 			super(block, expr);
3784 			addFlag(AstFlag.BREAKABLE);
3785 			addFlag(AstFlag.CONTINUEABLE);
3786 		}
3787 
3788 		@Override
3789 		public Address breakAddress() {
3790 			return breakAddress;
3791 		}
3792 
3793 		@Override
3794 		public Address continueAddress() {
3795 			return continueAddress;
3796 		}
3797 
3798 		@Override
3799 		public int populateTuples(AwkTuples tuples) {
3800 			pushSourceLineNumber(tuples);
3801 
3802 			breakAddress = tuples.createAddress("breakAddress");
3803 			continueAddress = tuples.createAddress("continueAddress");
3804 
3805 			// LOOP
3806 			Address loop = tuples.createAddress("loop");
3807 			tuples.address(loop);
3808 
3809 			if (getAst1() != null) {
3810 				getAst1().populateTuples(tuples);
3811 			}
3812 
3813 			// for do-while statements, the continue jump address is the loop condition
3814 			tuples.address(continueAddress);
3815 
3816 			// condition
3817 			getAst2().populateTuples(tuples);
3818 			tuples.ifTrue(loop);
3819 
3820 			// tuples.gotoAddress(loop);
3821 
3822 			tuples.address(breakAddress);
3823 
3824 			popSourceLineNumber(tuples);
3825 			return 0;
3826 		}
3827 	}
3828 
3829 	private final class ForStatementAst extends AST {
3830 
3831 		private Address breakAddress;
3832 		private Address continueAddress;
3833 
3834 		private ForStatementAst(AST expr1, AST expr2, AST expr3, AST block) {
3835 			super(expr1, expr2, expr3, block);
3836 			addFlag(AstFlag.BREAKABLE);
3837 			addFlag(AstFlag.CONTINUEABLE);
3838 		}
3839 
3840 		@Override
3841 		public Address breakAddress() {
3842 			return breakAddress;
3843 		}
3844 
3845 		@Override
3846 		public Address continueAddress() {
3847 			return continueAddress;
3848 		}
3849 
3850 		@Override
3851 		public int populateTuples(AwkTuples tuples) {
3852 			pushSourceLineNumber(tuples);
3853 
3854 			breakAddress = tuples.createAddress("breakAddress");
3855 			continueAddress = tuples.createAddress("continueAddress");
3856 
3857 			// initial actions
3858 			if (getAst1() != null) {
3859 				int ast1Result = getAst1().populateTuples(tuples);
3860 				for (int i = 0; i < ast1Result; i++) {
3861 					tuples.pop();
3862 				}
3863 			}
3864 			// LOOP
3865 			Address loop = tuples.createAddress("loop");
3866 			tuples.address(loop);
3867 
3868 			if (getAst2() != null) {
3869 				// condition
3870 				// assert(getAst2() != null);
3871 				getAst2().populateTuples(tuples);
3872 				tuples.ifFalse(breakAddress);
3873 			}
3874 
3875 			if (getAst4() != null) {
3876 				// post loop action
3877 				getAst4().populateTuples(tuples);
3878 			}
3879 
3880 			// for for-loops, the continue jump address is the post-loop-action
3881 			tuples.address(continueAddress);
3882 
3883 			// post-loop action
3884 			if (getAst3() != null) {
3885 				int ast3Result = getAst3().populateTuples(tuples);
3886 				for (int i = 0; i < ast3Result; i++) {
3887 					tuples.pop();
3888 				}
3889 			}
3890 
3891 			tuples.gotoAddress(loop);
3892 
3893 			tuples.address(breakAddress);
3894 
3895 			popSourceLineNumber(tuples);
3896 			return 0;
3897 		}
3898 	}
3899 
3900 	private final class ForInStatementAst extends AST {
3901 
3902 		private Address breakAddress;
3903 		private Address continueAddress;
3904 
3905 		private ForInStatementAst(AST keyIdAst, AST arrayIdAst, AST block) {
3906 			super(keyIdAst, arrayIdAst, block);
3907 			addFlag(AstFlag.BREAKABLE);
3908 			addFlag(AstFlag.CONTINUEABLE);
3909 		}
3910 
3911 		@Override
3912 		public Address breakAddress() {
3913 			return breakAddress;
3914 		}
3915 
3916 		@Override
3917 		public Address continueAddress() {
3918 			return continueAddress;
3919 		}
3920 
3921 		@Override
3922 		public int populateTuples(AwkTuples tuples) {
3923 			pushSourceLineNumber(tuples);
3924 
3925 			breakAddress = tuples.createAddress("breakAddress");
3926 
3927 			populateArrayOperandTuples(getAst2(), tuples, false, getAst2() + " is not an array");
3928 			// pops the array and pushes the keyset
3929 			tuples.keylist();
3930 
3931 			// stack now contains:
3932 			// keylist
3933 
3934 			// LOOP
3935 			Address loop = tuples.createAddress("loop");
3936 			tuples.address(loop);
3937 
3938 			// for for-in loops, the continue jump address is the start-of-loop address
3939 			continueAddress = loop;
3940 
3941 			// condition
3942 			tuples.dup();
3943 			tuples.isEmptyList(breakAddress);
3944 
3945 			// take an element off the set
3946 			tuples.dup();
3947 			tuples.getFirstAndRemoveFromList();
3948 			// assign it to the id
3949 			tuples.assign(((IDAst) getAst1()).offset, ((IDAst) getAst1()).isGlobal);
3950 			tuples.pop(); // remove the assignment result
3951 
3952 			if (getAst3() != null) {
3953 				// execute the block
3954 				getAst3().populateTuples(tuples);
3955 			}
3956 			// otherwise, there is no block to execute
3957 
3958 			tuples.gotoAddress(loop);
3959 
3960 			tuples.address(breakAddress);
3961 			tuples.pop(); // keylist
3962 
3963 			popSourceLineNumber(tuples);
3964 			return 0;
3965 		}
3966 	}
3967 
3968 	@SuppressWarnings("unused")
3969 	private final class EmptyStatementAst extends AST {
3970 
3971 		private EmptyStatementAst() {
3972 			super();
3973 		}
3974 
3975 		@Override
3976 		public int populateTuples(AwkTuples tuples) {
3977 			pushSourceLineNumber(tuples);
3978 			// nothing to populate!
3979 			popSourceLineNumber(tuples);
3980 			return 0;
3981 		}
3982 	}
3983 
3984 	/**
3985 	 * The AST for an expression used as a statement.
3986 	 * If the expression returns a value, the value is popped
3987 	 * off the stack and discarded.
3988 	 */
3989 	private final class ExpressionStatementAst extends AST {
3990 
3991 		private ExpressionStatementAst(AST expr) {
3992 			super(expr);
3993 		}
3994 
3995 		@Override
3996 		public int populateTuples(AwkTuples tuples) {
3997 			pushSourceLineNumber(tuples);
3998 			int exprCount = getAst1().populateTuples(tuples);
3999 			if (exprCount == 1) {
4000 				tuples.popScalar();
4001 			}
4002 			popSourceLineNumber(tuples);
4003 			return 0;
4004 		}
4005 	}
4006 
4007 	private final class AssignmentExpressionAst extends ScalarExpressionAst {
4008 
4009 		/** operand / operator */
4010 		private Token op;
4011 		private String text;
4012 
4013 		private AssignmentExpressionAst(AST lhs, Token op, String text, AST rhs) {
4014 			super(lhs, rhs);
4015 			this.op = op;
4016 			this.text = text;
4017 		}
4018 
4019 		@Override
4020 		public String toString() {
4021 			return super.toString() + " (" + op + "/" + text + ")";
4022 		}
4023 
4024 		@Override
4025 		public int populateTuples(AwkTuples tuples) {
4026 			pushSourceLineNumber(tuples);
4027 			getAst2().populateTuples(tuples); // here, stack contains one value
4028 			if (getAst1() instanceof IDAst) {
4029 				IDAst idAst = (IDAst) getAst1();
4030 				idAst.setScalar(true);
4031 				boolean isSpecial = isJrtManagedSpecialName(idAst.id);
4032 				if (isSpecial) {
4033 					// value is on stack from RHS
4034 					switch (op) {
4035 					case EQUALS:
4036 						assignSpecial(tuples, idAst.id);
4037 						break;
4038 					case PLUS_EQ:
4039 						pushSpecialThenSwap(tuples, idAst.id);
4040 						tuples.add();
4041 						assignSpecial(tuples, idAst.id);
4042 						break;
4043 					case MINUS_EQ:
4044 						pushSpecialThenSwap(tuples, idAst.id);
4045 						tuples.subtract();
4046 						assignSpecial(tuples, idAst.id);
4047 						break;
4048 					case MULT_EQ:
4049 						pushSpecialThenSwap(tuples, idAst.id);
4050 						tuples.multiply();
4051 						assignSpecial(tuples, idAst.id);
4052 						break;
4053 					case DIV_EQ:
4054 						pushSpecialThenSwap(tuples, idAst.id);
4055 						tuples.divide();
4056 						assignSpecial(tuples, idAst.id);
4057 						break;
4058 					case MOD_EQ:
4059 						pushSpecialThenSwap(tuples, idAst.id);
4060 						tuples.mod();
4061 						assignSpecial(tuples, idAst.id);
4062 						break;
4063 					case POW_EQ:
4064 						pushSpecialThenSwap(tuples, idAst.id);
4065 						tuples.pow();
4066 						assignSpecial(tuples, idAst.id);
4067 						break;
4068 					default:
4069 						throw new Error("Unhandled op: " + op + " / " + text);
4070 					}
4071 					if ("RS".equals(idAst.id)) {
4072 						tuples.applyRS();
4073 					}
4074 				} else {
4075 					if (op == Token.EQUALS) {
4076 						// Expected side effect:
4077 						// Upon assignment, if the var is RS, reapply RS to input streams.
4078 						tuples.assign(idAst.offset, idAst.isGlobal);
4079 					} else if (op == Token.PLUS_EQ) {
4080 						tuples.plusEq(idAst.offset, idAst.isGlobal);
4081 					} else if (op == Token.MINUS_EQ) {
4082 						tuples.minusEq(idAst.offset, idAst.isGlobal);
4083 					} else if (op == Token.MULT_EQ) {
4084 						tuples.multEq(idAst.offset, idAst.isGlobal);
4085 					} else if (op == Token.DIV_EQ) {
4086 						tuples.divEq(idAst.offset, idAst.isGlobal);
4087 					} else if (op == Token.MOD_EQ) {
4088 						tuples.modEq(idAst.offset, idAst.isGlobal);
4089 					} else if (op == Token.POW_EQ) {
4090 						tuples.powEq(idAst.offset, idAst.isGlobal);
4091 					} else {
4092 						throw new Error("Unhandled op: " + op + " / " + text);
4093 					}
4094 					if (idAst.id.equals("RS")) {
4095 						tuples.applyRS();
4096 					}
4097 				}
4098 			} else if (getAst1() instanceof ArrayReferenceAst) {
4099 				ArrayReferenceAst arr = (ArrayReferenceAst) getAst1();
4100 				if (arr.getAst1() instanceof IDAst) {
4101 					IDAst idAst = (IDAst) arr.getAst1();
4102 					idAst.setArray(true);
4103 				}
4104 				arr.populateTargetReferenceTuples(tuples);
4105 				if (op == Token.EQUALS) {
4106 					tuples.assignMapElement();
4107 				} else if (op == Token.PLUS_EQ) {
4108 					tuples.plusEqMapElement();
4109 				} else if (op == Token.MINUS_EQ) {
4110 					tuples.minusEqMapElement();
4111 				} else if (op == Token.MULT_EQ) {
4112 					tuples.multEqMapElement();
4113 				} else if (op == Token.DIV_EQ) {
4114 					tuples.divEqMapElement();
4115 				} else if (op == Token.MOD_EQ) {
4116 					tuples.modEqMapElement();
4117 				} else if (op == Token.POW_EQ) {
4118 					tuples.powEqMapElement();
4119 				} else {
4120 					throw new NotImplementedError("Unhandled op: " + op + " / " + text + " for arrays.");
4121 				}
4122 			} else if (getAst1() instanceof DollarExpressionAst) {
4123 				DollarExpressionAst dollarExpr = (DollarExpressionAst) getAst1();
4124 				dollarExpr.getAst1().populateTuples(tuples); // stack contains eval of dollar arg
4125 
4126 				if (op == Token.EQUALS) {
4127 					tuples.assignAsInputField();
4128 				} else if (op == Token.PLUS_EQ) {
4129 					tuples.plusEqInputField();
4130 				} else if (op == Token.MINUS_EQ) {
4131 					tuples.minusEqInputField();
4132 				} else if (op == Token.MULT_EQ) {
4133 					tuples.multEqInputField();
4134 				} else if (op == Token.DIV_EQ) {
4135 					tuples.divEqInputField();
4136 				} else if (op == Token.MOD_EQ) {
4137 					tuples.modEqInputField();
4138 				} else if (op == Token.POW_EQ) {
4139 					tuples.powEqInputField();
4140 				} else {
4141 					throw new NotImplementedError("Unhandled op: " + op + " / " + text + " for dollar expressions.");
4142 				}
4143 			} else {
4144 				throw new SemanticException("Cannot perform an assignment on: " + getAst1());
4145 			}
4146 			popSourceLineNumber(tuples);
4147 			return 1;
4148 		}
4149 
4150 		private void pushSpecialThenSwap(AwkTuples tuples, String id) {
4151 			pushSpecialVariable(tuples, id);
4152 			tuples.swap();
4153 		}
4154 
4155 		private void assignSpecial(AwkTuples tuples, String id) {
4156 			assignSpecialVariable(tuples, id);
4157 		}
4158 	}
4159 
4160 	private final class InExpressionAst extends ScalarExpressionAst {
4161 
4162 		private InExpressionAst(AST arg, AST arr) {
4163 			super(arg, arr);
4164 		}
4165 
4166 		@Override
4167 		public int populateTuples(AwkTuples tuples) {
4168 			pushSourceLineNumber(tuples);
4169 			if (!(getAst2() instanceof IDAst) && !(getAst2() instanceof ArrayReferenceAst)) {
4170 				throw new SemanticException("Expecting an array for rhs of IN. Got an expression.");
4171 			}
4172 
4173 			getAst1().populateTuples(tuples);
4174 			populateArrayOperandTuples(getAst2(), tuples, false, "Expecting an array for rhs of IN. Got a scalar.");
4175 			tuples.isIn();
4176 
4177 			popSourceLineNumber(tuples);
4178 			return 1;
4179 		}
4180 	}
4181 
4182 	private final class ComparisonExpressionAst extends ScalarExpressionAst {
4183 
4184 		/**
4185 		 * operand / operator
4186 		 */
4187 		private Token op;
4188 		private String text;
4189 
4190 		private ComparisonExpressionAst(AST lhs, Token op, String text, AST rhs) {
4191 			super(lhs, rhs);
4192 			this.op = op;
4193 			this.text = text;
4194 		}
4195 
4196 		@Override
4197 		public String toString() {
4198 			return super.toString() + " (" + op + "/" + text + ")";
4199 		}
4200 
4201 		@Override
4202 		public int populateTuples(AwkTuples tuples) {
4203 			pushSourceLineNumber(tuples);
4204 
4205 			getAst1().populateTuples(tuples);
4206 			if (op == Token.MATCHES || op == Token.NOT_MATCHES) {
4207 				populateRawRegexpParameterTuples(getAst2(), tuples);
4208 			} else {
4209 				getAst2().populateTuples(tuples);
4210 			}
4211 			// 2 values on the stack
4212 
4213 			if (op == Token.EQ) {
4214 				tuples.cmpEq();
4215 			} else if (op == Token.NE) {
4216 				tuples.cmpEq();
4217 				tuples.not();
4218 			} else if (op == Token.LT) {
4219 				tuples.cmpLt();
4220 			} else if (op == Token.GT) {
4221 				tuples.cmpGt();
4222 			} else if (op == Token.LE) {
4223 				tuples.cmpGt();
4224 				tuples.not();
4225 			} else if (op == Token.GE) {
4226 				tuples.cmpLt();
4227 				tuples.not();
4228 			} else if (op == Token.MATCHES) {
4229 				tuples.matches();
4230 			} else if (op == Token.NOT_MATCHES) {
4231 				tuples.matches();
4232 				tuples.not();
4233 			} else {
4234 				throw new Error("Unhandled op: " + op + " / " + text);
4235 			}
4236 
4237 			popSourceLineNumber(tuples);
4238 			return 1;
4239 		}
4240 	}
4241 
4242 	private final class LogicalExpressionAst extends ScalarExpressionAst {
4243 
4244 		/**
4245 		 * operand / operator
4246 		 */
4247 		private Token op;
4248 		private String text;
4249 
4250 		private LogicalExpressionAst(AST lhs, Token op, String text, AST rhs) {
4251 			super(lhs, rhs);
4252 			this.op = op;
4253 			this.text = text;
4254 		}
4255 
4256 		@Override
4257 		public String toString() {
4258 			return super.toString() + " (" + op + "/" + text + ")";
4259 		}
4260 
4261 		@Override
4262 		public int populateTuples(AwkTuples tuples) {
4263 			pushSourceLineNumber(tuples);
4264 			// exhibit short-circuit behavior
4265 			Address end = tuples.createAddress("end");
4266 			getAst1().populateTuples(tuples);
4267 			tuples.dup();
4268 			if (op == Token.OR) {
4269 				// shortCircuit when op is Token.OR and 1st arg is true
4270 				tuples.ifTrue(end);
4271 			} else if (op == Token.AND) {
4272 				tuples.ifFalse(end);
4273 			}
4274 			tuples.pop();
4275 			getAst2().populateTuples(tuples);
4276 			tuples.address(end);
4277 
4278 			// turn the result into boolean one or zero
4279 			tuples.toNumber();
4280 			popSourceLineNumber(tuples);
4281 			return 1;
4282 		}
4283 	}
4284 
4285 	private final class BinaryExpressionAst extends ScalarExpressionAst {
4286 
4287 		/**
4288 		 * operand / operator
4289 		 */
4290 		private Token op;
4291 		private String text;
4292 
4293 		private BinaryExpressionAst(AST lhs, Token op, String text, AST rhs) {
4294 			super(lhs, rhs);
4295 			this.op = op;
4296 			this.text = text;
4297 		}
4298 
4299 		@Override
4300 		public String toString() {
4301 			return super.toString() + " (" + op + "/" + text + ")";
4302 		}
4303 
4304 		@Override
4305 		public int populateTuples(AwkTuples tuples) {
4306 			pushSourceLineNumber(tuples);
4307 			getAst1().populateTuples(tuples);
4308 			getAst2().populateTuples(tuples);
4309 			if (op == Token.PLUS) {
4310 				tuples.add();
4311 			} else if (op == Token.MINUS) {
4312 				tuples.subtract();
4313 			} else if (op == Token.MULT) {
4314 				tuples.multiply();
4315 			} else if (op == Token.DIVIDE) {
4316 				tuples.divide();
4317 			} else if (op == Token.MOD) {
4318 				tuples.mod();
4319 			} else if (op == Token.POW) {
4320 				tuples.pow();
4321 			} else {
4322 				throw new Error("Unhandled op: " + op + " / " + this);
4323 			}
4324 			popSourceLineNumber(tuples);
4325 			return 1;
4326 		}
4327 	}
4328 
4329 	private final class ConcatExpressionAst extends ScalarExpressionAst {
4330 
4331 		private ConcatExpressionAst(AST lhs, AST rhs) {
4332 			super(lhs, rhs);
4333 		}
4334 
4335 		@Override
4336 		public int populateTuples(AwkTuples tuples) {
4337 			pushSourceLineNumber(tuples);
4338 			getAst1().populateTuples(tuples);
4339 			getAst2().populateTuples(tuples);
4340 			tuples.concat();
4341 			popSourceLineNumber(tuples);
4342 			return 1;
4343 		}
4344 	}
4345 
4346 	private final class NegativeExpressionAst extends ScalarExpressionAst {
4347 
4348 		private NegativeExpressionAst(AST expr) {
4349 			super(expr);
4350 		}
4351 
4352 		@Override
4353 		public int populateTuples(AwkTuples tuples) {
4354 			pushSourceLineNumber(tuples);
4355 			getAst1().populateTuples(tuples);
4356 			tuples.negate();
4357 			popSourceLineNumber(tuples);
4358 			return 1;
4359 		}
4360 	}
4361 
4362 	private final class UnaryPlusExpressionAst extends ScalarExpressionAst {
4363 
4364 		private UnaryPlusExpressionAst(AST expr) {
4365 			super(expr);
4366 		}
4367 
4368 		@Override
4369 		public int populateTuples(AwkTuples tuples) {
4370 			pushSourceLineNumber(tuples);
4371 			getAst1().populateTuples(tuples);
4372 			tuples.unaryPlus();
4373 			popSourceLineNumber(tuples);
4374 			return 1;
4375 		}
4376 	}
4377 
4378 	private final class NotExpressionAst extends ScalarExpressionAst {
4379 
4380 		private NotExpressionAst(AST expr) {
4381 			super(expr);
4382 		}
4383 
4384 		@Override
4385 		public int populateTuples(AwkTuples tuples) {
4386 			pushSourceLineNumber(tuples);
4387 			getAst1().populateTuples(tuples);
4388 			tuples.not();
4389 			popSourceLineNumber(tuples);
4390 			return 1;
4391 		}
4392 	}
4393 
4394 	private final class DollarExpressionAst extends ScalarExpressionAst {
4395 
4396 		private DollarExpressionAst(AST expr) {
4397 			super(expr);
4398 		}
4399 
4400 		@Override
4401 		public int populateTuples(AwkTuples tuples) {
4402 			pushSourceLineNumber(tuples);
4403 			getAst1().populateTuples(tuples);
4404 			tuples.getInputField();
4405 			popSourceLineNumber(tuples);
4406 			return 1;
4407 		}
4408 	}
4409 
4410 	private final class ArrayIndexAst extends ScalarExpressionAst {
4411 
4412 		private ArrayIndexAst(AST exprAst, AST next) {
4413 			super(exprAst, next);
4414 		}
4415 
4416 		@Override
4417 		public int populateTuples(AwkTuples tuples) {
4418 			pushSourceLineNumber(tuples);
4419 			AST ptr = this;
4420 			int cnt = 0;
4421 			while (ptr != null) {
4422 				ptr.getAst1().populateTuples(tuples);
4423 				++cnt;
4424 				ptr = ptr.getAst2();
4425 			}
4426 			if (cnt > 1) {
4427 				tuples.applySubsep(cnt);
4428 			}
4429 			popSourceLineNumber(tuples);
4430 			return 1;
4431 		}
4432 	}
4433 
4434 	// made classname all capitals to stand out in a syntax tree dump
4435 	private final class StatementListAst extends AST {
4436 
4437 		private StatementListAst(AST statementAst, AST rest) {
4438 			super(statementAst, rest);
4439 		}
4440 
4441 		/**
4442 		 * Recursively process statements within this statement list.
4443 		 * <p>
4444 		 * It originally was done linearly. However, quirks in the grammar required
4445 		 * a more general, recursive approach to processing this "list".
4446 		 * <p>
4447 		 * Note: this should be reevaluated periodically in case the grammar
4448 		 * becomes linear again.
4449 		 */
4450 		@Override
4451 		public int populateTuples(AwkTuples tuples) {
4452 			pushSourceLineNumber(tuples);
4453 			// typical recursive processing of a list
4454 			getAst1().populateTuples(tuples);
4455 			if (getAst2() != null) {
4456 				getAst2().populateTuples(tuples);
4457 			}
4458 			popSourceLineNumber(tuples);
4459 			return 0;
4460 		}
4461 
4462 		@Override
4463 		public String toString() {
4464 			return super.toString() + " <" + getAst1() + ">";
4465 		}
4466 	}
4467 
4468 	// made non-static to access the symbol table
4469 	private final class FunctionDefAst extends AST {
4470 
4471 		private String id;
4472 		private Address functionAddress;
4473 		private Address returnAddress;
4474 
4475 		@Override
4476 		public Address returnAddress() {
4477 			return returnAddress;
4478 		}
4479 
4480 		private FunctionDefAst(String id, AST params, AST funcBody) {
4481 			super(params, funcBody);
4482 			this.id = id;
4483 			setFunctionFlag(true);
4484 			addFlag(AstFlag.RETURNABLE);
4485 		}
4486 
4487 		public Address getAddress() {
4488 			return functionAddress;
4489 		}
4490 
4491 		@Override
4492 		public int populateTuples(AwkTuples tuples) {
4493 			pushSourceLineNumber(tuples);
4494 
4495 			functionAddress = tuples.createAddress("function: " + id);
4496 			returnAddress = tuples.createAddress("returnAddress for " + id);
4497 
4498 			// annotate the tuple list
4499 			// (useful for compilation,
4500 			// not necessary for interpretation)
4501 			tuples.function(id, paramCount());
4502 
4503 			// functionAddress refers to first function body statement
4504 			// rather than to function def opcode because during
4505 			// interpretation, the function definition is a nop,
4506 			// and for compilation, the next match of the function
4507 			// name can be used
4508 			tuples.address(functionAddress);
4509 
4510 			// the stack contains the parameters to the function call (in rev order, which is good)
4511 
4512 			// execute the body
4513 			// (function body could be empty [no statements])
4514 			if (getAst2() != null) {
4515 				getAst2().populateTuples(tuples);
4516 			}
4517 
4518 			tuples.address(returnAddress);
4519 
4520 			tuples.returnFromFunction();
4521 
4522 			/////////////////////////////////////////////
4523 
4524 			popSourceLineNumber(tuples);
4525 			return 0;
4526 		}
4527 
4528 		int paramCount() {
4529 			AST ptr = getAst1();
4530 			int count = 0;
4531 			while (ptr != null) {
4532 				++count;
4533 				ptr = ptr.getAst1();
4534 			}
4535 			return count;
4536 		}
4537 
4538 	}
4539 
4540 	private final class FunctionCallAst extends ScalarExpressionAst {
4541 
4542 		private FunctionProxy functionProxy;
4543 
4544 		private FunctionCallAst(FunctionProxy functionProxy, AST params) {
4545 			super(params);
4546 			this.functionProxy = functionProxy;
4547 		}
4548 
4549 		/**
4550 		 * Applies several semantic checks with respect
4551 		 * to user-defined-function calls.
4552 		 * <p>
4553 		 * The checks performed are:
4554 		 * <ul>
4555 		 * <li>Make sure the function is defined.
4556 		 * </ul>
4557 		 * A failure of any one of these checks
4558 		 * results in a SemanticException.
4559 		 *
4560 		 * @throws SemanticException upon a failure of
4561 		 *         any of the semantic checks specified above.
4562 		 */
4563 		@Override
4564 		public void semanticAnalysis() throws SemanticException {
4565 			if (!functionProxy.isDefined()) {
4566 				throw new SemanticException("function " + functionProxy + " not defined");
4567 			}
4568 		}
4569 
4570 		@Override
4571 		public int populateTuples(AwkTuples tuples) {
4572 			pushSourceLineNumber(tuples);
4573 			if (!functionProxy.isDefined()) {
4574 				throw new SemanticException("function " + functionProxy + " not defined");
4575 			}
4576 			tuples.scriptThis();
4577 			int actualParamCountLocal;
4578 			if (getAst1() == null) {
4579 				actualParamCountLocal = 0;
4580 			} else {
4581 				actualParamCountLocal = populateActualParametersUpTo(
4582 						tuples,
4583 						(FunctionCallParamListAst) getAst1(),
4584 						0,
4585 						functionProxy.getFunctionParamCount());
4586 			}
4587 			int formalParamCount = functionProxy.getFunctionParamCount();
4588 
4589 			if (actualParamCount() > formalParamCount) {
4590 				// gawk accepts the call but reports it each time it runs
4591 				tuples.warning(extraArgumentWarning());
4592 			}
4593 			tuples
4594 					.callFunction(
4595 							functionProxy,
4596 							functionProxy.getFunctionName(),
4597 							formalParamCount,
4598 							actualParamCountLocal);
4599 			popSourceLineNumber(tuples);
4600 			return 1;
4601 		}
4602 
4603 		private int actualParamCount() {
4604 			int cnt = 0;
4605 			AST ptr = getAst1();
4606 			while (ptr != null) {
4607 				++cnt;
4608 				ptr = ptr.getAst2();
4609 			}
4610 			return cnt;
4611 		}
4612 
4613 		private String extraArgumentWarning() {
4614 			return String
4615 					.format(
4616 							"gawk: %s:%d: warning: function `%s' called with more arguments than declared",
4617 							sourceBasename(),
4618 							warningLineNo(),
4619 							functionProxy.getFunctionName());
4620 		}
4621 
4622 		private int warningLineNo() {
4623 			if (getAst1() instanceof FunctionCallParamListAst) {
4624 				AST firstParam = getAst1().getAst1();
4625 				if (firstParam != null) {
4626 					return firstParam.getLineNo();
4627 				}
4628 			}
4629 			return getLineNo();
4630 		}
4631 
4632 	}
4633 
4634 	private final class IndirectFunctionCallAst extends ScalarExpressionAst {
4635 
4636 		private IndirectFunctionCallAst(AST functionNameAst, AST params) {
4637 			super(functionNameAst, params);
4638 		}
4639 
4640 		@Override
4641 		public int populateTuples(AwkTuples tuples) {
4642 			pushSourceLineNumber(tuples);
4643 			getAst1().populateTuples(tuples);
4644 			int actualParamCount = populateIndirectActualParameters(
4645 					tuples,
4646 					(FunctionCallParamListAst) getAst2());
4647 			tuples
4648 					.indirectCall(
4649 							symbolTable.indirectFunctionTargets(),
4650 							extensions,
4651 							actualParamCount,
4652 							sourceBasename(),
4653 							getLineNo());
4654 			popSourceLineNumber(tuples);
4655 			return 1;
4656 		}
4657 	}
4658 
4659 	private final class BuiltinFunctionCallAst extends ScalarExpressionAst {
4660 
4661 		private final String id;
4662 		private final BuiltinFunction builtin;
4663 
4664 		private BuiltinFunctionCallAst(String id, AST params) {
4665 			super(params);
4666 			this.id = id;
4667 			this.builtin = BuiltinFunction.of(id);
4668 		}
4669 
4670 		@Override
4671 		public int populateTuples(AwkTuples tuples) {
4672 			pushSourceLineNumber(tuples);
4673 			switch (builtin) {
4674 			case SPRINTF:
4675 				populateSprintfTuples(tuples);
4676 				break;
4677 			case CLOSE:
4678 				populateOneArgumentTuples(tuples, "close");
4679 				tuples.close();
4680 				break;
4681 			case LENGTH:
4682 				populateLengthTuples(tuples);
4683 				break;
4684 			case SRAND:
4685 				populateSrandTuples(tuples);
4686 				break;
4687 			case RAND:
4688 				if (getAst1() != null) {
4689 					throw new SemanticException("rand does not take arguments");
4690 				}
4691 				tuples.rand();
4692 				break;
4693 			case SQRT:
4694 				populateArgumentsTuples(tuples, 1, "sqrt requires only 1 argument");
4695 				tuples.sqrt();
4696 				break;
4697 			case INT:
4698 				populateArgumentsTuples(tuples, 1, "int requires only 1 argument");
4699 				tuples.intFunc();
4700 				break;
4701 			case LOG:
4702 				populateArgumentsTuples(tuples, 1, "log requires only 1 argument");
4703 				tuples.log();
4704 				break;
4705 			case EXP:
4706 				populateArgumentsTuples(tuples, 1, "exp requires only 1 argument");
4707 				tuples.exp();
4708 				break;
4709 			case SIN:
4710 				populateArgumentsTuples(tuples, 1, "sin requires only 1 argument");
4711 				tuples.sin();
4712 				break;
4713 			case COS:
4714 				populateArgumentsTuples(tuples, 1, "cos requires only 1 argument");
4715 				tuples.cos();
4716 				break;
4717 			case ATAN2:
4718 				populateArgumentsTuples(tuples, 2, "atan2 requires 2 arguments");
4719 				tuples.atan2();
4720 				break;
4721 			case MATCH:
4722 				populateMatchArgumentsTuples(tuples);
4723 				tuples.match();
4724 				break;
4725 			case INDEX:
4726 				populateArgumentsTuples(tuples, 2, "index requires 2 arguments");
4727 				tuples.index();
4728 				break;
4729 			case SUB:
4730 			case GSUB:
4731 				populateSubTuples(tuples, builtin == BuiltinFunction.GSUB);
4732 				break;
4733 			case SPLIT:
4734 				populateSplitTuples(tuples);
4735 				break;
4736 			case SUBSTR:
4737 				populateSubstrTuples(tuples);
4738 				break;
4739 			case TOLOWER:
4740 				populateOneArgumentTuples(tuples, "tolower");
4741 				tuples.tolower();
4742 				break;
4743 			case TOUPPER:
4744 				populateOneArgumentTuples(tuples, "toupper");
4745 				tuples.toupper();
4746 				break;
4747 			case SYSTEM:
4748 				populateOneArgumentTuples(tuples, "system");
4749 				tuples.system();
4750 				break;
4751 			default:
4752 				throw new NotImplementedError("builtin: " + id);
4753 			}
4754 			popSourceLineNumber(tuples);
4755 			return 1;
4756 		}
4757 
4758 		/**
4759 		 * Populates the tuples of the argument list, validating that
4760 		 * it produces exactly the expected number of values on the stack.
4761 		 *
4762 		 * @param tuples the tuples to populate
4763 		 * @param expectedCount the exact number of arguments required
4764 		 * @param errorMessage the message of the {@link SemanticException}
4765 		 *        thrown when the argument count does not match
4766 		 */
4767 		private void populateArgumentsTuples(AwkTuples tuples, int expectedCount, String errorMessage) {
4768 			int ast1Result = getAst1().populateTuples(tuples);
4769 			if (ast1Result != expectedCount) {
4770 				throw new SemanticException(errorMessage);
4771 			}
4772 		}
4773 
4774 		/**
4775 		 * Populates the tuples of the argument list for a built-in function
4776 		 * taking exactly one argument, rejecting missing or extra arguments.
4777 		 *
4778 		 * @param tuples the tuples to populate
4779 		 * @param functionName the name of the built-in function, used in
4780 		 *        error messages
4781 		 */
4782 		private void populateOneArgumentTuples(AwkTuples tuples, String functionName) {
4783 			if (getAst1() == null) {
4784 				throw new SemanticException(functionName + " requires 1 argument");
4785 			}
4786 			int ast1Result = getAst1().populateTuples(tuples);
4787 			if (ast1Result != 1) {
4788 				throw new SemanticException(functionName + " requires only 1 argument");
4789 			}
4790 		}
4791 
4792 		/**
4793 		 * Populates the tuples of the argument list for the <code>match</code>
4794 		 * built-in function, treating its 2nd argument as a literal regular
4795 		 * expression.
4796 		 *
4797 		 * @param tuples the tuples to populate
4798 		 */
4799 		private void populateMatchArgumentsTuples(AwkTuples tuples) {
4800 			int ast1Result = populateActualParameters(tuples, (FunctionCallParamListAst) getAst1(), 1);
4801 			if (ast1Result != 2) {
4802 				throw new SemanticException("match requires 2 arguments");
4803 			}
4804 		}
4805 
4806 		/**
4807 		 * Populates the tuples for the <code>sprintf</code> built-in function.
4808 		 *
4809 		 * @param tuples the tuples to populate
4810 		 */
4811 		private void populateSprintfTuples(AwkTuples tuples) {
4812 			if (getAst1() == null) {
4813 				throw new SemanticException("sprintf requires at least 1 argument");
4814 			}
4815 			int ast1Result = getAst1().populateTuples(tuples);
4816 			if (ast1Result == 0) {
4817 				throw new SemanticException("sprintf requires at minimum 1 argument");
4818 			}
4819 			tuples.sprintf(ast1Result);
4820 		}
4821 
4822 		/**
4823 		 * Populates the tuples for the <code>length</code> built-in function,
4824 		 * which takes either no argument (implying $0) or one argument.
4825 		 *
4826 		 * @param tuples the tuples to populate
4827 		 */
4828 		private void populateLengthTuples(AwkTuples tuples) {
4829 			if (getAst1() == null) {
4830 				tuples.length(0);
4831 			} else {
4832 				AST params = getAst1();
4833 				AST argument = params instanceof FunctionCallParamListAst
4834 						&& params.getAst2() == null ?
4835 								params.getAst1() : params;
4836 				int ast1Result;
4837 				if (argument instanceof IDAst
4838 						&& !isJrtManagedSpecialName(((IDAst) argument).id)) {
4839 					IDAst idAst = (IDAst) argument;
4840 					tuples.pushIndirectArgument(idAst.offset, idAst.isGlobal);
4841 					ast1Result = 1;
4842 				} else {
4843 					ast1Result = argument.populateTuples(tuples);
4844 				}
4845 				if (ast1Result != 1) {
4846 					throw new SemanticException("length requires at least one argument");
4847 				}
4848 				tuples.length(1);
4849 			}
4850 		}
4851 
4852 		/**
4853 		 * Populates the tuples for the <code>srand</code> built-in function,
4854 		 * which takes either no argument or one argument (the seed).
4855 		 *
4856 		 * @param tuples the tuples to populate
4857 		 */
4858 		private void populateSrandTuples(AwkTuples tuples) {
4859 			if (getAst1() == null) {
4860 				tuples.srand(0);
4861 			} else {
4862 				int ast1Result = getAst1().populateTuples(tuples);
4863 				if (ast1Result != 1) {
4864 					throw new SemanticException("srand takes either 0 or one argument, not " + ast1Result);
4865 				}
4866 				tuples.srand(1);
4867 			}
4868 		}
4869 
4870 		/**
4871 		 * Populates the tuples for the <code>sub</code> and <code>gsub</code>
4872 		 * built-in functions, which take 2 or 3 arguments; the optional 3rd
4873 		 * argument is the substitution target (a variable, an array element,
4874 		 * or an input field reference), defaulting to $0.
4875 		 *
4876 		 * @param tuples the tuples to populate
4877 		 * @param isGsub <code>true</code> for <code>gsub</code> (global
4878 		 *        substitution), <code>false</code> for <code>sub</code>
4879 		 */
4880 		private void populateSubTuples(AwkTuples tuples, boolean isGsub) {
4881 			if (getAst1() == null || getAst1().getAst2() == null || getAst1().getAst2().getAst1() == null) {
4882 				throw new SemanticException("sub needs at least 2 arguments");
4883 			}
4884 			int numargs = 0;
4885 			for (AST paramPtr = getAst1(); paramPtr != null; paramPtr = paramPtr.getAst2()) {
4886 				numargs++;
4887 			}
4888 			if (numargs != 2 && numargs != 3) {
4889 				throw new SemanticException("sub requires 2 or 3 arguments, not " + numargs);
4890 			}
4891 
4892 			populateRawRegexpParameterTuples(getAst1().getAst1(), tuples);
4893 			getAst1().getAst2().getAst1().populateTuples(tuples);
4894 			if (numargs == 3) {
4895 				AST targetAst = getAst1().getAst2().getAst2().getAst1();
4896 				if (targetAst instanceof ArrayReferenceAst) {
4897 					((ArrayReferenceAst) targetAst).populateTargetValueTuples(tuples);
4898 				} else {
4899 					targetAst.populateTuples(tuples);
4900 				}
4901 			}
4902 
4903 			// stack contains arg1,arg2[,arg3] - in that pop() order
4904 
4905 			if (numargs == 2) {
4906 				tuples.subForDollar0(isGsub);
4907 			} else if (numargs == 3) {
4908 				AST ptr = getAst1().getAst2().getAst2().getAst1();
4909 				if (ptr instanceof IDAst) {
4910 					IDAst idAst = (IDAst) ptr;
4911 					idAst.setScalar(true);
4912 					tuples.subForVariable(idAst.offset, idAst.isGlobal, isGsub);
4913 				} else if (ptr instanceof ArrayReferenceAst) {
4914 					ArrayReferenceAst arrAst = (ArrayReferenceAst) ptr;
4915 					if (arrAst.getAst1() instanceof IDAst) {
4916 						IDAst idAst = (IDAst) arrAst.getAst1();
4917 						idAst.setArray(true);
4918 					}
4919 					arrAst.populateTargetReferenceTuples(tuples);
4920 					tuples.subForMapReference(isGsub);
4921 				} else if (ptr instanceof DollarExpressionAst) {
4922 					// push the field ref
4923 					DollarExpressionAst dollarExpr = (DollarExpressionAst) ptr;
4924 					dollarExpr.getAst1().populateTuples(tuples);
4925 					tuples.subForDollarReference(isGsub);
4926 				} else {
4927 					throw new SemanticException(
4928 							"sub's 3rd argument must be either an id, an array reference, or an input field reference");
4929 				}
4930 			}
4931 		}
4932 
4933 		/**
4934 		 * Populates the tuples for the <code>split</code> built-in function.
4935 		 *
4936 		 * @param tuples the tuples to populate
4937 		 */
4938 		private void populateSplitTuples(AwkTuples tuples) {
4939 			// split can take 2 or 3 args:
4940 			// split (string, array [,fs])
4941 			// the 2nd argument is pass by reference, which is ok (?)
4942 
4943 			// funccallparamlist.funccallparamlist.idAst
4944 			if (getAst1() == null || getAst1().getAst2() == null || getAst1().getAst2().getAst1() == null) {
4945 				throw new SemanticException("split needs at least 2 arguments");
4946 			}
4947 			AST ptr = getAst1().getAst2().getAst1();
4948 			if (!(ptr instanceof IDAst) && !(ptr instanceof ArrayReferenceAst)) {
4949 				throw new SemanticException("split needs an array or subarray reference as its 2nd argument");
4950 			}
4951 			if (ptr instanceof IDAst) {
4952 				IDAst arrAst = (IDAst) ptr;
4953 				if (arrAst.isScalar()) {
4954 					throw new SemanticException("split's 2nd arg cannot be a scalar");
4955 				}
4956 				arrAst.setArray(true);
4957 			}
4958 
4959 			int ast1Result = 0;
4960 			for (AST paramPtr = getAst1(); paramPtr != null; paramPtr = paramPtr.getAst2()) {
4961 				ast1Result++;
4962 			}
4963 			if (ast1Result != 2 && ast1Result != 3) {
4964 				throw new SemanticException("split requires 2 or 3 arguments, not " + ast1Result);
4965 			}
4966 
4967 			getAst1().getAst1().populateTuples(tuples);
4968 			populateArrayOperandTuples(
4969 					ptr,
4970 					tuples,
4971 					true,
4972 					"split's 2nd arg must be an array or subarray reference");
4973 			if (ast1Result == 3) {
4974 				populateRawRegexpParameterTuples(getAst1().getAst2().getAst2().getAst1(), tuples);
4975 			}
4976 			tuples.split(ast1Result);
4977 		}
4978 
4979 		/**
4980 		 * Populates the tuples for the <code>substr</code> built-in function,
4981 		 * which takes 2 or 3 arguments.
4982 		 *
4983 		 * @param tuples the tuples to populate
4984 		 */
4985 		private void populateSubstrTuples(AwkTuples tuples) {
4986 			if (getAst1() == null) {
4987 				throw new SemanticException("substr requires at least 2 arguments");
4988 			}
4989 			int ast1Result = getAst1().populateTuples(tuples);
4990 			if (ast1Result != 2 && ast1Result != 3) {
4991 				throw new SemanticException("substr requires 2 or 3 arguments, not " + ast1Result);
4992 			}
4993 			tuples.substr(ast1Result);
4994 		}
4995 	}
4996 
4997 	private final class FunctionCallParamListAst extends AST {
4998 
4999 		private FunctionCallParamListAst(AST expr, AST rest) {
5000 			super(expr, rest);
5001 		}
5002 
5003 		@Override
5004 		public int populateTuples(AwkTuples tuples) {
5005 			pushSourceLineNumber(tuples);
5006 			int retval;
5007 			if (getAst2() == null) {
5008 				retval = getAst1().populateTuples(tuples);
5009 			} else {
5010 				retval = getAst1().populateTuples(tuples) + getAst2().populateTuples(tuples);
5011 			}
5012 			popSourceLineNumber(tuples);
5013 			return retval;
5014 		}
5015 	}
5016 
5017 	private final class FunctionDefParamListAst extends AST {
5018 
5019 		private String id;
5020 
5021 		private FunctionDefParamListAst(String id, AST rest) {
5022 			super(rest);
5023 			this.id = id;
5024 		}
5025 
5026 		public int populateTuples(AwkTuples tuples) {
5027 			throw new Error("Cannot 'execute' function definition parameter list (formal parameters) in this manner.");
5028 		}
5029 
5030 		/**
5031 		 * According to the spec
5032 		 * (http://www.opengroup.org/onlinepubs/007908799/xcu/awk.html)
5033 		 * formal function parameters cannot be special variables,
5034 		 * such as NF, NR, etc).
5035 		 *
5036 		 * @throws SemanticException upon a semantic error.
5037 		 */
5038 		@Override
5039 		public void semanticAnalysis() throws SemanticException {
5040 			// could do it recursively, but not necessary
5041 			// since all getAst1()'s are FunctionDefParamList's
5042 			// and, thus, terminals (no need to do further
5043 			// semantic analysis)
5044 
5045 			FunctionDefParamListAst ptr = this;
5046 			while (ptr != null) {
5047 				if (isSpecialVariableName(ptr.id)) {
5048 					throw new SemanticException("Special variable " + ptr.id + " cannot be used as a formal parameter");
5049 				}
5050 				ptr = (FunctionDefParamListAst) ptr.getAst1();
5051 			}
5052 		}
5053 	}
5054 
5055 	/**
5056 	 * Flag for non-statement expressions.
5057 	 * Unknown for certain, but I think this is done
5058 	 * to avoid partial variable assignment mistakes.
5059 	 * For example, instead of a=3, the programmer
5060 	 * inadvertently places the a on the line. If IDAsts
5061 	 * were not tagged with AstFlag.NON_STATEMENT, then the
5062 	 * incomplete assignment would parse properly, and
5063 	 * the developer might remain unaware of this issue.
5064 	 */
5065 
5066 	private final class IDAst extends AST {
5067 
5068 		private String id;
5069 		private int offset = AVM.NULL_OFFSET;
5070 		private boolean isGlobal;
5071 		private boolean referenced;
5072 
5073 		private IDAst(String id, boolean isGlobal) {
5074 			this.id = id;
5075 			this.isGlobal = isGlobal;
5076 			addFlag(AstFlag.NON_STATEMENT);
5077 		}
5078 
5079 		private boolean isArray = false;
5080 		private boolean isScalar = false;
5081 
5082 		@Override
5083 		public String toString() {
5084 			return super.toString() + " (" + id + ")";
5085 		}
5086 
5087 		@Override
5088 		public int populateTuples(AwkTuples tuples) {
5089 			pushSourceLineNumber(tuples);
5090 			if (isJrtManagedSpecialName(id)) {
5091 				// Use JRT-managed reads for specials
5092 				pushSpecialVariable(tuples, id);
5093 			} else {
5094 				// Bare identifiers are scalar uses. Array-only contexts emit a
5095 				// typed dereference through populateArrayOperandTuples().
5096 				tuples.dereference(offset, false, isGlobal);
5097 			}
5098 			popSourceLineNumber(tuples);
5099 			return 1;
5100 		}
5101 
5102 		@Override
5103 		public boolean isArray() {
5104 			return isArray;
5105 		}
5106 
5107 		@Override
5108 		public boolean isScalar() {
5109 			return isScalar;
5110 		}
5111 
5112 		private boolean isReferenced() {
5113 			return referenced;
5114 		}
5115 
5116 		private void markReferenced() {
5117 			referenced = true;
5118 		}
5119 
5120 		private void setArray(boolean b) {
5121 			isArray = b;
5122 		}
5123 
5124 		private void setScalar(boolean b) {
5125 			isScalar = b;
5126 		}
5127 	}
5128 
5129 	private final class ArrayReferenceAst extends ScalarExpressionAst {
5130 
5131 		private ArrayReferenceAst(AST idAst, AST idxAst) {
5132 			super(idAst, idxAst);
5133 		}
5134 
5135 		private ArrayReferenceAst(int lineNo, AST idAst, AST idxAst) {
5136 			super(lineNo, idAst, idxAst);
5137 		}
5138 
5139 		@Override
5140 		public String toString() {
5141 			return super.toString() + " (" + getAst1() + " [...])";
5142 		}
5143 
5144 		@Override
5145 		public int populateTuples(AwkTuples tuples) {
5146 			pushSourceLineNumber(tuples);
5147 			// get the containing array, autovivifying missing parent subarrays
5148 			populateContainerTuples(tuples);
5149 			// get the index
5150 			getAst2().populateTuples(tuples);
5151 			tuples.dereferenceArray();
5152 			popSourceLineNumber(tuples);
5153 			return 1;
5154 		}
5155 
5156 		private void populateTargetReferenceTuples(AwkTuples tuples) {
5157 			pushSourceLineNumber(tuples);
5158 			populateContainerTuples(tuples);
5159 			getAst2().populateTuples(tuples);
5160 			popSourceLineNumber(tuples);
5161 		}
5162 
5163 		private void populateArrayValueTuples(AwkTuples tuples, boolean createIfMissing) {
5164 			pushSourceLineNumber(tuples);
5165 			populateContainerTuples(tuples);
5166 			getAst2().populateTuples(tuples);
5167 			if (createIfMissing) {
5168 				tuples.ensureArrayElement();
5169 			} else {
5170 				tuples.peekArrayElement();
5171 			}
5172 			popSourceLineNumber(tuples);
5173 		}
5174 
5175 		private void populateTargetValueTuples(AwkTuples tuples) {
5176 			pushSourceLineNumber(tuples);
5177 			populateContainerTuples(tuples);
5178 			getAst2().populateTuples(tuples);
5179 			tuples.dereferenceArray();
5180 			popSourceLineNumber(tuples);
5181 		}
5182 
5183 		private void populateContainerTuples(AwkTuples tuples) {
5184 			if (getAst1() instanceof ArrayReferenceAst) {
5185 				((ArrayReferenceAst) getAst1()).populateArrayValueTuples(tuples, true);
5186 			} else if (getAst1() instanceof IDAst) {
5187 				IDAst idAst = (IDAst) getAst1();
5188 				if (isJrtManagedSpecialName(idAst.id)) {
5189 					idAst.populateTuples(tuples);
5190 				} else {
5191 					tuples.dereference(idAst.offset, true, idAst.isGlobal);
5192 				}
5193 			} else {
5194 				getAst1().populateTuples(tuples);
5195 			}
5196 		}
5197 	}
5198 
5199 	private final class IntegerAst extends ScalarExpressionAst {
5200 
5201 		private Long value;
5202 
5203 		private IntegerAst(Long value) {
5204 			this.value = value;
5205 			addFlag(AstFlag.NON_STATEMENT);
5206 		}
5207 
5208 		@Override
5209 		public String toString() {
5210 			return super.toString() + " (" + value + ")";
5211 		}
5212 
5213 		@Override
5214 		public int populateTuples(AwkTuples tuples) {
5215 			pushSourceLineNumber(tuples);
5216 			tuples.push(value);
5217 			popSourceLineNumber(tuples);
5218 			return 1;
5219 		}
5220 	}
5221 
5222 	/**
5223 	 * Can either assume the role of a double or an integer
5224 	 * by aggressively normalizing the value to an int if possible.
5225 	 */
5226 	private final class DoubleAst extends ScalarExpressionAst {
5227 
5228 		private Object value;
5229 
5230 		private DoubleAst(Double val) {
5231 			double d = val.doubleValue();
5232 			if (d == (int) d) {
5233 				this.value = (int) d;
5234 			} else {
5235 				this.value = d;
5236 			}
5237 			addFlag(AstFlag.NON_STATEMENT);
5238 		}
5239 
5240 		@Override
5241 		public String toString() {
5242 			return super.toString() + " (" + value + ")";
5243 		}
5244 
5245 		@Override
5246 		public int populateTuples(AwkTuples tuples) {
5247 			pushSourceLineNumber(tuples);
5248 			tuples.push(value);
5249 			popSourceLineNumber(tuples);
5250 			return 1;
5251 		}
5252 	}
5253 
5254 	/**
5255 	 * A string is a string; Awk doesn't attempt to normalize
5256 	 * it until it is used in an arithmetic operation!
5257 	 */
5258 	private final class StringAst extends ScalarExpressionAst {
5259 
5260 		private String value;
5261 
5262 		private StringAst(String str) {
5263 			this.value = str;
5264 			addFlag(AstFlag.NON_STATEMENT);
5265 		}
5266 
5267 		@Override
5268 		public String toString() {
5269 			return super.toString() + " (" + value + ")";
5270 		}
5271 
5272 		@Override
5273 		public int populateTuples(AwkTuples tuples) {
5274 			pushSourceLineNumber(tuples);
5275 			tuples.push(value);
5276 			popSourceLineNumber(tuples);
5277 			return 1;
5278 		}
5279 	}
5280 
5281 	private final class RegexpAst extends ScalarExpressionAst {
5282 
5283 		private String regexpStr;
5284 		private boolean typed;
5285 
5286 		private RegexpAst(String regexpStr, boolean typedParam) {
5287 			this.regexpStr = regexpStr;
5288 			this.typed = typedParam;
5289 		}
5290 
5291 		@Override
5292 		public String toString() {
5293 			return super.toString() + " (" + regexpStr + ")";
5294 		}
5295 
5296 		@Override
5297 		public int populateTuples(AwkTuples tuples) {
5298 			pushSourceLineNumber(tuples);
5299 			if (typed) {
5300 				tuples.regexp(regexpStr);
5301 			} else {
5302 				tuples.getInputField(0);
5303 				tuples.regexp(regexpStr);
5304 				tuples.matches();
5305 			}
5306 			popSourceLineNumber(tuples);
5307 			return 1;
5308 		}
5309 
5310 		private int populateRawRegexpTuples(AwkTuples tuples) {
5311 			tuples.regexp(regexpStr);
5312 			return 1;
5313 		}
5314 	}
5315 
5316 	private final class ConditionPairAst extends ScalarExpressionAst {
5317 
5318 		private final long conditionPairId;
5319 
5320 		private ConditionPairAst(AST booleanAst1, AST booleanAst2) {
5321 			super(booleanAst1, booleanAst2);
5322 			conditionPairId = ++conditionPairCount;
5323 		}
5324 
5325 		@Override
5326 		public int populateTuples(AwkTuples tuples) {
5327 			// The start condition is evaluated only while outside the range, and the
5328 			// end condition only once the range has started (including on the very
5329 			// record that starts it, so a range can begin and end on the same record)
5330 			pushSourceLineNumber(tuples);
5331 
5332 			Address enterRange = tuples.createAddress("conditionPairEnterRange");
5333 			Address testEnd = tuples.createAddress("conditionPairTestEnd");
5334 			Address withinRange = tuples.createAddress("conditionPairWithinRange");
5335 			Address end = tuples.createAddress("conditionPairEnd");
5336 
5337 			tuples.conditionPairInRange(conditionPairId);
5338 			tuples.ifTrue(testEnd);
5339 			getAst1().populateTuples(tuples);
5340 			tuples.ifTrue(enterRange);
5341 			tuples.push(0);
5342 			tuples.gotoAddress(end);
5343 
5344 			tuples.address(enterRange);
5345 			tuples.conditionPairEnter(conditionPairId);
5346 
5347 			tuples.address(testEnd);
5348 			getAst2().populateTuples(tuples);
5349 			tuples.ifFalse(withinRange);
5350 			tuples.conditionPairLeave(conditionPairId);
5351 
5352 			tuples.address(withinRange);
5353 			tuples.push(1);
5354 			tuples.address(end);
5355 
5356 			popSourceLineNumber(tuples);
5357 			return 1;
5358 		}
5359 	}
5360 
5361 	private final class BeginAst extends AST {
5362 
5363 		private BeginAst() {
5364 			super();
5365 			setBeginFlag(true);
5366 		}
5367 
5368 		@Override
5369 		public int populateTuples(AwkTuples tuples) {
5370 			pushSourceLineNumber(tuples);
5371 			tuples.push(1);
5372 			popSourceLineNumber(tuples);
5373 			return 1;
5374 		}
5375 	}
5376 
5377 	private final class EndAst extends AST {
5378 
5379 		private EndAst() {
5380 			super();
5381 			setEndFlag(true);
5382 		}
5383 
5384 		@Override
5385 		public int populateTuples(AwkTuples tuples) {
5386 			pushSourceLineNumber(tuples);
5387 			tuples.push(1);
5388 			popSourceLineNumber(tuples);
5389 			return 1;
5390 		}
5391 	}
5392 
5393 	private final class BeginFileAst extends AST {
5394 
5395 		private BeginFileAst() {
5396 			super();
5397 			setBeginFileFlag(true);
5398 		}
5399 
5400 		@Override
5401 		public int populateTuples(AwkTuples tuples) {
5402 			pushSourceLineNumber(tuples);
5403 			tuples.push(1);
5404 			popSourceLineNumber(tuples);
5405 			return 1;
5406 		}
5407 	}
5408 
5409 	private final class EndFileAst extends AST {
5410 
5411 		private EndFileAst() {
5412 			super();
5413 			setEndFileFlag(true);
5414 		}
5415 
5416 		@Override
5417 		public int populateTuples(AwkTuples tuples) {
5418 			pushSourceLineNumber(tuples);
5419 			tuples.push(1);
5420 			popSourceLineNumber(tuples);
5421 			return 1;
5422 		}
5423 	}
5424 
5425 	private final class PreIncAst extends ScalarExpressionAst {
5426 
5427 		private PreIncAst(AST symbolAst) {
5428 			super(symbolAst);
5429 		}
5430 
5431 		@Override
5432 		public int populateTuples(AwkTuples tuples) {
5433 			pushSourceLineNumber(tuples);
5434 			if (getAst1() instanceof IDAst && isJrtManagedSpecialName(((IDAst) getAst1()).id)) {
5435 				// the sequence already leaves the new value on the stack
5436 				populateSpecialIncDec(tuples, ((IDAst) getAst1()).id, true, false);
5437 				popSourceLineNumber(tuples);
5438 				return 1;
5439 			} else if (getAst1() instanceof IDAst) {
5440 				IDAst idAst = (IDAst) getAst1();
5441 				tuples.inc(idAst.offset, idAst.isGlobal);
5442 			} else if (getAst1() instanceof ArrayReferenceAst) {
5443 				ArrayReferenceAst arrAst = (ArrayReferenceAst) getAst1();
5444 				if (arrAst.getAst1() instanceof IDAst) {
5445 					IDAst idAst = (IDAst) arrAst.getAst1();
5446 					if (idAst.isScalar()) {
5447 						throw new SemanticException("Cannot use " + idAst + " as an array.");
5448 					}
5449 					idAst.setArray(true);
5450 				}
5451 				arrAst.populateTargetReferenceTuples(tuples);
5452 				tuples.incMapRef();
5453 			} else if (getAst1() instanceof DollarExpressionAst) {
5454 				DollarExpressionAst dollarExpr = (DollarExpressionAst) getAst1();
5455 				dollarExpr.getAst1().populateTuples(tuples); // OPTIMIATION: duplicate the x in $x here
5456 				// so that it is not evaluated again
5457 				tuples.dup();
5458 				// stack contains eval of dollar arg
5459 				// tuples.assignAsInputField();
5460 				tuples.incDollarRef();
5461 				// OPTIMIATION continued: now evaluate
5462 				// the dollar expression with x (for $x)
5463 				// instead of evaluating the expression again
5464 				tuples.getInputField();
5465 				popSourceLineNumber(tuples);
5466 				return 1; // NOTE, short-circuit return here!
5467 			} else {
5468 				throw new NotImplementedError("unhandled preinc for " + getAst1());
5469 			}
5470 			// else
5471 			// assert false : "cannot refer for preInc to "+getAst1();
5472 			getAst1().populateTuples(tuples);
5473 			popSourceLineNumber(tuples);
5474 			return 1;
5475 		}
5476 	}
5477 
5478 	private final class PreDecAst extends ScalarExpressionAst {
5479 
5480 		private PreDecAst(AST symbolAst) {
5481 			super(symbolAst);
5482 		}
5483 
5484 		@Override
5485 		public int populateTuples(AwkTuples tuples) {
5486 			pushSourceLineNumber(tuples);
5487 			if (getAst1() instanceof IDAst && isJrtManagedSpecialName(((IDAst) getAst1()).id)) {
5488 				// the sequence already leaves the new value on the stack
5489 				populateSpecialIncDec(tuples, ((IDAst) getAst1()).id, false, false);
5490 				popSourceLineNumber(tuples);
5491 				return 1;
5492 			} else if (getAst1() instanceof IDAst) {
5493 				IDAst idAst = (IDAst) getAst1();
5494 				tuples.dec(idAst.offset, idAst.isGlobal);
5495 			} else if (getAst1() instanceof ArrayReferenceAst) {
5496 				ArrayReferenceAst arrAst = (ArrayReferenceAst) getAst1();
5497 				if (arrAst.getAst1() instanceof IDAst) {
5498 					IDAst idAst = (IDAst) arrAst.getAst1();
5499 					if (idAst.isScalar()) {
5500 						throw new SemanticException("Cannot use " + idAst + " as an array.");
5501 					}
5502 					idAst.setArray(true);
5503 				}
5504 				arrAst.populateTargetReferenceTuples(tuples);
5505 				tuples.decMapRef();
5506 			} else if (getAst1() instanceof DollarExpressionAst) {
5507 				DollarExpressionAst dollarExpr = (DollarExpressionAst) getAst1();
5508 				dollarExpr.getAst1().populateTuples(tuples); // OPTIMIATION: duplicate the x in $x here
5509 				// so that it is not evaluated again
5510 				tuples.dup();
5511 				// stack contains eval of dollar arg
5512 				// tuples.assignAsInputField();
5513 				tuples.decDollarRef();
5514 				// OPTIMIATION continued: now evaluate
5515 				// the dollar expression with x (for $x)
5516 				// instead of evaluating the expression again
5517 				tuples.getInputField();
5518 				popSourceLineNumber(tuples);
5519 				return 1; // NOTE, short-circuit return here!
5520 			} else {
5521 				throw new NotImplementedError("unhandled predec for " + getAst1());
5522 			}
5523 			getAst1().populateTuples(tuples);
5524 			popSourceLineNumber(tuples);
5525 			return 1;
5526 		}
5527 	}
5528 
5529 	private final class PostIncAst extends ScalarExpressionAst {
5530 
5531 		private PostIncAst(AST symbolAst) {
5532 			super(symbolAst);
5533 		}
5534 
5535 		@Override
5536 		public int populateTuples(AwkTuples tuples) {
5537 			pushSourceLineNumber(tuples);
5538 			if (getAst1() instanceof DollarExpressionAst) {
5539 				DollarExpressionAst dollarExpr = (DollarExpressionAst) getAst1();
5540 				dollarExpr.getAst1().populateTuples(tuples);
5541 				tuples.incDollarRef();
5542 			} else if (getAst1() instanceof IDAst && isJrtManagedSpecialName(((IDAst) getAst1()).id)) {
5543 				populateSpecialIncDec(tuples, ((IDAst) getAst1()).id, true, true);
5544 			} else {
5545 				if (getAst1() instanceof ArrayReferenceAst) {
5546 					((ArrayReferenceAst) getAst1()).populateTargetValueTuples(tuples);
5547 					tuples.unaryPlus();
5548 				} else {
5549 					getAst1().populateTuples(tuples);
5550 				}
5551 				if (getAst1() instanceof IDAst) {
5552 					IDAst idAst = (IDAst) getAst1();
5553 					tuples.postInc(idAst.offset, idAst.isGlobal);
5554 				} else if (getAst1() instanceof ArrayReferenceAst) {
5555 					ArrayReferenceAst arrAst = (ArrayReferenceAst) getAst1();
5556 					if (arrAst.getAst1() instanceof IDAst) {
5557 						IDAst idAst = (IDAst) arrAst.getAst1();
5558 						if (idAst.isScalar()) {
5559 							throw new SemanticException("Cannot use " + idAst + " as an array.");
5560 						}
5561 						idAst.setArray(true);
5562 					}
5563 					arrAst.populateTargetReferenceTuples(tuples);
5564 					tuples.incMapRef();
5565 				} else {
5566 					throw new NotImplementedError("unhandled postinc for " + getAst1());
5567 				}
5568 			}
5569 			popSourceLineNumber(tuples);
5570 			return 1;
5571 		}
5572 	}
5573 
5574 	private final class PostDecAst extends ScalarExpressionAst {
5575 
5576 		private PostDecAst(AST symbolAst) {
5577 			super(symbolAst);
5578 		}
5579 
5580 		@Override
5581 		public int populateTuples(AwkTuples tuples) {
5582 			pushSourceLineNumber(tuples);
5583 			if (getAst1() instanceof IDAst && isJrtManagedSpecialName(((IDAst) getAst1()).id)) {
5584 				populateSpecialIncDec(tuples, ((IDAst) getAst1()).id, false, true);
5585 				popSourceLineNumber(tuples);
5586 				return 1;
5587 			}
5588 			if (getAst1() instanceof ArrayReferenceAst) {
5589 				((ArrayReferenceAst) getAst1()).populateTargetValueTuples(tuples);
5590 				tuples.unaryPlus();
5591 			} else {
5592 				getAst1().populateTuples(tuples);
5593 			}
5594 			if (getAst1() instanceof IDAst) {
5595 				IDAst idAst = (IDAst) getAst1();
5596 				tuples.postDec(idAst.offset, idAst.isGlobal);
5597 			} else if (getAst1() instanceof ArrayReferenceAst) {
5598 				ArrayReferenceAst arrAst = (ArrayReferenceAst) getAst1();
5599 				if (arrAst.getAst1() instanceof IDAst) {
5600 					IDAst idAst = (IDAst) arrAst.getAst1();
5601 					if (idAst.isScalar()) {
5602 						throw new SemanticException("Cannot use " + idAst + " as an array.");
5603 					}
5604 					idAst.setArray(true);
5605 				}
5606 				arrAst.populateTargetReferenceTuples(tuples);
5607 				tuples.decMapRef();
5608 			} else if (getAst1() instanceof DollarExpressionAst) {
5609 				DollarExpressionAst dollarExpr = (DollarExpressionAst) getAst1();
5610 				dollarExpr.getAst1().populateTuples(tuples);
5611 				tuples.decDollarRef();
5612 			} else {
5613 				throw new NotImplementedError("unhandled postinc for " + getAst1());
5614 			}
5615 			popSourceLineNumber(tuples);
5616 			return 1;
5617 		}
5618 	}
5619 
5620 	private final class PrintAst extends ScalarExpressionAst {
5621 
5622 		private Token outputToken;
5623 		private boolean parenthesized;
5624 
5625 		private PrintAst(AST exprList, Token outToken, AST outputExpr, boolean parenthesized) {
5626 			super(exprList, outputExpr);
5627 			this.outputToken = outToken;
5628 			this.parenthesized = parenthesized;
5629 		}
5630 
5631 		@Override
5632 		public int populateTuples(AwkTuples tuples) {
5633 			pushSourceLineNumber(tuples);
5634 
5635 			int paramCount;
5636 			if (getAst1() == null) {
5637 				if (parenthesized) {
5638 					throw new SemanticException("print() requires at least 1 argument");
5639 				}
5640 				paramCount = 0;
5641 			} else {
5642 				paramCount = getAst1().populateTuples(tuples);
5643 				if (paramCount == 0) {
5644 					throw new SemanticException("Cannot print the result. The expression doesn't return anything.");
5645 				}
5646 			}
5647 
5648 			if (getAst2() != null) {
5649 				getAst2().populateTuples(tuples);
5650 			}
5651 
5652 			if (outputToken == Token.GT) {
5653 				tuples.printToFile(paramCount, false); // false = no append
5654 			} else if (outputToken == Token.APPEND) {
5655 				tuples.printToFile(paramCount, true); // false = no append
5656 			} else if (outputToken == Token.PIPE) {
5657 				tuples.printToPipe(paramCount);
5658 			} else {
5659 				tuples.print(paramCount);
5660 			}
5661 
5662 			popSourceLineNumber(tuples);
5663 			return 0;
5664 		}
5665 	}
5666 
5667 	// we don't know if it is a scalar
5668 	/**
5669 	 * Returns whether the identifier names a JRT-managed special variable,
5670 	 * read and written through dedicated opcodes instead of a global slot.
5671 	 * ENVIRON and ARGV are excluded: they are special names but plain
5672 	 * slot-backed arrays, materialized by the preamble. SYMTAB and FUNCTAB
5673 	 * need no exclusion because they are not special names at all: they are
5674 	 * ordinary globals that the runtime populates when the script references
5675 	 * them.
5676 	 */
5677 	private boolean isJrtManagedSpecialName(String id) {
5678 		return isSpecialVariableName(id) && !"ENVIRON".equals(id) && !"ARGV".equals(id);
5679 	}
5680 
5681 	/**
5682 	 * Returns whether the identifier names a special variable in the current
5683 	 * compile-time mode. Most special names (NR, FS, FILENAME, ...) always
5684 	 * are. The gawk-only ERRNO and ARGIND are special outside POSIX mode
5685 	 * only: with {@code --posix} they are plain identifiers — usable as
5686 	 * function parameters and compiled as ordinary globals — exactly like
5687 	 * {@code gawk --posix} treats them.
5688 	 *
5689 	 * @param id the identifier to inspect
5690 	 * @return {@code true} when the name is special in the current mode
5691 	 */
5692 	private boolean isSpecialVariableName(String id) {
5693 		if (!SPECIAL_VAR_NAMES.containsKey(id)) {
5694 			return false;
5695 		}
5696 		if (JRT.isGawkOnlySpecialVariable(id)) {
5697 			return !posix;
5698 		}
5699 		return true;
5700 	}
5701 
5702 	/** Emits the tuple pushing the value of a JRT-managed special variable. */
5703 	private void pushSpecialVariable(AwkTuples tuples, String id) {
5704 		switch (id) {
5705 		case "NF":
5706 			tuples.pushNF();
5707 			break;
5708 		case "NR":
5709 			tuples.pushNR();
5710 			break;
5711 		case "FNR":
5712 			tuples.pushFNR();
5713 			break;
5714 		case "FS":
5715 			tuples.pushFS();
5716 			break;
5717 		case "RS":
5718 			tuples.pushRS();
5719 			break;
5720 		case "OFS":
5721 			tuples.pushOFS();
5722 			break;
5723 		case "ORS":
5724 			tuples.pushORS();
5725 			break;
5726 		case "RSTART":
5727 			tuples.pushRSTART();
5728 			break;
5729 		case "RLENGTH":
5730 			tuples.pushRLENGTH();
5731 			break;
5732 		case "IGNORECASE":
5733 			tuples.pushIGNORECASE();
5734 			break;
5735 		case "FILENAME":
5736 			tuples.pushFILENAME();
5737 			break;
5738 		case "SUBSEP":
5739 			tuples.pushSUBSEP();
5740 			break;
5741 		case "CONVFMT":
5742 			tuples.pushCONVFMT();
5743 			break;
5744 		case "OFMT":
5745 			tuples.pushOFMT();
5746 			break;
5747 		case "ARGC":
5748 			tuples.pushARGC();
5749 			break;
5750 		case "ERRNO":
5751 			tuples.pushERRNO();
5752 			break;
5753 		case "ARGIND":
5754 			tuples.pushARGIND();
5755 			break;
5756 		default:
5757 			throw new Error("Unhandled special var: " + id);
5758 		}
5759 	}
5760 
5761 	/** Emits the tuple assigning the top of the stack to a JRT-managed special variable. */
5762 	private void assignSpecialVariable(AwkTuples tuples, String id) {
5763 		switch (id) {
5764 		case "NF":
5765 			tuples.assignNF();
5766 			break;
5767 		case "NR":
5768 			tuples.assignNR();
5769 			break;
5770 		case "FNR":
5771 			tuples.assignFNR();
5772 			break;
5773 		case "FS":
5774 			tuples.assignFS();
5775 			break;
5776 		case "RS":
5777 			tuples.assignRS();
5778 			break;
5779 		case "OFS":
5780 			tuples.assignOFS();
5781 			break;
5782 		case "ORS":
5783 			tuples.assignORS();
5784 			break;
5785 		case "RSTART":
5786 			tuples.assignRSTART();
5787 			break;
5788 		case "RLENGTH":
5789 			tuples.assignRLENGTH();
5790 			break;
5791 		case "IGNORECASE":
5792 			tuples.assignIGNORECASE();
5793 			break;
5794 		case "FILENAME":
5795 			tuples.assignFILENAME();
5796 			break;
5797 		case "SUBSEP":
5798 			tuples.assignSUBSEP();
5799 			break;
5800 		case "CONVFMT":
5801 			tuples.assignCONVFMT();
5802 			break;
5803 		case "OFMT":
5804 			tuples.assignOFMT();
5805 			break;
5806 		case "ARGC":
5807 			tuples.assignARGC();
5808 			break;
5809 		case "ERRNO":
5810 			tuples.assignERRNO();
5811 			break;
5812 		case "ARGIND":
5813 			tuples.assignARGIND();
5814 			break;
5815 		default:
5816 			throw new Error("Unhandled special var: " + id);
5817 		}
5818 	}
5819 
5820 	/*
5821 	 * Increments or decrements a JRT-managed special variable. Specials live in
5822 	 * the JRT rather than in a global slot, so the slot-based INC/DEC opcodes
5823 	 * cannot be used; the sequence below reads, adjusts, and assigns through
5824 	 * the special-variable opcodes, leaving the expression value (old value for
5825 	 * postfix, new value for prefix) on the stack.
5826 	 */
5827 	private void populateSpecialIncDec(AwkTuples tuples, String id, boolean increment, boolean postfix) {
5828 		pushSpecialVariable(tuples, id);
5829 		if (postfix) {
5830 			tuples.dup();
5831 		}
5832 		tuples.push(Long.valueOf(1L));
5833 		if (increment) {
5834 			tuples.add();
5835 		} else {
5836 			tuples.subtract();
5837 		}
5838 		assignSpecialVariable(tuples, id);
5839 		if (postfix) {
5840 			tuples.pop();
5841 		}
5842 	}
5843 
5844 	/*
5845 	 * A call-expression node is normally stamped when it is reduced, after the
5846 	 * lexer may already have consumed the line terminator; the first argument
5847 	 * carries the line of the call site itself, which is what gawk-style
5848 	 * diagnostics report.
5849 	 */
5850 	private int extensionCallLineNumber(AST params) {
5851 		if (params != null && params.getAst1() != null) {
5852 			return params.getAst1().getLineNo();
5853 		}
5854 		return currentSourceLineNumber();
5855 	}
5856 
5857 	private final class ExtensionAst extends AST {
5858 
5859 		private final ExtensionFunction function;
5860 
5861 		private ExtensionAst(ExtensionFunction functionParam, AST paramAst, int lineNoParam) {
5862 			super(lineNoParam, paramAst);
5863 			this.function = functionParam;
5864 		}
5865 
5866 		@Override
5867 		public int populateTuples(AwkTuples tuples) {
5868 			pushSourceLineNumber(tuples);
5869 			int argCount;
5870 			if (getAst1() == null) {
5871 				argCount = 0;
5872 			} else {
5873 				argCount = countParams((FunctionCallParamListAst) getAst1());
5874 			}
5875 
5876 			int[] reqArrayIdxs = function.collectAssocArrayIndexes(argCount);
5877 			int[] rawValueIdxs = function.collectRawValueIndexes(argCount);
5878 
5879 			int paramCount;
5880 			if (getAst1() == null) {
5881 				paramCount = 0;
5882 			} else {
5883 				Set<Integer> arrayIndexes = new HashSet<Integer>();
5884 				Set<Integer> rawValueIndexes = new HashSet<Integer>();
5885 				Set<Integer> literalRegexpIndexes = new HashSet<Integer>();
5886 				for (int idx : rawValueIdxs) {
5887 					rawValueIndexes.add(Integer.valueOf(idx));
5888 				}
5889 				for (int idx : reqArrayIdxs) {
5890 					AST paramAst = getParamAst((FunctionCallParamListAst) getAst1(), idx).getAst1();
5891 					if (paramAst instanceof IDAst) {
5892 						IDAst idAst = (IDAst) paramAst;
5893 						if (idAst.isScalar()) {
5894 							throw new SemanticException(
5895 									"Extension '"
5896 											+ function.getKeyword()
5897 											+ "' requires parameter position "
5898 											+ idx
5899 											+ " be an associative array, not a scalar.");
5900 						}
5901 						idAst.setArray(true);
5902 						arrayIndexes.add(Integer.valueOf(idx));
5903 					} else if (paramAst instanceof ArrayReferenceAst) {
5904 						arrayIndexes.add(Integer.valueOf(idx));
5905 					}
5906 				}
5907 				for (int idx : function.collectRegexpIndexes(argCount)) {
5908 					literalRegexpIndexes.add(Integer.valueOf(idx));
5909 				}
5910 
5911 				paramCount = populateActualParameters(
5912 						tuples,
5913 						(FunctionCallParamListAst) getAst1(),
5914 						arrayIndexes,
5915 						rawValueIndexes,
5916 						literalRegexpIndexes,
5917 						0);
5918 			}
5919 			// isInitial == true ::
5920 			// retval of this extension is not a function parameter
5921 			// of another extension
5922 			// true iff Extension | FunctionCallParam | FunctionCallParam | etc.
5923 			boolean isInitial;
5924 			if (getParent() instanceof FunctionCallParamListAst) {
5925 				AST ptr = getParent();
5926 				while (ptr instanceof FunctionCallParamListAst) {
5927 					ptr = ptr.getParent();
5928 				}
5929 				isInitial = !(ptr instanceof ExtensionAst);
5930 			} else {
5931 				isInitial = true;
5932 			}
5933 			tuples.extension(function, paramCount, isInitial);
5934 			popSourceLineNumber(tuples);
5935 			// an extension always returns a value, even if it is blank/null
5936 			return 1;
5937 		}
5938 
5939 		private AST getParamAst(FunctionCallParamListAst pAst, int pos) {
5940 			for (int i = 0; i < pos; ++i) {
5941 				pAst = (FunctionCallParamListAst) pAst.getAst2();
5942 				if (pAst == null) {
5943 					throw new SemanticException("More arguments required for assoc array parameter position specification.");
5944 				}
5945 			}
5946 			return pAst;
5947 		}
5948 
5949 		private int countParams(FunctionCallParamListAst pAst) {
5950 			int cnt = 0;
5951 			while (pAst != null) {
5952 				pAst = (FunctionCallParamListAst) pAst.getAst2();
5953 				++cnt;
5954 			}
5955 			return cnt;
5956 		}
5957 
5958 		@Override
5959 		public String toString() {
5960 			return super.toString() + " (" + function.getKeyword() + ")";
5961 		}
5962 	}
5963 
5964 	private final class PrintfAst extends ScalarExpressionAst {
5965 
5966 		private Token outputToken;
5967 
5968 		private PrintfAst(AST exprList, Token outToken, AST outputExpr) {
5969 			super(exprList, outputExpr);
5970 			this.outputToken = outToken;
5971 		}
5972 
5973 		@Override
5974 		public int populateTuples(AwkTuples tuples) {
5975 			pushSourceLineNumber(tuples);
5976 
5977 			int paramCount;
5978 			if (getAst1() == null) {
5979 				throw new SemanticException("printf requires at least 1 argument");
5980 			} else {
5981 				paramCount = getAst1().populateTuples(tuples);
5982 				if (paramCount == 0) {
5983 					throw new SemanticException("Cannot printf the result. The expression doesn't return anything.");
5984 				}
5985 			}
5986 
5987 			if (getAst2() != null) {
5988 				getAst2().populateTuples(tuples);
5989 			}
5990 
5991 			if (outputToken == Token.GT) {
5992 				tuples.printfToFile(paramCount, false); // false = no append
5993 			} else if (outputToken == Token.APPEND) {
5994 				tuples.printfToFile(paramCount, true); // false = no append
5995 			} else if (outputToken == Token.PIPE) {
5996 				tuples.printfToPipe(paramCount);
5997 			} else {
5998 				tuples.printf(paramCount);
5999 			}
6000 
6001 			popSourceLineNumber(tuples);
6002 			return 0;
6003 		}
6004 	}
6005 
6006 	private final class GetlineAst extends ScalarExpressionAst {
6007 
6008 		private GetlineAst(AST pipeExpr, AST lvalueAst, AST inRedirect) {
6009 			super(pipeExpr, lvalueAst, inRedirect);
6010 		}
6011 
6012 		@Override
6013 		public int populateTuples(AwkTuples tuples) {
6014 			pushSourceLineNumber(tuples);
6015 			// gawk restriction: only redirected forms of getline may be used
6016 			// inside BEGINFILE/ENDFILE rules. Direct uses are rejected here at
6017 			// compile time; uses reached through user-defined functions are
6018 			// caught at runtime by the interpreter, which knows the current
6019 			// rule.
6020 			if (getAst1() == null && getAst3() == null) {
6021 				AST enclosingRule = searchFor(AstFlag.NEXTABLE);
6022 				AST pattern = enclosingRule == null ? null : enclosingRule.getAst1();
6023 				if (pattern != null && (pattern.isBeginFile() || pattern.isEndFile())) {
6024 					throw new SemanticException(
6025 							"non-redirected `getline' invalid inside `"
6026 									+ (pattern.isBeginFile() ? "BEGINFILE" : "ENDFILE")
6027 									+ "' rule");
6028 				}
6029 			}
6030 			if (getAst1() == null && getAst3() == null && getAst2() == null) {
6031 				tuples.getlineInput();
6032 				popSourceLineNumber(tuples);
6033 				return 1;
6034 			}
6035 			if (getAst1() != null) {
6036 				getAst1().populateTuples(tuples);// stack has getAst1() (i.e., "command")
6037 				tuples.useAsCommandInput();
6038 			} else if (getAst3() != null) {
6039 // getline ... < getAst3()
6040 				getAst3().populateTuples(tuples); // stack has getAst3() (i.e., "filename")
6041 				tuples.useAsFileInput();
6042 			} else {
6043 				tuples.getlineInputToTarget();
6044 			}
6045 			// 2 resultant values on the stack!
6046 			// 2nd - -1/0/1 for io-err,eof,success
6047 			// 1st(top) - the input
6048 			if (getAst2() == null) {
6049 				tuples.assignAsInput();
6050 				// stack still has the input, to be popped below...
6051 				// (all assignment results are placed on the stack)
6052 			} else if (getAst2() instanceof IDAst) {
6053 				IDAst idAst = (IDAst) getAst2();
6054 				tuples.assign(idAst.offset, idAst.isGlobal);
6055 				if (idAst.id.equals("RS")) {
6056 					tuples.applyRS();
6057 				}
6058 			} else if (getAst2() instanceof ArrayReferenceAst) {
6059 				ArrayReferenceAst arr = (ArrayReferenceAst) getAst2();
6060 				if (arr.getAst1() instanceof IDAst) {
6061 					IDAst idAst = (IDAst) arr.getAst1();
6062 					if (idAst.isScalar()) {
6063 						throw new SemanticException("Cannot use " + idAst + " as an array.");
6064 					}
6065 					idAst.setArray(true);
6066 				}
6067 				arr.populateTargetReferenceTuples(tuples);
6068 				tuples.assignMapElement();
6069 			} else if (getAst2() instanceof DollarExpressionAst) {
6070 				DollarExpressionAst dollarExpr = (DollarExpressionAst) getAst2();
6071 				if (dollarExpr.getAst2() != null) {
6072 					dollarExpr.getAst2().populateTuples(tuples);
6073 				}
6074 				// stack contains eval of dollar arg
6075 				tuples.assignAsInputField();
6076 			} else {
6077 				throw new SemanticException("Cannot getline into a " + getAst2());
6078 			}
6079 			// get rid of value left by the assignment
6080 			tuples.pop();
6081 			// one value is left on the stack
6082 			popSourceLineNumber(tuples);
6083 			return 1;
6084 		}
6085 	}
6086 
6087 	private final class ReturnStatementAst extends AST {
6088 
6089 		private ReturnStatementAst(AST expr) {
6090 			super(expr);
6091 		}
6092 
6093 		@Override
6094 		public int populateTuples(AwkTuples tuples) {
6095 			pushSourceLineNumber(tuples);
6096 			AST returnable = searchFor(AstFlag.RETURNABLE);
6097 			if (returnable == null) {
6098 				throw new SemanticException("Cannot use return here.");
6099 			}
6100 			if (getAst1() != null) {
6101 				getAst1().populateTuples(tuples);
6102 				tuples.setReturnResult();
6103 			}
6104 			tuples.gotoAddress(returnable.returnAddress());
6105 			popSourceLineNumber(tuples);
6106 			return 0;
6107 		}
6108 	}
6109 
6110 	private final class ExitStatementAst extends AST {
6111 
6112 		private ExitStatementAst(AST expr) {
6113 			super(expr);
6114 		}
6115 
6116 		@Override
6117 		public int populateTuples(AwkTuples tuples) {
6118 			pushSourceLineNumber(tuples);
6119 			if (getAst1() != null) {
6120 				getAst1().populateTuples(tuples);
6121 				tuples.exitWithCode();
6122 			} else {
6123 				tuples.exitWithoutCode();
6124 			}
6125 			popSourceLineNumber(tuples);
6126 			return 0;
6127 		}
6128 	}
6129 
6130 	private final class DeleteStatementAst extends AST {
6131 
6132 		private DeleteStatementAst(AST symbolAst) {
6133 			super(symbolAst);
6134 		}
6135 
6136 		@Override
6137 		public int populateTuples(AwkTuples tuples) {
6138 			pushSourceLineNumber(tuples);
6139 
6140 			if (getAst1() instanceof ArrayReferenceAst) {
6141 				ArrayReferenceAst arrAst = (ArrayReferenceAst) getAst1();
6142 				if (arrAst.getAst1() instanceof IDAst) {
6143 					IDAst idAst = (IDAst) arrAst.getAst1();
6144 					idAst.setArray(true);
6145 				}
6146 				arrAst.populateTargetReferenceTuples(tuples);
6147 				tuples.deleteMapElement();
6148 			} else if (getAst1() instanceof IDAst) {
6149 				IDAst idAst = (IDAst) getAst1();
6150 				idAst.setArray(true);
6151 				tuples.deleteArray(idAst.offset, idAst.isGlobal);
6152 			} else {
6153 				throw new Error("Should never reach here : delete for " + getAst1());
6154 			}
6155 
6156 			popSourceLineNumber(tuples);
6157 			return 0;
6158 		}
6159 	}
6160 
6161 	private class BreakStatementAst extends AST {
6162 
6163 		@Override
6164 		public int populateTuples(AwkTuples tuples) {
6165 			pushSourceLineNumber(tuples);
6166 			AST breakable = searchFor(AstFlag.BREAKABLE);
6167 			if (breakable == null) {
6168 				throw new SemanticException("cannot break; not within a loop");
6169 			}
6170 			tuples.gotoAddress(breakable.breakAddress());
6171 			popSourceLineNumber(tuples);
6172 			return 0;
6173 		}
6174 	}
6175 
6176 	private class NextStatementAst extends AST {
6177 
6178 		@Override
6179 		public int populateTuples(AwkTuples tuples) {
6180 			pushSourceLineNumber(tuples);
6181 			AST nextable = searchFor(AstFlag.NEXTABLE);
6182 			if (nextable == null) {
6183 				throw new SemanticException("cannot next; not within any input rules");
6184 			}
6185 			tuples.gotoAddress(nextable.nextAddress());
6186 			popSourceLineNumber(tuples);
6187 			return 0;
6188 		}
6189 	}
6190 
6191 	private class NextfileStatementAst extends AST {
6192 
6193 		@Override
6194 		public int populateTuples(AwkTuples tuples) {
6195 			pushSourceLineNumber(tuples);
6196 			AST nextable = searchFor(AstFlag.NEXTABLE);
6197 			if (nextable != null) {
6198 				// Direct use inside a rule: BEGIN, END, and ENDFILE reject
6199 				// nextfile at compile time, mirroring gawk's fatal errors.
6200 				// (Uses within user-defined functions are checked at runtime.)
6201 				AST pattern = nextable.getAst1();
6202 				if (pattern != null && (pattern.isBegin() || pattern.isEnd() || pattern.isEndFile())) {
6203 					String ruleName = pattern.isBegin() ? "BEGIN" : pattern.isEnd() ? "END" : "ENDFILE";
6204 					throw new SemanticException(
6205 							"`nextfile' cannot be called from a `" + ruleName + "' rule.");
6206 				}
6207 			}
6208 			tuples.execNextfile();
6209 			popSourceLineNumber(tuples);
6210 			return 0;
6211 		}
6212 	}
6213 
6214 	private final class ContinueStatementAst extends AST {
6215 
6216 		private ContinueStatementAst() {
6217 			super();
6218 		}
6219 
6220 		@Override
6221 		public int populateTuples(AwkTuples tuples) {
6222 			pushSourceLineNumber(tuples);
6223 			AST continueable = searchFor(AstFlag.CONTINUEABLE);
6224 			if (continueable == null) {
6225 				throw new SemanticException("cannot issue a continue; not within any loops");
6226 			}
6227 			tuples.gotoAddress(continueable.continueAddress());
6228 			popSourceLineNumber(tuples);
6229 			return 0;
6230 		}
6231 	}
6232 
6233 	// this was static...
6234 	// made non-static to throw a meaningful ParserException when necessary
6235 	private final class FunctionProxy implements Supplier<Address> {
6236 
6237 		private FunctionDefAst functionDefAst;
6238 		private String id;
6239 
6240 		private FunctionProxy(String id) {
6241 			this.id = id;
6242 		}
6243 
6244 		private void setFunctionDefinition(FunctionDefAst functionDef) {
6245 			if (functionDefAst != null) {
6246 				throw parserException("function " + functionDef + " already defined");
6247 			} else {
6248 				functionDefAst = functionDef;
6249 			}
6250 		}
6251 
6252 		private boolean isDefined() {
6253 			return functionDefAst != null;
6254 		}
6255 
6256 		@Override
6257 		public Address get() {
6258 			return functionDefAst.getAddress();
6259 		}
6260 
6261 		private String getFunctionName() {
6262 			return id;
6263 		}
6264 
6265 		private int getFunctionParamCount() {
6266 			return functionDefAst.paramCount();
6267 		}
6268 
6269 		@Override
6270 		public String toString() {
6271 			return super.toString() + " (" + id + ")";
6272 		}
6273 
6274 	}
6275 
6276 	/**
6277 	 * Adds {varName -&gt; offset} mappings to the tuples so that global variables
6278 	 * can be set by the interpreter while processing filename and name=value
6279 	 * entries from the command-line.
6280 	 * Also sends function names to the tuples, to provide the back end
6281 	 * with names to invalidate if name=value assignments are passed
6282 	 * in via the -v or ARGV arguments.
6283 	 *
6284 	 * @param tuples The tuples to add the mapping to.
6285 	 */
6286 	public void populateGlobalVariableNameToOffsetMappings(AwkTuples tuples) {
6287 		for (String varname : symbolTable.globalIds.keySet()) {
6288 			IDAst idAst = symbolTable.globalIds.get(varname);
6289 			// The last arg originally was ", idAst.isScalar", but this is not set true
6290 			// if the variable use is ambiguous. Therefore, assume it is a scalar
6291 			// if it's Token.NOT used as an array.
6292 			tuples.addGlobalVariableNameToOffsetMapping(varname, idAst.offset, idAst.isArray);
6293 		}
6294 		tuples.setFunctionNameSet(symbolTable.functionProxies.keySet());
6295 	}
6296 
6297 	private class AwkSymbolTableImpl {
6298 
6299 		int numGlobals() {
6300 			return globalIds.size();
6301 		}
6302 
6303 		// "constants"
6304 		private BeginAst beginAst = null;
6305 		private EndAst endAst = null;
6306 		private BeginFileAst beginFileAst = null;
6307 		private EndFileAst endFileAst = null;
6308 
6309 		// functions (proxies)
6310 		private Map<String, FunctionProxy> functionProxies = new HashMap<String, FunctionProxy>();
6311 		private Map<String, Tuple.IndirectFunctionTarget> cachedIndirectFunctionTargets;
6312 
6313 		// variable management
6314 		private Map<String, IDAst> globalIds = new HashMap<String, IDAst>();
6315 		private Map<String, Map<String, IDAst>> localIds = new HashMap<String, Map<String, IDAst>>();
6316 		private Map<String, Set<String>> functionParameters = new HashMap<String, Set<String>>();
6317 		private Set<String> ids = new HashSet<String>();
6318 
6319 		// current function definition for symbols
6320 		private String currentFunctionName = null;
6321 
6322 		// using set/clear rather than push/pop, it is impossible to define functions within functions
6323 		void setFunctionName(String functionName) {
6324 			this.currentFunctionName = functionName;
6325 		}
6326 
6327 		void clearFunctionName(String functionName) {
6328 			this.currentFunctionName = null;
6329 		}
6330 
6331 		AST addBEGIN() {
6332 			if (beginAst == null) {
6333 				beginAst = new BeginAst();
6334 			}
6335 			return beginAst;
6336 		}
6337 
6338 		AST addEND() {
6339 			if (endAst == null) {
6340 				endAst = new EndAst();
6341 			}
6342 			return endAst;
6343 		}
6344 
6345 		AST addBEGINFILE() {
6346 			if (beginFileAst == null) {
6347 				beginFileAst = new BeginFileAst();
6348 			}
6349 			return beginFileAst;
6350 		}
6351 
6352 		AST addENDFILE() {
6353 			if (endFileAst == null) {
6354 				endFileAst = new EndFileAst();
6355 			}
6356 			return endFileAst;
6357 		}
6358 
6359 		/**
6360 		 * Returns whether the script references the named global variable,
6361 		 * without creating a symbol for it.
6362 		 */
6363 		private boolean isGlobalReferenced(String id) {
6364 			IDAst idAst = globalIds.get(id);
6365 			return idAst != null && idAst.isReferenced();
6366 		}
6367 
6368 		private IDAst getID(String id) {
6369 			id = resolveVariableIdentifier(id);
6370 
6371 			Map<String, IDAst> map;
6372 			if (currentFunctionName == null) {
6373 				map = globalIds;
6374 			} else {
6375 				Set<String> set = functionParameters.get(currentFunctionName);
6376 				// we need "set != null && ..." here because if function
6377 				// is defined with no args (i.e., function f() ...),
6378 				// then set is null
6379 				if (set != null && set.contains(id)) {
6380 					map = localIds.get(currentFunctionName);
6381 					if (map == null) {
6382 						map = new HashMap<String, IDAst>();
6383 						localIds.put(currentFunctionName, map);
6384 					}
6385 				} else {
6386 					map = globalIds;
6387 				}
6388 			}
6389 			if (map == globalIds) {
6390 				// Only global variables share the namespace with function names.
6391 				// Formal parameters may legitimately have the same name as an
6392 				// unrelated function.
6393 				if (functionProxies.get(id) != null) {
6394 					throw parserException("cannot use " + id + " as a variable; it is a function");
6395 				}
6396 				ids.add(id);
6397 			}
6398 			IDAst idAst = map.get(id);
6399 			if (idAst == null) {
6400 				idAst = new IDAst(id, map == globalIds);
6401 				idAst.offset = map.size();
6402 				if (map == globalIds && !posix && ("SYMTAB".equals(id) || "FUNCTAB".equals(id))) {
6403 					// the runtime-provided meta tables are array-only, as in
6404 					// gawk: using them as scalars must fail like any array
6405 					idAst.setArray(true);
6406 				}
6407 				map.put(id, idAst);
6408 			}
6409 			return idAst;
6410 		}
6411 
6412 		private String resolveVariableIdentifier(String id) {
6413 			if (currentFunctionName != null) {
6414 				Set<String> parameters = functionParameters.get(currentFunctionName);
6415 				if (parameters != null && parameters.contains(id)) {
6416 					return id;
6417 				}
6418 			}
6419 			return qualifyGlobalIdentifier(id);
6420 		}
6421 
6422 		AST addID(String id) throws ParserException {
6423 			IDAst retVal = getID(id);
6424 			retVal.markReferenced();
6425 			/// ***
6426                         /// We really don't know if the evaluation is for an array or for a scalar
6427                         /// here, because we can use an array as a function parameter (passed by reference).
6428 			/// ***
6429 			// if (retVal.isArray)
6430 			// throw parserException("Cannot use "+retVal+" as a scalar.");
6431 			// retVal.isScalar = true;
6432 			return retVal;
6433 		}
6434 
6435 		int addFunctionParameter(String functionName, String id) {
6436 			int namespaceSeparator = functionName.indexOf("::");
6437 			String unqualifiedFunctionName = namespaceSeparator < 0 ?
6438 					functionName : functionName.substring(namespaceSeparator + 2);
6439 			if (unqualifiedFunctionName.equals(id)) {
6440 				throw parserException("cannot use " + id + " as a parameter; it is the function name");
6441 			}
6442 			Set<String> set = functionParameters.get(functionName);
6443 			if (set == null) {
6444 				set = new HashSet<String>();
6445 				functionParameters.put(functionName, set);
6446 			}
6447 			if (set.contains(id)) {
6448 				throw parserException("multiply defined parameter " + id + " in function " + functionName);
6449 			}
6450 			int retval = set.size();
6451 			set.add(id);
6452 			Map<String, IDAst> map = localIds.get(functionName);
6453 			if (map == null) {
6454 				map = new HashMap<String, IDAst>();
6455 				localIds.put(functionName, map);
6456 			}
6457 			IDAst idAst = map.get(id);
6458 			if (idAst == null) {
6459 				idAst = new IDAst(id, map == globalIds);
6460 				idAst.offset = map.size();
6461 				map.put(id, idAst);
6462 			}
6463 
6464 			return retval;
6465 		}
6466 
6467 		IDAst getFunctionParameterIDAST(String functionName, String fIdString) {
6468 			return localIds.get(functionName).get(fIdString);
6469 		}
6470 
6471 		AST addArrayID(String id) throws ParserException {
6472 			IDAst retVal = getID(id);
6473 			retVal.markReferenced();
6474 			retVal.setArray(true);
6475 			return retVal;
6476 		}
6477 
6478 		AST addFunctionDef(String functionName, AST paramList, AST block) {
6479 			if (ids.contains(functionName)) {
6480 				throw parserException("cannot use " + functionName + " as a function; it is a variable");
6481 			}
6482 			FunctionProxy functionProxy = functionProxies.get(functionName);
6483 			if (functionProxy == null) {
6484 				functionProxy = new FunctionProxy(functionName);
6485 				functionProxies.put(functionName, functionProxy);
6486 			}
6487 			FunctionDefAst functionDef = new FunctionDefAst(functionName, paramList, block);
6488 			functionProxy.setFunctionDefinition(functionDef);
6489 			return functionDef;
6490 		}
6491 
6492 		AST addFunctionCall(String id, AST paramList) {
6493 			id = qualifyGlobalIdentifier(id);
6494 			FunctionProxy functionProxy = functionProxies.get(id);
6495 			if (functionProxy == null) {
6496 				functionProxy = new FunctionProxy(id);
6497 				functionProxies.put(id, functionProxy);
6498 			}
6499 			return new FunctionCallAst(functionProxy, paramList);
6500 		}
6501 
6502 		Map<String, Tuple.IndirectFunctionTarget> indirectFunctionTargets() {
6503 			if (cachedIndirectFunctionTargets != null) {
6504 				return cachedIndirectFunctionTargets;
6505 			}
6506 			Map<String, Tuple.IndirectFunctionTarget> targets = new HashMap<String, Tuple.IndirectFunctionTarget>();
6507 			for (Map.Entry<String, FunctionProxy> entry : functionProxies.entrySet()) {
6508 				FunctionProxy proxy = entry.getValue();
6509 				if (proxy.isDefined()) {
6510 					targets
6511 							.put(
6512 									entry.getKey(),
6513 									new Tuple.IndirectFunctionTarget(
6514 											proxy,
6515 											proxy.getFunctionParamCount(),
6516 											collectArrayParameterIndexes(proxy.functionDefAst)));
6517 				}
6518 			}
6519 			cachedIndirectFunctionTargets = Collections.unmodifiableMap(targets);
6520 			return cachedIndirectFunctionTargets;
6521 		}
6522 
6523 		AST addArrayReference(String id, AST idxAst, int lineNo) throws ParserException {
6524 			return new ArrayReferenceAst(lineNo, addArrayID(id), idxAst);
6525 		}
6526 
6527 		// constants are no longer cached/hashed so that individual ASTs
6528 		// can report accurate line numbers upon errors
6529 
6530 		AST addINTEGER(String integer) {
6531 			return new IntegerAst(Long.parseLong(integer));
6532 		}
6533 
6534 		AST addDOUBLE(String dbl) {
6535 			return new DoubleAst(Double.valueOf(dbl));
6536 		}
6537 
6538 		AST addSTRING(String str) {
6539 			return new StringAst(str);
6540 		}
6541 
6542 		AST addREGEXP(String localRegexp) {
6543 			return new RegexpAst(localRegexp, false);
6544 		}
6545 
6546 		AST addTYPED_REGEXP(String localRegexp) {
6547 			return new RegexpAst(localRegexp, true);
6548 		}
6549 	}
6550 
6551 	private ParserException parserException(String msg) {
6552 		return new ParserException(
6553 				msg,
6554 				currentScriptSource.getDescription(),
6555 				reader.getLineNumber());
6556 	}
6557 }