Skip to content

Commit 1863d50

Browse files
committed
feat(parser): adds support for variable bindings
1 parent a3d9f0f commit 1863d50

File tree

7 files changed

+155
-19
lines changed

7 files changed

+155
-19
lines changed

modules/change_detection/src/parser/ast.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {FIELD, autoConvertAdd, isBlank, isPresent, FunctionWrapper, BaseException} from "facade/lang";
22
import {List, Map, ListWrapper, MapWrapper} from "facade/collection";
3+
import {ContextWithVariableBindings} from "./context_with_variable_bindings";
34

45
export class AST {
56
eval(context) {
@@ -97,15 +98,33 @@ export class AccessMember extends AST {
9798
}
9899

99100
eval(context) {
100-
return this.getter(this.receiver.eval(context));
101+
var evaluatedContext = this.receiver.eval(context);
102+
103+
while (evaluatedContext instanceof ContextWithVariableBindings) {
104+
if (evaluatedContext.hasBinding(this.name)) {
105+
return evaluatedContext.get(this.name);
106+
}
107+
evaluatedContext = evaluatedContext.parent;
108+
}
109+
110+
return this.getter(evaluatedContext);
101111
}
102112

103113
get isAssignable() {
104114
return true;
105115
}
106116

107117
assign(context, value) {
108-
return this.setter(this.receiver.eval(context), value);
118+
var evaluatedContext = this.receiver.eval(context);
119+
120+
while (evaluatedContext instanceof ContextWithVariableBindings) {
121+
if (evaluatedContext.hasBinding(this.name)) {
122+
throw new BaseException(`Cannot reassign a variable binding ${this.name}`)
123+
}
124+
evaluatedContext = evaluatedContext.parent;
125+
}
126+
127+
return this.setter(evaluatedContext, value);
109128
}
110129

111130
visit(visitor, args) {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {MapWrapper} from 'facade/collection';
2+
3+
export class ContextWithVariableBindings {
4+
parent:any;
5+
/// varBindings are read-only. updating/adding keys is not supported.
6+
varBindings:Map;
7+
8+
constructor(parent:any, varBindings:Map) {
9+
this.parent = parent;
10+
this.varBindings = varBindings;
11+
}
12+
13+
hasBinding(name:string):boolean {
14+
return MapWrapper.contains(this.varBindings, name);
15+
}
16+
17+
get(name:string) {
18+
return MapWrapper.get(this.varBindings, name);
19+
}
20+
}

modules/change_detection/src/record.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class ProtoRecord {
3030
context:any;
3131
funcOrValue:any;
3232
arity:int;
33+
name:string;
3334
dest;
3435

3536
next:ProtoRecord;
@@ -39,12 +40,14 @@ export class ProtoRecord {
3940
mode:int,
4041
funcOrValue,
4142
arity:int,
43+
name:string,
4244
dest) {
4345

4446
this.recordRange = recordRange;
4547
this._mode = mode;
4648
this.funcOrValue = funcOrValue;
4749
this.arity = arity;
50+
this.name = name;
4851
this.dest = dest;
4952

5053
this.next = null;

modules/change_detection/src/record_range.js

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {List, Map, ListWrapper, MapWrapper} from 'facade/collection';
1515
import {AST, AccessMember, ImplicitReceiver, AstVisitor, LiteralPrimitive,
1616
Binary, Formatter, MethodCall, FunctionCall, PrefixNot, Conditional,
1717
LiteralArray, LiteralMap, KeyedAccess, Chain, Assignment} from './parser/ast';
18-
18+
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
1919

2020
export class ProtoRecordRange {
2121
headRecord:ProtoRecord;
@@ -304,10 +304,35 @@ export class RecordRange {
304304
for (var record:Record = this.headRecord;
305305
record != null;
306306
record = record.next) {
307+
307308
if (record.isImplicitReceiver) {
308-
record.updateContext(context);
309+
this._setContextForRecord(context, record);
310+
}
311+
}
312+
}
313+
314+
_setContextForRecord(context, record:Record) {
315+
var proto = record.protoRecord;
316+
317+
while (context instanceof ContextWithVariableBindings) {
318+
if (context.hasBinding(proto.name)) {
319+
this._setVarBindingGetter(context, record, proto);
320+
return;
309321
}
322+
context = context.parent;
310323
}
324+
325+
this._setRegularGetter(context, record, proto);
326+
}
327+
328+
_setVarBindingGetter(context, record:Record, proto:ProtoRecord) {
329+
record.funcOrValue = _mapGetter(proto.name);
330+
record.updateContext(context.varBindings);
331+
}
332+
333+
_setRegularGetter(context, record:Record, proto:ProtoRecord) {
334+
record.funcOrValue = proto.funcOrValue;
335+
record.updateContext(context);
311336
}
312337
}
313338

@@ -353,25 +378,25 @@ class ProtoRecordCreator {
353378
}
354379

355380
visitLiteralPrimitive(ast:LiteralPrimitive, dest) {
356-
this.add(this.construct(RECORD_TYPE_CONST, ast.value, 0, dest));
381+
this.add(this.construct(RECORD_TYPE_CONST, ast.value, 0, null, dest));
357382
}
358383

359384
visitBinary(ast:Binary, dest) {
360385
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION,
361-
_operationToFunction(ast.operation), 2, dest);
386+
_operationToFunction(ast.operation), 2, null, dest);
362387
ast.left.visit(this, new Destination(record, 0));
363388
ast.right.visit(this, new Destination(record, 1));
364389
this.add(record);
365390
}
366391

367392
visitPrefixNot(ast:PrefixNot, dest) {
368-
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _operation_negate, 1, dest);
393+
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _operation_negate, 1, null, dest);
369394
ast.expression.visit(this, new Destination(record, 0));
370395
this.add(record);
371396
}
372397

373398
visitAccessMember(ast:AccessMember, dest) {
374-
var record = this.construct(RECORD_TYPE_PROPERTY, ast.getter, 0, dest);
399+
var record = this.construct(RECORD_TYPE_PROPERTY, ast.getter, 0, ast.name, dest);
375400
if (ast.receiver instanceof ImplicitReceiver) {
376401
record.setIsImplicitReceiver();
377402
} else {
@@ -381,15 +406,15 @@ class ProtoRecordCreator {
381406
}
382407

383408
visitFormatter(ast:Formatter, dest) {
384-
var record = this.construct(RECORD_TYPE_INVOKE_FORMATTER, ast.name, ast.allArgs.length, dest);
409+
var record = this.construct(RECORD_TYPE_INVOKE_FORMATTER, ast.name, ast.allArgs.length, null, dest);
385410
for (var i = 0; i < ast.allArgs.length; ++i) {
386411
ast.allArgs[i].visit(this, new Destination(record, i));
387412
}
388413
this.add(record);
389414
}
390415

391416
visitMethodCall(ast:MethodCall, dest) {
392-
var record = this.construct(RECORD_TYPE_INVOKE_METHOD, ast.fn, ast.args.length, dest);
417+
var record = this.construct(RECORD_TYPE_INVOKE_METHOD, ast.fn, ast.args.length, null, dest);
393418
for (var i = 0; i < ast.args.length; ++i) {
394419
ast.args[i].visit(this, new Destination(record, i));
395420
}
@@ -402,7 +427,7 @@ class ProtoRecordCreator {
402427
}
403428

404429
visitFunctionCall(ast:FunctionCall, dest) {
405-
var record = this.construct(RECORD_TYPE_INVOKE_CLOSURE, null, ast.args.length, dest);
430+
var record = this.construct(RECORD_TYPE_INVOKE_CLOSURE, null, ast.args.length, null, dest);
406431
ast.target.visit(this, new Destination(record, null));
407432
for (var i = 0; i < ast.args.length; ++i) {
408433
ast.args[i].visit(this, new Destination(record, i));
@@ -411,7 +436,7 @@ class ProtoRecordCreator {
411436
}
412437

413438
visitConditional(ast:Conditional, dest) {
414-
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _cond, 3, dest);
439+
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _cond, 3, null, dest);
415440
ast.condition.visit(this, new Destination(record, 0));
416441
ast.trueExp.visit(this, new Destination(record, 1));
417442
ast.falseExp.visit(this, new Destination(record, 2));
@@ -422,7 +447,7 @@ class ProtoRecordCreator {
422447

423448
visitLiteralArray(ast:LiteralArray, dest) {
424449
var length = ast.expressions.length;
425-
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _arrayFn(length), length, dest);
450+
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _arrayFn(length), length, null, dest);
426451
for (var i = 0; i < length; ++i) {
427452
ast.expressions[i].visit(this, new Destination(record, i));
428453
}
@@ -431,7 +456,7 @@ class ProtoRecordCreator {
431456

432457
visitLiteralMap(ast:LiteralMap, dest) {
433458
var length = ast.values.length;
434-
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _mapFn(ast.keys, length), length, dest);
459+
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _mapFn(ast.keys, length), length, null, dest);
435460
for (var i = 0; i < length; ++i) {
436461
ast.values[i].visit(this, new Destination(record, i));
437462
}
@@ -448,8 +473,8 @@ class ProtoRecordCreator {
448473
ast.visit(this, memento);
449474
}
450475

451-
construct(recordType, funcOrValue, arity, dest) {
452-
return new ProtoRecord(this.protoRecordRange, recordType, funcOrValue, arity, dest);
476+
construct(recordType, funcOrValue, arity, name, dest) {
477+
return new ProtoRecord(this.protoRecordRange, recordType, funcOrValue, arity, name, dest);
453478
}
454479

455480
add(protoRecord:ProtoRecord) {
@@ -540,3 +565,10 @@ function _mapFn(keys:List, length:int) {
540565
default: throw new BaseException(`Does not support literal maps with more than 9 elements`);
541566
}
542567
}
568+
569+
//TODO: cache the getters
570+
function _mapGetter(key) {
571+
return function(map) {
572+
return MapWrapper.get(map, key);
573+
}
574+
}

modules/change_detection/test/change_detector_spec.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {ddescribe, describe, it, iit, xit, expect} from 'test_lib/test_lib';
22

33
import {isPresent} from 'facade/lang';
44
import {List, ListWrapper, MapWrapper} from 'facade/collection';
5+
import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings';
56
import {Parser} from 'change_detection/parser/parser';
67
import {Lexer} from 'change_detection/parser/lexer';
78

@@ -183,6 +184,34 @@ export function main() {
183184
expect(counter).toEqual(2);
184185
});
185186
});
187+
188+
describe("ContextWithVariableBindings", () => {
189+
it('should read a field from ContextWithVariableBindings', () => {
190+
var locals = new ContextWithVariableBindings(null,
191+
MapWrapper.createFromPairs([["key", "value"]]));
192+
193+
expect(executeWatch('key', 'key', locals))
194+
.toEqual(['key=value']);
195+
});
196+
197+
it('should handle nested ContextWithVariableBindings', () => {
198+
var nested = new ContextWithVariableBindings(null,
199+
MapWrapper.createFromPairs([["key", "value"]]));
200+
var locals = new ContextWithVariableBindings(nested, MapWrapper.create());
201+
202+
expect(executeWatch('key', 'key', locals))
203+
.toEqual(['key=value']);
204+
});
205+
206+
it("should fall back to a regular field read when ContextWithVariableBindings " +
207+
"does not have the requested field", () => {
208+
var locals = new ContextWithVariableBindings(new Person("Jim"),
209+
MapWrapper.createFromPairs([["key", "value"]]));
210+
211+
expect(executeWatch('name', 'name', locals))
212+
.toEqual(['name=Jim']);
213+
});
214+
});
186215
});
187216
});
188217
}

