| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 // Utilities for building JS ASTs at runtime. Contains a builder class | |
| 6 // and a parser that parses part of the language. | |
| 7 | |
| 8 part of js_ast; | |
| 9 | |
| 10 | |
| 11 /** | |
| 12 * Global template manager. We should aim to have a fixed number of | |
| 13 * templates. This implies that we do not use js('xxx') to parse text that is | |
| 14 * constructed from values that depend on names in the Dart program. | |
| 15 * | |
| 16 * TODO(sra): Find the remaining places where js('xxx') used to parse an | |
| 17 * unbounded number of expression, or institute a cache policy. | |
| 18 */ | |
| 19 TemplateManager templateManager = new TemplateManager(); | |
| 20 | |
| 21 | |
| 22 /** | |
| 23 | |
| 24 [js] is a singleton instace of JsBuilder. JsBuilder is a set of conveniences | |
| 25 for constructing JavaScript ASTs. | |
| 26 | |
| 27 [string] and [number] are used to create leaf AST nodes: | |
| 28 | |
| 29 var s = js.string('hello'); // s = new LiteralString('"hello"') | |
| 30 var n = js.number(123); // n = new LiteralNumber(123) | |
| 31 | |
| 32 In the line above `a --> b` means Dart expression `a` evaluates to a JavaScript | |
| 33 AST that would pretty-print as `b`. | |
| 34 | |
| 35 The [call] method constructs an Expression AST. | |
| 36 | |
| 37 No argument | |
| 38 | |
| 39 js('window.alert("hello")') --> window.alert("hello") | |
| 40 | |
| 41 The input text can contain placeholders `#` that are replaced with provided | |
| 42 arguments. A single argument can be passed directly: | |
| 43 | |
| 44 js('window.alert(#)', s) --> window.alert("hello") | |
| 45 | |
| 46 Multiple arguments are passed as a list: | |
| 47 | |
| 48 js('# + #', [s, s]) --> "hello" + "hello" | |
| 49 | |
| 50 The [statement] method constructs a Statement AST, but is otherwise like the | |
| 51 [call] method. This constructs a Return AST: | |
| 52 | |
| 53 var ret = js.statement('return #;', n); --> return 123; | |
| 54 | |
| 55 A placeholder in a Statement context must be followed by a semicolon ';'. You | |
| 56 can think of a statement placeholder as being `#;` to explain why the output | |
| 57 still has one semicolon: | |
| 58 | |
| 59 js.statement('if (happy) #;', ret) | |
| 60 --> | |
| 61 if (happy) | |
| 62 return 123; | |
| 63 | |
| 64 If the placeholder is not followed by a semicolon, it is part of an expression. | |
| 65 Here the paceholder is in the position of the function in a function call: | |
| 66 | |
| 67 var vFoo = new VariableUse('foo'); | |
| 68 js.statement('if (happy) #("Happy!")', vFoo) | |
| 69 --> | |
| 70 if (happy) | |
| 71 foo("Happy!"); | |
| 72 | |
| 73 Generally, a placeholder in an expression position requires an Expression AST as | |
| 74 an argument and a placeholder in a statement position requires a Statement AST. | |
| 75 An expression will be converted to a Statement if needed by creating an | |
| 76 ExpessionStatement. A String argument will be converted into a VariableUse and | |
| 77 requires that the string is a JavaScript identifier. | |
| 78 | |
| 79 js('# + 1', vFoo) --> foo + 1 | |
| 80 js('# + 1', 'foo') --> foo + 1 | |
| 81 js('# + 1', 'foo.bar') --> assertion failure | |
| 82 | |
| 83 Some placeholder positions are _splicing contexts_. A function argument list is | |
| 84 a splicing expression context. A placeholder in a splicing expression context | |
| 85 can take a single Expression (or String, converted to VariableUse) or an | |
| 86 Iterable of Expressions (and/or Strings). | |
| 87 | |
| 88 // non-splicing argument: | |
| 89 js('#(#)', ['say', s]) --> say("hello") | |
| 90 // splicing arguments: | |
| 91 js('#(#)', ['say', []]) --> say() | |
| 92 js('#(#)', ['say', [s]]) --> say("hello") | |
| 93 js('#(#)', ['say', [s, n]]) --> say("hello", 123) | |
| 94 | |
| 95 A splicing context can be used to append 'lists' and add extra elements: | |
| 96 | |
| 97 js('foo(#, #, 1)', [ ['a', n], s]) --> foo(a, 123, "hello", 1) | |
| 98 js('foo(#, #, 1)', [ ['a', n], [s, n]]) --> foo(a, 123, "hello", 123, 1) | |
| 99 js('foo(#, #, 1)', [ [], [s, n]]) --> foo("hello", 123, 1) | |
| 100 js('foo(#, #, 1)', [ [], [] ]) --> foo(1) | |
| 101 | |
| 102 The generation of a compile-time optional argument expression can be chosen by | |
| 103 providing an empty or singleton list. | |
| 104 | |
| 105 In addition to Expressions and Statements, there are Parameters, which occur | |
| 106 only in the parameter list of a function expression or declaration. | |
| 107 Placeholders in parameter positions behave like placeholders in Expression | |
| 108 positions, except only Parameter AST nodes are permitted. String arguments for | |
| 109 parameter placeholders are converted to Parameter AST nodes. | |
| 110 | |
| 111 var pFoo = new Parameter('foo') | |
| 112 js('function(#) { return #; }', [pFoo, vFoo]) | |
| 113 --> | |
| 114 function(foo) { return foo; } | |
| 115 | |
| 116 Expressions and Parameters are not compatible with each other's context: | |
| 117 | |
| 118 js('function(#) { return #; }', [vFoo, vFoo]) --> error | |
| 119 js('function(#) { return #; }', [pFoo, pFoo]) --> error | |
| 120 | |
| 121 The parameter context is a splicing context. When combined with the | |
| 122 context-sensitive conversion of Strings, this simplifies the construction of | |
| 123 trampoline-like functions: | |
| 124 | |
| 125 var args = ['a', 'b']; | |
| 126 js('function(#) { return f(this, #); }', [args, args]) | |
| 127 --> | |
| 128 function(a, b) { return f(this, a, b); } | |
| 129 | |
| 130 A statement placeholder in a Block is also in a splicing context. In addition | |
| 131 to splicing Iterables, statement placeholders in a Block will also splice a | |
| 132 Block or an EmptyStatement. This flattens nested blocks and allows blocks to be | |
| 133 appended. | |
| 134 | |
| 135 var b1 = js.statement('{ 1; 2; }'); | |
| 136 var sEmpty = new Emptystatement(); | |
| 137 js.statement('{ #; #; #; #; }', [sEmpty, b1, b1, sEmpty]) | |
| 138 --> | |
| 139 { 1; 2; 1; 2; } | |
| 140 | |
| 141 A placeholder in the context of an if-statement condition also accepts a Dart | |
| 142 bool argument, which selects the then-part or else-part of the if-statement: | |
| 143 | |
| 144 js.statement('if (#) return;', vFoo) --> if (foo) return; | |
| 145 js.statement('if (#) return;', true) --> return; | |
| 146 js.statement('if (#) return;', false) --> ; // empty statement | |
| 147 var eTrue = new LiteralBool(true); | |
| 148 js.statement('if (#) return;', eTrue) --> if (true) return; | |
| 149 | |
| 150 Combined with block splicing, if-statement condition context placeholders allows | |
| 151 the creation of tenplates that select code depending on variables. | |
| 152 | |
| 153 js.statement('{ 1; if (#) 2; else { 3; 4; } 5;}', true) | |
| 154 --> { 1; 2; 5; } | |
| 155 | |
| 156 js.statement('{ 1; if (#) 2; else { 3; 4; } 5;}', false) | |
| 157 --> { 1; 3; 4; 5; } | |
| 158 | |
| 159 A placeholder following a period in a property access is in a property access | |
| 160 context. This is just like an expression context, except String arguments are | |
| 161 converted to JavaScript property accesses. In JavaScript, `a.b` is short-hand | |
| 162 for `a["b"]`: | |
| 163 | |
| 164 js('a[#]', vFoo) --> a[foo] | |
| 165 js('a[#]', s) --> a.hello (i.e. a["hello"]). | |
| 166 js('a[#]', 'x') --> a[x] | |
| 167 | |
| 168 js('a.#', vFoo) --> a[foo] | |
| 169 js('a.#', s) --> a.hello (i.e. a["hello"]) | |
| 170 js('a.#', 'x') --> a.x (i.e. a["x"]) | |
| 171 | |
| 172 (Question - should `.#` be restricted to permit only String arguments? The | |
| 173 template should probably be writted with `[]` if non-strings are accepted.) | |
| 174 | |
| 175 | |
| 176 Object initialiers allow placeholders in the key property name position: | |
| 177 | |
| 178 js('{#:1, #:2}', [s, 'bye']) --> {hello: 1, bye: 2} | |
| 179 | |
| 180 | |
| 181 What is not implemented: | |
| 182 | |
| 183 - Array initializers and object initializers could support splicing. In the | |
| 184 array case, we would need some way to know if an ArrayInitializer argument | |
| 185 should be splice or is intended as a single value. | |
| 186 | |
| 187 - There are no placeholders in definition contexts: | |
| 188 | |
| 189 function #(){} | |
| 190 var # = 1; | |
| 191 | |
| 192 */ | |
| 193 const JsBuilder js = const JsBuilder(); | |
| 194 | |
| 195 | |
| 196 class JsBuilder { | |
| 197 const JsBuilder(); | |
| 198 | |
| 199 /** | |
| 200 * Parses a bit of JavaScript, and returns an expression. | |
| 201 * | |
| 202 * See the MiniJsParser class. | |
| 203 * | |
| 204 * [arguments] can be a single [Node] (e.g. an [Expression] or [Statement]) or | |
| 205 * a list of [Node]s, which will be interpolated into the source at the '#' | |
| 206 * signs. | |
| 207 */ | |
| 208 Expression call(String source, [var arguments]) { | |
| 209 Template template = _findExpressionTemplate(source); | |
| 210 if (arguments == null) return template.instantiate([]); | |
| 211 // We allow a single argument to be given directly. | |
| 212 if (arguments is! List && arguments is! Map) arguments = [arguments]; | |
| 213 return template.instantiate(arguments); | |
| 214 } | |
| 215 | |
| 216 /** | |
| 217 * Parses a JavaScript Statement, otherwise just like [call]. | |
| 218 */ | |
| 219 Statement statement(String source, [var arguments]) { | |
| 220 Template template = _findStatementTemplate(source); | |
| 221 if (arguments == null) return template.instantiate([]); | |
| 222 // We allow a single argument to be given directly. | |
| 223 if (arguments is! List && arguments is! Map) arguments = [arguments]; | |
| 224 return template.instantiate(arguments); | |
| 225 } | |
| 226 | |
| 227 /** | |
| 228 * Parses JavaScript written in the `JS` foreign instruction. | |
| 229 * | |
| 230 * The [source] must be a JavaScript expression or a JavaScript throw | |
| 231 * statement. | |
| 232 */ | |
| 233 Template parseForeignJS(String source) { | |
| 234 // TODO(sra): Parse with extra validation to forbid `#` interpolation in | |
| 235 // functions, as this leads to unanticipated capture of temporaries that are | |
| 236 // reused after capture. | |
| 237 if (source.startsWith("throw ")) { | |
| 238 return _findStatementTemplate(source); | |
| 239 } else { | |
| 240 return _findExpressionTemplate(source); | |
| 241 } | |
| 242 } | |
| 243 | |
| 244 Template _findExpressionTemplate(String source) { | |
| 245 Template template = templateManager.lookupExpressionTemplate(source); | |
| 246 if (template == null) { | |
| 247 MiniJsParser parser = new MiniJsParser(source); | |
| 248 Expression expression = parser.expression(); | |
| 249 template = templateManager.defineExpressionTemplate(source, expression); | |
| 250 } | |
| 251 return template; | |
| 252 } | |
| 253 | |
| 254 Template _findStatementTemplate(String source) { | |
| 255 Template template = templateManager.lookupStatementTemplate(source); | |
| 256 if (template == null) { | |
| 257 MiniJsParser parser = new MiniJsParser(source); | |
| 258 Statement statement = parser.statement(); | |
| 259 template = templateManager.defineStatementTemplate(source, statement); | |
| 260 } | |
| 261 return template; | |
| 262 } | |
| 263 | |
| 264 /** | |
| 265 * Creates an Expression template without caching the result. | |
| 266 */ | |
| 267 Template uncachedExpressionTemplate(String source) { | |
| 268 MiniJsParser parser = new MiniJsParser(source); | |
| 269 Expression expression = parser.expression(); | |
| 270 return new Template( | |
| 271 source, expression, isExpression: true, forceCopy: false); | |
| 272 } | |
| 273 | |
| 274 /** | |
| 275 * Creates a Statement template without caching the result. | |
| 276 */ | |
| 277 Template uncachedStatementTemplate(String source) { | |
| 278 MiniJsParser parser = new MiniJsParser(source); | |
| 279 Statement statement = parser.statement(); | |
| 280 return new Template( | |
| 281 source, statement, isExpression: false, forceCopy: false); | |
| 282 } | |
| 283 | |
| 284 /** | |
| 285 * Create an Expression template which has [ast] as the result. This is used | |
| 286 * to wrap a generated AST in a zero-argument Template so it can be passed to | |
| 287 * context that expects a template. | |
| 288 */ | |
| 289 Template expressionTemplateYielding(Node ast) { | |
| 290 return new Template.withExpressionResult(ast); | |
| 291 } | |
| 292 | |
| 293 Template statementTemplateYielding(Node ast) { | |
| 294 return new Template.withStatementResult(ast); | |
| 295 } | |
| 296 | |
| 297 /// Creates a literal js string from [value]. | |
| 298 LiteralString escapedString(String value) { | |
| 299 // Start by escaping the backslashes. | |
| 300 String escaped = value.replaceAll('\\', '\\\\'); | |
| 301 // Do not escape unicode characters and ' because they are allowed in the | |
| 302 // string literal anyway. | |
| 303 escaped = escaped.replaceAllMapped(new RegExp('\n|"|\b|\t|\v'), (match) { | |
| 304 switch (match.group(0)) { | |
| 305 case "\n" : return r"\n"; | |
| 306 case "\"" : return r'\"'; | |
| 307 case "\b" : return r"\b"; | |
| 308 case "\t" : return r"\t"; | |
| 309 case "\f" : return r"\f"; | |
| 310 case "\v" : return r"\v"; | |
| 311 } | |
| 312 }); | |
| 313 LiteralString result = string(escaped); | |
| 314 // We don't escape ' under the assumption that the string is wrapped | |
| 315 // into ". Verify that assumption. | |
| 316 assert(result.value.codeUnitAt(0) == '"'.codeUnitAt(0)); | |
| 317 return result; | |
| 318 } | |
| 319 | |
| 320 /// Creates a literal js string from [value]. | |
| 321 /// | |
| 322 /// Note that this function only puts quotes around [value]. It does not do | |
| 323 /// any escaping, so use only when you can guarantee that [value] does not | |
| 324 /// contain newlines or backslashes. For escaping the string use | |
| 325 /// [escapedString]. | |
| 326 LiteralString string(String value) => new LiteralString('"$value"'); | |
| 327 | |
| 328 LiteralNumber number(num value) => new LiteralNumber('$value'); | |
| 329 | |
| 330 LiteralBool boolean(bool value) => new LiteralBool(value); | |
| 331 | |
| 332 ArrayInitializer numArray(Iterable<int> list) => | |
| 333 new ArrayInitializer(list.map(number).toList()); | |
| 334 | |
| 335 ArrayInitializer stringArray(Iterable<String> list) => | |
| 336 new ArrayInitializer(list.map(string).toList()); | |
| 337 | |
| 338 Comment comment(String text) => new Comment(text); | |
| 339 | |
| 340 Call propertyCall(Expression receiver, | |
| 341 String fieldName, | |
| 342 List<Expression> arguments) { | |
| 343 return new Call(new PropertyAccess.field(receiver, fieldName), arguments); | |
| 344 } | |
| 345 } | |
| 346 | |
| 347 LiteralString string(String value) => js.string(value); | |
| 348 LiteralNumber number(num value) => js.number(value); | |
| 349 ArrayInitializer numArray(Iterable<int> list) => js.numArray(list); | |
| 350 ArrayInitializer stringArray(Iterable<String> list) => js.stringArray(list); | |
| 351 Call propertyCall(Expression receiver, | |
| 352 String fieldName, | |
| 353 List<Expression> arguments) { | |
| 354 return js.propertyCall(receiver, fieldName, arguments); | |
| 355 } | |
| 356 | |
| 357 class MiniJsParserError { | |
| 358 MiniJsParserError(this.parser, this.message) { } | |
| 359 | |
| 360 final MiniJsParser parser; | |
| 361 final String message; | |
| 362 | |
| 363 String toString() { | |
| 364 int pos = parser.lastPosition; | |
| 365 | |
| 366 // Discard lines following the line containing lastPosition. | |
| 367 String src = parser.src; | |
| 368 int newlinePos = src.indexOf('\n', pos); | |
| 369 if (newlinePos >= pos) src = src.substring(0, newlinePos); | |
| 370 | |
| 371 // Extract the prefix of the error line before lastPosition. | |
| 372 String line = src; | |
| 373 int lastLineStart = line.lastIndexOf('\n'); | |
| 374 if (lastLineStart >= 0) line = line.substring(lastLineStart + 1); | |
| 375 String prefix = line.substring(0, pos - (src.length - line.length)); | |
| 376 | |
| 377 // Replace non-tabs with spaces, giving a print indent that matches the text | |
| 378 // for tabbing. | |
| 379 String spaces = prefix.replaceAll(new RegExp(r'[^\t]'), ' '); | |
| 380 return 'Error in MiniJsParser:\n${src}\n$spaces^\n$spaces$message\n'; | |
| 381 } | |
| 382 } | |
| 383 | |
| 384 /// Mini JavaScript parser for tiny snippets of code that we want to make into | |
| 385 /// AST nodes. Handles: | |
| 386 /// * identifiers. | |
| 387 /// * dot access. | |
| 388 /// * method calls. | |
| 389 /// * [] access. | |
| 390 /// * array, string, regexp, boolean, null and numeric literals. | |
| 391 /// * most operators. | |
| 392 /// * brackets. | |
| 393 /// * var declarations. | |
| 394 /// * operator precedence. | |
| 395 /// * anonymous funtions and named function expressions and declarations. | |
| 396 /// Notable things it can't do yet include: | |
| 397 /// * some statements are still missing (do-while, while, switch). | |
| 398 /// | |
| 399 /// It's a fairly standard recursive descent parser. | |
| 400 /// | |
| 401 /// Literal strings are passed through to the final JS source code unchanged, | |
| 402 /// including the choice of surrounding quotes, so if you parse | |
| 403 /// r'var x = "foo\n\"bar\""' you will end up with | |
| 404 /// var x = "foo\n\"bar\"" in the final program. \x and \u escapes are not | |
| 405 /// allowed in string and regexp literals because the machinery for checking | |
| 406 /// their correctness is rather involved. | |
| 407 class MiniJsParser { | |
| 408 MiniJsParser(this.src) | |
| 409 : lastCategory = NONE, | |
| 410 lastToken = null, | |
| 411 lastPosition = 0, | |
| 412 position = 0 { | |
| 413 getToken(); | |
| 414 } | |
| 415 | |
| 416 int lastCategory = NONE; | |
| 417 String lastToken = null; | |
| 418 int lastPosition = 0; | |
| 419 int position = 0; | |
| 420 bool skippedNewline = false; // skipped newline in last getToken? | |
| 421 final String src; | |
| 422 | |
| 423 final List<InterpolatedNode> interpolatedValues = <InterpolatedNode>[]; | |
| 424 bool get hasNamedHoles => | |
| 425 interpolatedValues.isNotEmpty && interpolatedValues.first.isNamed; | |
| 426 bool get hasPositionalHoles => | |
| 427 interpolatedValues.isNotEmpty && interpolatedValues.first.isPositional; | |
| 428 | |
| 429 static const NONE = -1; | |
| 430 static const ALPHA = 0; | |
| 431 static const NUMERIC = 1; | |
| 432 static const STRING = 2; | |
| 433 static const SYMBOL = 3; | |
| 434 static const ASSIGNMENT = 4; | |
| 435 static const DOT = 5; | |
| 436 static const LPAREN = 6; | |
| 437 static const RPAREN = 7; | |
| 438 static const LBRACE = 8; | |
| 439 static const RBRACE = 9; | |
| 440 static const LSQUARE = 10; | |
| 441 static const RSQUARE = 11; | |
| 442 static const COMMA = 12; | |
| 443 static const QUERY = 13; | |
| 444 static const COLON = 14; | |
| 445 static const SEMICOLON = 15; | |
| 446 static const HASH = 16; | |
| 447 static const WHITESPACE = 17; | |
| 448 static const OTHER = 18; | |
| 449 | |
| 450 // Make sure that ]] is two symbols. | |
| 451 bool singleCharCategory(int category) => category >= DOT; | |
| 452 | |
| 453 static String categoryToString(int cat) { | |
| 454 switch (cat) { | |
| 455 case NONE: return "NONE"; | |
| 456 case ALPHA: return "ALPHA"; | |
| 457 case NUMERIC: return "NUMERIC"; | |
| 458 case SYMBOL: return "SYMBOL"; | |
| 459 case ASSIGNMENT: return "ASSIGNMENT"; | |
| 460 case DOT: return "DOT"; | |
| 461 case LPAREN: return "LPAREN"; | |
| 462 case RPAREN: return "RPAREN"; | |
| 463 case LBRACE: return "LBRACE"; | |
| 464 case RBRACE: return "RBRACE"; | |
| 465 case LSQUARE: return "LSQUARE"; | |
| 466 case RSQUARE: return "RSQUARE"; | |
| 467 case STRING: return "STRING"; | |
| 468 case COMMA: return "COMMA"; | |
| 469 case QUERY: return "QUERY"; | |
| 470 case COLON: return "COLON"; | |
| 471 case SEMICOLON: return "SEMICOLON"; | |
| 472 case HASH: return "HASH"; | |
| 473 case WHITESPACE: return "WHITESPACE"; | |
| 474 case OTHER: return "OTHER"; | |
| 475 } | |
| 476 return "Unknown: $cat"; | |
| 477 } | |
| 478 | |
| 479 static const CATEGORIES = const <int>[ | |
| 480 OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, // 0-7 | |
| 481 OTHER, WHITESPACE, WHITESPACE, OTHER, OTHER, WHITESPACE, // 8-13 | |
| 482 OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, // 14-21 | |
| 483 OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, // 22-29 | |
| 484 OTHER, OTHER, WHITESPACE, // 30-32 | |
| 485 SYMBOL, OTHER, HASH, ALPHA, SYMBOL, SYMBOL, OTHER, // !"#$%&´ | |
| 486 LPAREN, RPAREN, SYMBOL, SYMBOL, COMMA, SYMBOL, DOT, SYMBOL, // ()*+,-./ | |
| 487 NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, // 01234 | |
| 488 NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, // 56789 | |
| 489 COLON, SEMICOLON, SYMBOL, SYMBOL, SYMBOL, QUERY, OTHER, // :;<=>?@ | |
| 490 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // ABCDEFGH | |
| 491 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // IJKLMNOP | |
| 492 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // QRSTUVWX | |
| 493 ALPHA, ALPHA, LSQUARE, OTHER, RSQUARE, SYMBOL, ALPHA, OTHER, // YZ[\]^_' | |
| 494 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // abcdefgh | |
| 495 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // ijklmnop | |
| 496 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // qrstuvwx | |
| 497 ALPHA, ALPHA, LBRACE, SYMBOL, RBRACE, SYMBOL]; // yz{|}~ | |
| 498 | |
| 499 // This must be a >= the highest precedence number handled by parseBinary. | |
| 500 static var HIGHEST_PARSE_BINARY_PRECEDENCE = 16; | |
| 501 static bool isAssignment(String symbol) => BINARY_PRECEDENCE[symbol] == 17; | |
| 502 | |
| 503 // From https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operator
s/Operator_Precedence | |
| 504 static final BINARY_PRECEDENCE = { | |
| 505 '+=': 17, '-=': 17, '*=': 17, '/=': 17, '%=': 17, '^=': 17, '|=': 17, | |
| 506 '&=': 17, '<<=': 17, '>>=': 17, '>>>=': 17, '=': 17, | |
| 507 '||': 14, | |
| 508 '&&': 13, | |
| 509 '|': 12, | |
| 510 '^': 11, | |
| 511 '&': 10, | |
| 512 '!=': 9, '==': 9, '!==': 9, '===': 9, | |
| 513 '<': 8, '<=': 8, '>=': 8, '>': 8, 'in': 8, 'instanceof': 8, | |
| 514 '<<': 7, '>>': 7, '>>>': 7, | |
| 515 '+': 6, '-': 6, | |
| 516 '*': 5, '/': 5, '%': 5 | |
| 517 }; | |
| 518 static final UNARY_OPERATORS = | |
| 519 ['++', '--', '+', '-', '~', '!', 'typeof', 'void', 'delete', 'await'] | |
| 520 .toSet(); | |
| 521 | |
| 522 static final OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS = | |
| 523 ['typeof', 'void', 'delete', 'in', 'instanceof', 'await'].toSet(); | |
| 524 | |
| 525 static int category(int code) { | |
| 526 if (code >= CATEGORIES.length) return OTHER; | |
| 527 return CATEGORIES[code]; | |
| 528 } | |
| 529 | |
| 530 String getDelimited(int startPosition) { | |
| 531 position = startPosition; | |
| 532 int delimiter = src.codeUnitAt(startPosition); | |
| 533 int currentCode; | |
| 534 do { | |
| 535 position++; | |
| 536 if (position >= src.length) error("Unterminated literal"); | |
| 537 currentCode = src.codeUnitAt(position); | |
| 538 if (currentCode == charCodes.$LF) error("Unterminated literal"); | |
| 539 if (currentCode == charCodes.$BACKSLASH) { | |
| 540 if (++position >= src.length) error("Unterminated literal"); | |
| 541 int escaped = src.codeUnitAt(position); | |
| 542 if (escaped == charCodes.$x || escaped == charCodes.$X || | |
| 543 escaped == charCodes.$u || escaped == charCodes.$U || | |
| 544 category(escaped) == NUMERIC) { | |
| 545 error('Numeric and hex escapes are not allowed in literals'); | |
| 546 } | |
| 547 } | |
| 548 } while (currentCode != delimiter); | |
| 549 position++; | |
| 550 return src.substring(lastPosition, position); | |
| 551 } | |
| 552 | |
| 553 void getToken() { | |
| 554 skippedNewline = false; | |
| 555 for (;;) { | |
| 556 if (position >= src.length) break; | |
| 557 int code = src.codeUnitAt(position); | |
| 558 // Skip '//' and '/*' style comments. | |
| 559 if (code == charCodes.$SLASH && | |
| 560 position + 1 < src.length) { | |
| 561 if (src.codeUnitAt(position + 1) == charCodes.$SLASH) { | |
| 562 int nextPosition = src.indexOf('\n', position); | |
| 563 if (nextPosition == -1) nextPosition = src.length; | |
| 564 position = nextPosition; | |
| 565 continue; | |
| 566 } else if (src.codeUnitAt(position + 1) == charCodes.$STAR) { | |
| 567 int nextPosition = src.indexOf('*/', position + 2); | |
| 568 if (nextPosition == -1) error('Unterminated comment'); | |
| 569 position = nextPosition + 2; | |
| 570 continue; | |
| 571 } | |
| 572 } | |
| 573 if (category(code) != WHITESPACE) break; | |
| 574 if (code == charCodes.$LF) skippedNewline = true; | |
| 575 ++position; | |
| 576 } | |
| 577 | |
| 578 if (position == src.length) { | |
| 579 lastCategory = NONE; | |
| 580 lastToken = null; | |
| 581 lastPosition = position; | |
| 582 return; | |
| 583 } | |
| 584 int code = src.codeUnitAt(position); | |
| 585 lastPosition = position; | |
| 586 if (code == charCodes.$SQ || code == charCodes.$DQ) { | |
| 587 // String literal. | |
| 588 lastCategory = STRING; | |
| 589 lastToken = getDelimited(position); | |
| 590 } else if (code == charCodes.$0 && | |
| 591 position + 2 < src.length && | |
| 592 src.codeUnitAt(position + 1) == charCodes.$x) { | |
| 593 // Hex literal. | |
| 594 for (position += 2; position < src.length; position++) { | |
| 595 int cat = category(src.codeUnitAt(position)); | |
| 596 if (cat != NUMERIC && cat != ALPHA) break; | |
| 597 } | |
| 598 lastCategory = NUMERIC; | |
| 599 lastToken = src.substring(lastPosition, position); | |
| 600 int.parse(lastToken, onError: (_) { | |
| 601 error("Unparseable number"); | |
| 602 }); | |
| 603 } else if (code == charCodes.$SLASH) { | |
| 604 // Tokens that start with / are special due to regexp literals. | |
| 605 lastCategory = SYMBOL; | |
| 606 position++; | |
| 607 if (position < src.length && src.codeUnitAt(position) == charCodes.$EQ) { | |
| 608 position++; | |
| 609 } | |
| 610 lastToken = src.substring(lastPosition, position); | |
| 611 } else { | |
| 612 // All other tokens handled here. | |
| 613 int cat = category(src.codeUnitAt(position)); | |
| 614 int newCat; | |
| 615 do { | |
| 616 position++; | |
| 617 if (position == src.length) break; | |
| 618 int code = src.codeUnitAt(position); | |
| 619 // Special code to disallow ! and / in non-first position in token, so | |
| 620 // that !! parses as two tokens and != parses as one, while =/ parses | |
| 621 // as a an equals token followed by a regexp literal start. | |
| 622 newCat = (code == charCodes.$BANG || code == charCodes.$SLASH) | |
| 623 ? NONE | |
| 624 : category(code); | |
| 625 } while (!singleCharCategory(cat) && | |
| 626 (cat == newCat || | |
| 627 (cat == ALPHA && newCat == NUMERIC) || // eg. level42. | |
| 628 (cat == NUMERIC && newCat == DOT))); // eg. 3.1415 | |
| 629 lastCategory = cat; | |
| 630 lastToken = src.substring(lastPosition, position); | |
| 631 if (cat == NUMERIC) { | |
| 632 double.parse(lastToken, (_) { | |
| 633 error("Unparseable number"); | |
| 634 }); | |
| 635 } else if (cat == SYMBOL) { | |
| 636 int binaryPrecendence = BINARY_PRECEDENCE[lastToken]; | |
| 637 if (binaryPrecendence == null && !UNARY_OPERATORS.contains(lastToken)) { | |
| 638 error("Unknown operator"); | |
| 639 } | |
| 640 if (isAssignment(lastToken)) lastCategory = ASSIGNMENT; | |
| 641 } else if (cat == ALPHA) { | |
| 642 if (OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS.contains(lastToken)) { | |
| 643 lastCategory = SYMBOL; | |
| 644 } | |
| 645 } | |
| 646 } | |
| 647 } | |
| 648 | |
| 649 void expectCategory(int cat) { | |
| 650 if (cat != lastCategory) error("Expected ${categoryToString(cat)}"); | |
| 651 getToken(); | |
| 652 } | |
| 653 | |
| 654 bool acceptCategory(int cat) { | |
| 655 if (cat == lastCategory) { | |
| 656 getToken(); | |
| 657 return true; | |
| 658 } | |
| 659 return false; | |
| 660 } | |
| 661 | |
| 662 void expectSemicolon() { | |
| 663 if (acceptSemicolon()) return; | |
| 664 error('Expected SEMICOLON'); | |
| 665 } | |
| 666 | |
| 667 bool acceptSemicolon() { | |
| 668 // Accept semicolon or automatically inserted semicolon before close brace. | |
| 669 // Miniparser forbids other kinds of semicolon insertion. | |
| 670 if (RBRACE == lastCategory) return true; | |
| 671 if (NONE == lastCategory) return true; // end of input | |
| 672 if (skippedNewline) { | |
| 673 error('No automatic semicolon insertion at preceding newline'); | |
| 674 } | |
| 675 return acceptCategory(SEMICOLON); | |
| 676 } | |
| 677 | |
| 678 bool acceptString(String string) { | |
| 679 if (lastToken == string) { | |
| 680 getToken(); | |
| 681 return true; | |
| 682 } | |
| 683 return false; | |
| 684 } | |
| 685 | |
| 686 void error(message) { | |
| 687 throw new MiniJsParserError(this, message); | |
| 688 } | |
| 689 | |
| 690 /// Returns either the name for the hole, or its integer position. | |
| 691 parseHash() { | |
| 692 String holeName = lastToken; | |
| 693 if (acceptCategory(ALPHA)) { | |
| 694 // Named hole. Example: 'function #funName() { ... }' | |
| 695 if (hasPositionalHoles) { | |
| 696 error('Holes must all be positional or named. $holeName'); | |
| 697 } | |
| 698 return holeName; | |
| 699 } else { | |
| 700 if (hasNamedHoles) { | |
| 701 error('Holes must all be positional or named. $holeName'); | |
| 702 } | |
| 703 int position = interpolatedValues.length; | |
| 704 return position; | |
| 705 } | |
| 706 } | |
| 707 | |
| 708 Expression parsePrimary() { | |
| 709 String last = lastToken; | |
| 710 if (acceptCategory(ALPHA)) { | |
| 711 if (last == "true") { | |
| 712 return new LiteralBool(true); | |
| 713 } else if (last == "false") { | |
| 714 return new LiteralBool(false); | |
| 715 } else if (last == "null") { | |
| 716 return new LiteralNull(); | |
| 717 } else if (last == "function") { | |
| 718 return parseFunctionExpression(); | |
| 719 } else if (last == "this") { | |
| 720 return new This(); | |
| 721 } else { | |
| 722 return new VariableUse(last); | |
| 723 } | |
| 724 } else if (acceptCategory(LPAREN)) { | |
| 725 Expression expression = parseExpression(); | |
| 726 expectCategory(RPAREN); | |
| 727 return expression; | |
| 728 } else if (acceptCategory(STRING)) { | |
| 729 return new LiteralString(last); | |
| 730 } else if (acceptCategory(NUMERIC)) { | |
| 731 return new LiteralNumber(last); | |
| 732 } else if (acceptCategory(LBRACE)) { | |
| 733 return parseObjectInitializer(); | |
| 734 } else if (acceptCategory(LSQUARE)) { | |
| 735 var values = <Expression>[]; | |
| 736 | |
| 737 while (true) { | |
| 738 if (acceptCategory(COMMA)) { | |
| 739 values.add(new ArrayHole()); | |
| 740 continue; | |
| 741 } | |
| 742 if (acceptCategory(RSQUARE)) break; | |
| 743 values.add(parseAssignment()); | |
| 744 if (acceptCategory(RSQUARE)) break; | |
| 745 expectCategory(COMMA); | |
| 746 } | |
| 747 return new ArrayInitializer(values); | |
| 748 } else if (last != null && last.startsWith("/")) { | |
| 749 String regexp = getDelimited(lastPosition); | |
| 750 getToken(); | |
| 751 String flags = lastToken; | |
| 752 if (!acceptCategory(ALPHA)) flags = ""; | |
| 753 Expression expression = new RegExpLiteral(regexp + flags); | |
| 754 return expression; | |
| 755 } else if (acceptCategory(HASH)) { | |
| 756 var nameOrPosition = parseHash(); | |
| 757 InterpolatedExpression expression = | |
| 758 new InterpolatedExpression(nameOrPosition); | |
| 759 interpolatedValues.add(expression); | |
| 760 return expression; | |
| 761 } else { | |
| 762 error("Expected primary expression"); | |
| 763 return null; | |
| 764 } | |
| 765 } | |
| 766 | |
| 767 Expression parseFunctionExpression() { | |
| 768 String last = lastToken; | |
| 769 if (acceptCategory(ALPHA)) { | |
| 770 String functionName = last; | |
| 771 return new NamedFunction(new VariableDeclaration(functionName), | |
| 772 parseFun()); | |
| 773 } | |
| 774 return parseFun(); | |
| 775 } | |
| 776 | |
| 777 Expression parseFun() { | |
| 778 List<Parameter> params = <Parameter>[]; | |
| 779 | |
| 780 expectCategory(LPAREN); | |
| 781 if (!acceptCategory(RPAREN)) { | |
| 782 for (;;) { | |
| 783 if (acceptCategory(HASH)) { | |
| 784 var nameOrPosition = parseHash(); | |
| 785 InterpolatedParameter parameter = | |
| 786 new InterpolatedParameter(nameOrPosition); | |
| 787 interpolatedValues.add(parameter); | |
| 788 params.add(parameter); | |
| 789 } else { | |
| 790 String argumentName = lastToken; | |
| 791 expectCategory(ALPHA); | |
| 792 params.add(new Parameter(argumentName)); | |
| 793 } | |
| 794 if (acceptCategory(COMMA)) continue; | |
| 795 expectCategory(RPAREN); | |
| 796 break; | |
| 797 } | |
| 798 } | |
| 799 AsyncModifier asyncModifier; | |
| 800 if (acceptString('async')) { | |
| 801 if (acceptString('*')) { | |
| 802 asyncModifier = const AsyncModifier.asyncStar(); | |
| 803 } else { | |
| 804 asyncModifier = const AsyncModifier.async(); | |
| 805 } | |
| 806 } else if (acceptString('sync')) { | |
| 807 if (!acceptString('*')) error("Only sync* is valid - sync is implied"); | |
| 808 asyncModifier = const AsyncModifier.syncStar(); | |
| 809 } else { | |
| 810 asyncModifier = const AsyncModifier.sync(); | |
| 811 } | |
| 812 expectCategory(LBRACE); | |
| 813 Block block = parseBlock(); | |
| 814 return new Fun(params, block, asyncModifier: asyncModifier); | |
| 815 } | |
| 816 | |
| 817 Expression parseObjectInitializer() { | |
| 818 List<Property> properties = <Property>[]; | |
| 819 for (;;) { | |
| 820 if (acceptCategory(RBRACE)) break; | |
| 821 // Limited subset: keys are identifiers, no 'get' or 'set' properties. | |
| 822 Literal propertyName; | |
| 823 String identifier = lastToken; | |
| 824 if (acceptCategory(ALPHA)) { | |
| 825 propertyName = new LiteralString('"$identifier"'); | |
| 826 } else if (acceptCategory(STRING)) { | |
| 827 propertyName = new LiteralString(identifier); | |
| 828 } else if (acceptCategory(SYMBOL)) { // e.g. void | |
| 829 propertyName = new LiteralString('"$identifier"'); | |
| 830 } else if (acceptCategory(HASH)) { | |
| 831 var nameOrPosition = parseHash(); | |
| 832 InterpolatedLiteral interpolatedLiteral = | |
| 833 new InterpolatedLiteral(nameOrPosition); | |
| 834 interpolatedValues.add(interpolatedLiteral); | |
| 835 propertyName = interpolatedLiteral; | |
| 836 } else { | |
| 837 error('Expected property name'); | |
| 838 } | |
| 839 expectCategory(COLON); | |
| 840 Expression value = parseAssignment(); | |
| 841 properties.add(new Property(propertyName, value)); | |
| 842 if (acceptCategory(RBRACE)) break; | |
| 843 expectCategory(COMMA); | |
| 844 } | |
| 845 return new ObjectInitializer(properties); | |
| 846 } | |
| 847 | |
| 848 Expression parseMember() { | |
| 849 Expression receiver = parsePrimary(); | |
| 850 while (true) { | |
| 851 if (acceptCategory(DOT)) { | |
| 852 receiver = getDotRhs(receiver); | |
| 853 } else if (acceptCategory(LSQUARE)) { | |
| 854 Expression inBraces = parseExpression(); | |
| 855 expectCategory(RSQUARE); | |
| 856 receiver = new PropertyAccess(receiver, inBraces); | |
| 857 } else { | |
| 858 break; | |
| 859 } | |
| 860 } | |
| 861 return receiver; | |
| 862 } | |
| 863 | |
| 864 Expression parseCall() { | |
| 865 bool constructor = acceptString("new"); | |
| 866 Expression receiver = parseMember(); | |
| 867 while (true) { | |
| 868 if (acceptCategory(LPAREN)) { | |
| 869 final arguments = <Expression>[]; | |
| 870 if (!acceptCategory(RPAREN)) { | |
| 871 while (true) { | |
| 872 Expression argument = parseAssignment(); | |
| 873 arguments.add(argument); | |
| 874 if (acceptCategory(RPAREN)) break; | |
| 875 expectCategory(COMMA); | |
| 876 } | |
| 877 } | |
| 878 receiver = constructor ? | |
| 879 new New(receiver, arguments) : | |
| 880 new Call(receiver, arguments); | |
| 881 constructor = false; | |
| 882 } else if (!constructor && acceptCategory(LSQUARE)) { | |
| 883 Expression inBraces = parseExpression(); | |
| 884 expectCategory(RSQUARE); | |
| 885 receiver = new PropertyAccess(receiver, inBraces); | |
| 886 } else if (!constructor && acceptCategory(DOT)) { | |
| 887 receiver = getDotRhs(receiver); | |
| 888 } else { | |
| 889 // JS allows new without (), but we don't. | |
| 890 if (constructor) error("Parentheses are required for new"); | |
| 891 break; | |
| 892 } | |
| 893 } | |
| 894 return receiver; | |
| 895 } | |
| 896 | |
| 897 Expression getDotRhs(Expression receiver) { | |
| 898 if (acceptCategory(HASH)) { | |
| 899 var nameOrPosition = parseHash(); | |
| 900 InterpolatedSelector property = new InterpolatedSelector(nameOrPosition); | |
| 901 interpolatedValues.add(property); | |
| 902 return new PropertyAccess(receiver, property); | |
| 903 } | |
| 904 String identifier = lastToken; | |
| 905 // In ES5 keywords like delete and continue are allowed as property | |
| 906 // names, and the IndexedDB API uses that, so we need to allow it here. | |
| 907 if (acceptCategory(SYMBOL)) { | |
| 908 if (!OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS.contains(identifier)) { | |
| 909 error("Expected alphanumeric identifier"); | |
| 910 } | |
| 911 } else { | |
| 912 expectCategory(ALPHA); | |
| 913 } | |
| 914 return new PropertyAccess.field(receiver, identifier); | |
| 915 } | |
| 916 | |
| 917 Expression parsePostfix() { | |
| 918 Expression expression = parseCall(); | |
| 919 String operator = lastToken; | |
| 920 // JavaScript grammar is: | |
| 921 // LeftHandSideExpression [no LineTerminator here] ++ | |
| 922 if (lastCategory == SYMBOL && | |
| 923 !skippedNewline && | |
| 924 (acceptString("++") || acceptString("--"))) { | |
| 925 return new Postfix(operator, expression); | |
| 926 } | |
| 927 // If we don't accept '++' or '--' due to skippedNewline a newline, no other | |
| 928 // part of the parser will accept the token and we will get an error at the | |
| 929 // whole expression level. | |
| 930 return expression; | |
| 931 } | |
| 932 | |
| 933 Expression parseUnaryHigh() { | |
| 934 String operator = lastToken; | |
| 935 if (lastCategory == SYMBOL && UNARY_OPERATORS.contains(operator) && | |
| 936 (acceptString("++") || acceptString("--") || acceptString('await'))) { | |
| 937 if (operator == "await") return new Await(parsePostfix()); | |
| 938 return new Prefix(operator, parsePostfix()); | |
| 939 } | |
| 940 return parsePostfix(); | |
| 941 } | |
| 942 | |
| 943 Expression parseUnaryLow() { | |
| 944 String operator = lastToken; | |
| 945 if (lastCategory == SYMBOL && UNARY_OPERATORS.contains(operator) && | |
| 946 operator != "++" && operator != "--") { | |
| 947 expectCategory(SYMBOL); | |
| 948 if (operator == "await") return new Await(parsePostfix()); | |
| 949 return new Prefix(operator, parseUnaryLow()); | |
| 950 } | |
| 951 return parseUnaryHigh(); | |
| 952 } | |
| 953 | |
| 954 Expression parseBinary(int maxPrecedence) { | |
| 955 Expression lhs = parseUnaryLow(); | |
| 956 int minPrecedence; | |
| 957 String lastSymbol; | |
| 958 Expression rhs; // This is null first time around. | |
| 959 while (true) { | |
| 960 String symbol = lastToken; | |
| 961 if (lastCategory != SYMBOL || | |
| 962 !BINARY_PRECEDENCE.containsKey(symbol) || | |
| 963 BINARY_PRECEDENCE[symbol] > maxPrecedence) { | |
| 964 break; | |
| 965 } | |
| 966 expectCategory(SYMBOL); | |
| 967 if (rhs == null || BINARY_PRECEDENCE[symbol] >= minPrecedence) { | |
| 968 if (rhs != null) lhs = new Binary(lastSymbol, lhs, rhs); | |
| 969 minPrecedence = BINARY_PRECEDENCE[symbol]; | |
| 970 rhs = parseUnaryLow(); | |
| 971 lastSymbol = symbol; | |
| 972 } else { | |
| 973 Expression higher = parseBinary(BINARY_PRECEDENCE[symbol]); | |
| 974 rhs = new Binary(symbol, rhs, higher); | |
| 975 } | |
| 976 } | |
| 977 if (rhs == null) return lhs; | |
| 978 return new Binary(lastSymbol, lhs, rhs); | |
| 979 } | |
| 980 | |
| 981 Expression parseConditional() { | |
| 982 Expression lhs = parseBinary(HIGHEST_PARSE_BINARY_PRECEDENCE); | |
| 983 if (!acceptCategory(QUERY)) return lhs; | |
| 984 Expression ifTrue = parseAssignment(); | |
| 985 expectCategory(COLON); | |
| 986 Expression ifFalse = parseAssignment(); | |
| 987 return new Conditional(lhs, ifTrue, ifFalse); | |
| 988 } | |
| 989 | |
| 990 | |
| 991 Expression parseAssignment() { | |
| 992 Expression lhs = parseConditional(); | |
| 993 String assignmentOperator = lastToken; | |
| 994 if (acceptCategory(ASSIGNMENT)) { | |
| 995 Expression rhs = parseAssignment(); | |
| 996 if (assignmentOperator == "=") { | |
| 997 return new Assignment(lhs, rhs); | |
| 998 } else { | |
| 999 // Handle +=, -=, etc. | |
| 1000 String operator = | |
| 1001 assignmentOperator.substring(0, assignmentOperator.length - 1); | |
| 1002 return new Assignment.compound(lhs, operator, rhs); | |
| 1003 } | |
| 1004 } | |
| 1005 return lhs; | |
| 1006 } | |
| 1007 | |
| 1008 Expression parseExpression() { | |
| 1009 Expression expression = parseAssignment(); | |
| 1010 while (acceptCategory(COMMA)) { | |
| 1011 Expression right = parseAssignment(); | |
| 1012 expression = new Binary(',', expression, right); | |
| 1013 } | |
| 1014 return expression; | |
| 1015 } | |
| 1016 | |
| 1017 VariableDeclarationList parseVariableDeclarationList() { | |
| 1018 String firstVariable = lastToken; | |
| 1019 expectCategory(ALPHA); | |
| 1020 return finishVariableDeclarationList(firstVariable); | |
| 1021 } | |
| 1022 | |
| 1023 VariableDeclarationList finishVariableDeclarationList(String firstVariable) { | |
| 1024 var initialization = []; | |
| 1025 | |
| 1026 void declare(String variable) { | |
| 1027 Expression initializer = null; | |
| 1028 if (acceptString("=")) { | |
| 1029 initializer = parseAssignment(); | |
| 1030 } | |
| 1031 var declaration = new VariableDeclaration(variable); | |
| 1032 initialization.add(new VariableInitialization(declaration, initializer)); | |
| 1033 } | |
| 1034 | |
| 1035 declare(firstVariable); | |
| 1036 while (acceptCategory(COMMA)) { | |
| 1037 String variable = lastToken; | |
| 1038 expectCategory(ALPHA); | |
| 1039 declare(variable); | |
| 1040 } | |
| 1041 return new VariableDeclarationList(initialization); | |
| 1042 } | |
| 1043 | |
| 1044 Expression parseVarDeclarationOrExpression() { | |
| 1045 if (acceptString("var")) { | |
| 1046 return parseVariableDeclarationList(); | |
| 1047 } else { | |
| 1048 return parseExpression(); | |
| 1049 } | |
| 1050 } | |
| 1051 | |
| 1052 Expression expression() { | |
| 1053 Expression expression = parseVarDeclarationOrExpression(); | |
| 1054 if (lastCategory != NONE || position != src.length) { | |
| 1055 error("Unparsed junk: ${categoryToString(lastCategory)}"); | |
| 1056 } | |
| 1057 return expression; | |
| 1058 } | |
| 1059 | |
| 1060 Statement statement() { | |
| 1061 Statement statement = parseStatement(); | |
| 1062 if (lastCategory != NONE || position != src.length) { | |
| 1063 error("Unparsed junk: ${categoryToString(lastCategory)}"); | |
| 1064 } | |
| 1065 // TODO(sra): interpolated capture here? | |
| 1066 return statement; | |
| 1067 } | |
| 1068 | |
| 1069 Block parseBlock() { | |
| 1070 List<Statement> statements = <Statement>[]; | |
| 1071 | |
| 1072 while (!acceptCategory(RBRACE)) { | |
| 1073 Statement statement = parseStatement(); | |
| 1074 statements.add(statement); | |
| 1075 } | |
| 1076 return new Block(statements); | |
| 1077 } | |
| 1078 | |
| 1079 Statement parseStatement() { | |
| 1080 if (acceptCategory(LBRACE)) return parseBlock(); | |
| 1081 | |
| 1082 if (acceptCategory(SEMICOLON)) return new EmptyStatement(); | |
| 1083 | |
| 1084 if (lastCategory == ALPHA) { | |
| 1085 if (acceptString('return')) return parseReturn(); | |
| 1086 | |
| 1087 if (acceptString('throw')) return parseThrow(); | |
| 1088 | |
| 1089 if (acceptString('break')) { | |
| 1090 return parseBreakOrContinue((label) => new Break(label)); | |
| 1091 } | |
| 1092 | |
| 1093 if (acceptString('continue')) { | |
| 1094 return parseBreakOrContinue((label) => new Continue(label)); | |
| 1095 } | |
| 1096 | |
| 1097 if (acceptString('if')) return parseIfThenElse(); | |
| 1098 | |
| 1099 if (acceptString('for')) return parseFor(); | |
| 1100 | |
| 1101 if (acceptString('function')) return parseFunctionDeclaration(); | |
| 1102 | |
| 1103 if (acceptString('try')) return parseTry(); | |
| 1104 | |
| 1105 if (acceptString('var')) { | |
| 1106 Expression declarations = parseVariableDeclarationList(); | |
| 1107 expectSemicolon(); | |
| 1108 return new ExpressionStatement(declarations); | |
| 1109 } | |
| 1110 | |
| 1111 if (acceptString('while')) return parseWhile(); | |
| 1112 | |
| 1113 if (acceptString('do')) return parseDo(); | |
| 1114 | |
| 1115 if (acceptString('switch')) return parseSwitch(); | |
| 1116 | |
| 1117 if (lastToken == 'case') error("Case outside switch."); | |
| 1118 | |
| 1119 if (lastToken == 'default') error("Default outside switch."); | |
| 1120 | |
| 1121 if (lastToken == 'yield') return parseYield(); | |
| 1122 | |
| 1123 if (lastToken == 'with') { | |
| 1124 error('Not implemented in mini parser'); | |
| 1125 } | |
| 1126 | |
| 1127 } | |
| 1128 | |
| 1129 bool checkForInterpolatedStatement = lastCategory == HASH; | |
| 1130 | |
| 1131 Expression expression = parseExpression(); | |
| 1132 | |
| 1133 if (expression is VariableUse && acceptCategory(COLON)) { | |
| 1134 return new LabeledStatement(expression.name, parseStatement()); | |
| 1135 } | |
| 1136 | |
| 1137 expectSemicolon(); | |
| 1138 | |
| 1139 if (checkForInterpolatedStatement) { | |
| 1140 // 'Promote' the interpolated expression `#;` to an interpolated | |
| 1141 // statement. | |
| 1142 if (expression is InterpolatedExpression) { | |
| 1143 assert(identical(interpolatedValues.last, expression)); | |
| 1144 InterpolatedStatement statement = | |
| 1145 new InterpolatedStatement(expression.nameOrPosition); | |
| 1146 interpolatedValues[interpolatedValues.length - 1] = statement; | |
| 1147 return statement; | |
| 1148 } | |
| 1149 } | |
| 1150 | |
| 1151 return new ExpressionStatement(expression); | |
| 1152 } | |
| 1153 | |
| 1154 Statement parseReturn() { | |
| 1155 if (acceptSemicolon()) return new Return(); | |
| 1156 Expression expression = parseExpression(); | |
| 1157 expectSemicolon(); | |
| 1158 return new Return(expression); | |
| 1159 } | |
| 1160 | |
| 1161 Statement parseYield() { | |
| 1162 bool hasStar = acceptString('*'); | |
| 1163 Expression expression = parseExpression(); | |
| 1164 expectSemicolon(); | |
| 1165 return new DartYield(expression, hasStar); | |
| 1166 } | |
| 1167 | |
| 1168 Statement parseThrow() { | |
| 1169 if (skippedNewline) error('throw expression must be on same line'); | |
| 1170 Expression expression = parseExpression(); | |
| 1171 expectSemicolon(); | |
| 1172 return new Throw(expression); | |
| 1173 } | |
| 1174 | |
| 1175 Statement parseBreakOrContinue(constructor) { | |
| 1176 var identifier = lastToken; | |
| 1177 if (!skippedNewline && acceptCategory(ALPHA)) { | |
| 1178 expectSemicolon(); | |
| 1179 return constructor(identifier); | |
| 1180 } | |
| 1181 expectSemicolon(); | |
| 1182 return constructor(null); | |
| 1183 } | |
| 1184 | |
| 1185 Statement parseIfThenElse() { | |
| 1186 expectCategory(LPAREN); | |
| 1187 Expression condition = parseExpression(); | |
| 1188 expectCategory(RPAREN); | |
| 1189 Statement thenStatement = parseStatement(); | |
| 1190 if (acceptString('else')) { | |
| 1191 // Resolves dangling else by binding 'else' to closest 'if'. | |
| 1192 Statement elseStatement = parseStatement(); | |
| 1193 return new If(condition, thenStatement, elseStatement); | |
| 1194 } else { | |
| 1195 return new If.noElse(condition, thenStatement); | |
| 1196 } | |
| 1197 } | |
| 1198 | |
| 1199 Statement parseFor() { | |
| 1200 // For-init-condition-increment style loops are fully supported. | |
| 1201 // | |
| 1202 // Only one for-in variant is currently implemented: | |
| 1203 // | |
| 1204 // for (var variable in Expression) Statement | |
| 1205 // | |
| 1206 Statement finishFor(Expression init) { | |
| 1207 Expression condition = null; | |
| 1208 if (!acceptCategory(SEMICOLON)) { | |
| 1209 condition = parseExpression(); | |
| 1210 expectCategory(SEMICOLON); | |
| 1211 } | |
| 1212 Expression update = null; | |
| 1213 if (!acceptCategory(RPAREN)) { | |
| 1214 update = parseExpression(); | |
| 1215 expectCategory(RPAREN); | |
| 1216 } | |
| 1217 Statement body = parseStatement(); | |
| 1218 return new For(init, condition, update, body); | |
| 1219 } | |
| 1220 | |
| 1221 expectCategory(LPAREN); | |
| 1222 if (acceptCategory(SEMICOLON)) { | |
| 1223 return finishFor(null); | |
| 1224 } | |
| 1225 | |
| 1226 if (acceptString('var')) { | |
| 1227 String identifier = lastToken; | |
| 1228 expectCategory(ALPHA); | |
| 1229 if (acceptString('in')) { | |
| 1230 Expression objectExpression = parseExpression(); | |
| 1231 expectCategory(RPAREN); | |
| 1232 Statement body = parseStatement(); | |
| 1233 return new ForIn( | |
| 1234 new VariableDeclarationList([ | |
| 1235 new VariableInitialization( | |
| 1236 new VariableDeclaration(identifier), null)]), | |
| 1237 objectExpression, | |
| 1238 body); | |
| 1239 } | |
| 1240 Expression declarations = finishVariableDeclarationList(identifier); | |
| 1241 expectCategory(SEMICOLON); | |
| 1242 return finishFor(declarations); | |
| 1243 } | |
| 1244 | |
| 1245 Expression init = parseExpression(); | |
| 1246 expectCategory(SEMICOLON); | |
| 1247 return finishFor(init); | |
| 1248 } | |
| 1249 | |
| 1250 Statement parseFunctionDeclaration() { | |
| 1251 String name = lastToken; | |
| 1252 expectCategory(ALPHA); | |
| 1253 Expression fun = parseFun(); | |
| 1254 return new FunctionDeclaration(new VariableDeclaration(name), fun); | |
| 1255 } | |
| 1256 | |
| 1257 Statement parseTry() { | |
| 1258 expectCategory(LBRACE); | |
| 1259 Block body = parseBlock(); | |
| 1260 String token = lastToken; | |
| 1261 Catch catchPart = null; | |
| 1262 if (acceptString('catch')) catchPart = parseCatch(); | |
| 1263 Block finallyPart = null; | |
| 1264 if (acceptString('finally')) { | |
| 1265 expectCategory(LBRACE); | |
| 1266 finallyPart = parseBlock(); | |
| 1267 } else { | |
| 1268 if (catchPart == null) error("expected 'finally'"); | |
| 1269 } | |
| 1270 return new Try(body, catchPart, finallyPart); | |
| 1271 } | |
| 1272 | |
| 1273 SwitchClause parseSwitchClause() { | |
| 1274 Expression expression = null; | |
| 1275 if (acceptString('case')) { | |
| 1276 expression = parseExpression(); | |
| 1277 expectCategory(COLON); | |
| 1278 } else { | |
| 1279 if (!acceptString('default')) { | |
| 1280 error('expected case or default'); | |
| 1281 } | |
| 1282 expectCategory(COLON); | |
| 1283 } | |
| 1284 List statements = new List<Statement>(); | |
| 1285 while (lastCategory != RBRACE && | |
| 1286 lastToken != 'case' && | |
| 1287 lastToken != 'default') { | |
| 1288 statements.add(parseStatement()); | |
| 1289 } | |
| 1290 return expression == null | |
| 1291 ? new Default(new Block(statements)) | |
| 1292 : new Case(expression, new Block(statements)); | |
| 1293 } | |
| 1294 | |
| 1295 Statement parseWhile() { | |
| 1296 expectCategory(LPAREN); | |
| 1297 Expression condition = parseExpression(); | |
| 1298 expectCategory(RPAREN); | |
| 1299 Statement body = parseStatement(); | |
| 1300 return new While(condition, body); | |
| 1301 } | |
| 1302 | |
| 1303 Statement parseDo() { | |
| 1304 Statement body = parseStatement(); | |
| 1305 if (lastToken != "while") error("Missing while after do body."); | |
| 1306 getToken(); | |
| 1307 expectCategory(LPAREN); | |
| 1308 Expression condition = parseExpression(); | |
| 1309 expectCategory(RPAREN); | |
| 1310 expectSemicolon(); | |
| 1311 return new Do(body, condition); | |
| 1312 } | |
| 1313 | |
| 1314 Statement parseSwitch() { | |
| 1315 expectCategory(LPAREN); | |
| 1316 Expression key = parseExpression(); | |
| 1317 expectCategory(RPAREN); | |
| 1318 expectCategory(LBRACE); | |
| 1319 List<SwitchClause> clauses = new List<SwitchClause>(); | |
| 1320 while(lastCategory != RBRACE) { | |
| 1321 clauses.add(parseSwitchClause()); | |
| 1322 } | |
| 1323 expectCategory(RBRACE); | |
| 1324 return new Switch(key, clauses); | |
| 1325 } | |
| 1326 | |
| 1327 Catch parseCatch() { | |
| 1328 expectCategory(LPAREN); | |
| 1329 String identifier = lastToken; | |
| 1330 expectCategory(ALPHA); | |
| 1331 expectCategory(RPAREN); | |
| 1332 expectCategory(LBRACE); | |
| 1333 Block body = parseBlock(); | |
| 1334 return new Catch(new VariableDeclaration(identifier), body); | |
| 1335 } | |
| 1336 } | |
| OLD | NEW |