feat(parser): adds support for variable bindings

This commit is contained in:
vsavkin 2014-11-26 09:44:31 -08:00
parent a3d9f0fead
commit 1863d50978
7 changed files with 155 additions and 19 deletions

View File

@ -1,5 +1,6 @@
import {FIELD, autoConvertAdd, isBlank, isPresent, FunctionWrapper, BaseException} from "facade/lang"; import {FIELD, autoConvertAdd, isBlank, isPresent, FunctionWrapper, BaseException} from "facade/lang";
import {List, Map, ListWrapper, MapWrapper} from "facade/collection"; import {List, Map, ListWrapper, MapWrapper} from "facade/collection";
import {ContextWithVariableBindings} from "./context_with_variable_bindings";
export class AST { export class AST {
eval(context) { eval(context) {
@ -97,7 +98,16 @@ export class AccessMember extends AST {
} }
eval(context) { eval(context) {
return this.getter(this.receiver.eval(context)); var evaluatedContext = this.receiver.eval(context);
while (evaluatedContext instanceof ContextWithVariableBindings) {
if (evaluatedContext.hasBinding(this.name)) {
return evaluatedContext.get(this.name);
}
evaluatedContext = evaluatedContext.parent;
}
return this.getter(evaluatedContext);
} }
get isAssignable() { get isAssignable() {
@ -105,7 +115,16 @@ export class AccessMember extends AST {
} }
assign(context, value) { assign(context, value) {
return this.setter(this.receiver.eval(context), value); var evaluatedContext = this.receiver.eval(context);
while (evaluatedContext instanceof ContextWithVariableBindings) {
if (evaluatedContext.hasBinding(this.name)) {
throw new BaseException(`Cannot reassign a variable binding ${this.name}`)
}
evaluatedContext = evaluatedContext.parent;
}
return this.setter(evaluatedContext, value);
} }
visit(visitor, args) { visit(visitor, args) {

View File

@ -0,0 +1,20 @@
import {MapWrapper} from 'facade/collection';
export class ContextWithVariableBindings {
parent:any;
/// varBindings are read-only. updating/adding keys is not supported.
varBindings:Map;
constructor(parent:any, varBindings:Map) {
this.parent = parent;
this.varBindings = varBindings;
}
hasBinding(name:string):boolean {
return MapWrapper.contains(this.varBindings, name);
}
get(name:string) {
return MapWrapper.get(this.varBindings, name);
}
}

View File

@ -30,6 +30,7 @@ export class ProtoRecord {
context:any; context:any;
funcOrValue:any; funcOrValue:any;
arity:int; arity:int;
name:string;
dest; dest;
next:ProtoRecord; next:ProtoRecord;
@ -39,12 +40,14 @@ export class ProtoRecord {
mode:int, mode:int,
funcOrValue, funcOrValue,
arity:int, arity:int,
name:string,
dest) { dest) {
this.recordRange = recordRange; this.recordRange = recordRange;
this._mode = mode; this._mode = mode;
this.funcOrValue = funcOrValue; this.funcOrValue = funcOrValue;
this.arity = arity; this.arity = arity;
this.name = name;
this.dest = dest; this.dest = dest;
this.next = null; this.next = null;

View File

@ -15,7 +15,7 @@ import {List, Map, ListWrapper, MapWrapper} from 'facade/collection';
import {AST, AccessMember, ImplicitReceiver, AstVisitor, LiteralPrimitive, import {AST, AccessMember, ImplicitReceiver, AstVisitor, LiteralPrimitive,
Binary, Formatter, MethodCall, FunctionCall, PrefixNot, Conditional, Binary, Formatter, MethodCall, FunctionCall, PrefixNot, Conditional,
LiteralArray, LiteralMap, KeyedAccess, Chain, Assignment} from './parser/ast'; LiteralArray, LiteralMap, KeyedAccess, Chain, Assignment} from './parser/ast';
import {ContextWithVariableBindings} from './parser/context_with_variable_bindings';
export class ProtoRecordRange { export class ProtoRecordRange {
headRecord:ProtoRecord; headRecord:ProtoRecord;
@ -304,11 +304,36 @@ export class RecordRange {
for (var record:Record = this.headRecord; for (var record:Record = this.headRecord;
record != null; record != null;
record = record.next) { record = record.next) {
if (record.isImplicitReceiver) { if (record.isImplicitReceiver) {
record.updateContext(context); this._setContextForRecord(context, record);
} }
} }
} }
_setContextForRecord(context, record:Record) {
var proto = record.protoRecord;
while (context instanceof ContextWithVariableBindings) {
if (context.hasBinding(proto.name)) {
this._setVarBindingGetter(context, record, proto);
return;
}
context = context.parent;
}
this._setRegularGetter(context, record, proto);
}
_setVarBindingGetter(context, record:Record, proto:ProtoRecord) {
record.funcOrValue = _mapGetter(proto.name);
record.updateContext(context.varBindings);
}
_setRegularGetter(context, record:Record, proto:ProtoRecord) {
record.funcOrValue = proto.funcOrValue;
record.updateContext(context);
}
} }
function _link(a:Record, b:Record) { function _link(a:Record, b:Record) {
@ -353,25 +378,25 @@ class ProtoRecordCreator {
} }
visitLiteralPrimitive(ast:LiteralPrimitive, dest) { visitLiteralPrimitive(ast:LiteralPrimitive, dest) {
this.add(this.construct(RECORD_TYPE_CONST, ast.value, 0, dest)); this.add(this.construct(RECORD_TYPE_CONST, ast.value, 0, null, dest));
} }
visitBinary(ast:Binary, dest) { visitBinary(ast:Binary, dest) {
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION,
_operationToFunction(ast.operation), 2, dest); _operationToFunction(ast.operation), 2, null, dest);
ast.left.visit(this, new Destination(record, 0)); ast.left.visit(this, new Destination(record, 0));
ast.right.visit(this, new Destination(record, 1)); ast.right.visit(this, new Destination(record, 1));
this.add(record); this.add(record);
} }
visitPrefixNot(ast:PrefixNot, dest) { visitPrefixNot(ast:PrefixNot, dest) {
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _operation_negate, 1, dest); var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _operation_negate, 1, null, dest);
ast.expression.visit(this, new Destination(record, 0)); ast.expression.visit(this, new Destination(record, 0));
this.add(record); this.add(record);
} }
visitAccessMember(ast:AccessMember, dest) { visitAccessMember(ast:AccessMember, dest) {
var record = this.construct(RECORD_TYPE_PROPERTY, ast.getter, 0, dest); var record = this.construct(RECORD_TYPE_PROPERTY, ast.getter, 0, ast.name, dest);
if (ast.receiver instanceof ImplicitReceiver) { if (ast.receiver instanceof ImplicitReceiver) {
record.setIsImplicitReceiver(); record.setIsImplicitReceiver();
} else { } else {
@ -381,7 +406,7 @@ class ProtoRecordCreator {
} }
visitFormatter(ast:Formatter, dest) { visitFormatter(ast:Formatter, dest) {
var record = this.construct(RECORD_TYPE_INVOKE_FORMATTER, ast.name, ast.allArgs.length, dest); var record = this.construct(RECORD_TYPE_INVOKE_FORMATTER, ast.name, ast.allArgs.length, null, dest);
for (var i = 0; i < ast.allArgs.length; ++i) { for (var i = 0; i < ast.allArgs.length; ++i) {
ast.allArgs[i].visit(this, new Destination(record, i)); ast.allArgs[i].visit(this, new Destination(record, i));
} }
@ -389,7 +414,7 @@ class ProtoRecordCreator {
} }
visitMethodCall(ast:MethodCall, dest) { visitMethodCall(ast:MethodCall, dest) {
var record = this.construct(RECORD_TYPE_INVOKE_METHOD, ast.fn, ast.args.length, dest); var record = this.construct(RECORD_TYPE_INVOKE_METHOD, ast.fn, ast.args.length, null, dest);
for (var i = 0; i < ast.args.length; ++i) { for (var i = 0; i < ast.args.length; ++i) {
ast.args[i].visit(this, new Destination(record, i)); ast.args[i].visit(this, new Destination(record, i));
} }
@ -402,7 +427,7 @@ class ProtoRecordCreator {
} }
visitFunctionCall(ast:FunctionCall, dest) { visitFunctionCall(ast:FunctionCall, dest) {
var record = this.construct(RECORD_TYPE_INVOKE_CLOSURE, null, ast.args.length, dest); var record = this.construct(RECORD_TYPE_INVOKE_CLOSURE, null, ast.args.length, null, dest);
ast.target.visit(this, new Destination(record, null)); ast.target.visit(this, new Destination(record, null));
for (var i = 0; i < ast.args.length; ++i) { for (var i = 0; i < ast.args.length; ++i) {
ast.args[i].visit(this, new Destination(record, i)); ast.args[i].visit(this, new Destination(record, i));
@ -411,7 +436,7 @@ class ProtoRecordCreator {
} }
visitConditional(ast:Conditional, dest) { visitConditional(ast:Conditional, dest) {
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _cond, 3, dest); var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _cond, 3, null, dest);
ast.condition.visit(this, new Destination(record, 0)); ast.condition.visit(this, new Destination(record, 0));
ast.trueExp.visit(this, new Destination(record, 1)); ast.trueExp.visit(this, new Destination(record, 1));
ast.falseExp.visit(this, new Destination(record, 2)); ast.falseExp.visit(this, new Destination(record, 2));
@ -422,7 +447,7 @@ class ProtoRecordCreator {
visitLiteralArray(ast:LiteralArray, dest) { visitLiteralArray(ast:LiteralArray, dest) {
var length = ast.expressions.length; var length = ast.expressions.length;
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _arrayFn(length), length, dest); var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _arrayFn(length), length, null, dest);
for (var i = 0; i < length; ++i) { for (var i = 0; i < length; ++i) {
ast.expressions[i].visit(this, new Destination(record, i)); ast.expressions[i].visit(this, new Destination(record, i));
} }
@ -431,7 +456,7 @@ class ProtoRecordCreator {
visitLiteralMap(ast:LiteralMap, dest) { visitLiteralMap(ast:LiteralMap, dest) {
var length = ast.values.length; var length = ast.values.length;
var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _mapFn(ast.keys, length), length, dest); var record = this.construct(RECORD_TYPE_INVOKE_PURE_FUNCTION, _mapFn(ast.keys, length), length, null, dest);
for (var i = 0; i < length; ++i) { for (var i = 0; i < length; ++i) {
ast.values[i].visit(this, new Destination(record, i)); ast.values[i].visit(this, new Destination(record, i));
} }
@ -448,8 +473,8 @@ class ProtoRecordCreator {
ast.visit(this, memento); ast.visit(this, memento);
} }
construct(recordType, funcOrValue, arity, dest) { construct(recordType, funcOrValue, arity, name, dest) {
return new ProtoRecord(this.protoRecordRange, recordType, funcOrValue, arity, dest); return new ProtoRecord(this.protoRecordRange, recordType, funcOrValue, arity, name, dest);
} }
add(protoRecord:ProtoRecord) { add(protoRecord:ProtoRecord) {
@ -540,3 +565,10 @@ function _mapFn(keys:List, length:int) {
default: throw new BaseException(`Does not support literal maps with more than 9 elements`); default: throw new BaseException(`Does not support literal maps with more than 9 elements`);
} }
} }
//TODO: cache the getters
function _mapGetter(key) {
return function(map) {
return MapWrapper.get(map, key);
}
}

View File

@ -2,6 +2,7 @@ import {ddescribe, describe, it, iit, xit, expect} from 'test_lib/test_lib';
import {isPresent} from 'facade/lang'; import {isPresent} from 'facade/lang';
import {List, ListWrapper, MapWrapper} from 'facade/collection'; import {List, ListWrapper, MapWrapper} from 'facade/collection';
import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings';
import {Parser} from 'change_detection/parser/parser'; import {Parser} from 'change_detection/parser/parser';
import {Lexer} from 'change_detection/parser/lexer'; import {Lexer} from 'change_detection/parser/lexer';
@ -183,6 +184,34 @@ export function main() {
expect(counter).toEqual(2); expect(counter).toEqual(2);
}); });
}); });
describe("ContextWithVariableBindings", () => {
it('should read a field from ContextWithVariableBindings', () => {
var locals = new ContextWithVariableBindings(null,
MapWrapper.createFromPairs([["key", "value"]]));
expect(executeWatch('key', 'key', locals))
.toEqual(['key=value']);
});
it('should handle nested ContextWithVariableBindings', () => {
var nested = new ContextWithVariableBindings(null,
MapWrapper.createFromPairs([["key", "value"]]));
var locals = new ContextWithVariableBindings(nested, MapWrapper.create());
expect(executeWatch('key', 'key', locals))
.toEqual(['key=value']);
});
it("should fall back to a regular field read when ContextWithVariableBindings " +
"does not have the requested field", () => {
var locals = new ContextWithVariableBindings(new Person("Jim"),
MapWrapper.createFromPairs([["key", "value"]]));
expect(executeWatch('name', 'name', locals))
.toEqual(['name=Jim']);
});
});
}); });
}); });
} }

