Skip to content

Commit 8cc008b

Browse files
committed
feat(Parser): add support for assignments
1 parent 8e6326f commit 8cc008b

File tree

7 files changed

+159
-65
lines changed

7 files changed

+159
-65
lines changed

modules/change_detection/src/parser/ast.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export class AST {
66
throw new BaseException("Not supported");
77
}
88

9+
assign(context, value) {
10+
throw new BaseException("Not supported");
11+
}
12+
913
visit(visitor) {
1014
}
1115
}
@@ -20,6 +24,22 @@ export class ImplicitReceiver extends AST {
2024
}
2125
}
2226

27+
export class Chain extends AST {
28+
constructor(expressions:List) {
29+
this.expressions = expressions;
30+
}
31+
32+
eval(context) {
33+
var result;
34+
for (var i = 0; i < this.expressions.length; i++) {
35+
var last = this.expressions[i].eval(context);
36+
if (last != null) result = last;
37+
}
38+
return result;
39+
}
40+
}
41+
42+
2343
export class Conditional extends AST {
2444
@FIELD('final condition:AST')
2545
@FIELD('final trueExp:AST')
@@ -43,21 +63,34 @@ export class FieldRead extends AST {
4363
@FIELD('final receiver:AST')
4464
@FIELD('final name:string')
4565
@FIELD('final getter:Function')
46-
constructor(receiver:AST, name:string, getter:Function) {
66+
@FIELD('final setter:Function')
67+
constructor(receiver:AST, name:string, getter:Function, setter:Function) {
4768
this.receiver = receiver;
4869
this.name = name;
4970
this.getter = getter;
71+
this.setter = setter;
5072
}
5173

5274
eval(context) {
5375
return this.getter(this.receiver.eval(context));
5476
}
5577

78+
assign(context, value) {
79+
return this.setter(this.receiver.eval(context), value);
80+
}
81+
5682
visit(visitor) {
5783
visitor.visitFieldRead(this);
5884
}
5985
}
6086

87+
export class KeyedAccess extends AST {
88+
constructor(obj:AST, key:AST) {
89+
this.obj = obj;
90+
this.key = key;
91+
}
92+
}
93+
6194
export class Formatter extends AST {
6295
@FIELD('final exp:AST')
6396
@FIELD('final name:string')
@@ -147,6 +180,20 @@ export class PrefixNot extends AST {
147180
}
148181
}
149182

183+
export class Assignment extends AST {
184+
@FIELD('final target:AST')
185+
@FIELD('final value:AST')
186+
constructor(target:AST, value:AST) {
187+
this.target = target;
188+
this.value = value;
189+
}
190+
visit(visitor) { visitor.visitAssignment(this); }
191+
192+
eval(context) {
193+
return this.target.assign(context, this.value.eval(context));
194+
}
195+
}
196+
150197
//INTERFACE
151198
export class AstVisitor {
152199
visitImplicitReceiver(ast:ImplicitReceiver) {}
@@ -155,6 +202,7 @@ export class AstVisitor {
155202
visitPrefixNot(ast:PrefixNot) {}
156203
visitLiteralPrimitive(ast:LiteralPrimitive) {}
157204
visitFormatter(ast:Formatter) {}
205+
visitAssignment(ast:Assignment) {}
158206
}
159207

160208
var _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]];

modules/change_detection/src/parser/closure_map.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,9 @@ class ClosureMap {
77
var symbol = new Symbol(name);
88
return (receiver) => reflect(receiver).getField(symbol).reflectee;
99
}
10+
11+
Function setter(String name) {
12+
var symbol = new Symbol(name);
13+
return (receiver, value) => reflect(receiver).setField(symbol, value).reflectee;
14+
}
1015
}

modules/change_detection/src/parser/closure_map.es6

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@ export class ClosureMap {
44
getter(name:string) {
55
return new Function('o', 'return o.' + name + ';');
66
}
7+
8+
setter(name:string) {
9+
return new Function('o', 'v', 'return o.' + name + ' = v;');
10+
}
711
}

modules/change_detection/src/parser/parser.js

Lines changed: 63 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {FIELD, int, isBlank} from 'facade/lang';
1+
import {FIELD, int, isBlank, BaseException} from 'facade/lang';
22
import {ListWrapper, List} from 'facade/collection';
3-
import {Lexer, EOF, Token, $PERIOD, $COLON, $SEMICOLON} from './lexer';
3+
import {Lexer, EOF, Token, $PERIOD, $COLON, $SEMICOLON, $LBRACKET, $RBRACKET} from './lexer';
44
import {ClosureMap} from './closure_map';
55
import {
66
AST,
@@ -11,7 +11,10 @@ import {
1111
Binary,
1212
PrefixNot,
1313
Conditional,
14-
Formatter
14+
Formatter,
15+
Assignment,
16+
Chain,
17+
KeyedAccess
1518
} from './ast';
1619

1720
var _implicitReceiver = new ImplicitReceiver();
@@ -75,6 +78,12 @@ class _ParseAST {
7578
}
7679
}
7780

