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; | |
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 return template.instantiate(arguments is List ? arguments : [arguments]); | |
212 } | |
213 | |
214 /** | |
215 * Parses a JavaScript Statement, otherwise just like [call]. | |
216 */ | |
217 Statement statement(String source, [var arguments]) { | |
218 Template template = _findStatementTemplate(source); | |
219 if (arguments == null) return template.instantiate([]); | |
220 return template.instantiate(arguments is List ? arguments : [arguments]); | |
221 } | |
222 | |
223 /** | |
224 * Parses JavaScript written in the `JS` foreign instruction. | |
225 * | |
226 * The [source] must be a JavaScript expression or a JavaScript throw | |
227 * statement. | |
228 */ | |
229 Template parseForeignJS(String source) { | |
230 // TODO(sra): Parse with extra validation to forbid `#` interpolation in | |
231 // functions, as this leads to unanticipated capture of temporaries that are | |
232 // reused after capture. | |
233 if (source.startsWith("throw ")) { | |
234 return _findStatementTemplate(source); | |
235 } else { | |
236 return _findExpressionTemplate(source); | |
237 } | |
238 } | |
239 | |
240 Template _findExpressionTemplate(String source) { | |
241 Template template = templateManager.lookupExpressionTemplate(source); | |
242 if (template == null) { | |
243 MiniJsParser parser = new MiniJsParser(source); | |
244 Expression expression = parser.expression(); | |
245 template = templateManager.defineExpressionTemplate(source, expression); | |
246 } | |
247 return template; | |
248 } | |
249 | |
250 Template _findStatementTemplate(String source) { | |
251 Template template = templateManager.lookupStatementTemplate(source); | |
252 if (template == null) { | |
253 MiniJsParser parser = new MiniJsParser(source); | |
254 Statement statement = parser.statement(); | |
255 template = templateManager.defineStatementTemplate(source, statement); | |
256 } | |
257 return template; | |
258 } | |
259 | |
260 /** | |
261 * Creates an Expression template without caching the result. | |
262 */ | |
263 Template uncachedExpressionTemplate(String source) { | |
264 MiniJsParser parser = new MiniJsParser(source); | |
265 Expression expression = parser.expression(); | |
266 return new Template( | |
267 source, expression, isExpression: true, forceCopy: false); | |
268 } | |
269 | |
270 /** | |
271 * Creates a Statement template without caching the result. | |
272 */ | |
273 Template uncachedStatementTemplate(String source) { | |
274 MiniJsParser parser = new MiniJsParser(source); | |
275 Statement statement = parser.statement(); | |
276 return new Template( | |
277 source, statement, isExpression: false, forceCopy: false); | |
278 } | |
279 | |
280 /** | |
281 * Create an Expression template which has [ast] as the result. This is used | |
282 * to wrap a generated AST in a zero-argument Template so it can be passed to | |
283 * context that expects a template. | |
284 */ | |
285 Template expressionTemplateYielding(Node ast) { | |
286 return new Template.withExpressionResult(ast); | |
287 } | |
288 | |
289 Template statementTemplateYielding(Node ast) { | |
290 return new Template.withStatementResult(ast); | |
291 } | |
292 | |
293 /// Creates a literal js string from [value]. | |
294 LiteralString escapedString(String value) { | |
295 // Start by escaping the backslashes. | |
296 String escaped = value.replaceAll('\\', '\\\\'); | |
297 // Do not escape unicode characters and ' because they are allowed in the | |
298 // string literal anyway. | |
299 escaped = escaped.replaceAllMapped(new RegExp('\n|"|\b|\t|\v'), (match) { | |
300 switch (match.group(0)) { | |
301 case "\n" : return r"\n"; | |
302 case "\"" : return r'\"'; | |
303 case "\b" : return r"\b"; | |
304 case "\t" : return r"\t"; | |
305 case "\f" : return r"\f"; | |
306 case "\v" : return r"\v"; | |
307 } | |
308 }); | |
309 LiteralString result = string(escaped); | |
310 // We don't escape ' under the assumption that the string is wrapped | |
311 // into ". Verify that assumption. | |
312 assert(result.value.codeUnitAt(0) == '"'.codeUnitAt(0)); | |
313 return result; | |
314 } | |
315 | |
316 /// Creates a literal js string from [value]. | |
317 /// | |
318 /// Note that this function only puts quotes around [value]. It does not do | |
319 /// any escaping, so use only when you can guarantee that [value] does not | |
320 /// contain newlines or backslashes. For escaping the string use | |
321 /// [escapedString]. | |
322 LiteralString string(String value) => new LiteralString('"$value"'); | |
323 | |
324 LiteralNumber number(num value) => new LiteralNumber('$value'); | |
325 | |
326 ArrayInitializer numArray(Iterable<int> list) => | |
327 new ArrayInitializer.from(list.map(number)); | |
328 | |
329 ArrayInitializer stringArray(Iterable<String> list) => | |
330 new ArrayInitializer.from(list.map(string)); | |
331 | |
332 Comment comment(String text) => new Comment(text); | |
333 } | |
334 | |
335 LiteralString string(String value) => js.string(value); | |
336 LiteralNumber number(num value) => js.number(value); | |
337 ArrayInitializer numArray(Iterable<int> list) => js.numArray(list); | |
338 ArrayInitializer stringArray(Iterable<String> list) => js.stringArray(list); | |
339 | |
340 class MiniJsParserError { | |
341 MiniJsParserError(this.parser, this.message) { } | |
342 | |
343 final MiniJsParser parser; | |
344 final String message; | |
345 | |
346 String toString() { | |
347 int pos = parser.lastPosition; | |
348 | |
349 // Discard lines following the line containing lastPosition. | |
350 String src = parser.src; | |
351 int newlinePos = src.indexOf('\n', pos); | |
352 if (newlinePos >= pos) src = src.substring(0, newlinePos); | |
353 | |
354 // Extract the prefix of the error line before lastPosition. | |
355 String line = src; | |
356 int lastLineStart = line.lastIndexOf('\n'); | |
357 if (lastLineStart >= 0) line = line.substring(lastLineStart + 1); | |
358 String prefix = line.substring(0, pos - (src.length - line.length)); | |
359 | |
360 // Replace non-tabs with spaces, giving a print indent that matches the text | |
361 // for tabbing. | |
362 String spaces = prefix.replaceAll(new RegExp(r'[^\t]'), ' '); | |
363 return 'Error in MiniJsParser:\n${src}\n$spaces^\n$spaces$message\n'; | |
364 } | |
365 } | |
366 | |
367 /// Mini JavaScript parser for tiny snippets of code that we want to make into | |
368 /// AST nodes. Handles: | |
369 /// * identifiers. | |
370 /// * dot access. | |
371 /// * method calls. | |
372 /// * [] access. | |
373 /// * array, string, regexp, boolean, null and numeric literals. | |
374 /// * most operators. | |
375 /// * brackets. | |
376 /// * var declarations. | |
377 /// * operator precedence. | |
378 /// * anonymous funtions and named function expressions and declarations. | |
379 /// Notable things it can't do yet include: | |
380 /// * some statements are still missing (do-while, while, switch). | |
381 /// | |
382 /// It's a fairly standard recursive descent parser. | |
383 /// | |
384 /// Literal strings are passed through to the final JS source code unchanged, | |
385 /// including the choice of surrounding quotes, so if you parse | |
386 /// r'var x = "foo\n\"bar\""' you will end up with | |
387 /// var x = "foo\n\"bar\"" in the final program. \x and \u escapes are not | |
388 /// allowed in string and regexp literals because the machinery for checking | |
389 /// their correctness is rather involved. | |
390 class MiniJsParser { | |
391 MiniJsParser(this.src) | |
392 : lastCategory = NONE, | |
393 lastToken = null, | |
394 lastPosition = 0, | |
395 position = 0 { | |
396 getToken(); | |
397 } | |
398 | |
399 int lastCategory = NONE; | |
400 String lastToken = null; | |
401 int lastPosition = 0; | |
402 int position = 0; | |
403 bool skippedNewline = false; // skipped newline in last getToken? | |
404 final String src; | |
405 final List<InterpolatedNode> interpolatedValues = <InterpolatedNode>[]; | |
406 | |
407 static const NONE = -1; | |
408 static const ALPHA = 0; | |
409 static const NUMERIC = 1; | |
410 static const STRING = 2; | |
411 static const SYMBOL = 3; | |
412 static const ASSIGNMENT = 4; | |
413 static const DOT = 5; | |
414 static const LPAREN = 6; | |
415 static const RPAREN = 7; | |
416 static const LBRACE = 8; | |
417 static const RBRACE = 9; | |
418 static const LSQUARE = 10; | |
419 static const RSQUARE = 11; | |
420 static const COMMA = 12; | |
421 static const QUERY = 13; | |
422 static const COLON = 14; | |
423 static const SEMICOLON = 15; | |
424 static const HASH = 16; | |
425 static const WHITESPACE = 17; | |
426 static const OTHER = 18; | |
427 | |
428 // Make sure that ]] is two symbols. | |
429 bool singleCharCategory(int category) => category >= DOT; | |
430 | |
431 static String categoryToString(int cat) { | |
432 switch (cat) { | |
433 case NONE: return "NONE"; | |
434 case ALPHA: return "ALPHA"; | |
435 case NUMERIC: return "NUMERIC"; | |
436 case SYMBOL: return "SYMBOL"; | |
437 case ASSIGNMENT: return "ASSIGNMENT"; | |
438 case DOT: return "DOT"; | |
439 case LPAREN: return "LPAREN"; | |
440 case RPAREN: return "RPAREN"; | |
441 case LBRACE: return "LBRACE"; | |
442 case RBRACE: return "RBRACE"; | |
443 case LSQUARE: return "LSQUARE"; | |
444 case RSQUARE: return "RSQUARE"; | |
445 case STRING: return "STRING"; | |
446 case COMMA: return "COMMA"; | |
447 case QUERY: return "QUERY"; | |
448 case COLON: return "COLON"; | |
449 case SEMICOLON: return "SEMICOLON"; | |
450 case HASH: return "HASH"; | |
451 case WHITESPACE: return "WHITESPACE"; | |
452 case OTHER: return "OTHER"; | |
453 } | |
454 return "Unknown: $cat"; | |
455 } | |
456 | |
457 static const CATEGORIES = const <int>[ | |
458 OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, // 0-7 | |
459 OTHER, WHITESPACE, WHITESPACE, OTHER, OTHER, WHITESPACE, // 8-13 | |
460 OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, // 14-21 | |
461 OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, OTHER, // 22-29 | |
462 OTHER, OTHER, WHITESPACE, // 30-32 | |
463 SYMBOL, OTHER, HASH, ALPHA, SYMBOL, SYMBOL, OTHER, // !"#$%&´ | |
464 LPAREN, RPAREN, SYMBOL, SYMBOL, COMMA, SYMBOL, DOT, SYMBOL, // ()*+,-./ | |
465 NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, // 01234 | |
466 NUMERIC, NUMERIC, NUMERIC, NUMERIC, NUMERIC, // 56789 | |
467 COLON, SEMICOLON, SYMBOL, SYMBOL, SYMBOL, QUERY, OTHER, // :;<=>?@ | |
468 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // ABCDEFGH | |
469 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // IJKLMNOP | |
470 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // QRSTUVWX | |
471 ALPHA, ALPHA, LSQUARE, OTHER, RSQUARE, SYMBOL, ALPHA, OTHER, // YZ[\]^_' | |
472 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // abcdefgh | |
473 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // ijklmnop | |
474 ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, ALPHA, // qrstuvwx | |
475 ALPHA, ALPHA, LBRACE, SYMBOL, RBRACE, SYMBOL]; // yz{|}~ | |
476 | |
477 // This must be a >= the highest precedence number handled by parseBinary. | |
478 static var HIGHEST_PARSE_BINARY_PRECEDENCE = 16; | |
479 static bool isAssignment(String symbol) => BINARY_PRECEDENCE[symbol] == 17; | |
480 | |
481 // From https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operator
s/Operator_Precedence | |
482 static final BINARY_PRECEDENCE = { | |
483 '+=': 17, '-=': 17, '*=': 17, '/=': 17, '%=': 17, '^=': 17, '|=': 17, | |
484 '&=': 17, '<<=': 17, '>>=': 17, '>>>=': 17, '=': 17, | |
485 '||': 14, | |
486 '&&': 13, | |
487 '|': 12, | |
488 '^': 11, | |
489 '&': 10, | |
490 '!=': 9, '==': 9, '!==': 9, '===': 9, | |
491 '<': 8, '<=': 8, '>=': 8, '>': 8, 'in': 8, 'instanceof': 8, | |
492 '<<': 7, '>>': 7, '>>>': 7, | |
493 '+': 6, '-': 6, | |
494 '*': 5, '/': 5, '%': 5 | |
495 }; | |
496 static final UNARY_OPERATORS = | |
497 ['++', '--', '+', '-', '~', '!', 'typeof', 'void', 'delete'].toSet(); | |
498 | |
499 static final OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS = | |
500 ['typeof', 'void', 'delete', 'in', 'instanceof'].toSet(); | |
501 | |
502 static int category(int code) { | |
503 if (code >= CATEGORIES.length) return OTHER; | |
504 return CATEGORIES[code]; | |
505 } | |
506 | |
507 String getDelimited(int startPosition) { | |
508 position = startPosition; | |
509 int delimiter = src.codeUnitAt(startPosition); | |
510 int currentCode; | |
511 do { | |
512 position++; | |
513 if (position >= src.length) error("Unterminated literal"); | |
514 currentCode = src.codeUnitAt(position); | |
515 if (currentCode == charCodes.$LF) error("Unterminated literal"); | |
516 if (currentCode == charCodes.$BACKSLASH) { | |
517 if (++position >= src.length) error("Unterminated literal"); | |
518 int escaped = src.codeUnitAt(position); | |
519 if (escaped == charCodes.$x || escaped == charCodes.$X || | |
520 escaped == charCodes.$u || escaped == charCodes.$U || | |
521 category(escaped) == NUMERIC) { | |
522 error('Numeric and hex escapes are not allowed in literals'); | |
523 } | |
524 } | |
525 } while (currentCode != delimiter); | |
526 position++; | |
527 return src.substring(lastPosition, position); | |
528 } | |
529 | |
530 void getToken() { | |
531 skippedNewline = false; | |
532 for (;;) { | |
533 if (position >= src.length) break; | |
534 int code = src.codeUnitAt(position); | |
535 // Skip '//' and '/*' style comments. | |
536 if (code == charCodes.$SLASH && | |
537 position + 1 < src.length) { | |
538 if (src.codeUnitAt(position + 1) == charCodes.$SLASH) { | |
539 int nextPosition = src.indexOf('\n', position); | |
540 if (nextPosition == -1) nextPosition = src.length; | |
541 position = nextPosition; | |
542 continue; | |
543 } else if (src.codeUnitAt(position + 1) == charCodes.$STAR) { | |
544 int nextPosition = src.indexOf('*/', position + 2); | |
545 if (nextPosition == -1) error('Unterminated comment'); | |
546 position = nextPosition + 2; | |
547 continue; | |
548 } | |
549 } | |
550 if (category(code) != WHITESPACE) break; | |
551 if (code == charCodes.$LF) skippedNewline = true; | |
552 ++position; | |
553 } | |
554 | |
555 if (position == src.length) { | |
556 lastCategory = NONE; | |
557 lastToken = null; | |
558 lastPosition = position; | |
559 return; | |
560 } | |
561 int code = src.codeUnitAt(position); | |
562 lastPosition = position; | |
563 if (code == charCodes.$SQ || code == charCodes.$DQ) { | |
564 // String literal. | |
565 lastCategory = STRING; | |
566 lastToken = getDelimited(position); | |
567 } else if (code == charCodes.$0 && | |
568 position + 2 < src.length && | |
569 src.codeUnitAt(position + 1) == charCodes.$x) { | |
570 // Hex literal. | |
571 for (position += 2; position < src.length; position++) { | |
572 int cat = category(src.codeUnitAt(position)); | |
573 if (cat != NUMERIC && cat != ALPHA) break; | |
574 } | |
575 lastCategory = NUMERIC; | |
576 lastToken = src.substring(lastPosition, position); | |
577 int.parse(lastToken, onError: (_) { | |
578 error("Unparseable number"); | |
579 }); | |
580 } else if (code == charCodes.$SLASH) { | |
581 // Tokens that start with / are special due to regexp literals. | |
582 lastCategory = SYMBOL; | |
583 position++; | |
584 if (position < src.length && src.codeUnitAt(position) == charCodes.$EQ) { | |
585 position++; | |
586 } | |
587 lastToken = src.substring(lastPosition, position); | |
588 } else { | |
589 // All other tokens handled here. | |
590 int cat = category(src.codeUnitAt(position)); | |
591 int newCat; | |
592 do { | |
593 position++; | |
594 if (position == src.length) break; | |
595 int code = src.codeUnitAt(position); | |
596 // Special code to disallow ! and / in non-first position in token, so | |
597 // that !! parses as two tokens and != parses as one, while =/ parses | |
598 // as a an equals token followed by a regexp literal start. | |
599 newCat = (code == charCodes.$BANG || code == charCodes.$SLASH) | |
600 ? NONE | |
601 : category(code); | |
602 } while (!singleCharCategory(cat) && | |
603 (cat == newCat || | |
604 (cat == ALPHA && newCat == NUMERIC) || // eg. level42. | |
605 (cat == NUMERIC && newCat == DOT))); // eg. 3.1415 | |
606 lastCategory = cat; | |
607 lastToken = src.substring(lastPosition, position); | |
608 if (cat == NUMERIC) { | |
609 double.parse(lastToken, (_) { | |
610 error("Unparseable number"); | |
611 }); | |
612 } else if (cat == SYMBOL) { | |
613 int binaryPrecendence = BINARY_PRECEDENCE[lastToken]; | |
614 if (binaryPrecendence == null && !UNARY_OPERATORS.contains(lastToken)) { | |
615 error("Unknown operator"); | |
616 } | |
617 if (isAssignment(lastToken)) lastCategory = ASSIGNMENT; | |
618 } else if (cat == ALPHA) { | |
619 if (OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS.contains(lastToken)) { | |
620 lastCategory = SYMBOL; | |
621 } | |
622 } | |
623 } | |
624 } | |
625 | |
626 void expectCategory(int cat) { | |
627 if (cat != lastCategory) error("Expected ${categoryToString(cat)}"); | |
628 getToken(); | |
629 } | |
630 | |
631 bool acceptCategory(int cat) { | |
632 if (cat == lastCategory) { | |
633 getToken(); | |
634 return true; | |
635 } | |
636 return false; | |
637 } | |
638 | |
639 void expectSemicolon() { | |
640 if (acceptSemicolon()) return; | |
641 error('Expected SEMICOLON'); | |
642 } | |
643 | |
644 bool acceptSemicolon() { | |
645 // Accept semicolon or automatically inserted semicolon before close brace. | |
646 // Miniparser forbids other kinds of semicolon insertion. | |
647 if (RBRACE == lastCategory) return true; | |
648 if (NONE == lastCategory) return true; // end of input | |
649 if (skippedNewline) { | |
650 error('No automatic semicolon insertion at preceding newline'); | |
651 } | |
652 return acceptCategory(SEMICOLON); | |
653 } | |
654 | |
655 bool acceptString(String string) { | |
656 if (lastToken == string) { | |
657 getToken(); | |
658 return true; | |
659 } | |
660 return false; | |
661 } | |
662 | |
663 void error(message) { | |
664 throw new MiniJsParserError(this, message); | |
665 } | |
666 | |
667 Expression parsePrimary() { | |
668 String last = lastToken; | |
669 if (acceptCategory(ALPHA)) { | |
670 if (last == "true") { | |
671 return new LiteralBool(true); | |
672 } else if (last == "false") { | |
673 return new LiteralBool(false); | |
674 } else if (last == "null") { | |
675 return new LiteralNull(); | |
676 } else if (last == "function") { | |
677 return parseFunctionExpression(); | |
678 } else { | |
679 return new VariableUse(last); | |
680 } | |
681 } else if (acceptCategory(LPAREN)) { | |
682 Expression expression = parseExpression(); | |
683 expectCategory(RPAREN); | |
684 return expression; | |
685 } else if (acceptCategory(STRING)) { | |
686 return new LiteralString(last); | |
687 } else if (acceptCategory(NUMERIC)) { | |
688 return new LiteralNumber(last); | |
689 } else if (acceptCategory(LBRACE)) { | |
690 return parseObjectInitializer(); | |
691 } else if (acceptCategory(LSQUARE)) { | |
692 var values = <ArrayElement>[]; | |
693 if (!acceptCategory(RSQUARE)) { | |
694 do { | |
695 values.add(new ArrayElement(values.length, parseAssignment())); | |
696 } while (acceptCategory(COMMA)); | |
697 expectCategory(RSQUARE); | |
698 } | |
699 return new ArrayInitializer(values.length, values); | |
700 } else if (last != null && last.startsWith("/")) { | |
701 String regexp = getDelimited(lastPosition); | |
702 getToken(); | |
703 String flags = lastToken; | |
704 if (!acceptCategory(ALPHA)) flags = ""; | |
705 Expression expression = new RegExpLiteral(regexp + flags); | |
706 return expression; | |
707 } else if (acceptCategory(HASH)) { | |
708 InterpolatedExpression expression = | |
709 new InterpolatedExpression(interpolatedValues.length); | |
710 interpolatedValues.add(expression); | |
711 return expression; | |
712 } else { | |
713 error("Expected primary expression"); | |
714 return null; | |
715 } | |
716 } | |
717 | |
718 Expression parseFunctionExpression() { | |
719 String last = lastToken; | |
720 if (acceptCategory(ALPHA)) { | |
721 String functionName = last; | |
722 return new NamedFunction(new VariableDeclaration(functionName), | |
723 parseFun()); | |
724 } | |
725 return parseFun(); | |
726 } | |
727 | |
728 Expression parseFun() { | |
729 List<Parameter> params = <Parameter>[]; | |
730 | |
731 expectCategory(LPAREN); | |
732 if (!acceptCategory(RPAREN)) { | |
733 for (;;) { | |
734 if (acceptCategory(HASH)) { | |
735 InterpolatedParameter parameter = | |
736 new InterpolatedParameter(interpolatedValues.length); | |
737 interpolatedValues.add(parameter); | |
738 params.add(parameter); | |
739 } else { | |
740 String argumentName = lastToken; | |
741 expectCategory(ALPHA); | |
742 params.add(new Parameter(argumentName)); | |
743 } | |
744 if (acceptCategory(COMMA)) continue; | |
745 expectCategory(RPAREN); | |
746 break; | |
747 } | |
748 } | |
749 | |
750 expectCategory(LBRACE); | |
751 Block block = parseBlock(); | |
752 return new Fun(params, block); | |
753 } | |
754 | |
755 Expression parseObjectInitializer() { | |
756 List<Property> properties = <Property>[]; | |
757 for (;;) { | |
758 if (acceptCategory(RBRACE)) break; | |
759 // Limited subset: keys are identifiers, no 'get' or 'set' properties. | |
760 Literal propertyName; | |
761 String identifier = lastToken; | |
762 if (acceptCategory(ALPHA)) { | |
763 propertyName = new LiteralString('"$identifier"'); | |
764 } else if (acceptCategory(STRING)) { | |
765 propertyName = new LiteralString(identifier); | |
766 } else if (acceptCategory(SYMBOL)) { // e.g. void | |
767 propertyName = new LiteralString('"$identifier"'); | |
768 } else if (acceptCategory(HASH)) { | |
769 InterpolatedLiteral interpolatedLiteral = | |
770 new InterpolatedLiteral(interpolatedValues.length); | |
771 interpolatedValues.add(interpolatedLiteral); | |
772 propertyName = interpolatedLiteral; | |
773 } else { | |
774 error('Expected property name'); | |
775 } | |
776 expectCategory(COLON); | |
777 Expression value = parseAssignment(); | |
778 properties.add(new Property(propertyName, value)); | |
779 if (acceptCategory(RBRACE)) break; | |
780 expectCategory(COMMA); | |
781 } | |
782 return new ObjectInitializer(properties); | |
783 } | |
784 | |
785 Expression parseMember() { | |
786 Expression receiver = parsePrimary(); | |
787 while (true) { | |
788 if (acceptCategory(DOT)) { | |
789 receiver = getDotRhs(receiver); | |
790 } else if (acceptCategory(LSQUARE)) { | |
791 Expression inBraces = parseExpression(); | |
792 expectCategory(RSQUARE); | |
793 receiver = new PropertyAccess(receiver, inBraces); | |
794 } else { | |
795 break; | |
796 } | |
797 } | |
798 return receiver; | |
799 } | |
800 | |
801 Expression parseCall() { | |
802 bool constructor = acceptString("new"); | |
803 Expression receiver = parseMember(); | |
804 while (true) { | |
805 if (acceptCategory(LPAREN)) { | |
806 final arguments = <Expression>[]; | |
807 if (!acceptCategory(RPAREN)) { | |
808 while (true) { | |
809 Expression argument = parseAssignment(); | |
810 arguments.add(argument); | |
811 if (acceptCategory(RPAREN)) break; | |
812 expectCategory(COMMA); | |
813 } | |
814 } | |
815 receiver = constructor ? | |
816 new New(receiver, arguments) : | |
817 new Call(receiver, arguments); | |
818 constructor = false; | |
819 } else if (!constructor && acceptCategory(LSQUARE)) { | |
820 Expression inBraces = parseExpression(); | |
821 expectCategory(RSQUARE); | |
822 receiver = new PropertyAccess(receiver, inBraces); | |
823 } else if (!constructor && acceptCategory(DOT)) { | |
824 receiver = getDotRhs(receiver); | |
825 } else { | |
826 // JS allows new without (), but we don't. | |
827 if (constructor) error("Parentheses are required for new"); | |
828 break; | |
829 } | |
830 } | |
831 return receiver; | |
832 } | |
833 | |
834 Expression getDotRhs(Expression receiver) { | |
835 if (acceptCategory(HASH)) { | |
836 InterpolatedSelector property = | |
837 new InterpolatedSelector(interpolatedValues.length); | |
838 interpolatedValues.add(property); | |
839 return new PropertyAccess(receiver, property); | |
840 } | |
841 String identifier = lastToken; | |
842 // In ES5 keywords like delete and continue are allowed as property | |
843 // names, and the IndexedDB API uses that, so we need to allow it here. | |
844 if (acceptCategory(SYMBOL)) { | |
845 if (!OPERATORS_THAT_LOOK_LIKE_IDENTIFIERS.contains(identifier)) { | |
846 error("Expected alphanumeric identifier"); | |
847 } | |
848 } else { | |
849 expectCategory(ALPHA); | |
850 } | |
851 return new PropertyAccess.field(receiver, identifier); | |
852 } | |
853 | |
854 Expression parsePostfix() { | |
855 Expression expression = parseCall(); | |
856 String operator = lastToken; | |
857 // JavaScript grammar is: | |
858 // LeftHandSideExpression [no LineTerminator here] ++ | |
859 if (lastCategory == SYMBOL && | |
860 !skippedNewline && | |
861 (acceptString("++") || acceptString("--"))) { | |
862 return new Postfix(operator, expression); | |
863 } | |
864 // If we don't accept '++' or '--' due to skippedNewline a newline, no other | |
865 // part of the parser will accept the token and we will get an error at the | |
866 // whole expression level. | |
867 return expression; | |
868 } | |
869 | |
870 Expression parseUnaryHigh() { | |
871 String operator = lastToken; | |
872 if (lastCategory == SYMBOL && UNARY_OPERATORS.contains(operator) && | |
873 (acceptString("++") || acceptString("--"))) { | |
874 return new Prefix(operator, parsePostfix()); | |
875 } | |
876 return parsePostfix(); | |
877 } | |
878 | |
879 Expression parseUnaryLow() { | |
880 String operator = lastToken; | |
881 if (lastCategory == SYMBOL && UNARY_OPERATORS.contains(operator) && | |
882 operator != "++" && operator != "--") { | |
883 expectCategory(SYMBOL); | |
884 return new Prefix(operator, parseUnaryLow()); | |
885 } | |
886 return parseUnaryHigh(); | |
887 } | |
888 | |
889 Expression parseBinary(int maxPrecedence) { | |
890 Expression lhs = parseUnaryLow(); | |
891 int minPrecedence; | |
892 String lastSymbol; | |
893 Expression rhs; // This is null first time around. | |
894 while (true) { | |
895 String symbol = lastToken; | |
896 if (lastCategory != SYMBOL || | |
897 !BINARY_PRECEDENCE.containsKey(symbol) || | |
898 BINARY_PRECEDENCE[symbol] > maxPrecedence) { | |
899 break; | |
900 } | |
901 expectCategory(SYMBOL); | |
902 if (rhs == null || BINARY_PRECEDENCE[symbol] >= minPrecedence) { | |
903 if (rhs != null) lhs = new Binary(lastSymbol, lhs, rhs); | |
904 minPrecedence = BINARY_PRECEDENCE[symbol]; | |
905 rhs = parseUnaryLow(); | |
906 lastSymbol = symbol; | |
907 } else { | |
908 Expression higher = parseBinary(BINARY_PRECEDENCE[symbol]); | |
909 rhs = new Binary(symbol, rhs, higher); | |
910 } | |
911 } | |
912 if (rhs == null) return lhs; | |
913 return new Binary(lastSymbol, lhs, rhs); | |
914 } | |
915 | |
916 Expression parseConditional() { | |
917 Expression lhs = parseBinary(HIGHEST_PARSE_BINARY_PRECEDENCE); | |
918 if (!acceptCategory(QUERY)) return lhs; | |
919 Expression ifTrue = parseAssignment(); | |
920 expectCategory(COLON); | |
921 Expression ifFalse = parseAssignment(); | |
922 return new Conditional(lhs, ifTrue, ifFalse); | |
923 } | |
924 | |
925 | |
926 Expression parseAssignment() { | |
927 Expression lhs = parseConditional(); | |
928 String assignmentOperator = lastToken; | |
929 if (acceptCategory(ASSIGNMENT)) { | |
930 Expression rhs = parseAssignment(); | |
931 if (assignmentOperator == "=") { | |
932 return new Assignment(lhs, rhs); | |
933 } else { | |
934 // Handle +=, -=, etc. | |
935 String operator = | |
936 assignmentOperator.substring(0, assignmentOperator.length - 1); | |
937 return new Assignment.compound(lhs, operator, rhs); | |
938 } | |
939 } | |
940 return lhs; | |
941 } | |
942 | |
943 Expression parseExpression() { | |
944 Expression expression = parseAssignment(); | |
945 while (acceptCategory(COMMA)) { | |
946 Expression right = parseAssignment(); | |
947 expression = new Binary(',', expression, right); | |
948 } | |
949 return expression; | |
950 } | |
951 | |
952 VariableDeclarationList parseVariableDeclarationList() { | |
953 String firstVariable = lastToken; | |
954 expectCategory(ALPHA); | |
955 return finishVariableDeclarationList(firstVariable); | |
956 } | |
957 | |
958 VariableDeclarationList finishVariableDeclarationList(String firstVariable) { | |
959 var initialization = []; | |
960 | |
961 void declare(String variable) { | |
962 Expression initializer = null; | |
963 if (acceptString("=")) { | |
964 initializer = parseAssignment(); | |
965 } | |
966 var declaration = new VariableDeclaration(variable); | |
967 initialization.add(new VariableInitialization(declaration, initializer)); | |
968 } | |
969 | |
970 declare(firstVariable); | |
971 while (acceptCategory(COMMA)) { | |
972 String variable = lastToken; | |
973 expectCategory(ALPHA); | |
974 declare(variable); | |
975 } | |
976 return new VariableDeclarationList(initialization); | |
977 } | |
978 | |
979 Expression parseVarDeclarationOrExpression() { | |
980 if (acceptString("var")) { | |
981 return parseVariableDeclarationList(); | |
982 } else { | |
983 return parseExpression(); | |
984 } | |
985 } | |
986 | |
987 Expression expression() { | |
988 Expression expression = parseVarDeclarationOrExpression(); | |
989 if (lastCategory != NONE || position != src.length) { | |
990 error("Unparsed junk: ${categoryToString(lastCategory)}"); | |
991 } | |
992 return expression; | |
993 } | |
994 | |
995 Statement statement() { | |
996 Statement statement = parseStatement(); | |
997 if (lastCategory != NONE || position != src.length) { | |
998 error("Unparsed junk: ${categoryToString(lastCategory)}"); | |
999 } | |
1000 // TODO(sra): interpolated capture here? | |
1001 return statement; | |
1002 } | |
1003 | |
1004 Block parseBlock() { | |
1005 List<Statement> statements = <Statement>[]; | |
1006 | |
1007 while (!acceptCategory(RBRACE)) { | |
1008 Statement statement = parseStatement(); | |
1009 statements.add(statement); | |
1010 } | |
1011 return new Block(statements); | |
1012 } | |
1013 | |
1014 Statement parseStatement() { | |
1015 if (acceptCategory(LBRACE)) return parseBlock(); | |
1016 | |
1017 if (acceptCategory(SEMICOLON)) return new EmptyStatement(); | |
1018 | |
1019 if (lastCategory == ALPHA) { | |
1020 if (acceptString('return')) return parseReturn(); | |
1021 | |
1022 if (acceptString('throw')) return parseThrow(); | |
1023 | |
1024 if (acceptString('break')) { | |
1025 return parseBreakOrContinue((label) => new Break(label)); | |
1026 } | |
1027 | |
1028 if (acceptString('continue')) { | |
1029 return parseBreakOrContinue((label) => new Continue(label)); | |
1030 } | |
1031 | |
1032 if (acceptString('if')) return parseIfThenElse(); | |
1033 | |
1034 if (acceptString('for')) return parseFor(); | |
1035 | |
1036 if (acceptString('function')) return parseFunctionDeclaration(); | |
1037 | |
1038 if (acceptString('try')) return parseTry(); | |
1039 | |
1040 if (acceptString('var')) { | |
1041 Expression declarations = parseVariableDeclarationList(); | |
1042 expectSemicolon(); | |
1043 return new ExpressionStatement(declarations); | |
1044 } | |
1045 | |
1046 if (lastToken == 'case' || | |
1047 lastToken == 'do' || | |
1048 lastToken == 'while' || | |
1049 lastToken == 'switch' || | |
1050 lastToken == 'with') { | |
1051 error('Not implemented in mini parser'); | |
1052 } | |
1053 } | |
1054 | |
1055 | |
1056 // TODO: label: statement | |
1057 | |
1058 bool checkForInterpolatedStatement = lastCategory == HASH; | |
1059 | |
1060 Expression expression = parseExpression(); | |
1061 expectSemicolon(); | |
1062 | |
1063 if (checkForInterpolatedStatement) { | |
1064 // 'Promote' the interpolated expression `#;` to an interpolated | |
1065 // statement. | |
1066 if (expression is InterpolatedExpression) { | |
1067 assert(identical(interpolatedValues.last, expression)); | |
1068 InterpolatedStatement statement = | |
1069 new InterpolatedStatement(expression.name); | |
1070 interpolatedValues[interpolatedValues.length - 1] = statement; | |
1071 return statement; | |
1072 } | |
1073 } | |
1074 | |
1075 return new ExpressionStatement(expression); | |
1076 } | |
1077 | |
1078 Statement parseReturn() { | |
1079 if (acceptSemicolon()) return new Return(); | |
1080 Expression expression = parseExpression(); | |
1081 expectSemicolon(); | |
1082 return new Return(expression); | |
1083 } | |
1084 | |
1085 Statement parseThrow() { | |
1086 if (skippedNewline) error('throw expression must be on same line'); | |
1087 Expression expression = parseExpression(); | |
1088 expectSemicolon(); | |
1089 return new Throw(expression); | |
1090 } | |
1091 | |
1092 Statement parseBreakOrContinue(constructor) { | |
1093 var identifier = lastToken; | |
1094 if (!skippedNewline && acceptCategory(ALPHA)) { | |
1095 expectSemicolon(); | |
1096 return constructor(identifier); | |
1097 } | |
1098 expectSemicolon(); | |
1099 return constructor(null); | |
1100 } | |
1101 | |
1102 Statement parseIfThenElse() { | |
1103 expectCategory(LPAREN); | |
1104 Expression condition = parseExpression(); | |
1105 expectCategory(RPAREN); | |
1106 Statement thenStatement = parseStatement(); | |
1107 if (acceptString('else')) { | |
1108 // Resolves dangling else by binding 'else' to closest 'if'. | |
1109 Statement elseStatement = parseStatement(); | |
1110 return new If(condition, thenStatement, elseStatement); | |
1111 } else { | |
1112 return new If.noElse(condition, thenStatement); | |
1113 } | |
1114 } | |
1115 | |
1116 Statement parseFor() { | |
1117 // For-init-condition-increment style loops are fully supported. | |
1118 // | |
1119 // Only one for-in variant is currently implemented: | |
1120 // | |
1121 // for (var variable in Expression) Statement | |
1122 // | |
1123 Statement finishFor(Expression init) { | |
1124 Expression condition = null; | |
1125 if (!acceptCategory(SEMICOLON)) { | |
1126 condition = parseExpression(); | |
1127 expectCategory(SEMICOLON); | |
1128 } | |
1129 Expression update = null; | |
1130 if (!acceptCategory(RPAREN)) { | |
1131 update = parseExpression(); | |
1132 expectCategory(RPAREN); | |
1133 } | |
1134 Statement body = parseStatement(); | |
1135 return new For(init, condition, update, body); | |
1136 } | |
1137 | |
1138 expectCategory(LPAREN); | |
1139 if (acceptCategory(SEMICOLON)) { | |
1140 return finishFor(null); | |
1141 } | |
1142 | |
1143 if (acceptString('var')) { | |
1144 String identifier = lastToken; | |
1145 expectCategory(ALPHA); | |
1146 if (acceptString('in')) { | |
1147 Expression objectExpression = parseExpression(); | |
1148 expectCategory(RPAREN); | |
1149 Statement body = parseStatement(); | |
1150 return new ForIn( | |
1151 new VariableDeclarationList([ | |
1152 new VariableInitialization( | |
1153 new VariableDeclaration(identifier), null)]), | |
1154 objectExpression, | |
1155 body); | |
1156 } | |
1157 Expression declarations = finishVariableDeclarationList(identifier); | |
1158 expectCategory(SEMICOLON); | |
1159 return finishFor(declarations); | |
1160 } | |
1161 | |
1162 Expression init = parseExpression(); | |
1163 expectCategory(SEMICOLON); | |
1164 return finishFor(init); | |
1165 } | |
1166 | |
1167 Statement parseFunctionDeclaration() { | |
1168 String name = lastToken; | |
1169 expectCategory(ALPHA); | |
1170 Expression fun = parseFun(); | |
1171 return new FunctionDeclaration(new VariableDeclaration(name), fun); | |
1172 } | |
1173 | |
1174 Statement parseTry() { | |
1175 expectCategory(LBRACE); | |
1176 Block body = parseBlock(); | |
1177 String token = lastToken; | |
1178 Catch catchPart = null; | |
1179 if (acceptString('catch')) catchPart = parseCatch(); | |
1180 Block finallyPart = null; | |
1181 if (acceptString('finally')) { | |
1182 expectCategory(LBRACE); | |
1183 finallyPart = parseBlock(); | |
1184 } else { | |
1185 if (catchPart == null) error("expected 'finally'"); | |
1186 } | |
1187 return new Try(body, catchPart, finallyPart); | |
1188 } | |
1189 | |
1190 Catch parseCatch() { | |
1191 expectCategory(LPAREN); | |
1192 String identifier = lastToken; | |
1193 expectCategory(ALPHA); | |
1194 expectCategory(RPAREN); | |
1195 expectCategory(LBRACE); | |
1196 Block body = parseBlock(); | |
1197 return new Catch(new VariableDeclaration(identifier), body); | |
1198 } | |
1199 } | |
OLD | NEW |