feat(parser): adds support for variable bindings
This commit is contained in:
parent
a3d9f0fead
commit
1863d50978
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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", () => {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
Loading…
Reference in New Issue