81+
expectCharacter(code:int) {
82+
if (this.optionalCharacter(code)) return;
83+
this.error(`Missing expected ${code}`);
84+
}
85+
86+
7887
optionalOperator(op:string):boolean {
7988
if (this.next.isOperator(op)) {
8089
this.advance();
@@ -84,6 +93,20 @@ class _ParseAST {
8493
}
8594
}
8695

96+
expectOperator(operator:string) {
97+
if (this.optionalOperator(operator)) return;
98+
this.error(`Missing expected operator ${operator}`);
99+
}
100+
101+
expectIdentifierOrKeyword():string {
102+
var n = this.next;
103+
if (!n.isIdentifier() && !n.isKeyword()) {
104+
this.error(`Unexpected token ${n}, expected identifier or keyword`)
105+
}
106+
this.advance();
107+
return n.toString();
108+
}
109+
87110
parseChain():AST {
88111
var exprs = [];
89112
while (this.index < this.tokens.length) {
@@ -96,7 +119,7 @@ class _ParseAST {
96119
}
97120
}
98121
}
99-
return ListWrapper.first(exprs);
122+
return exprs.length == 1 ? exprs[0] : new Chain(exprs);
100123
}
101124

102125
parseFormatter() {
@@ -105,7 +128,7 @@ class _ParseAST {
105128
if (this.parseAction) {
106129
this.error("Cannot have a formatter in an action expression");
107130
}
108-
var name = this.parseIdentifier();
131+
var name = this.expectIdentifierOrKeyword();
109132
var args = ListWrapper.create();
110133
while (this.optionalCharacter($COLON)) {
111134
ListWrapper.push(args, this.parseExpression());
@@ -116,7 +139,19 @@ class _ParseAST {
116139
}
117140

118141
parseExpression() {
119-
return this.parseConditional();
142+
var result = this.parseConditional();
143+
144+
while (this.next.isOperator('=')) {
145+
//if (!backend.isAssignable(result)) {
146+
// int end = (index < tokens.length) ? next.index : input.length;
147+
// String expression = input.substring(start, end);
148+
// error('Expression $expression is not assignable');
149+
// }
150+
this.expectOperator('=');
151+
result = new Assignment(result, this.parseConditional());
152+
}
153+
154+
return result;
120155
}
121156

122157
parseConditional() {
@@ -202,7 +237,7 @@ class _ParseAST {
202237
}
203238

204239
parseMultiplicative() {
205-
// '*', '%', '/', '~/'
240+
// '*', '%', '/'
206241
var result = this.parsePrefix();
207242
while (true) {
208243
if (this.optionalOperator('*')) {
@@ -211,9 +246,6 @@ class _ParseAST {
211246
result = new Binary('%', result, this.parsePrefix());
212247
} else if (this.optionalOperator('/')) {
213248
result = new Binary('/', result, this.parsePrefix());
214-
// TODO(rado): This exists only in Dart, figure out whether to support it.
215-
// } else if (this.optionalOperator('~/')) {
216-
// result = new BinaryTruncatingDivide(result, this.parsePrefix());
217249
} else {
218250
return result;
219251
}
@@ -232,59 +264,54 @@ class _ParseAST {
232264
}
233265
}
234266

235-
parseAccessOrCallMember() {
267+
parseAccessOrCallMember():AST {
236268
var result = this.parsePrimary();
237-
// TODO: add missing cases.
238-
return result;
269+
while (true) {
270+
if (this.optionalCharacter($PERIOD)) {
271+
result = this.parseFieldRead(result);
272+
} else {
273+
return result;
274+
}
275+
}
239276
}
240277

241278
parsePrimary() {
242279
if (this.next.isKeywordNull() || this.next.isKeywordUndefined()) {
243280
this.advance();
244281
return new LiteralPrimitive(null);
282+
245283
} else if (this.next.isKeywordTrue()) {
246284
this.advance();
247285
return new LiteralPrimitive(true);
286+
248287
} else if (this.next.isKeywordFalse()) {
249288
this.advance();
250289
return new LiteralPrimitive(false);
290+
251291
} else if (this.next.isIdentifier()) {
252-
return this.parseAccess();
292+
return this.parseFieldRead(_implicitReceiver);
293+
253294
} else if (this.next.isNumber()) {
254295
var value = this.next.toNumber();
255296
this.advance();
256297
return new LiteralPrimitive(value);
298+
257299
} else if (this.next.isString()) {
258300
var value = this.next.toString();
259301
this.advance();
260302
return new LiteralPrimitive(value);
303+
261304
} else if (this.index >= this.tokens.length) {
262-
throw `Unexpected end of expression: ${this.input}`;
263-
} else {
264-
throw `Unexpected token ${this.next}`;
265-
}
266-
}
305+
this.error(`Unexpected end of expression: ${this.input}`);
267306

268-
parseAccess():AST {
269-
var result = this.parseFieldRead(_implicitReceiver);
270-
while(this.optionalCharacter($PERIOD)) {
271-
result = this.parseFieldRead(result);
307+
} else {
308+
this.error(`Unexpected token ${this.next}`);
272309
}
273-
return result;
274310
}
275311

276312
parseFieldRead(receiver):AST {
277-
var id = this.parseIdentifier();
278-
return new FieldRead(receiver, id, this.closureMap.getter(id));
279-
}
280-
281-
parseIdentifier():string {
282-
var n = this.next;
283-
if (!n.isIdentifier() && !n.isKeyword()) {
284-
this.error(`Unexpected token ${n}, expected identifier or keyword`)
285-
}
286-
this.advance();
287-
return n.toString();
313+
var id = this.expectIdentifierOrKeyword();
314+
return new FieldRead(receiver, id, this.closureMap.getter(id), this.closureMap.setter(id));
288315
}
289316

290317
error(message:string, index:int = null) {
@@ -294,16 +321,6 @@ class _ParseAST {
294321
? `at column ${this.tokens[index].index + 1} in`
295322
: `at the end of the expression`;
296323

297-
throw new ParserError(`Parser Error: ${message} ${location} [${this.input}]`);
298-
}
299-
}
300-
301-
class ParserError extends Error {
302-
constructor(message) {
303-
this.message = message;
304-
}
305-
306-
toString() {
307-
return this.message;
324+
throw new BaseException(`Parser Error: ${message} ${location} [${this.input}]`);
308325
}
309326
}

modules/change_detection/test/change_detector_spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function main() {
1919
var parts = exp.split(".");
2020
var cm = new ClosureMap();
2121
return ListWrapper.reduce(parts, function (ast, fieldName) {
22-
return new FieldRead(ast, fieldName, cm.getter(fieldName));
22+
return new FieldRead(ast, fieldName, cm.getter(fieldName), cm.setter(fieldName));
2323
}, new ImplicitReceiver());
2424
}
2525

modules/change_detection/test/parser/parser_spec.js

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -150,25 +150,43 @@ export function main() {
150150
createParser().parseAction('boo').eval(new ContextWithErrors());
151151
}).toThrowError('boo to you');
152152
});
153-
});
154153

155-
describe("parseBinding", () => {
156-
it("should parse formatters", function () {
157-
var exp = parseBinding("'Foo'|uppercase");
158-
expect(exp).toBeAnInstanceOf(Formatter);
159-
expect(exp.name).toEqual("uppercase");
160-
});
154+
it('should evaluate assignments', () => {
155+
var context = td();
156+
expectEval("a=12", context).toEqual(12);
157+
expect(context.a).toEqual(12);
161158

162-
it("should parse formatters with args", function () {
163-
var exp = parseBinding("1|increment:2");
164-
expect(exp).toBeAnInstanceOf(Formatter);
165-
expect(exp.name).toEqual("increment");
166-
expect(exp.args[0]).toBeAnInstanceOf(LiteralPrimitive);
159+
context = td(td(td()));
160+
expectEval("a.a.a=123;", context).toEqual(123);
161+
expect(context.a.a.a).toEqual(123);
162+
163+
context = td();
164+
expectEval("a=123; b=234", context).toEqual(234);
165+
expect(context.a).toEqual(123);
166+
expect(context.b).toEqual(234);
167167
});
168168

169-
it('should throw on chain expressions', () => {
170-
expect(() => parseBinding("1;2")).toThrowError(new RegExp("contain chained expression"));
169+
describe("parseBinding", () => {
170+
//throw on assignment
171+
172+
it("should parse formatters", function () {
173+
var exp = parseBinding("'Foo'|uppercase");
174+
expect(exp).toBeAnInstanceOf(Formatter);
175+
expect(exp.name).toEqual("uppercase");
176+
});
177+
178+
it("should parse formatters with args", function () {
179+
var exp = parseBinding("1|increment:2");
180+
expect(exp).toBeAnInstanceOf(Formatter);
181+
expect(exp.name).toEqual("increment");
182+
expect(exp.args[0]).toBeAnInstanceOf(LiteralPrimitive);
183+
});
184+
185+
it('should throw on chain expressions', () => {
186+
expect(() => parseBinding("1;2")).toThrowError(new RegExp("contain chained expression"));
187+
});
171188
});
172189
});
173190
});
174191
}
192+

0 commit comments

Comments
 (0)