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 Identifier('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 Identifier 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 Identifier) 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, [String quote = '"']) { | |
299 // Start by escaping the backslashes. | |
300 String escaped = value.replaceAll('\\', '\\\\'); | |
301 | |
302 | |
303 // Replace $ in template strings: | |
304 // http://www.ecma-international.org/ecma-262/6.0/#sec-template-literal-lexi
cal-components | |
305 var quoteReplace = quote == '`' ? r'`$' : quote; | |
306 | |
307 // http://www.ecma-international.org/ecma-262/6.0/#sec-literals-string-liter
als | |
308 // > All code points may appear literally in a string literal except for the | |
309 // > closing quote code points, U+005C (REVERSE SOLIDUS), | |
310 // > U+000D (CARRIAGE RETURN), U+2028 (LINE SEPARATOR), | |
311 // > U+2029 (PARAGRAPH SEPARATOR), and U+000A (LINE FEED). | |
312 var re = new RegExp('[\n\r$quoteReplace\b\f\t\v\u2028\u2029]'); | |
313 escaped = escaped.replaceAllMapped(re, (m) { | |
314 switch (m.group(0)) { | |
315 case "\n" : return r"\n"; | |
316 case "\r" : return r"\r"; | |
317 case "\u2028": return r"\u2028"; | |
318 case "\u2029": return r"\u2029"; | |
319 // Quotes and $ are only replaced if they conflict with the containing | |
320 // quote, see regex above. | |
321 case '"': return r'\"'; | |
322 case "'": return r"\'"; | |
323 case "`": return r"\`"; | |
324 case r"$": return r"\$"; | |
325 // TODO(jmesserly): these don't need to be escaped for correctness, | |
326 // but they are conventionally escaped. | |
327 case "\b": return r"\b"; | |
328 case "\t": return r"\t"; | |
329 case "\f": return r"\f"; | |
330 case "\v": return r"\v"; | |
331 } | |
332 }); | |
333 LiteralString result = new LiteralString('$quote$escaped$quote'); | |
334 // We don't escape quotes of a different style under the assumption that the | |
335 // string is wrapped into quotes. Verify that assumption. | |
336 assert(result.value.codeUnitAt(0) == quote.codeUnitAt(0)); | |
337 return result; | |
338 } | |
339 | |
340 /// Creates a literal js string from [value]. | |
341 /// | |
342 /// Note that this function only puts quotes around [value]. It does not do | |
343 /// any escaping, so use only when you can guarantee that [value] does not | |
344 /// contain newlines or backslashes. For escaping the string use | |
345 /// [escapedString]. | |
346 LiteralString string(String value, [String quote = '"']) => | |
347 new LiteralString('$quote$value$quote'); | |
348 | |
349 LiteralNumber number(num value) => new LiteralNumber('$value'); | |
350 | |
351 LiteralBool boolean(bool value) => new LiteralBool(value); | |
352 | |
353 ArrayInitializer numArray(Iterable<int> list) => | |
354 new ArrayInitializer(list.map(number).toList()); | |
355 | |
356 ArrayInitializer stringArray(Iterable<String> list) => | |
357 new ArrayInitializer(list.map(string).toList()); | |
358 | |
359 Comment comment(String text) => new Comment(text); | |
360 CommentExpression commentExpression(String text, Expression expression) => | |
361 new CommentExpression(text, expression); | |
362 | |
363 Call propertyCall(Expression receiver, | |
364 String fieldName, | |
365 List<Expression> arguments) { | |
366 return new Call(new PropertyAccess.field(receiver, fieldName), arguments); | |
367 } | |
368 } | |
369 | |
370 LiteralString string(String value) => js.string(value); | |
371 LiteralNumber number(num value) => js.number(value); | |
372 ArrayInitializer numArray(Iterable<int> list) => js.numArray(list); | |
373 ArrayInitializer stringArray(Iterable<String> list) => js.stringArray(list); | |
374 Call propertyCall(Expression receiver, | |
375 String fieldName, | |
376 List<Expression> arguments) { | |
377 return js.propertyCall(receiver, fieldName, arguments); | |
378 } | |
379 | |
380 class MiniJsParserError { | |
381 MiniJsParserError(this.parser, this.message) { } | |
382 | |
383 final MiniJsParser parser; | |
384 final String message; | |
385 | |
386 String toString() { | |
387 int pos = parser.lastPosition; | |
388 | |
389 // Discard lines following the line containing lastPosition. | |
390 String src = parser.src; | |
391 int newlinePos = src.indexOf('\n', pos); | |
392 if (newlinePos >= pos) src = src.substring(0, newlinePos); | |
393 | |
394 // Extract the prefix of the error line before lastPosition. | |
395 String line = src; | |
396 int lastLineStart = line.lastIndexOf('\n'); | |
397 if (lastLineStart >= 0) line = line.substring(lastLineStart + 1); | |
398 String prefix = line.substring(0, pos - (src.length - line.length)); | |
399 | |
400 // Replace non-tabs with spaces, giving a print indent that matches the text | |
401 // for tabbing. | |
402 String spaces = prefix.replaceAll(new RegExp(r'[^\t]'), ' '); | |
403 return 'Error in MiniJsParser:\n${src}\n$spaces^\n$spaces$message\n'; | |
404 } | |
405 } | |
406 | |
407 /// Mini JavaScript parser for tiny snippets of code that we want to make into | |
408 /// AST nodes. Handles: | |
409 /// * identifiers. | |
410 /// * dot access. | |
411 /// * method calls. | |
412 /// * [] access. | |
413 /// * array, string, regexp, boolean, null and numeric literals. | |
414 /// * most operators. | |
415 /// * brackets. | |
416 /// * var declarations. | |
417 /// * operator precedence. | |
418 /// * anonymous funtions and named function expressions and declarations. | |
419 /// Notable things it can't do yet include: | |
420 /// * some statements are still missing (do-while, while, switch). | |
421 /// | |
422 /// It's a fairly standard recursive descent parser. | |
423 /// | |
424 /// Literal strings are passed through to the final JS source code unchanged, | |
425 /// including the choice of surrounding quotes, so if you parse | |
426 /// r'var x = "foo\n\"bar\""' you will end up with | |
427 /// var x = "foo\n\"bar\"" in the final program. \x and \u escapes are not | |
428 /// allowed in string and regexp literals because the machinery for checking | |
429 /// their correctness is rather involved. | |
430 class MiniJsParser { | |
431 MiniJsParser(this.src) | |
432 : lastCategory = NONE, | |
433 lastToken = null, | |
434 lastPosition = 0, | |
435 position = 0 { | |
436 getToken(); | |
437 } | |
438 | |
439 int lastCategory = NONE; | |
440 String lastToken = null; | |
441 int lastPosition = 0; | |
442 int position = 0; | |
443 bool skippedNewline = false; // skipped newline in last getToken? | |
444 final String src; | |
445 | |
446 final List<InterpolatedNode> interpolatedValues = <InterpolatedNode>[]; | |
447 bool get hasNamedHoles => | |
448 interpolatedValues.isNotEmpty && interpolatedValues.first.isNamed; | |
449 bool get hasPositionalHoles => | |
450 interpolatedValues.isNotEmpty && interpolatedValues.first.isPositional; | |
451 | |
452 static const NONE = -1; | |
453 static const ALPHA = 0; | |
454 static const NUMERIC = 1; | |
455 static const STRING = 2; | |
456 static const SYMBOL = 3; | |
457 static const ASSIGNMENT = 4; | |
458 static const DOT = 5; | |
459 static const LPAREN = 6; | |
460 static const RPAREN = 7; | |
461 static const LBRACE = 8; | |
462 static const RBRACE = 9; | |
463 static const LSQUARE = 10; | |
464 static const RSQUARE = 11; | |
465 static const COMMA = 12; | |
466 static const QUERY = 13; | |
467 static const COLON = 14; | |
468 static const SEMICOLON = 15; | |
469 static const ARROW = 16; | |
470 static const ELLIPSIS = 17; | |
471 static const HASH = 18; | |
472 static const WHITESPACE = 19; | |
473 static const OTHER = 20; | |
474 | |
475 // Make sure that ]] is two symbols. | |
476 // TODO(jmesserly): => and ... are not single char tokens, should we change | |
477 // their numbers? It shouldn't matter because this is only called on values | |
478 // from the [CATEGORIES] table. | |
479 bool singleCharCategory(int category) => category > DOT; | |
480 | |
481 static String categoryToString(int cat) { | |
482 switch (cat) { | |
483 case NONE: return "NONE"; | |
484 case ALPHA: return "ALPHA"; | |
485 case NUMERIC: return "NUMERIC"; | |
486 case SYMBOL: return "SYMBOL"; | |
487 case ASSIGNMENT: return "ASSIGNMENT"; | |
488 case DOT: return "DOT"; | |
489 case LPAREN: return "LPAREN"; | |
490 case RPAREN: return "RPAREN"; | |
491 case LBRACE: return "LBRACE"; | |
492 case RBRACE: return "RBRACE"; | |
493 case LSQUARE: return "LSQUARE"; | |
494 case RSQUARE: return "RSQUARE"; | |
495 case STRING: return "STRING"; | |
496 case COMMA: return "COMMA"; | |
497 case QUERY: return "QUERY"; | |
498 case COLON: return "COLON"; | |
499 case SEMICOLON: return "SEMICOLON"; | |
500 case ARROW: return "ARROW"; | |
501 case ELLIPSIS: return "ELLIPSIS"; | |
502 case HASH: return "HASH"; | |
503 case WHITESPACE: return "WHITESPACE"; | |
504 case OTHER: return "OTHER"; | |
505 } | |
506 return "Unknown: $cat"; | |
507 } | |
508 | |
509 static const CATEGORIES = const <int>[ | |
510 OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, // 0-7 | |
511 OTHER, WHITESPACE, WHITESPACE, OTHER, OTHER, WHITESPACE, // 8-13 | |
512 OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, // 14-21 | |
513 OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, // 22-29 | |
514 OTHER, OTHER, WHITESPACE, // 30-32 | |
515 SYMBOL, OTHER, HASH, ALPHA, SYMBOL, SYMBOL, OTHER, // !"#$%&´ | |
516 LPAREN, RPAREN, SYMBOL, SYMBOL, COMMA, SYMBOL, DOT, SYMBOL, // ()*+,-./ | |
517 NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, // 01234 | |
518 NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, // 56789 | |
519 COLON, SEMICOLON, SYMBOL, SYMBOL, SYMBOL, QUERY, OTHER, // :;<=>?@ | |
520 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // ABCDEFGH | |
521 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // IJKLMNOP | |
522 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // QRSTUVWX | |
523 ALPHA, ALPHA, LSQUARE, OTHER, RSQUARE, SYMBOL, ALPHA, OTHER, // YZ[\]^_' | |
524 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // abcdefgh | |
525 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // ijklmnop | |
526 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // qrstuvwx | |
527 ALPHA, ALPHA, LBRACE, SYMBOL, RBRACE, SYMBOL]; // yz{|}~ | |
528 | |
529 // This must be a >= the highest precedence number handled by parseBinary. | |
530 static var HIGHEST_PARSE_BINARY_PRECEDENCE = 16; | |
531 static bool isAssignment(String symbol) => BINARY_PRECEDENCE[symbol] == 17; | |
532 | |
533 // From https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operator
s/Operator_Precedence | |
534 static final BINARY_PRECEDENCE = { | |
535 '+=': 17, '-=': 17, '*=': 17, '/=': 17, '%=': 17, '^=': 17, '|=': 17, | |
536 '&=': 17, '<<=': 17, '>>=': 17, '>>>=': 17, '=': 17, | |
537 '||': 14, | |
538 '&&': 13, | |
539 '|': 12, | |
540 '^': 11, | |
541 '&': 10, | |
542 '!=': 9, '==': 9, '!==': 9, '===': 9, | |
543 '<': 8, '<=': 8, '>=': 8, '>': 8, 'in': 8, 'instanceof': 8, | |
544 '<<': 7, '>>': 7, '>>>': 7, | |
545 '+': 6, '-': 6, | |
546 '*': 5, '/': 5, '%': 5 | |
547 }; | |
548 static final UNARY_OPERATORS = | |
549 ['++', '--', '+', '-', '~', '!', 'typeof', 'void', 'delete', 'await'] | |
550 .toSet(); | |
551 | |
552 static final ARROW_TOKEN = '=>'; | |
553 static final ELLIPSIS_TOKEN = '...'; | |
554 | |
555 static final OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS = | |
556 ['typeof', 'void', 'delete', 'in', 'instanceof', 'await'].toSet(); | |
557 | |
558 static int category(int code) { | |
559 if (code >= CATEGORIES.length) return OTHER; | |
560 return CATEGORIES[code]; | |
561 } | |
562 | |
563 String getDelimited(int startPosition) { | |
564 position = startPosition; | |
565 int delimiter = src.codeUnitAt(startPosition); | |
566 int currentCode; | |
567 do { | |
568 position++; | |
569 if (position >= src.length) error("Unterminated literal"); | |
570 currentCode = src.codeUnitAt(position); | |
571 if (currentCode == charCodes.$LF) error("Unterminated literal"); | |
572 if (currentCode == charCodes.$BACKSLASH) { | |
573 if (++position >= src.length) error("Unterminated literal"); | |
574 int escaped = src.codeUnitAt(position); | |
575 if (escaped == charCodes.$x || escaped == charCodes.$X || | |
576 escaped == charCodes.$u || escaped == charCodes.$U || | |
577 category(escaped) == NUMERIC) { | |
578 error('Numeric and hex escapes are not allowed in literals'); | |
579 } | |
580 } | |
581 } while (currentCode != delimiter); | |
582 position++; | |
583 return src.substring(lastPosition, position); | |
584 } | |
585 | |
586 void getToken() { | |
587 skippedNewline = false; | |
588 for (;;) { | |
589 if (position >= src.length) break; | |
590 int code = src.codeUnitAt(position); | |
591 // Skip '//' and '/*' style comments. | |
592 if (code == charCodes.$SLASH && | |
593 position + 1 < src.length) { | |
594 if (src.codeUnitAt(position + 1) == charCodes.$SLASH) { | |
595 int nextPosition = src.indexOf('\n', position); | |
596 if (nextPosition == -1) nextPosition = src.length; | |
597 position = nextPosition; | |
598 continue; | |
599 } else if (src.codeUnitAt(position + 1) == charCodes.$STAR) { | |
600 int nextPosition = src.indexOf('*/', position + 2); | |
601 if (nextPosition == -1) error('Unterminated comment'); | |
602 position = nextPosition + 2; | |
603 continue; | |
604 } | |
605 } | |
606 if (category(code) != WHITESPACE) break; | |
607 if (code == charCodes.$LF) skippedNewline = true; | |
608 ++position; | |
609 } | |
610 | |
611 if (position == src.length) { | |
612 lastCategory = NONE; | |
613 lastToken = null; | |
614 lastPosition = position; | |
615 return; | |
616 } | |
617 int code = src.codeUnitAt(position); | |
618 lastPosition = position; | |
619 if (code == charCodes.$SQ || code == charCodes.$DQ) { | |
620 // String literal. | |
621 lastCategory = STRING; | |
622 lastToken = getDelimited(position); | |
623 } else if (code == charCodes.$0 && | |
624 position + 2 < src.length && | |
625 src.codeUnitAt(position + 1) == charCodes.$x) { | |
626 // Hex literal. | |
627 for (position += 2; position < src.length; position++) { | |
628 int cat = category(src.codeUnitAt(position)); | |
629 if (cat != NUMERIC && cat != ALPHA) break; | |
630 } | |
631 lastCategory = NUMERIC; | |
632 lastToken = src.substring(lastPosition, position); | |
633 int.parse(lastToken, onError: (_) { | |
634 error("Unparseable number"); | |
635 }); | |
636 } else if (code == charCodes.$SLASH) { | |
637 // Tokens that start with / are special due to regexp literals. | |
638 lastCategory = SYMBOL; | |
639 position++; | |
640 if (position < src.length && src.codeUnitAt(position) == charCodes.$EQ) { | |
641 position++; | |
642 } | |
643 lastToken = src.substring(lastPosition, position); | |
644 } else { | |
645 // All other tokens handled here. | |
646 int cat = category(src.codeUnitAt(position)); | |
647 int newCat; | |
648 do { | |
649 position++; | |
650 if (position == src.length) break; | |
651 int code = src.codeUnitAt(position); | |
652 // Special code to disallow ! and / in non-first position in token, so | |
653 // that !! parses as two tokens and != parses as one, while =/ parses | |
654 // as a an equals token followed by a regexp literal start. | |
655 newCat = (code == charCodes.$BANG || code == charCodes.$SLASH) | |
656 ? NONE | |
657 : category(code); | |
658 } while (!singleCharCategory(cat) && | |
659 (cat == newCat || | |
660 (cat == ALPHA && newCat == NUMERIC) || // eg. level42. | |
661 (cat == NUMERIC && newCat == DOT))); // eg. 3.1415 | |
662 lastCategory = cat; | |
663 lastToken = src.substring(lastPosition, position); | |
664 if (cat == NUMERIC) { | |
665 double.parse(lastToken, (_) { | |
666 error("Unparseable number"); | |
667 }); | |
668 } else if (cat == DOT && lastToken.length > 1) { | |
669 if (lastToken == ELLIPSIS_TOKEN) { | |
670 lastCategory = ELLIPSIS; | |
671 } else { | |
672 error("Unknown operator"); | |
673 } | |
674 } else if (cat == SYMBOL) { | |
675 if (lastToken == ARROW_TOKEN) { | |
676 lastCategory = ARROW; | |
677 } else { | |
678 int binaryPrecendence = BINARY_PRECEDENCE[lastToken]; | |
679 if (binaryPrecendence == null && !UNARY_OPERATORS.contains(lastToken))
{ | |
680 error("Unknown operator"); | |
681 } | |
682 if (isAssignment(lastToken)) lastCategory = ASSIGNMENT; | |
683 } | |
684 } else if (cat == ALPHA) { | |
685 if (OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS.contains(lastToken)) { | |
686 lastCategory = SYMBOL; | |
687 } | |
688 } | |
689 } | |
690 } | |
691 | |
692 void expectCategory(int cat) { | |
693 if (cat != lastCategory) error("Expected ${categoryToString(cat)}"); | |
694 getToken(); | |
695 } | |
696 | |
697 bool acceptCategory(int cat) { | |
698 if (cat == lastCategory) { | |
699 getToken(); | |
700 return true; | |
701 } | |
702 return false; | |
703 } | |
704 | |
705 void expectSemicolon() { | |
706 if (acceptSemicolon()) return; | |
707 error('Expected SEMICOLON'); | |
708 } | |
709 | |
710 bool acceptSemicolon() { | |
711 // Accept semicolon or automatically inserted semicolon before close brace. | |
712 // Miniparser forbids other kinds of semicolon insertion. | |
713 if (RBRACE == lastCategory) return true; | |
714 if (NONE == lastCategory) return true; // end of input | |
715 if (skippedNewline) { | |
716 error('No automatic semicolon insertion at preceding newline'); | |
717 } | |
718 return acceptCategory(SEMICOLON); | |
719 } | |
720 | |
721 bool acceptString(String string) { | |
722 if (lastToken == string) { | |
723 getToken(); | |
724 return true; | |
725 } | |
726 return false; | |
727 } | |
728 | |
729 void error(message) { | |
730 throw new MiniJsParserError(this, message); | |
731 } | |
732 | |
733 /// Returns either the name for the hole, or its integer position. | |
734 parseHash() { | |
735 String holeName = lastToken; | |
736 if (acceptCategory(ALPHA)) { | |
737 // Named hole. Example: 'function #funName() { ... }' | |
738 if (hasPositionalHoles) { | |
739 error('Holes must all be positional or named. $holeName'); | |
740 } | |
741 return holeName; | |
742 } else { | |
743 if (hasNamedHoles) { | |
744 error('Holes must all be positional or named. $holeName'); | |
745 } | |
746 int position = interpolatedValues.length; | |
747 return position; | |
748 } | |
749 } | |
750 | |
751 Expression parsePrimary() { | |
752 String last = lastToken; | |
753 if (acceptCategory(ALPHA)) { | |
754 if (last == "true") { | |
755 return new LiteralBool(true); | |
756 } else if (last == "false") { | |
757 return new LiteralBool(false); | |
758 } else if (last == "null") { | |
759 return new LiteralNull(); | |
760 } else if (last == "function") { | |
761 return parseFunctionExpression(); | |
762 } else if (last == "this") { | |
763 return new This(); | |
764 } else if (last == "super") { | |
765 return new Super(); | |
766 } else if (last == "class") { | |
767 return parseClass(); | |
768 } else { | |
769 return new Identifier(last); | |
770 } | |
771 } else if (acceptCategory(LPAREN)) { | |
772 return parseExpressionOrArrowFunction(); | |
773 } else if (acceptCategory(STRING)) { | |
774 return new LiteralString(last); | |
775 } else if (acceptCategory(NUMERIC)) { | |
776 return new LiteralNumber(last); | |
777 } else if (acceptCategory(LBRACE)) { | |
778 return parseObjectInitializer(); | |
779 } else if (acceptCategory(LSQUARE)) { | |
780 var values = <Expression>[]; | |
781 | |
782 while (true) { | |
783 if (acceptCategory(COMMA)) { | |
784 values.add(new ArrayHole()); | |
785 continue; | |
786 } | |
787 if (acceptCategory(RSQUARE)) break; | |
788 values.add(parseAssignment()); | |
789 if (acceptCategory(RSQUARE)) break; | |
790 expectCategory(COMMA); | |
791 } | |
792 return new ArrayInitializer(values); | |
793 } else if (last != null && last.startsWith("/")) { | |
794 String regexp = getDelimited(lastPosition); | |
795 getToken(); | |
796 String flags = lastToken; | |
797 if (!acceptCategory(ALPHA)) flags = ""; | |
798 Expression expression = new RegExpLiteral(regexp + flags); | |
799 return expression; | |
800 } else if (acceptCategory(HASH)) { | |
801 return parseInterpolatedExpression(); | |
802 } else { | |
803 error("Expected primary expression"); | |
804 return null; | |
805 } | |
806 } | |
807 | |
808 InterpolatedExpression parseInterpolatedExpression() { | |
809 var expression = new InterpolatedExpression(parseHash()); | |
810 interpolatedValues.add(expression); | |
811 return expression; | |
812 } | |
813 | |
814 InterpolatedIdentifier parseInterpolatedIdentifier() { | |
815 var id = new InterpolatedIdentifier(parseHash()); | |
816 interpolatedValues.add(id); | |
817 return id; | |
818 } | |
819 | |
820 Identifier parseIdentifier() { | |
821 if (acceptCategory(HASH)) { | |
822 return parseInterpolatedIdentifier(); | |
823 } else { | |
824 var id = new Identifier(lastToken); | |
825 expectCategory(ALPHA); | |
826 return id; | |
827 } | |
828 } | |
829 | |
830 /** | |
831 * CoverParenthesizedExpressionAndArrowParameterList[Yield] : | |
832 * ( Expression ) | |
833 * ( ) | |
834 * ( ... BindingIdentifier ) | |
835 * ( Expression , ... BindingIdentifier ) | |
836 */ | |
837 Expression parseExpressionOrArrowFunction() { | |
838 if (acceptCategory(RPAREN)) { | |
839 expectCategory(ARROW); | |
840 return parseArrowFunctionBody(<Parameter>[]); | |
841 } | |
842 if (acceptCategory(ELLIPSIS)) { | |
843 var params = <Parameter>[new RestParameter(parseParameter())]; | |
844 expectCategory(RPAREN); | |
845 expectCategory(ARROW); | |
846 return parseArrowFunctionBody(params); | |
847 } | |
848 Expression expression = parseAssignment(); | |
849 while (acceptCategory(COMMA)) { | |
850 if (acceptCategory(ELLIPSIS)) { | |
851 var params = <Parameter>[]; | |
852 _expressionToParameterList(expression, params); | |
853 params.add(new RestParameter(parseParameter())); | |
854 expectCategory(RPAREN); | |
855 expectCategory(ARROW); | |
856 return parseArrowFunctionBody(params); | |
857 } | |
858 Expression right = parseAssignment(); | |
859 expression = new Binary(',', expression, right); | |
860 } | |
861 expectCategory(RPAREN); | |
862 if (acceptCategory(ARROW)) { | |
863 var params = <Parameter>[]; | |
864 _expressionToParameterList(expression, params); | |
865 return parseArrowFunctionBody(params); | |
866 } | |
867 return expression; | |
868 } | |
869 | |
870 /** | |
871 * Converts a parenthesized expression into a list of parameters, issuing an | |
872 * error if the conversion fails. | |
873 */ | |
874 void _expressionToParameterList(Expression node, List<Parameter> params) { | |
875 if (node is Identifier) { | |
876 params.add(node); | |
877 } else if (node is Binary && node.op == ',') { | |
878 // TODO(jmesserly): this will allow illegal parens, such as | |
879 // `((a, b), (c, d))`. Fixing it on the left side needs an explicit | |
880 // ParenthesizedExpression node, so we can distinguish | |
881 // `((a, b), c)` from `(a, b, c)`. | |
882 _expressionToParameterList(node.left, params); | |
883 _expressionToParameterList(node.right, params); | |
884 } else if (node is InterpolatedExpression) { | |
885 params.add(new InterpolatedParameter(node.nameOrPosition)); | |
886 } else { | |
887 error("Expected arrow function parameter list"); | |
888 } | |
889 } | |
890 | |
891 Expression parseArrowFunctionBody(List<Parameter> params) { | |
892 Node body; | |
893 if (acceptCategory(LBRACE)) { | |
894 body = parseBlock(); | |
895 } else { | |
896 body = parseAssignment(); | |
897 } | |
898 return new ArrowFun(params, body); | |
899 } | |
900 | |
901 Expression parseFunctionExpression() { | |
902 String last = lastToken; | |
903 if (acceptCategory(ALPHA)) { | |
904 String functionName = last; | |
905 return new NamedFunction(new Identifier(functionName), | |
906 parseFun()); | |
907 } | |
908 return parseFun(); | |
909 } | |
910 | |
911 Expression parseFun() { | |
912 List<Parameter> params = <Parameter>[]; | |
913 | |
914 expectCategory(LPAREN); | |
915 if (!acceptCategory(RPAREN)) { | |
916 for (;;) { | |
917 if (acceptCategory(ELLIPSIS)) { | |
918 params.add(new RestParameter(parseParameter())); | |
919 expectCategory(RPAREN); | |
920 break; | |
921 } | |
922 | |
923 params.add(parseParameter()); | |
924 if (!acceptCategory(COMMA)) { | |
925 expectCategory(RPAREN); | |
926 break; | |
927 } | |
928 } | |
929 } | |
930 AsyncModifier asyncModifier; | |
931 if (acceptString('async')) { | |
932 if (acceptString('*')) { | |
933 asyncModifier = const AsyncModifier.asyncStar(); | |
934 } else { | |
935 asyncModifier = const AsyncModifier.async(); | |
936 } | |
937 } else if (acceptString('sync')) { | |
938 if (!acceptString('*')) error("Only sync* is valid - sync is implied"); | |
939 asyncModifier = const AsyncModifier.syncStar(); | |
940 } else { | |
941 asyncModifier = const AsyncModifier.sync(); | |
942 } | |
943 expectCategory(LBRACE); | |
944 Block block = parseBlock(); | |
945 return new Fun(params, block, asyncModifier: asyncModifier); | |
946 } | |
947 | |
948 /** Parse parameter name or interpolated parameter. */ | |
949 Identifier parseParameter() { | |
950 if (acceptCategory(HASH)) { | |
951 var nameOrPosition = parseHash(); | |
952 var parameter = new InterpolatedParameter(nameOrPosition); | |
953 interpolatedValues.add(parameter); | |
954 return parameter; | |
955 } else { | |
956 // TODO(jmesserly): validate this is not a keyword | |
957 String argumentName = lastToken; | |
958 expectCategory(ALPHA); | |
959 return new Identifier(argumentName); | |
960 } | |
961 } | |
962 | |
963 Expression parseObjectInitializer() { | |
964 List<Property> properties = <Property>[]; | |
965 for (;;) { | |
966 if (acceptCategory(RBRACE)) break; | |
967 // Limited subset of ES6 object initializers. | |
968 // | |
969 // PropertyDefinition : | |
970 // PropertyName : AssignmentExpression | |
971 // MethodDefinition | |
972 properties.add(parseMethodOrProperty()); | |
973 | |
974 if (acceptCategory(RBRACE)) break; | |
975 expectCategory(COMMA); | |
976 } | |
977 return new ObjectInitializer(properties); | |
978 } | |
979 | |
980 Expression parseMember() { | |
981 Expression receiver = parsePrimary(); | |
982 while (true) { | |
983 if (acceptCategory(DOT)) { | |
984 receiver = getDotRhs(receiver); | |
985 } else if (acceptCategory(LSQUARE)) { | |
986 Expression inBraces = parseExpression(); | |
987 expectCategory(RSQUARE); | |
988 receiver = new PropertyAccess(receiver, inBraces); | |
989 } else { | |
990 break; | |
991 } | |
992 } | |
993 return receiver; | |
994 } | |
995 | |
996 Expression parseCall() { | |
997 bool constructor = acceptString("new"); | |
998 Expression receiver = parseMember(); | |
999 while (true) { | |
1000 if (acceptCategory(LPAREN)) { | |
1001 final arguments = <Expression>[]; | |
1002 if (!acceptCategory(RPAREN)) { | |
1003 while (true) { | |
1004 if (acceptCategory(ELLIPSIS)) { | |
1005 arguments.add(new Spread(parseAssignment())); | |
1006 expectCategory(RPAREN); | |
1007 break; | |
1008 } | |
1009 arguments.add(parseAssignment()); | |
1010 if (acceptCategory(RPAREN)) break; | |
1011 expectCategory(COMMA); | |
1012 } | |
1013 } | |
1014 receiver = constructor ? | |
1015 new New(receiver, arguments) : | |
1016 new Call(receiver, arguments); | |
1017 constructor = false; | |
1018 } else if (!constructor && acceptCategory(LSQUARE)) { | |
1019 Expression inBraces = parseExpression(); | |
1020 expectCategory(RSQUARE); | |
1021 receiver = new PropertyAccess(receiver, inBraces); | |
1022 } else if (!constructor && acceptCategory(DOT)) { | |
1023 receiver = getDotRhs(receiver); | |
1024 } else { | |
1025 // JS allows new without (), but we don't. | |
1026 if (constructor) error("Parentheses are required for new"); | |
1027 break; | |
1028 } | |
1029 } | |
1030 return receiver; | |
1031 } | |
1032 | |
1033 Expression getDotRhs(Expression receiver) { | |
1034 if (acceptCategory(HASH)) { | |
1035 var nameOrPosition = parseHash(); | |
1036 InterpolatedSelector property = new InterpolatedSelector(nameOrPosition); | |
1037 interpolatedValues.add(property); | |
1038 return new PropertyAccess(receiver, property); | |
1039 } | |
1040 String identifier = lastToken; | |
1041 // In ES5 keywords like delete and continue are allowed as property | |
1042 // names, and the IndexedDB API uses that, so we need to allow it here. | |
1043 if (acceptCategory(SYMBOL)) { | |
1044 if (!OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS.contains(identifier)) { | |
1045 error("Expected alphanumeric identifier"); | |
1046 } | |
1047 } else { | |
1048 expectCategory(ALPHA); | |
1049 } | |
1050 return new PropertyAccess.field(receiver, identifier); | |
1051 } | |
1052 | |
1053 Expression parsePostfix() { | |
1054 Expression expression = parseCall(); | |
1055 String operator = lastToken; | |
1056 // JavaScript grammar is: | |
1057 // LeftHandSideExpression [no LineTerminator here] ++ | |
1058 if (lastCategory == SYMBOL && | |
1059 !skippedNewline && | |
1060 (acceptString("++") || acceptString("--"))) { | |
1061 return new Postfix(operator, expression); | |
1062 } | |
1063 // If we don't accept '++' or '--' due to skippedNewline a newline, no other | |
1064 // part of the parser will accept the token and we will get an error at the | |
1065 // whole expression level. | |
1066 return expression; | |
1067 } | |
1068 | |
1069 Expression parseUnaryHigh() { | |
1070 String operator = lastToken; | |
1071 if (lastCategory == SYMBOL && UNARY_OPERATORS.contains(operator) && | |
1072 (acceptString("++") || acceptString("--") || acceptString('await'))) { | |
1073 if (operator == "await") return new Await(parsePostfix()); | |
1074 return new Prefix(operator, parsePostfix()); | |
1075 } | |
1076 return parsePostfix(); | |
1077 } | |
1078 | |
1079 Expression parseUnaryLow() { | |
1080 String operator = lastToken; | |
1081 if (lastCategory == SYMBOL && UNARY_OPERATORS.contains(operator) && | |
1082 operator != "++" && operator != "--") { | |
1083 expectCategory(SYMBOL); | |
1084 if (operator == "await") return new Await(parsePostfix()); | |
1085 return new Prefix(operator, parseUnaryLow()); | |
1086 } | |
1087 return parseUnaryHigh(); | |
1088 } | |
1089 | |
1090 Expression parseBinary(int maxPrecedence) { | |
1091 Expression lhs = parseUnaryLow(); | |
1092 int minPrecedence; | |
1093 String lastSymbol; | |
1094 Expression rhs; // This is null first time around. | |
1095 while (true) { | |
1096 String symbol = lastToken; | |
1097 if (lastCategory != SYMBOL || | |
1098 !BINARY_PRECEDENCE.containsKey(symbol) || | |
1099 BINARY_PRECEDENCE[symbol] > maxPrecedence) { | |
1100 break; | |
1101 } | |
1102 expectCategory(SYMBOL); | |
1103 if (rhs == null || BINARY_PRECEDENCE[symbol] >= minPrecedence) { | |
1104 if (rhs != null) lhs = new Binary(lastSymbol, lhs, rhs); | |
1105 minPrecedence = BINARY_PRECEDENCE[symbol]; | |
1106 rhs = parseUnaryLow(); | |
1107 lastSymbol = symbol; | |
1108 } else { | |
1109 Expression higher = parseBinary(BINARY_PRECEDENCE[symbol]); | |
1110 rhs = new Binary(symbol, rhs, higher); | |
1111 } | |
1112 } | |
1113 if (rhs == null) return lhs; | |
1114 return new Binary(lastSymbol, lhs, rhs); | |
1115 } | |
1116 | |
1117 Expression parseConditional() { | |
1118 Expression lhs = parseBinary(HIGHEST_PARSE_BINARY_PRECEDENCE); | |
1119 if (!acceptCategory(QUERY)) return lhs; | |
1120 Expression ifTrue = parseAssignment(); | |
1121 expectCategory(COLON); | |
1122 Expression ifFalse = parseAssignment(); | |
1123 return new Conditional(lhs, ifTrue, ifFalse); | |
1124 } | |
1125 | |
1126 Expression parseLeftHandSide() => parseConditional(); | |
1127 | |
1128 Expression parseAssignment() { | |
1129 Expression lhs = parseLeftHandSide(); | |
1130 String assignmentOperator = lastToken; | |
1131 if (acceptCategory(ASSIGNMENT)) { | |
1132 Expression rhs = parseAssignment(); | |
1133 if (assignmentOperator == "=") { | |
1134 return new Assignment(lhs, rhs); | |
1135 } else { | |
1136 // Handle +=, -=, etc. | |
1137 String operator = | |
1138 assignmentOperator.substring(0, assignmentOperator.length - 1); | |
1139 return new Assignment.compound(lhs, operator, rhs); | |
1140 } | |
1141 } | |
1142 return lhs; | |
1143 } | |
1144 | |
1145 Expression parseExpression() { | |
1146 Expression expression = parseAssignment(); | |
1147 while (acceptCategory(COMMA)) { | |
1148 Expression right = parseAssignment(); | |
1149 expression = new Binary(',', expression, right); | |
1150 } | |
1151 return expression; | |
1152 } | |
1153 | |
1154 /** Parse a variable declaration list, with `var` or `let` [keyword] */ | |
1155 VariableDeclarationList parseVariableDeclarationList( | |
1156 String keyword, [String firstIdentifier]) { | |
1157 var initialization = []; | |
1158 | |
1159 do { | |
1160 var declarator; | |
1161 if (firstIdentifier != null) { | |
1162 declarator = new Identifier(firstIdentifier); | |
1163 firstIdentifier = null; | |
1164 } else { | |
1165 declarator = parseVariableBinding(); | |
1166 } | |
1167 | |
1168 var initializer = acceptString("=") ? parseAssignment() : null; | |
1169 initialization.add(new VariableInitialization(declarator, initializer)); | |
1170 } while (acceptCategory(COMMA)); | |
1171 | |
1172 return new VariableDeclarationList(keyword, initialization); | |
1173 } | |
1174 | |
1175 VariableBinding parseVariableBinding() { | |
1176 switch (lastCategory) { | |
1177 case ALPHA: | |
1178 case HASH: | |
1179 return parseIdentifier(); | |
1180 case LBRACE: | |
1181 case LSQUARE: | |
1182 return parseBindingPattern(); | |
1183 default: | |
1184 error('Unexpected token $lastToken: ${categoryToString(lastCategory)}'); | |
1185 return null; | |
1186 } | |
1187 } | |
1188 | |
1189 /// Note: this doesn't deal with general-case destructuring yet, it just | |
1190 /// supports it in variable initialization. | |
1191 /// See ES6 spec: | |
1192 /// http://www.ecma-international.org/ecma-262/6.0/#sec-destructuring-binding-
patterns | |
1193 /// http://www.ecma-international.org/ecma-262/6.0/#sec-destructuring-assignme
nt | |
1194 /// TODO(ochafik): Support destructuring in LeftHandSideExpression. | |
1195 BindingPattern parseBindingPattern() { | |
1196 if (acceptCategory(LBRACE)) { | |
1197 return parseObjectBindingPattern(); | |
1198 } else { | |
1199 expectCategory(LSQUARE); | |
1200 return parseArrayBindingPattern(); | |
1201 } | |
1202 } | |
1203 | |
1204 ArrayBindingPattern parseArrayBindingPattern() { | |
1205 var variables = <DestructuredVariable>[]; | |
1206 do { | |
1207 var name; | |
1208 var structure; | |
1209 var defaultValue; | |
1210 | |
1211 var declarator = parseVariableBinding(); | |
1212 if (declarator is Identifier) name = declarator; | |
1213 else if (declarator is BindingPattern) structure = declarator; | |
1214 else error("Unexpected LHS: $declarator"); | |
1215 | |
1216 if (acceptString("=")) { | |
1217 defaultValue = parseExpression(); | |
1218 } | |
1219 variables.add(new DestructuredVariable( | |
1220 name: name, structure: structure, defaultValue: defaultValue)); | |
1221 } while (acceptCategory(COMMA)); | |
1222 | |
1223 expectCategory(RSQUARE); | |
1224 return new ArrayBindingPattern(variables); | |
1225 } | |
1226 | |
1227 ObjectBindingPattern parseObjectBindingPattern() { | |
1228 var variables = <DestructuredVariable>[]; | |
1229 do { | |
1230 var name = parseIdentifier(); | |
1231 var structure; | |
1232 var defaultValue; | |
1233 | |
1234 if (acceptCategory(COLON)) { | |
1235 structure = parseBindingPattern(); | |
1236 } else if (acceptString("=")) { | |
1237 defaultValue = parseExpression(); | |
1238 } | |
1239 variables.add(new DestructuredVariable( | |
1240 name: name, structure: structure, defaultValue: defaultValue)); | |
1241 } while (acceptCategory(COMMA)); | |
1242 | |
1243 expectCategory(RBRACE); | |
1244 return new ObjectBindingPattern(variables); | |
1245 } | |
1246 | |
1247 Expression parseVarDeclarationOrExpression() { | |
1248 var keyword = acceptVarLetOrConst(); | |
1249 if (keyword != null) { | |
1250 return parseVariableDeclarationList(keyword); | |
1251 } else { | |
1252 return parseExpression(); | |
1253 } | |
1254 } | |
1255 | |
1256 /** Accepts a `var` or `let` keyword. If neither is found, returns null. */ | |
1257 String acceptVarLetOrConst() { | |
1258 if (acceptString('var')) return 'var'; | |
1259 if (acceptString('let')) return 'let'; | |
1260 if (acceptString('const')) return 'const'; | |
1261 return null; | |
1262 } | |
1263 | |
1264 Expression expression() { | |
1265 Expression expression = parseVarDeclarationOrExpression(); | |
1266 if (lastCategory != NONE || position != src.length) { | |
1267 error("Unparsed junk: ${categoryToString(lastCategory)}"); | |
1268 } | |
1269 return expression; | |
1270 } | |
1271 | |
1272 Statement statement() { | |
1273 Statement statement = parseStatement(); | |
1274 if (lastCategory != NONE || position != src.length) { | |
1275 error("Unparsed junk: ${categoryToString(lastCategory)}"); | |
1276 } | |
1277 // TODO(sra): interpolated capture here? | |
1278 return statement; | |
1279 } | |
1280 | |
1281 Block parseBlock() { | |
1282 List<Statement> statements = <Statement>[]; | |
1283 | |
1284 while (!acceptCategory(RBRACE)) { | |
1285 Statement statement = parseStatement(); | |
1286 statements.add(statement); | |
1287 } | |
1288 return new Block(statements); | |
1289 } | |
1290 | |
1291 Statement parseStatement() { | |
1292 if (acceptCategory(LBRACE)) return parseBlock(); | |
1293 | |
1294 if (acceptCategory(SEMICOLON)) return new EmptyStatement(); | |
1295 | |
1296 if (lastCategory == ALPHA) { | |
1297 if (acceptString('return')) return parseReturn(); | |
1298 | |
1299 if (acceptString('throw')) return parseThrow(); | |
1300 | |
1301 if (acceptString('break')) { | |
1302 return parseBreakOrContinue((label) => new Break(label)); | |
1303 } | |
1304 | |
1305 if (acceptString('continue')) { | |
1306 return parseBreakOrContinue((label) => new Continue(label)); | |
1307 } | |
1308 | |
1309 if (acceptString('if')) return parseIfThenElse(); | |
1310 | |
1311 if (acceptString('for')) return parseFor(); | |
1312 | |
1313 if (acceptString('function')) return parseFunctionDeclaration(); | |
1314 | |
1315 if (acceptString('class')) return new ClassDeclaration(parseClass()); | |
1316 | |
1317 if (acceptString('try')) return parseTry(); | |
1318 | |
1319 var keyword = acceptVarLetOrConst(); | |
1320 if (keyword != null) { | |
1321 Expression declarations = parseVariableDeclarationList(keyword); | |
1322 expectSemicolon(); | |
1323 return new ExpressionStatement(declarations); | |
1324 } | |
1325 | |
1326 if (acceptString('while')) return parseWhile(); | |
1327 | |
1328 if (acceptString('do')) return parseDo(); | |
1329 | |
1330 if (acceptString('switch')) return parseSwitch(); | |
1331 | |
1332 if (lastToken == 'case') error("Case outside switch."); | |
1333 | |
1334 if (lastToken == 'default') error("Default outside switch."); | |
1335 | |
1336 if (lastToken == 'yield') return parseYield(); | |
1337 | |
1338 if (lastToken == 'with') { | |
1339 error('Not implemented in mini parser'); | |
1340 } | |
1341 | |
1342 } | |
1343 | |
1344 bool checkForInterpolatedStatement = lastCategory == HASH; | |
1345 | |
1346 Expression expression = parseExpression(); | |
1347 | |
1348 if (expression is Identifier && acceptCategory(COLON)) { | |
1349 return new LabeledStatement(expression.name, parseStatement()); | |
1350 } | |
1351 | |
1352 expectSemicolon(); | |
1353 | |
1354 if (checkForInterpolatedStatement) { | |
1355 // 'Promote' the interpolated expression `#;` to an interpolated | |
1356 // statement. | |
1357 if (expression is InterpolatedExpression) { | |
1358 assert(identical(interpolatedValues.last, expression)); | |
1359 InterpolatedStatement statement = | |
1360 new InterpolatedStatement(expression.nameOrPosition); | |
1361 interpolatedValues[interpolatedValues.length - 1] = statement; | |
1362 return statement; | |
1363 } | |
1364 } | |
1365 | |
1366 return new ExpressionStatement(expression); | |
1367 } | |
1368 | |
1369 Statement parseReturn() { | |
1370 if (acceptSemicolon()) return new Return(); | |
1371 Expression expression = parseExpression(); | |
1372 expectSemicolon(); | |
1373 return new Return(expression); | |
1374 } | |
1375 | |
1376 Statement parseYield() { | |
1377 bool hasStar = acceptString('*'); | |
1378 Expression expression = parseExpression(); | |
1379 expectSemicolon(); | |
1380 return new DartYield(expression, hasStar); | |
1381 } | |
1382 | |
1383 Statement parseThrow() { | |
1384 if (skippedNewline) error('throw expression must be on same line'); | |
1385 Expression expression = parseExpression(); | |
1386 expectSemicolon(); | |
1387 return new Throw(expression); | |
1388 } | |
1389 | |
1390 Statement parseBreakOrContinue(constructor) { | |
1391 var identifier = lastToken; | |
1392 if (!skippedNewline && acceptCategory(ALPHA)) { | |
1393 expectSemicolon(); | |
1394 return constructor(identifier); | |
1395 } | |
1396 expectSemicolon(); | |
1397 return constructor(null); | |
1398 } | |
1399 | |
1400 Statement parseIfThenElse() { | |
1401 expectCategory(LPAREN); | |
1402 Expression condition = parseExpression(); | |
1403 expectCategory(RPAREN); | |
1404 Statement thenStatement = parseStatement(); | |
1405 if (acceptString('else')) { | |
1406 // Resolves dangling else by binding 'else' to closest 'if'. | |
1407 Statement elseStatement = parseStatement(); | |
1408 return new If(condition, thenStatement, elseStatement); | |
1409 } else { | |
1410 return new If.noElse(condition, thenStatement); | |
1411 } | |
1412 } | |
1413 | |
1414 Statement parseFor() { | |
1415 // For-init-condition-increment style loops are fully supported. | |
1416 // | |
1417 // Only one for-in variant is currently implemented: | |
1418 // | |
1419 // for (var variable in Expression) Statement | |
1420 // | |
1421 // One variant of ES6 for-of is also implemented: | |
1422 // | |
1423 // for (let variable of Expression) Statement | |
1424 // | |
1425 Statement finishFor(Expression init) { | |
1426 Expression condition = null; | |
1427 if (!acceptCategory(SEMICOLON)) { | |
1428 condition = parseExpression(); | |
1429 expectCategory(SEMICOLON); | |
1430 } | |
1431 Expression update = null; | |
1432 if (!acceptCategory(RPAREN)) { | |
1433 update = parseExpression(); | |
1434 expectCategory(RPAREN); | |
1435 } | |
1436 Statement body = parseStatement(); | |
1437 return new For(init, condition, update, body); | |
1438 } | |
1439 | |
1440 expectCategory(LPAREN); | |
1441 if (acceptCategory(SEMICOLON)) { | |
1442 return finishFor(null); | |
1443 } | |
1444 | |
1445 var keyword = acceptVarLetOrConst(); | |
1446 if (keyword != null) { | |
1447 String identifier = lastToken; | |
1448 expectCategory(ALPHA); | |
1449 | |
1450 if (acceptString('in')) { | |
1451 Expression objectExpression = parseExpression(); | |
1452 expectCategory(RPAREN); | |
1453 Statement body = parseStatement(); | |
1454 return new ForIn( | |
1455 _createVariableDeclarationList(keyword, identifier), | |
1456 objectExpression, | |
1457 body); | |
1458 } else if (acceptString('of')) { | |
1459 Expression iterableExpression = parseAssignment(); | |
1460 expectCategory(RPAREN); | |
1461 Statement body = parseStatement(); | |
1462 return new ForOf( | |
1463 _createVariableDeclarationList(keyword, identifier), | |
1464 iterableExpression, | |
1465 body); | |
1466 } | |
1467 var declarations = parseVariableDeclarationList(keyword, identifier); | |
1468 expectCategory(SEMICOLON); | |
1469 return finishFor(declarations); | |
1470 } | |
1471 | |
1472 Expression init = parseExpression(); | |
1473 expectCategory(SEMICOLON); | |
1474 return finishFor(init); | |
1475 } | |
1476 | |
1477 static VariableDeclarationList _createVariableDeclarationList( | |
1478 String keyword, String identifier) { | |
1479 return new VariableDeclarationList(keyword, [ | |
1480 new VariableInitialization( | |
1481 new Identifier(identifier), null)]); | |
1482 } | |
1483 | |
1484 Statement parseFunctionDeclaration() { | |
1485 String name = lastToken; | |
1486 expectCategory(ALPHA); | |
1487 Expression fun = parseFun(); | |
1488 return new FunctionDeclaration(new Identifier(name), fun); | |
1489 } | |
1490 | |
1491 Statement parseTry() { | |
1492 expectCategory(LBRACE); | |
1493 Block body = parseBlock(); | |
1494 Catch catchPart = null; | |
1495 if (acceptString('catch')) catchPart = parseCatch(); | |
1496 Block finallyPart = null; | |
1497 if (acceptString('finally')) { | |
1498 expectCategory(LBRACE); | |
1499 finallyPart = parseBlock(); | |
1500 } else { | |
1501 if (catchPart == null) error("expected 'finally'"); | |
1502 } | |
1503 return new Try(body, catchPart, finallyPart); | |
1504 } | |
1505 | |
1506 SwitchClause parseSwitchClause() { | |
1507 Expression expression = null; | |
1508 if (acceptString('case')) { | |
1509 expression = parseExpression(); | |
1510 expectCategory(COLON); | |
1511 } else { | |
1512 if (!acceptString('default')) { | |
1513 error('expected case or default'); | |
1514 } | |
1515 expectCategory(COLON); | |
1516 } | |
1517 List statements = new List<Statement>(); | |
1518 while (lastCategory != RBRACE && | |
1519 lastToken != 'case' && | |
1520 lastToken != 'default') { | |
1521 statements.add(parseStatement()); | |
1522 } | |
1523 return expression == null | |
1524 ? new Default(new Block(statements)) | |
1525 : new Case(expression, new Block(statements)); | |
1526 } | |
1527 | |
1528 Statement parseWhile() { | |
1529 expectCategory(LPAREN); | |
1530 Expression condition = parseExpression(); | |
1531 expectCategory(RPAREN); | |
1532 Statement body = parseStatement(); | |
1533 return new While(condition, body); | |
1534 } | |
1535 | |
1536 Statement parseDo() { | |
1537 Statement body = parseStatement(); | |
1538 if (lastToken != "while") error("Missing while after do body."); | |
1539 getToken(); | |
1540 expectCategory(LPAREN); | |
1541 Expression condition = parseExpression(); | |
1542 expectCategory(RPAREN); | |
1543 expectSemicolon(); | |
1544 return new Do(body, condition); | |
1545 } | |
1546 | |
1547 Statement parseSwitch() { | |
1548 expectCategory(LPAREN); | |
1549 Expression key = parseExpression(); | |
1550 expectCategory(RPAREN); | |
1551 expectCategory(LBRACE); | |
1552 List<SwitchClause> clauses = new List<SwitchClause>(); | |
1553 while(lastCategory != RBRACE) { | |
1554 clauses.add(parseSwitchClause()); | |
1555 } | |
1556 expectCategory(RBRACE); | |
1557 return new Switch(key, clauses); | |
1558 } | |
1559 | |
1560 Catch parseCatch() { | |
1561 expectCategory(LPAREN); | |
1562 String identifier = lastToken; | |
1563 expectCategory(ALPHA); | |
1564 expectCategory(RPAREN); | |
1565 expectCategory(LBRACE); | |
1566 Block body = parseBlock(); | |
1567 return new Catch(new Identifier(identifier), body); | |
1568 } | |
1569 | |
1570 ClassExpression parseClass() { | |
1571 Identifier name = parseIdentifier(); | |
1572 Expression heritage = null; | |
1573 if (acceptString('extends')) { | |
1574 heritage = parseConditional(); | |
1575 } | |
1576 expectCategory(LBRACE); | |
1577 var methods = new List<Method>(); | |
1578 while (lastCategory != RBRACE) { | |
1579 methods.add(parseMethodOrProperty(onlyMethods: true)); | |
1580 } | |
1581 expectCategory(RBRACE); | |
1582 return new ClassExpression(name, heritage, methods); | |
1583 } | |
1584 | |
1585 /** | |
1586 * Parses a [Method] or a [Property]. | |
1587 * | |
1588 * Most of the complexity is from supporting interpolation. Several forms | |
1589 * are supported: | |
1590 * | |
1591 * - getter/setter names: `get #() { ... }` | |
1592 * - method names: `#() { ... }` | |
1593 * - property names: `#: ...` | |
1594 * - entire methods: `#` | |
1595 */ | |
1596 Property parseMethodOrProperty({bool onlyMethods: false}) { | |
1597 bool isStatic = acceptString('static'); | |
1598 | |
1599 bool isGetter = lastToken == 'get'; | |
1600 bool isSetter = lastToken == 'set'; | |
1601 Expression name = null; | |
1602 if (isGetter || isSetter) { | |
1603 var token = lastToken; | |
1604 getToken(); | |
1605 if (lastCategory == COLON) { | |
1606 // That wasn't a accessor but the 'get' or 'set' property: retropedal. | |
1607 isGetter = isSetter = false; | |
1608 name = new LiteralString('"$token"'); | |
1609 } | |
1610 } | |
1611 if (acceptCategory(HASH)) { | |
1612 if (lastCategory != LPAREN && (onlyMethods || lastCategory != COLON)) { | |
1613 // Interpolated method | |
1614 var member = new InterpolatedMethod(parseHash()); | |
1615 interpolatedValues.add(member); | |
1616 return member; | |
1617 } | |
1618 name = parseInterpolatedExpression(); | |
1619 } else { | |
1620 name ??= parsePropertyName(); | |
1621 } | |
1622 | |
1623 if (!onlyMethods && acceptCategory(COLON)) { | |
1624 Expression value = parseAssignment(); | |
1625 return new Property(name, value); | |
1626 } else { | |
1627 var fun = parseFun(); | |
1628 return new Method(name, fun, | |
1629 isGetter: isGetter, isSetter: isSetter, isStatic: isStatic); | |
1630 } | |
1631 } | |
1632 | |
1633 Expression parsePropertyName() { | |
1634 String identifier = lastToken; | |
1635 if (acceptCategory(STRING)) { | |
1636 return new LiteralString(identifier); | |
1637 } else if (acceptCategory(ALPHA) || acceptCategory(SYMBOL)) { | |
1638 // ALPHA or a SYMBOL, e.g. void | |
1639 return new LiteralString('"$identifier"'); | |
1640 } else if (acceptCategory(LSQUARE)) { | |
1641 var expr = parseAssignment(); | |
1642 expectCategory(RSQUARE); | |
1643 return expr; | |
1644 } else if (acceptCategory(HASH)) { | |
1645 return parseInterpolatedExpression(); | |
1646 } else { | |
1647 error('Expected property name'); | |
1648 return null; | |
1649 } | |
1650 } | |
1651 } | |
OLD | NEW |