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