| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| /// Utilities for building JS ASTs at runtime. Contains a builder class and a | |
| /// parser that parses part of the language. | |
| library; | |
| import 'characters.dart' as char_codes; | |
| import 'nodes.dart'; | |
| import 'template.dart'; | |
| /// Global template manager. | |
| /// | |
| /// We should aim to have a fixed number of templates. This implies that we do | |
| /// not use js('xxx') to parse text that is constructed from values that depend | |
| /// on names in the Dart program. | |
| // TODO(sra): Find the remaining places where js('xxx') used to parse an | |
| // unbounded number of expression, or institute a cache policy. | |
| TemplateManager templateManager = TemplateManager(); | |
| /// [js] is a singleton instance of JsBuilder. | |
| /// | |
| /// JsBuilder is a set of conveniences for constructing JavaScript ASTs. | |
| /// | |
| /// [string] and [number] are used to create leaf AST nodes: | |
| /// | |
| /// var s = js.string('hello'); // s = new LiteralString('"hello"') | |
| /// var n = js.number(123); // n = new LiteralNumber(123) | |
| /// | |
| /// In the line above `a --> b` means Dart expression `a` evaluates to a | |
| /// JavaScript AST that would pretty-print as `b`. | |
| /// | |
| /// The [call] method constructs an Expression AST. | |
| /// | |
| /// No argument | |
| /// | |
| /// js('window.alert("hello")') --> window.alert("hello") | |
| /// | |
| /// The input text can contain placeholders `#` that are replaced with provided | |
| /// arguments. A single argument can be passed directly: | |
| /// | |
| /// js('window.alert(#)', s) --> window.alert("hello") | |
| /// | |
| /// Multiple arguments are passed as a list: | |
| /// | |
| /// js('# + #', [s, s]) --> "hello" + "hello" | |
| /// | |
| /// The [statement] method constructs a Statement AST, but is otherwise like the | |
| /// [call] method. This constructs a Return AST: | |
| /// | |
| /// var ret = js.statement('return #;', n); --> return 123; | |
| /// | |
| /// A placeholder in a Statement context must be followed by a semicolon ';'. | |
| /// One can think of a statement placeholder as being `#;` to explain why the | |
| /// output still has one semicolon: | |
| /// | |
| /// js.statement('if (happy) #;', ret) | |
| /// --> | |
| /// if (happy) | |
| /// return 123; | |
| /// | |
| /// If the placeholder is not followed by a semicolon, it is part of an | |
| /// expression. Here the placeholder is in the position of the function in a | |
| /// function call: | |
| /// | |
| /// var vFoo = new VariableUse('foo'); | |
| /// js.statement('if (happy) #("Happy!")', vFoo) | |
| /// --> | |
| /// if (happy) | |
| /// foo("Happy!"); | |
| /// | |
| /// Generally, a placeholder in an expression position requires an Expression | |
| /// AST as an argument and a placeholder in a statement position requires a | |
| /// Statement AST. An expression will be converted to a Statement if needed by | |
| /// creating an ExpressionStatement. A String argument will be converted into a | |
| /// VariableUse and requires that the string is a JavaScript identifier. | |
| /// | |
| /// js('# + 1', vFoo) --> foo + 1 | |
| /// js('# + 1', 'foo') --> foo + 1 | |
| /// js('# + 1', 'foo.bar') --> assertion failure | |
| /// | |
| /// Some placeholder positions are _splicing contexts_. A function argument list | |
| /// is a splicing expression context. A placeholder in a splicing expression | |
| /// context can take a single Expression (or String, converted to VariableUse) | |
| /// or an Iterable of Expressions (and/or Strings). | |
| /// | |
| /// // non-splicing argument: | |
| /// js('#(#)', ['say', s]) --> say("hello") | |
| /// // splicing arguments: | |
| /// js('#(#)', ['say', []]) --> say() | |
| /// js('#(#)', ['say', [s]]) --> say("hello") | |
| /// js('#(#)', ['say', [s, n]]) --> say("hello", 123) | |
| /// | |
| /// A splicing context can be used to append 'lists' and add extra elements: | |
| /// | |
| /// js('foo(#, #, 1)', [ ['a', n], s]) --> foo(a, 123, "hello", 1) | |
| /// js('foo(#, #, 1)', [ ['a', n], [s, n]]) --> foo(a, 123, "hello", 123, 1) | |
| /// js('foo(#, #, 1)', [ [], [s, n]]) --> foo("hello", 123, 1) | |
| /// js('foo(#, #, 1)', [ [], [] ]) --> foo(1) | |
| /// | |
| /// The generation of a compile-time optional argument expression can be chosen | |
| /// by providing an empty or singleton list. | |
| /// | |
| /// In addition to Expressions and Statements, there are Parameters, which occur | |
| /// only in the parameter list of a function expression or declaration. | |
| /// Placeholders in parameter positions behave like placeholders in Expression | |
| /// positions, except only Parameter AST nodes are permitted. String arguments | |
| /// for parameter placeholders are converted to Parameter AST nodes. | |
| /// | |
| /// var pFoo = new Parameter('foo') | |
| /// js('function(#) { return #; }', [pFoo, vFoo]) | |
| /// --> | |
| /// function(foo) { return foo; } | |
| /// | |
| /// Expressions and Parameters are not compatible with each other's context: | |
| /// | |
| /// js('function(#) { return #; }', [vFoo, vFoo]) --> error | |
| /// js('function(#) { return #; }', [pFoo, pFoo]) --> error | |
| /// | |
| /// The parameter context is a splicing context. When combined with the | |
| /// context-sensitive conversion of Strings, this simplifies the construction of | |
| /// trampoline-like functions: | |
| /// | |
| /// var args = ['a', 'b']; | |
| /// js('function(#) { return f(this, #); }', [args, args]) | |
| /// --> | |
| /// function(a, b) { return f(this, a, b); } | |
| /// | |
| /// A statement placeholder in a Block is also in a splicing context. In | |
| /// addition to splicing Iterables, statement placeholders in a Block will also | |
| /// splice a Block or an EmptyStatement. This flattens nested blocks and allows | |
| /// blocks to be appended. | |
| /// | |
| /// var b1 = js.statement('{ 1; 2; }'); | |
| /// var sEmpty = new EmptyStatement(); | |
| /// js.statement('{ #; #; #; #; }', [sEmpty, b1, b1, sEmpty]) | |
| /// --> | |
| /// { 1; 2; 1; 2; } | |
| /// | |
| /// A placeholder in the context of an if-statement condition also accepts a | |
| /// Dart bool argument, which selects the then-part or else-part of the | |
| /// if-statement: | |
| /// | |
| /// js.statement('if (#) return;', vFoo) --> if (foo) return; | |
| /// js.statement('if (#) return;', true) --> return; | |
| /// js.statement('if (#) return;', false) --> ; // empty statement | |
| /// var eTrue = new LiteralBool(true); | |
| /// js.statement('if (#) return;', eTrue) --> if (true) return; | |
| /// | |
| /// Combined with block splicing, if-statement condition context placeholders | |
| /// allows the creation of templates that select code depending on variables. | |
| /// | |
| /// js.statement('{ 1; if (#) 2; else { 3; 4; } 5;}', true) | |
| /// --> { 1; 2; 5; } | |
| /// | |
| /// js.statement('{ 1; if (#) 2; else { 3; 4; } 5;}', false) | |
| /// --> { 1; 3; 4; 5; } | |
| /// | |
| /// A placeholder following a period in a property access is in a property | |
| /// access context. This is just like an expression context, except String | |
| /// arguments are converted to JavaScript property accesses. In JavaScript, | |
| /// `a.b` is short-hand for `a["b"]`: | |
| /// | |
| /// js('a[#]', vFoo) --> a[foo] | |
| /// js('a[#]', s) --> a.hello (i.e. a["hello"]). | |
| /// js('a[#]', 'x') --> a[x] | |
| /// | |
| /// js('a.#', vFoo) --> a[foo] | |
| /// js('a.#', s) --> a.hello (i.e. a["hello"]) | |
| /// js('a.#', 'x') --> a.x (i.e. a["x"]) | |
| /// | |
| /// (Question - should `.#` be restricted to permit only String arguments? The | |
| /// template should probably be written with `[]` if non-strings are accepted.) | |
| /// | |
| /// | |
| /// Object initializers allow placeholders in the key property name position: | |
| /// | |
| /// js('{#:1, #:2}', [s, 'bye']) --> {hello: 1, bye: 2} | |
| /// | |
| /// | |
| /// What is not implemented: | |
| /// | |
| /// - Array initializers and object initializers could support splicing. In | |
| /// the array case, we would need some way to know if an ArrayInitializer | |
| /// argument should be splice or is intended as a single value. | |
| const JsBuilder js = JsBuilder(); | |
| class const JsBuilder() { | |
| /// Parses a bit of JavaScript, and returns an expression. | |
| /// | |
| /// See the MiniJsParser class. | |
| /// | |
| /// [arguments] can be a single [Node] (e.g. an [Expression] or [Statement]) | |
| /// or a list of [Node]s, which will be interpolated into the source at the | |
| /// '#' signs. | |
| Expression call(String source, [arguments]) { | |
| Template template = _findExpressionTemplate(source); | |
| arguments ??= []; | |
| // We allow a single argument to be given directly. | |
| if (arguments is! List && arguments is! Map) arguments = [arguments]; | |
| return template.instantiate(arguments) as Expression; | |
| } | |
| /// Parses a JavaScript Statement, otherwise just like [call]. | |
| Statement statement(String source, [arguments]) { | |
| Template template = _findStatementTemplate(source); | |
| arguments ??= []; | |
| // We allow a single argument to be given directly. | |
| if (arguments is! List && arguments is! Map) arguments = [arguments]; | |
| return template.instantiate(arguments) as Statement; | |
| } | |
| /// Parses JavaScript written in the `JS` foreign instruction. | |
| /// | |
| /// The [source] must be a JavaScript expression or a JavaScript throw | |
| /// statement. | |
| Template parseForeignJS(String source) { | |
| // TODO(sra): Parse with extra validation to forbid `#` interpolation in | |
| // functions, as this leads to unanticipated capture of temporaries that are | |
| // reused after capture. | |
| if (source.startsWith('throw ')) { | |
| return _findStatementTemplate(source); | |
| } else { | |
| return _findExpressionTemplate(source); | |
| } | |
| } | |
| Template _findExpressionTemplate(String source) { | |
| Template? template = templateManager.lookupExpressionTemplate(source); | |
| if (template == null) { | |
| MiniJsParser parser = MiniJsParser(source); | |
| Expression expression = parser.expression(); | |
| template = templateManager.defineExpressionTemplate(source, expression); | |
| } | |
| return template; | |
| } | |
| Template _findStatementTemplate(String source) { | |
| Template? template = templateManager.lookupStatementTemplate(source); | |
| if (template == null) { | |
| MiniJsParser parser = MiniJsParser(source); | |
| Statement statement = parser.statement(); | |
| template = templateManager.defineStatementTemplate(source, statement); | |
| } | |
| return template; | |
| } | |
| /// Creates an Expression template for the given [source]. | |
| /// | |
| /// The returned template is cached. | |
| Template expressionTemplateFor(String source) { | |
| return _findExpressionTemplate(source); | |
| } | |
| /// Creates an Expression template without caching the result. | |
| Template uncachedExpressionTemplate(String source) { | |
| MiniJsParser parser = MiniJsParser(source); | |
| Expression expression = parser.expression(); | |
| return Template(source, expression, isExpression: true, forceCopy: false); | |
| } | |
| /// Creates a Statement template without caching the result. | |
| Template uncachedStatementTemplate(String source) { | |
| MiniJsParser parser = MiniJsParser(source); | |
| Statement statement = parser.statement(); | |
| return Template(source, statement, isExpression: false, forceCopy: false); | |
| } | |
| /// Create an Expression template which has [ast] as the result. This is used | |
| /// to wrap a generated AST in a zero-argument Template so it can be passed to | |
| /// context that expects a template. | |
| Template expressionTemplateYielding(Expression ast) { | |
| return Template.withExpressionResult(ast); | |
| } | |
| Template statementTemplateYielding(Statement ast) { | |
| return Template.withStatementResult(ast); | |
| } | |
| /// Creates a literal js string from [value]. | |
| LiteralString string(String value) => LiteralString(value); | |
| StringConcatenation concatenateStrings(Iterable<Literal> parts) { | |
| return StringConcatenation(List.of(parts, growable: false)); | |
| } | |
| Iterable<Literal> joinLiterals( | |
| Iterable<Literal> items, | |
| Literal separator, | |
| ) sync* { | |
| bool first = true; | |
| for (final item in items) { | |
| if (!first) yield separator; | |
| yield item; | |
| first = false; | |
| } | |
| } | |
| LiteralString quoteName(Name name) { | |
| return LiteralStringFromName(name); | |
| } | |
| LiteralNumber number(num value) => LiteralNumber('$value'); | |
| LiteralBool boolean(bool value) => LiteralBool(value); | |
| ArrayInitializer numArray(Iterable<int> list) => | |
| ArrayInitializer(list.map(number).toList()); | |
| ArrayInitializer stringArray(Iterable<String> list) => | |
| ArrayInitializer(list.map(string).toList()); | |
| Comment comment(String text) => Comment(text); | |
| Call propertyCall( | |
| Expression receiver, | |
| Expression fieldName, | |
| List<Expression> arguments, | |
| ) { | |
| return Call(PropertyAccess(receiver, fieldName), arguments); | |
| } | |
| ObjectInitializer objectLiteral(Map<String, Expression> map) { | |
| List<Property> properties = []; | |
| map.forEach((name, value) { | |
| properties.add(Property(string(name), value)); | |
| }); | |
| return ObjectInitializer(properties); | |
| } | |
| } | |
| LiteralString string(String value) => js.string(value); | |
| /// Returns a LiteralString which has contents determined by [Name]. | |
| /// | |
| /// This is used to force a Name to be a string literal regardless of | |
| /// context. It is not necessary for properties. | |
| LiteralString quoteName(Name name) => js.quoteName(name); | |
| Iterable<Literal> joinLiterals(Iterable<Literal> list, Literal separator) { | |
| return js.joinLiterals(list, separator); | |
| } | |
| StringConcatenation concatenateStrings(Iterable<Literal> parts) { | |
| return js.concatenateStrings(parts); | |
| } | |
| LiteralNumber number(num value) => js.number(value); | |
| ArrayInitializer numArray(Iterable<int> list) => js.numArray(list); | |
| ArrayInitializer stringArray(Iterable<String> list) => js.stringArray(list); | |
| Call propertyCall( | |
| Expression receiver, | |
| Expression fieldName, | |
| List<Expression> arguments, | |
| ) { | |
| return js.propertyCall(receiver, fieldName, arguments); | |
| } | |
| ObjectInitializer objectLiteral(Map<String, Expression> map) { | |
| return js.objectLiteral(map); | |
| } | |
| class MiniJsParserError { | |
| MiniJsParserError(this.parser, this.message); | |
| final MiniJsParser parser; | |
| final String message; | |
| @override | |
| String toString() { | |
| int pos = parser.lastPosition; | |
| // Discard lines following the line containing lastPosition. | |
| String src = parser.src; | |
| int newlinePos = src.indexOf('\n', pos); | |
| if (newlinePos >= pos) src = src.substring(0, newlinePos); | |
| // Extract the prefix of the error line before lastPosition. | |
| String line = src; | |
| int lastLineStart = line.lastIndexOf('\n'); | |
| if (lastLineStart >= 0) line = line.substring(lastLineStart + 1); | |
| String prefix = line.substring(0, pos - (src.length - line.length)); | |
| // Replace non-tabs with spaces, giving a print indent that matches the text | |
| // for tabbing. | |
| String spaces = prefix.replaceAll(RegExp(r'[^\t]'), ' '); | |
| return 'Error in MiniJsParser:\n$src\n$spaces^\n$spaces$message\n'; | |
| } | |
| } | |
| enum _Category { | |
| none, | |
| alpha, | |
| numeric, | |
| string, | |
| symbol, | |
| assignment, | |
| dot, | |
| lparen, | |
| rparen, | |
| lbrace, | |
| rbrace, | |
| lsquare, | |
| rsquare, | |
| comma, | |
| query, | |
| colon, | |
| semicolon, | |
| arrow, | |
| rest, | |
| hash, | |
| whitespace, | |
| other; | |
| static const _asciiTable = <_Category>[ | |
| other, other, other, other, other, other, other, other, // 0-7 | |
| other, whitespace, whitespace, other, other, whitespace, // 8-13 | |
| other, other, other, other, other, other, other, other, // 14-21 | |
| other, other, other, other, other, other, other, other, // 22-29 | |
| other, other, whitespace, // 30-32 | |
| symbol, other, hash, alpha, symbol, symbol, other, // !"#$%&` | |
| lparen, rparen, symbol, symbol, comma, symbol, dot, symbol, // ()*+,-./ | |
| numeric, numeric, numeric, numeric, numeric, // 01234 | |
| numeric, numeric, numeric, numeric, numeric, // 56789 | |
| colon, semicolon, symbol, symbol, symbol, query, other, // :;<=>?@ | |
| alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha, // ABCDEFGH | |
| alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha, // IJKLMNOP | |
| alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha, // QRSTUVWX | |
| alpha, alpha, lsquare, other, rsquare, symbol, alpha, other, // YZ[\]^_' | |
| alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha, // abcdefgh | |
| alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha, // ijklmnop | |
| alpha, alpha, alpha, alpha, alpha, alpha, alpha, alpha, // qrstuvwx | |
| alpha, alpha, lbrace, symbol, rbrace, symbol, // yx{|}~ | |
| ]; | |
| } | |
| /// Mini JavaScript parser for tiny snippets of code that we want to make into | |
| /// AST nodes. Handles: | |
| /// * identifiers. | |
| /// * dot access. | |
| /// * method calls. | |
| /// * [] access. | |
| /// * array, string, regexp, boolean, null and numeric literals. | |
| /// * most operators. | |
| /// * brackets. | |
| /// * var declarations. | |
| /// * operator precedence. | |
| /// * anonymous functions and named function expressions and declarations. | |
| /// Notable things it can't do yet include: | |
| /// * some statements are still missing (do-while, while, switch). | |
| /// | |
| /// It's a fairly standard recursive descent parser. | |
| /// | |
| /// Literal strings are passed through to the final JS source code unchanged, | |
| /// including the choice of surrounding quotes, so if you parse | |
| /// r'var x = "foo\n\"bar\""' you will end up with | |
| /// var x = "foo\n\"bar\"" in the final program. \x and \u escapes are not | |
| /// allowed in string and regexp literals because the machinery for checking | |
| /// their correctness is rather involved. | |
| class MiniJsParser { | |
| MiniJsParser(this.src) { | |
| getToken(); | |
| } | |
| _Category _lastCategory = _Category.none; | |
| String lastToken = ''; | |
| int lastPosition = 0; | |
| int position = 0; | |
| bool skippedNewline = false; // skipped newline in last getToken? | |
| final String src; | |
| final List<InterpolatedNode> interpolatedValues = []; | |
| bool get hasNamedHoles => | |
| interpolatedValues.isNotEmpty && interpolatedValues.first.isNamed; | |
| bool get hasPositionalHoles => | |
| interpolatedValues.isNotEmpty && interpolatedValues.first.isPositional; | |
| // Make sure that ]] is two symbols. | |
| bool _singleCharCategory(_Category category) => | |
| category.index >= _Category.dot.index; | |
| // This must be a >= the highest precedence number handled by parseBinary. | |
| static var HIGHEST_PARSE_BINARY_PRECEDENCE = 16; | |
| static bool isAssignment(String symbol) => BINARY_PRECEDENCE[symbol] == 17; | |
| // From https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/Operator_Precedence | |
| static final BINARY_PRECEDENCE = { | |
| '+=': 17, | |
| '-=': 17, | |
| '*=': 17, | |
| '/=': 17, | |
| '%=': 17, | |
| '^=': 17, | |
| '|=': 17, | |
| '&=': 17, | |
| '<<=': 17, | |
| '>>=': 17, | |
| '>>>=': 17, | |
| '**=': 17, | |
| '=': 17, | |
| '||': 14, | |
| '&&': 13, | |
| '|': 12, | |
| '^': 11, | |
| '&': 10, | |
| '!=': 9, | |
| '==': 9, | |
| '!==': 9, | |
| '===': 9, | |
| '<': 8, | |
| '<=': 8, | |
| '>=': 8, | |
| '>': 8, | |
| 'in': 8, | |
| 'instanceof': 8, | |
| '<<': 7, | |
| '>>': 7, | |
| '>>>': 7, | |
| '+': 6, | |
| '-': 6, | |
| '*': 5, | |
| '/': 5, | |
| '%': 5, | |
| '**': 4, | |
| }; | |
| static final UNARY_OPERATORS = { | |
| '++', | |
| '--', | |
| '+', | |
| '-', | |
| '~', | |
| '!', | |
| 'typeof', | |
| 'void', | |
| 'delete', | |
| 'await', | |
| }; | |
| static final OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS = { | |
| 'typeof', | |
| 'void', | |
| 'delete', | |
| 'in', | |
| 'instanceof', | |
| 'await', | |
| }; | |
| static _Category _category(int code) { | |
| if (code >= _Category._asciiTable.length) return _Category.other; | |
| return _Category._asciiTable[code]; | |
| } | |
| String getRegExp(int startPosition) { | |
| position = startPosition; | |
| int delimiter = src.codeUnitAt(startPosition); | |
| int currentCode; | |
| do { | |
| position++; | |
| if (position >= src.length) error('Unterminated literal'); | |
| currentCode = src.codeUnitAt(position); | |
| if (currentCode == char_codes.$LF) error('Unterminated literal'); | |
| if (currentCode == char_codes.$BACKSLASH) { | |
| if (++position >= src.length) error('Unterminated literal'); | |
| int escaped = src.codeUnitAt(position); | |
| if (escaped == char_codes.$x || | |
| escaped == char_codes.$X || | |
| escaped == char_codes.$u || | |
| escaped == char_codes.$U || | |
| _category(escaped) == _Category.numeric) { | |
| error('Numeric and hex escapes are not supported in RegExp literals'); | |
| } | |
| } | |
| } while (currentCode != delimiter); | |
| position++; | |
| return src.substring(lastPosition, position); | |
| } | |
| String getString(int startPosition, int quote) { | |
| assert(src.codeUnitAt(startPosition) == quote); | |
| position = startPosition + 1; | |
| final value = StringBuffer(); | |
| while (true) { | |
| if (position >= src.length) error('Unterminated literal'); | |
| int code = src.codeUnitAt(position++); | |
| if (code == quote) break; | |
| if (code == char_codes.$LF) error('Unterminated literal'); | |
| if (code == char_codes.$BACKSLASH) { | |
| if (position >= src.length) error('Unterminated literal'); | |
| code = src.codeUnitAt(position++); | |
| if (code == char_codes.$f) { | |
| value.writeCharCode(12); | |
| } else if (code == char_codes.$n) { | |
| value.writeCharCode(10); | |
| } else if (code == char_codes.$r) { | |
| value.writeCharCode(13); | |
| } else if (code == char_codes.$t) { | |
| value.writeCharCode(8); | |
| } else if (code == char_codes.$BACKSLASH || | |
| code == char_codes.$SQ || | |
| code == char_codes.$DQ) { | |
| value.writeCharCode(code); | |
| } else if (code == char_codes.$x || code == char_codes.$X) { | |
| error('Hex escapes not supported in string literals'); | |
| } else if (code == char_codes.$u || code == char_codes.$U) { | |
| error('Unicode escapes not supported in string literals'); | |
| } else if (char_codes.$0 <= code && code <= char_codes.$9) { | |
| error('Numeric escapes not supported in string literals'); | |
| } else { | |
| error('Unknown escape U+${code.toRadixString(16).padLeft(4, '0')}'); | |
| } | |
| continue; | |
| } | |
| value.writeCharCode(code); | |
| } | |
| return value.toString(); | |
| } | |
| void getToken() { | |
| skippedNewline = false; | |
| for (;;) { | |
| if (position >= src.length) break; | |
| int code = src.codeUnitAt(position); | |
| // Skip '//' and '/*' style comments. | |
| if (code == char_codes.$SLASH && position + 1 < src.length) { | |
| if (src.codeUnitAt(position + 1) == char_codes.$SLASH) { | |
| int nextPosition = src.indexOf('\n', position); | |
| if (nextPosition == -1) nextPosition = src.length; | |
| position = nextPosition; | |
| continue; | |
| } else if (src.codeUnitAt(position + 1) == char_codes.$STAR) { | |
| int nextPosition = src.indexOf('*/', position + 2); | |
| if (nextPosition == -1) error('Unterminated comment'); | |
| position = nextPosition + 2; | |
| continue; | |
| } | |
| } | |
| if (_category(code) != _Category.whitespace) break; | |
| if (code == char_codes.$LF) skippedNewline = true; | |
| ++position; | |
| } | |
| if (position == src.length) { | |
| _lastCategory = _Category.none; | |
| lastToken = ''; | |
| lastPosition = position; | |
| return; | |
| } | |
| int code = src.codeUnitAt(position); | |
| lastPosition = position; | |
| if (code == char_codes.$SQ || code == char_codes.$DQ) { | |
| // String literal. | |
| _lastCategory = _Category.string; | |
| lastToken = getString(position, code); | |
| } else if (code == char_codes.$0 && | |
| position + 2 < src.length && | |
| src.codeUnitAt(position + 1) == char_codes.$x) { | |
| // Hex literal. | |
| for (position += 2; position < src.length; position++) { | |
| final cat = _category(src.codeUnitAt(position)); | |
| if (cat != _Category.numeric && cat != _Category.alpha) break; | |
| } | |
| _lastCategory = _Category.numeric; | |
| lastToken = src.substring(lastPosition, position); | |
| if (int.tryParse(lastToken) == null) { | |
| error('Unparseable number'); | |
| } | |
| } else if (code == char_codes.$SLASH) { | |
| // Tokens that start with / are special due to regexp literals. | |
| _lastCategory = _Category.symbol; | |
| position++; | |
| if (position < src.length && src.codeUnitAt(position) == char_codes.$EQ) { | |
| position++; | |
| } | |
| lastToken = src.substring(lastPosition, position); | |
| } else if (code == char_codes.$PERIOD && | |
| position + 2 < src.length && | |
| src.codeUnitAt(position + 1) == char_codes.$PERIOD && | |
| src.codeUnitAt(position + 2) == char_codes.$PERIOD) { | |
| _lastCategory = _Category.rest; | |
| position += 3; | |
| lastToken = '...'; | |
| } else { | |
| // All other tokens handled here. | |
| final cat = _category(src.codeUnitAt(position)); | |
| _Category newCat; | |
| do { | |
| position++; | |
| if (position == src.length) break; | |
| int code = src.codeUnitAt(position); | |
| // Special code to disallow !, ~ and / in non-first position in token, | |
| // so that !! and ~~ parse as two tokens and != parses as one, while =/ | |
| // parses as a an equals token followed by a regexp literal start. | |
| newCat = | |
| (code == char_codes.$BANG || | |
| code == char_codes.$SLASH || | |
| code == char_codes.$TILDE) | |
| ? _Category.none | |
| : _category(code); | |
| } while (!_singleCharCategory(cat) && | |
| (cat == newCat || | |
| (cat == _Category.alpha && | |
| newCat == _Category.numeric) || // eg. level42. | |
| (cat == _Category.numeric && | |
| newCat == _Category.dot))); // eg. 3.1415 | |
| _lastCategory = cat; | |
| lastToken = src.substring(lastPosition, position); | |
| if (cat == _Category.numeric) { | |
| if (double.tryParse(lastToken) == null) { | |
| error('Unparseable number'); | |
| } | |
| } else if (cat == _Category.symbol) { | |
| if (lastToken == '=>') { | |
| _lastCategory = _Category.arrow; | |
| } else { | |
| int? binaryPrecedence = BINARY_PRECEDENCE[lastToken]; | |
| if (binaryPrecedence == null && | |
| !UNARY_OPERATORS.contains(lastToken)) { | |
| error('Unknown operator'); | |
| } | |
| if (isAssignment(lastToken)) _lastCategory = _Category.assignment; | |
| } | |
| } else if (cat == _Category.alpha) { | |
| if (OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS.contains(lastToken)) { | |
| _lastCategory = _Category.symbol; | |
| } | |
| } | |
| } | |
| } | |
| void _expectCategory(_Category cat) { | |
| if (cat != _lastCategory) error('Expected $cat'); | |
| getToken(); | |
| } | |
| bool _acceptCategory(_Category cat) { | |
| if (cat == _lastCategory) { | |
| getToken(); | |
| return true; | |
| } | |
| return false; | |
| } | |
| void expectSemicolon() { | |
| if (acceptSemicolon()) return; | |
| error('Expected SEMICOLON'); | |
| } | |
| bool acceptSemicolon() { | |
| // Accept semicolon or automatically inserted semicolon before close brace. | |
| // Miniparser forbids other kinds of semicolon insertion. | |
| if (_Category.rbrace == _lastCategory) return true; | |
| if (_Category.none == _lastCategory) return true; // end of input | |
| if (skippedNewline) { | |
| error('No automatic semicolon insertion at preceding newline'); | |
| } | |
| return _acceptCategory(_Category.semicolon); | |
| } | |
| bool acceptString(String string) { | |
| if (lastToken == string) { | |
| getToken(); | |
| return true; | |
| } | |
| return false; | |
| } | |
| Never error(String message) { | |
| throw MiniJsParserError(this, message); | |
| } | |
| /// Returns either the name for the hole, or its integer position. | |
| Object parseHash() { | |
| String holeName = lastToken; | |
| if (_acceptCategory(_Category.alpha)) { | |
| // Named hole. Example: 'function #funName() { ... }' | |
| if (hasPositionalHoles) { | |
| error('Holes must all be positional or named. $holeName'); | |
| } | |
| return holeName; | |
| } else { | |
| if (hasNamedHoles) { | |
| error('Holes must all be positional or named. $holeName'); | |
| } | |
| int position = interpolatedValues.length; | |
| return position; | |
| } | |
| } | |
| Expression parsePrimary() { | |
| String last = lastToken; | |
| if (_acceptCategory(_Category.alpha)) { | |
| if (last == 'true') { | |
| return LiteralBool(true); | |
| } else if (last == 'false') { | |
| return LiteralBool(false); | |
| } else if (last == 'null') { | |
| return LiteralNull(); | |
| } else if (last == 'function') { | |
| return parseFunctionExpression(); | |
| } else if (last == 'this') { | |
| return This(); | |
| } else { | |
| return VariableUse(last); | |
| } | |
| } else if (_acceptCategory(_Category.lparen)) { | |
| return parseExpressionOrArrowFunction(); | |
| } else if (_acceptCategory(_Category.string)) { | |
| return LiteralString(last); | |
| } else if (_acceptCategory(_Category.numeric)) { | |
| return LiteralNumber(last); | |
| } else if (_acceptCategory(_Category.lbrace)) { | |
| return parseObjectInitializer(); | |
| } else if (_acceptCategory(_Category.lsquare)) { | |
| var values = <Expression>[]; | |
| while (true) { | |
| if (_acceptCategory(_Category.comma)) { | |
| values.add(ArrayHole()); | |
| continue; | |
| } | |
| if (_acceptCategory(_Category.rsquare)) break; | |
| values.add(parseAssignment()); | |
| if (_acceptCategory(_Category.rsquare)) break; | |
| _expectCategory(_Category.comma); | |
| } | |
| return ArrayInitializer(values); | |
| } else if (last.startsWith('/')) { | |
| String regexp = getRegExp(lastPosition); | |
| getToken(); | |
| String flags = lastToken; | |
| if (!_acceptCategory(_Category.alpha)) flags = ''; | |
| Expression expression = RegExpLiteral(regexp + flags); | |
| return expression; | |
| } else if (_acceptCategory(_Category.hash)) { | |
| var nameOrPosition = parseHash(); | |
| InterpolatedExpression expression = InterpolatedExpression( | |
| nameOrPosition, | |
| ); | |
| interpolatedValues.add(expression); | |
| return expression; | |
| } else { | |
| error('Expected primary expression'); | |
| } | |
| } | |
| Expression parseFunctionExpression() { | |
| if (_lastCategory == _Category.alpha || _lastCategory == _Category.hash) { | |
| Declaration name = parseVariableDeclaration(); | |
| return NamedFunction(name, parseFun()); | |
| } | |
| return parseFun(); | |
| } | |
| Fun parseFun() { | |
| _expectCategory(_Category.lparen); | |
| var params = parseParameters(); | |
| _expectCategory(_Category.rparen); | |
| AsyncModifier asyncModifier; | |
| if (acceptString('async')) { | |
| if (acceptString('*')) { | |
| asyncModifier = AsyncModifier.asyncStar; | |
| } else { | |
| asyncModifier = AsyncModifier.async; | |
| } | |
| } else if (acceptString('sync')) { | |
| if (!acceptString('*')) error('Only sync* is valid - sync is implied'); | |
| asyncModifier = AsyncModifier.syncStar; | |
| } else { | |
| asyncModifier = AsyncModifier.sync; | |
| } | |
| _expectCategory(_Category.lbrace); | |
| Block block = parseBlock(); | |
| return Fun(params, block, asyncModifier: asyncModifier); | |
| } | |
| /// Consumes a parameter list but not any parentheses surrounding it. | |
| List<Parameter> parseParameters() { | |
| if (_lastCategory == _Category.rparen) return []; | |
| List<Parameter> params = []; | |
| for (;;) { | |
| if (_acceptCategory(_Category.hash)) { | |
| var nameOrPosition = parseHash(); | |
| InterpolatedParameter parameter = InterpolatedParameter(nameOrPosition); | |
| interpolatedValues.add(parameter); | |
| params.add(parameter); | |
| } else if (_acceptCategory(_Category.rest)) { | |
| String argumentName = lastToken; | |
| _expectCategory(_Category.alpha); | |
| params.add(RestParameter(argumentName)); | |
| } else { | |
| String argumentName = lastToken; | |
| _expectCategory(_Category.alpha); | |
| params.add(Parameter(argumentName)); | |
| } | |
| if (!_acceptCategory(_Category.comma)) return params; | |
| } | |
| } | |
| Expression parseObjectInitializer() { | |
| List<Property> properties = []; | |
| for (;;) { | |
| if (_acceptCategory(_Category.rbrace)) break; | |
| properties.add(parseMethodDefinitionOrProperty()); | |
| if (_acceptCategory(_Category.rbrace)) break; | |
| _expectCategory(_Category.comma); | |
| } | |
| return ObjectInitializer(properties); | |
| } | |
| Property parseMethodDefinitionOrProperty() { | |
| // Limited subset: keys are identifiers, no 'get' or 'set' properties. | |
| Literal propertyName; | |
| String identifier = lastToken; | |
| if (_acceptCategory(_Category.alpha)) { | |
| propertyName = LiteralString(identifier); | |
| } else if (_acceptCategory(_Category.string)) { | |
| propertyName = LiteralString(identifier); | |
| } else if (_acceptCategory(_Category.symbol)) { | |
| // e.g. void | |
| propertyName = LiteralString(identifier); | |
| } else if (_acceptCategory(_Category.hash)) { | |
| var nameOrPosition = parseHash(); | |
| InterpolatedLiteral interpolatedLiteral = InterpolatedLiteral( | |
| nameOrPosition, | |
| ); | |
| interpolatedValues.add(interpolatedLiteral); | |
| propertyName = interpolatedLiteral; | |
| } else { | |
| error('Expected property name'); | |
| } | |
| if (_acceptCategory(_Category.colon)) { | |
| Expression value = parseAssignment(); | |
| return Property(propertyName, value); | |
| } else { | |
| final fun = parseFun(); | |
| return MethodDefinition(propertyName, fun); | |
| } | |
| } | |
| Expression parseMember() { | |
| Expression receiver = parsePrimary(); | |
| while (true) { | |
| if (_acceptCategory(_Category.dot)) { | |
| receiver = getDotRhs(receiver); | |
| } else if (_acceptCategory(_Category.lsquare)) { | |
| Expression inBraces = parseExpression(); | |
| _expectCategory(_Category.rsquare); | |
| receiver = PropertyAccess(receiver, inBraces); | |
| } else { | |
| break; | |
| } | |
| } | |
| return receiver; | |
| } | |
| Expression parseCall() { | |
| bool constructor = acceptString('new'); | |
| Expression receiver = parseMember(); | |
| while (true) { | |
| if (_acceptCategory(_Category.lparen)) { | |
| final arguments = <Expression>[]; | |
| if (!_acceptCategory(_Category.rparen)) { | |
| while (true) { | |
| Expression argument = parseAssignment(); | |
| arguments.add(argument); | |
| if (_acceptCategory(_Category.rparen)) break; | |
| _expectCategory(_Category.comma); | |
| } | |
| } | |
| receiver = constructor | |
| ? New(receiver, arguments) | |
| : Call(receiver, arguments); | |
| constructor = false; | |
| } else if (!constructor && _acceptCategory(_Category.lsquare)) { | |
| Expression inBraces = parseExpression(); | |
| _expectCategory(_Category.rsquare); | |
| receiver = PropertyAccess(receiver, inBraces); | |
| } else if (!constructor && _acceptCategory(_Category.dot)) { | |
| receiver = getDotRhs(receiver); | |
| } else { | |
| // JS allows new without (), but we don't. | |
| if (constructor) error('Parentheses are required for new'); | |
| break; | |
| } | |
| } | |
| return receiver; | |
| } | |
| Expression getDotRhs(Expression receiver) { | |
| if (_acceptCategory(_Category.hash)) { | |
| var nameOrPosition = parseHash(); | |
| InterpolatedSelector property = InterpolatedSelector(nameOrPosition); | |
| interpolatedValues.add(property); | |
| return PropertyAccess(receiver, property); | |
| } | |
| String identifier = lastToken; | |
| // In ES5 keywords like delete and continue are allowed as property | |
| // names, and the IndexedDB API uses that, so we need to allow it here. | |
| if (_acceptCategory(_Category.symbol)) { | |
| if (!OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS.contains(identifier)) { | |
| error('Expected alphanumeric identifier'); | |
| } | |
| } else { | |
| _expectCategory(_Category.alpha); | |
| } | |
| return PropertyAccess.field(receiver, identifier); | |
| } | |
| Expression parsePostfix() { | |
| Expression expression = parseCall(); | |
| String operator = lastToken; | |
| // JavaScript grammar is: | |
| // LeftHandSideExpression [no LineTerminator here] ++ | |
| if (_lastCategory == _Category.symbol && | |
| !skippedNewline && | |
| (acceptString('++') || acceptString('--'))) { | |
| return Postfix(operator, expression); | |
| } | |
| // If we don't accept '++' or '--' due to skippedNewline a newline, no other | |
| // part of the parser will accept the token and we will get an error at the | |
| // whole expression level. | |
| return expression; | |
| } | |
| Expression parseUnaryHigh() { | |
| String operator = lastToken; | |
| if (_lastCategory == _Category.symbol && | |
| UNARY_OPERATORS.contains(operator) && | |
| (acceptString('++') || acceptString('--') || acceptString('await'))) { | |
| if (operator == 'await') return Await(parsePostfix()); | |
| return Prefix(operator, parsePostfix()); | |
| } | |
| return parsePostfix(); | |
| } | |
| Expression parseUnaryLow() { | |
| String operator = lastToken; | |
| if (_lastCategory == _Category.symbol && | |
| UNARY_OPERATORS.contains(operator) && | |
| operator != '++' && | |
| operator != '--') { | |
| _expectCategory(_Category.symbol); | |
| if (operator == 'await') return Await(parsePostfix()); | |
| return Prefix(operator, parseUnaryLow()); | |
| } | |
| return parseUnaryHigh(); | |
| } | |
| Expression parseBinary(int maxPrecedence) { | |
| Expression lhs = parseUnaryLow(); | |
| Expression? rhs; // This is null first time around. | |
| late int minPrecedence; | |
| late String lastSymbol; | |
| while (true) { | |
| final symbol = lastToken; | |
| if (_lastCategory != _Category.symbol) break; | |
| final symbolPrecedence = BINARY_PRECEDENCE[symbol]; | |
| if (symbolPrecedence == null) break; | |
| if (symbolPrecedence > maxPrecedence) break; | |
| _expectCategory(_Category.symbol); | |
| if (rhs == null || symbolPrecedence >= minPrecedence) { | |
| if (rhs != null) lhs = Binary(lastSymbol, lhs, rhs); | |
| minPrecedence = symbolPrecedence; | |
| rhs = parseUnaryLow(); | |
| lastSymbol = symbol; | |
| } else { | |
| Expression higher = parseBinary(symbolPrecedence); | |
| rhs = Binary(symbol, rhs, higher); | |
| } | |
| } | |
| if (rhs == null) return lhs; | |
| return Binary(lastSymbol, lhs, rhs); | |
| } | |
| Expression parseConditional() { | |
| Expression lhs = parseBinary(HIGHEST_PARSE_BINARY_PRECEDENCE); | |
| if (!_acceptCategory(_Category.query)) return lhs; | |
| Expression ifTrue = parseAssignment(); | |
| _expectCategory(_Category.colon); | |
| Expression ifFalse = parseAssignment(); | |
| return Conditional(lhs, ifTrue, ifFalse); | |
| } | |
| Expression parseAssignment() { | |
| Expression lhs = parseConditional(); | |
| String assignmentOperator = lastToken; | |
| if (_acceptCategory(_Category.assignment)) { | |
| Expression rhs = parseAssignment(); | |
| if (assignmentOperator == '=') { | |
| return Assignment(lhs, rhs); | |
| } else { | |
| // Handle +=, -=, etc. | |
| String operator = assignmentOperator.substring( | |
| 0, | |
| assignmentOperator.length - 1, | |
| ); | |
| return Assignment.compound(lhs, operator, rhs); | |
| } | |
| } | |
| return lhs; | |
| } | |
| Expression parseExpression() { | |
| Expression expression = parseAssignment(); | |
| while (_acceptCategory(_Category.comma)) { | |
| Expression right = parseAssignment(); | |
| expression = Binary(',', expression, right); | |
| } | |
| return expression; | |
| } | |
| Expression parseExpressionOrArrowFunction() { | |
| if (_acceptCategory(_Category.rparen)) { | |
| _expectCategory(_Category.arrow); | |
| return parseArrowFunctionBody([]); | |
| } | |
| if (_lastCategory == _Category.rest) { | |
| var params = parseParameters(); | |
| _expectCategory(_Category.rparen); | |
| _expectCategory(_Category.arrow); | |
| return parseArrowFunctionBody(params); | |
| } | |
| List<Expression> expressions = [parseAssignment()]; | |
| while (_acceptCategory(_Category.comma)) { | |
| if (_lastCategory == _Category.rest) { | |
| var params = [ | |
| ...reparseAsParameters(expressions), | |
| ...parseParameters(), | |
| ]; | |
| _expectCategory(_Category.rparen); | |
| _expectCategory(_Category.arrow); | |
| return parseArrowFunctionBody(params); | |
| } | |
| expressions.add(parseAssignment()); | |
| } | |
| _expectCategory(_Category.rparen); | |
| if (_acceptCategory(_Category.arrow)) { | |
| return parseArrowFunctionBody(reparseAsParameters(expressions)); | |
| } | |
| return expressions.reduce( | |
| (Expression value, Expression element) => Binary(',', value, element), | |
| ); | |
| } | |
| /// Returns a copy of [expressions] reinterpreted as parameter declarations. | |
| List<Parameter> reparseAsParameters(List<Expression> expressions) { | |
| return [ | |
| for (var e in expressions) | |
| switch (e) { | |
| VariableUse() => Parameter(e.name), | |
| InterpolatedExpression() => InterpolatedParameter(e.nameOrPosition), | |
| _ => error('Expected arrow function parameter list'), | |
| }, | |
| ]; | |
| } | |
| Expression parseArrowFunctionBody(List<Parameter> params) { | |
| Node body; | |
| if (_acceptCategory(_Category.lbrace)) { | |
| body = parseBlock(); | |
| } else { | |
| body = parseAssignment(); | |
| } | |
| return ArrowFunction(params, body); | |
| } | |
| VariableDeclarationList parseVariableDeclarationList() { | |
| Declaration firstVariable = parseVariableDeclaration(); | |
| return finishVariableDeclarationList(firstVariable); | |
| } | |
| VariableDeclarationList finishVariableDeclarationList( | |
| Declaration firstVariable, | |
| ) { | |
| var initialization = <VariableInitialization>[]; | |
| void declare(Declaration declaration) { | |
| Expression? initializer; | |
| if (acceptString('=')) { | |
| initializer = parseAssignment(); | |
| } | |
| initialization.add(VariableInitialization(declaration, initializer)); | |
| } | |
| declare(firstVariable); | |
| while (_acceptCategory(_Category.comma)) { | |
| Declaration variable = parseVariableDeclaration(); | |
| declare(variable); | |
| } | |
| return VariableDeclarationList(initialization); | |
| } | |
| Expression parseVarDeclarationOrExpression() { | |
| if (acceptString('var')) { | |
| return parseVariableDeclarationList(); | |
| } else { | |
| return parseExpression(); | |
| } | |
| } | |
| Expression expression() { | |
| Expression expression = parseVarDeclarationOrExpression(); | |
| if (_lastCategory != _Category.none || position != src.length) { | |
| error('Unparsed junk: $_lastCategory'); | |
| } | |
| return expression; | |
| } | |
| Statement statement() { | |
| Statement statement = parseStatement(); | |
| if (_lastCategory != _Category.none || position != src.length) { | |
| error('Unparsed junk: $_lastCategory'); | |
| } | |
| // TODO(sra): interpolated capture here? | |
| return statement; | |
| } | |
| Block parseBlock() { | |
| List<Statement> statements = []; | |
| while (!_acceptCategory(_Category.rbrace)) { | |
| Statement statement = parseStatement(); | |
| statements.add(statement); | |
| } | |
| return Block(statements); | |
| } | |
| Statement parseStatement() { | |
| if (_acceptCategory(_Category.lbrace)) return parseBlock(); | |
| if (_acceptCategory(_Category.semicolon)) return EmptyStatement(); | |
| if (_lastCategory == _Category.alpha) { | |
| if (acceptString('return')) return parseReturn(); | |
| if (acceptString('throw')) return parseThrow(); | |
| if (acceptString('break')) { | |
| return parseBreakOrContinue((label) => Break(label)); | |
| } | |
| if (acceptString('continue')) { | |
| return parseBreakOrContinue((label) => Continue(label)); | |
| } | |
| if (acceptString('if')) return parseIfThenElse(); | |
| if (acceptString('for')) return parseFor(); | |
| if (acceptString('function')) return parseFunctionDeclaration(); | |
| if (acceptString('try')) return parseTry(); | |
| if (acceptString('var')) { | |
| Expression declarations = parseVariableDeclarationList(); | |
| expectSemicolon(); | |
| return ExpressionStatement(declarations); | |
| } | |
| if (acceptString('while')) return parseWhile(); | |
| if (acceptString('do')) return parseDo(); | |
| if (acceptString('switch')) return parseSwitch(); | |
| if (lastToken == 'case') error('Case outside switch.'); | |
| if (lastToken == 'default') error('Default outside switch.'); | |
| if (lastToken == 'yield') return parseYield(); | |
| if (lastToken == 'with') { | |
| error('Not implemented in mini parser'); | |
| } | |
| } | |
| bool checkForInterpolatedStatement = _lastCategory == _Category.hash; | |
| Expression expression = parseExpression(); | |
| if (expression is VariableUse && _acceptCategory(_Category.colon)) { | |
| return LabeledStatement(expression.name, parseStatement()); | |
| } | |
| expectSemicolon(); | |
| if (checkForInterpolatedStatement) { | |
| // 'Promote' the interpolated expression `#;` to an interpolated | |
| // statement. | |
| if (expression is InterpolatedExpression) { | |
| assert(identical(interpolatedValues.last, expression)); | |
| InterpolatedStatement statement = InterpolatedStatement( | |
| expression.nameOrPosition, | |
| ); | |
| interpolatedValues[interpolatedValues.length - 1] = statement; | |
| return statement; | |
| } | |
| } | |
| return ExpressionStatement(expression); | |
| } | |
| Statement parseReturn() { | |
| if (acceptSemicolon()) return Return(); | |
| Expression expression = parseExpression(); | |
| expectSemicolon(); | |
| return Return(expression); | |
| } | |
| Statement parseYield() { | |
| bool hasStar = acceptString('*'); | |
| Expression expression = parseExpression(); | |
| expectSemicolon(); | |
| return DartYield(expression, hasStar); | |
| } | |
| Statement parseThrow() { | |
| if (skippedNewline) error('throw expression must be on same line'); | |
| Expression expression = parseExpression(); | |
| expectSemicolon(); | |
| return Throw(expression); | |
| } | |
| Statement parseBreakOrContinue(Statement Function(String?) constructor) { | |
| var identifier = lastToken; | |
| if (!skippedNewline && _acceptCategory(_Category.alpha)) { | |
| expectSemicolon(); | |
| return constructor(identifier); | |
| } | |
| expectSemicolon(); | |
| return constructor(null); | |
| } | |
| Statement parseIfThenElse() { | |
| _expectCategory(_Category.lparen); | |
| Expression condition = parseExpression(); | |
| _expectCategory(_Category.rparen); | |
| Statement thenStatement = parseStatement(); | |
| if (acceptString('else')) { | |
| // Resolves dangling else by binding 'else' to closest 'if'. | |
| Statement elseStatement = parseStatement(); | |
| return If(condition, thenStatement, elseStatement); | |
| } else { | |
| return If.noElse(condition, thenStatement); | |
| } | |
| } | |
| Statement parseFor() { | |
| // For-init-condition-increment style loops are fully supported. | |
| // | |
| // Only one for-in variant is currently implemented: | |
| // | |
| // for (var variable in Expression) Statement | |
| // | |
| Statement finishFor(Expression? init) { | |
| Expression? condition; | |
| if (!_acceptCategory(_Category.semicolon)) { | |
| condition = parseExpression(); | |
| _expectCategory(_Category.semicolon); | |
| } | |
| Expression? update; | |
| if (!_acceptCategory(_Category.rparen)) { | |
| update = parseExpression(); | |
| _expectCategory(_Category.rparen); | |
| } | |
| Statement body = parseStatement(); | |
| return For(init, condition, update, body); | |
| } | |
| _expectCategory(_Category.lparen); | |
| if (_acceptCategory(_Category.semicolon)) { | |
| return finishFor(null); | |
| } | |
| if (acceptString('var')) { | |
| Declaration declaration = parseVariableDeclaration(); | |
| if (acceptString('in')) { | |
| Expression objectExpression = parseExpression(); | |
| _expectCategory(_Category.rparen); | |
| Statement body = parseStatement(); | |
| return ForIn( | |
| VariableDeclarationList([VariableInitialization(declaration, null)]), | |
| objectExpression, | |
| body, | |
| ); | |
| } | |
| Expression declarations = finishVariableDeclarationList(declaration); | |
| _expectCategory(_Category.semicolon); | |
| return finishFor(declarations); | |
| } | |
| Expression init = parseExpression(); | |
| _expectCategory(_Category.semicolon); | |
| return finishFor(init); | |
| } | |
| Declaration parseVariableDeclaration() { | |
| if (_acceptCategory(_Category.hash)) { | |
| var nameOrPosition = parseHash(); | |
| InterpolatedDeclaration declaration = InterpolatedDeclaration( | |
| nameOrPosition, | |
| ); | |
| interpolatedValues.add(declaration); | |
| return declaration; | |
| } else { | |
| String token = lastToken; | |
| _expectCategory(_Category.alpha); | |
| return VariableDeclaration(token); | |
| } | |
| } | |
| Statement parseFunctionDeclaration() { | |
| Declaration name = parseVariableDeclaration(); | |
| Fun fun = parseFun(); | |
| return FunctionDeclaration(name, fun); | |
| } | |
| Statement parseTry() { | |
| _expectCategory(_Category.lbrace); | |
| Block body = parseBlock(); | |
| Catch? catchPart; | |
| if (acceptString('catch')) catchPart = parseCatch(); | |
| Block? finallyPart; | |
| if (acceptString('finally')) { | |
| _expectCategory(_Category.lbrace); | |
| finallyPart = parseBlock(); | |
| } else { | |
| if (catchPart == null) error("expected 'finally'"); | |
| } | |
| return Try(body, catchPart, finallyPart); | |
| } | |
| SwitchClause parseSwitchClause() { | |
| Expression? expression; | |
| if (acceptString('case')) { | |
| expression = parseExpression(); | |
| _expectCategory(_Category.colon); | |
| } else { | |
| if (!acceptString('default')) { | |
| error('expected case or default'); | |
| } | |
| _expectCategory(_Category.colon); | |
| } | |
| List<Statement> statements = []; | |
| while (_lastCategory != _Category.rbrace && | |
| lastToken != 'case' && | |
| lastToken != 'default') { | |
| statements.add(parseStatement()); | |
| } | |
| return expression == null | |
| ? Default(Block(statements)) | |
| : Case(expression, Block(statements)); | |
| } | |
| Statement parseWhile() { | |
| _expectCategory(_Category.lparen); | |
| Expression condition = parseExpression(); | |
| _expectCategory(_Category.rparen); | |
| Statement body = parseStatement(); | |
| return While(condition, body); | |
| } | |
| Statement parseDo() { | |
| Statement body = parseStatement(); | |
| if (lastToken != 'while') error('Missing while after do body.'); | |
| getToken(); | |
| _expectCategory(_Category.lparen); | |
| Expression condition = parseExpression(); | |
| _expectCategory(_Category.rparen); | |
| expectSemicolon(); | |
| return Do(body, condition); | |
| } | |
| Statement parseSwitch() { | |
| _expectCategory(_Category.lparen); | |
| Expression key = parseExpression(); | |
| _expectCategory(_Category.rparen); | |
| _expectCategory(_Category.lbrace); | |
| List<SwitchClause> clauses = []; | |
| while (_lastCategory != _Category.rbrace) { | |
| clauses.add(parseSwitchClause()); | |
| } | |
| _expectCategory(_Category.rbrace); | |
| return Switch(key, clauses); | |
| } | |
| Catch parseCatch() { | |
| _expectCategory(_Category.lparen); | |
| Declaration errorName = parseVariableDeclaration(); | |
| _expectCategory(_Category.rparen); | |
| _expectCategory(_Category.lbrace); | |
| Block body = parseBlock(); | |
| return Catch(errorName, body); | |
| } | |
| } |