View File

@ -4,6 +4,7 @@ import {reflector} from 'reflection/reflection';
import {MapWrapper, ListWrapper} from 'facade/collection'; import {MapWrapper, ListWrapper} from 'facade/collection';
import {Parser} from 'change_detection/parser/parser'; import {Parser} from 'change_detection/parser/parser';
import {Lexer} from 'change_detection/parser/lexer'; import {Lexer} from 'change_detection/parser/lexer';
import {ContextWithVariableBindings} from 'change_detection/parser/context_with_variable_bindings';
import {Formatter, LiteralPrimitive} from 'change_detection/parser/ast'; import {Formatter, LiteralPrimitive} from 'change_detection/parser/ast';
class TestData { class TestData {
@ -51,8 +52,9 @@ export function main() {
return expect(parseAction(text).eval(c)); return expect(parseAction(text).eval(c));
} }
function expectEvalError(text) { function expectEvalError(text, passedInContext = null) {
return expect(() => parseAction(text).eval(td())); var c = isBlank(passedInContext) ? td() : passedInContext;
return expect(() => parseAction(text).eval(c));
} }
function evalAsts(asts, passedInContext = null) { function evalAsts(asts, passedInContext = null) {
@ -196,6 +198,25 @@ export function main() {
expectEvalError('x. 1234').toThrowError(new RegExp('identifier or keyword')); expectEvalError('x. 1234').toThrowError(new RegExp('identifier or keyword'));
expectEvalError('x."foo"').toThrowError(new RegExp('identifier or keyword')); expectEvalError('x."foo"').toThrowError(new RegExp('identifier or keyword'));
}); });
it("should read a field from ContextWithVariableBindings", () => {
var locals = new ContextWithVariableBindings(null,
MapWrapper.createFromPairs([["key", "value"]]));
expectEval("key", locals).toEqual("value");
});
it("should handle nested ContextWithVariableBindings", () => {
var nested = new ContextWithVariableBindings(null,
MapWrapper.createFromPairs([["key", "value"]]));
var locals = new ContextWithVariableBindings(nested, MapWrapper.create());
expectEval("key", locals).toEqual("value");
});
it("should fall back to a regular field read when ContextWithVariableBindings "+
"does not have the requested field", () => {
var locals = new ContextWithVariableBindings(td(999), MapWrapper.create());
expectEval("a", locals).toEqual(999);
});
}); });
describe("method calls", () => { describe("method calls", () => {
@ -284,6 +305,18 @@ export function main() {
it('should throw on bad assignment', () => { it('should throw on bad assignment', () => {
expectEvalError("5=4").toThrowError(new RegExp("Expression 5 is not assignable")); expectEvalError("5=4").toThrowError(new RegExp("Expression 5 is not assignable"));
}); });
it('should reassign when no variable binding with the given name', () => {
var context = td();
var locals = new ContextWithVariableBindings(context, MapWrapper.create());
expectEval('a = 200', locals).toEqual(200);
expect(context.a).toEqual(200);
});
it('should throw when reassigning a variable binding', () => {
var locals = new ContextWithVariableBindings(null, MapWrapper.createFromPairs([["key", "value"]]));
expectEvalError('key = 200', locals).toThrowError(new RegExp("Cannot reassign a variable binding"));
});
}); });
describe("general error handling", () => { describe("general error handling", () => {

View File

@ -46,7 +46,7 @@ export function main() {
} }
function createRecord(rr) { function createRecord(rr) {
return new Record(rr, new ProtoRecord(null, 0, null, null, null), null); return new Record(rr, new ProtoRecord(null, 0, null, null, null, null), null);
} }
describe('record range', () => { describe('record range', () => {