modules/change_detection/test/parser/parser_spec.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {reflector} from 'reflection/reflection';
44
import {MapWrapper, ListWrapper} from 'facade/collection';
55
import {Parser} from 'change_detection/parser/parser';
66
import {Lexer} from 'change_detection/parser/lexer';
7+
import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings';
78
import {Formatter, LiteralPrimitive} from 'change_detection/parser/ast';
89

910
class TestData {
@@ -51,8 +52,9 @@ export function main() {
5152
return expect(parseAction(text).eval(c));
5253
}
5354

54-
function expectEvalError(text) {
55-
return expect(() => parseAction(text).eval(td()));
55+
function expectEvalError(text, passedInContext = null) {
56+
var c = isBlank(passedInContext) ? td() : passedInContext;
57+
return expect(() => parseAction(text).eval(c));
5658
}
5759

5860
function evalAsts(asts, passedInContext = null) {
@@ -196,6 +198,25 @@ export function main() {
196198
expectEvalError('x. 1234').toThrowError(new RegExp('identifier or keyword'));
197199
expectEvalError('x."foo"').toThrowError(new RegExp('identifier or keyword'));
198200
});
201+
202+
it("should read a field from ContextWithVariableBindings", () => {
203+
var locals = new ContextWithVariableBindings(null,
204+
MapWrapper.createFromPairs([["key", "value"]]));
205+
expectEval("key", locals).toEqual("value");
206+
});
207+
208+
it("should handle nested ContextWithVariableBindings", () => {
209+
var nested = new ContextWithVariableBindings(null,
210+
MapWrapper.createFromPairs([["key", "value"]]));
211+
var locals = new ContextWithVariableBindings(nested, MapWrapper.create());
212+
expectEval("key", locals).toEqual("value");
213+
});
214+
215+
it("should fall back to a regular field read when ContextWithVariableBindings "+
216+
"does not have the requested field", () => {
217+
var locals = new ContextWithVariableBindings(td(999), MapWrapper.create());
218+
expectEval("a", locals).toEqual(999);
219+
});
199220
});
200221

201222
describe("method calls", () => {
@@ -284,6 +305,18 @@ export function main() {
284305
it('should throw on bad assignment', () => {
285306
expectEvalError("5=4").toThrowError(new RegExp("Expression 5 is not assignable"));
286307
});
308+
309+
it('should reassign when no variable binding with the given name', () => {
310+
var context = td();
311+
var locals = new ContextWithVariableBindings(context, MapWrapper.create());
312+
expectEval('a = 200', locals).toEqual(200);
313+
expect(context.a).toEqual(200);
314+
});
315+
316+
it('should throw when reassigning a variable binding', () => {
317+
var locals = new ContextWithVariableBindings(null, MapWrapper.createFromPairs([["key", "value"]]));
318+
expectEvalError('key = 200', locals).toThrowError(new RegExp("Cannot reassign a variable binding"));
319+
});
287320
});
288321

289322
describe("general error handling", () => {

modules/change_detection/test/record_range_spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function main() {
4646
}
4747

4848
function createRecord(rr) {
49-
return new Record(rr, new ProtoRecord(null, 0, null, null, null), null);
49+
return new Record(rr, new ProtoRecord(null, 0, null, null, null, null), null);
5050
}
5151

5252
describe('record range', () => {

0 commit comments

Comments
 (0)