From f2651d4259ed8f69663bac084240d38e0efd4fcf Mon Sep 17 00:00:00 2001 From: Kasper Lund Date: Fri, 3 Jan 2014 13:50:02 +0100 Subject: [PATCH] feat(parser): Add new AST based parser The evaluation of the AST nodes is deliberately left out of this change to keep it as simple as possible, but if you're interested I do have a full implementation of it. It probably makes sense to rewrite the static parser generator to use the new AST visiting support -- so I'll probably do that as the next step to start cutting down on the dependencies on the old parser. Once the new lexer lands there are a number of ways to improve parsing performance through changes to the token representation. --- bin/parser_generator_for_spec.dart | 32 +- lib/angular.dart | 3 +- lib/core/interpolate.dart | 7 +- lib/core/module.dart | 8 +- lib/core/parser/backend.dart | 362 ----------- lib/core/parser/dynamic_parser.dart | 158 +++++ lib/core/parser/dynamic_parser_impl.dart | 310 ++++++++++ lib/core/parser/eval.dart | 96 +++ lib/core/parser/eval_access.dart | 179 ++++++ lib/core/parser/eval_calls.dart | 121 ++++ lib/core/parser/lexer.dart | 64 +- lib/core/parser/parser.dart | 580 +++--------------- lib/core/parser/parser_library.dart | 19 - lib/core/parser/static_parser.dart | 69 ++- lib/core/parser/syntax.dart | 202 ++++++ lib/core/parser/unparser.dart | 135 ++++ lib/core/parser/utils.dart | 96 +++ lib/core/scope.dart | 3 +- lib/core_dom/compiler.dart | 6 +- lib/core_dom/module.dart | 7 +- lib/directive/module.dart | 2 +- lib/directive/ng_pluralize.dart | 5 +- lib/filter/filter.dart | 2 +- lib/filter/limit_to.dart | 3 +- lib/filter/module.dart | 2 +- lib/filter/order_by.dart | 4 +- lib/tools/expression_extractor.dart | 18 +- lib/tools/parser_generator/dart_code_gen.dart | 485 ++++++++------- lib/tools/parser_generator/generator.dart | 82 ++- lib/tools/parser_generator/source.dart | 92 --- lib/tools/parser_getter_setter/generator.dart | 128 ++-- perf/lexer_perf.dart | 2 +- perf/parser_perf.dart | 12 +- perf/scope_perf.dart | 5 +- test/_specs.dart | 5 + test/core/parser/generated_functions.dart | 6 +- test/core/parser/generated_getter_setter.dart | 7 +- .../parser/generated_getter_setter_spec.dart | 5 +- test/core/parser/generated_parser_spec.dart | 4 +- test/core/parser/lexer_spec.dart | 24 +- test/core/parser/parser_spec.dart | 87 ++- test/core/parser/static_parser_spec.dart | 12 +- 42 files changed, 2032 insertions(+), 1417 deletions(-) delete mode 100644 lib/core/parser/backend.dart create mode 100644 lib/core/parser/dynamic_parser.dart create mode 100644 lib/core/parser/dynamic_parser_impl.dart create mode 100644 lib/core/parser/eval.dart create mode 100644 lib/core/parser/eval_access.dart create mode 100644 lib/core/parser/eval_calls.dart delete mode 100644 lib/core/parser/parser_library.dart create mode 100644 lib/core/parser/syntax.dart create mode 100644 lib/core/parser/unparser.dart create mode 100644 lib/core/parser/utils.dart delete mode 100644 lib/tools/parser_generator/source.dart diff --git a/bin/parser_generator_for_spec.dart b/bin/parser_generator_for_spec.dart index 1dcdc16b7..17267cc26 100644 --- a/bin/parser_generator_for_spec.dart +++ b/bin/parser_generator_for_spec.dart @@ -1,17 +1,28 @@ import 'dart:io'; import 'package:di/di.dart'; import 'package:di/dynamic_injector.dart'; -import 'package:angular/core/parser/parser_library.dart'; -import 'package:angular/tools/parser_generator/dart_code_gen.dart'; +import 'package:angular/core/module.dart'; +import 'package:angular/core/parser/parser.dart'; import 'package:angular/tools/parser_generator/generator.dart'; import 'package:angular/tools/parser_getter_setter/generator.dart'; +class NullFilterMap implements FilterMap { + call(name) => null; + Type operator[](annotation) => null; + forEach(fn) { } + annotationsFor(type) => null; +} + main(arguments) { var isGetter = !arguments.isEmpty; - Module module = new Module() - ..type(ParserBackend, implementedBy: isGetter ? DartGetterSetterGen : DartCodeGen); - + Module module = new Module()..type(Parser, implementedBy: DynamicParser); + if (isGetter) { + module.type(ParserBackend, implementedBy: DartGetterSetterGen); + } else { + module.type(ParserBackend, implementedBy: DynamicParserBackend); + module.type(FilterMap, implementedBy: NullFilterMap); + } Injector injector = new DynamicInjector(modules: [module], allowImplicitInjection: true); @@ -23,12 +34,21 @@ main(arguments) { "const", "null", "[1, 2].length", + "doesNotExist", + "doesNotExist()", + "doesNotExist(1)", + "doesNotExist(1, 2)", + "a.doesNotExist()", + "a.doesNotExist(1)", + "a.doesNotExist(1, 2)", + "a.b.c", "x.b.c", "e1.b", "o.f()", "1", "-1", "+1", + "true?1", "!true", "3*4/2%5", "3+6-2", "2<3", "2>3", "2<=2", "2>=2", @@ -77,7 +97,6 @@ main(arguments) { 'a[x()]()', 'boo', '[].count(', - 'doesNotExist()', 'false', 'false && run()', '!false || true', @@ -137,6 +156,7 @@ main(arguments) { '6[3]=2', 'map.dot = 7', + 'map.null', 'exists(doesNotExist())', 'doesNotExists(exists())', 'a[0]()', diff --git a/lib/angular.dart b/lib/angular.dart index 67e99d1a2..904efd6ac 100644 --- a/lib/angular.dart +++ b/lib/angular.dart @@ -25,7 +25,8 @@ import 'package:angular/routing/module.dart'; export 'package:di/di.dart'; export 'package:angular/core/module.dart'; export 'package:angular/core_dom/module.dart'; -export 'package:angular/core/parser/parser_library.dart'; +export 'package:angular/core/parser/parser.dart'; +export 'package:angular/core/parser/lexer.dart'; export 'package:angular/directive/module.dart'; export 'package:angular/filter/module.dart'; export 'package:angular/routing/module.dart'; diff --git a/lib/core/interpolate.dart b/lib/core/interpolate.dart index e618f1e76..1d6e7bd11 100644 --- a/lib/core/interpolate.dart +++ b/lib/core/interpolate.dart @@ -8,7 +8,7 @@ int _endSymbolLength = _endSymbol.length; class Interpolation { final String template; final List seperators; - final List watchExpressions; + final List watchExpressions; Function setter = (_) => _; Interpolation(this.template, this.seperators, this.watchExpressions); @@ -58,14 +58,15 @@ class Interpolate { bool shouldAddSeparator = true; String exp; List separators = []; - List watchExpressions = []; + List watchExpressions = []; while(index < length) { if ( ((startIndex = template.indexOf(_startSymbol, index)) != -1) && ((endIndex = template.indexOf(_endSymbol, startIndex + _startSymbolLength)) != -1) ) { separators.add(template.substring(index, startIndex)); exp = template.substring(startIndex + _startSymbolLength, endIndex); - watchExpressions.add((_parse(exp)..exp = exp).eval); + Expression expression = _parse(exp); + watchExpressions.add(expression.eval); index = endIndex + _endSymbolLength; hasInterpolation = true; } else { diff --git a/lib/core/module.dart b/lib/core/module.dart index 4f69bd3c6..68016768d 100644 --- a/lib/core/module.dart +++ b/lib/core/module.dart @@ -8,7 +8,8 @@ import 'dart:mirrors'; import 'package:di/di.dart'; import 'package:perf_api/perf_api.dart'; -import 'package:angular/core/parser/parser_library.dart'; +import 'package:angular/core/parser/parser.dart'; +import 'package:angular/core/parser/lexer.dart'; import 'package:angular/utils.dart'; import 'service.dart'; @@ -39,9 +40,10 @@ class NgCoreModule extends Module { type(NgZone); type(Parser, implementedBy: DynamicParser); + type(ParserBackend, implementedBy: DynamicParserBackend); type(DynamicParser); + type(DynamicParserBackend); type(Lexer); - type(ParserBackend); - type(GetterSetter); + type(ClosureMap); } } diff --git a/lib/core/parser/backend.dart b/lib/core/parser/backend.dart deleted file mode 100644 index d5961eecc..000000000 --- a/lib/core/parser/backend.dart +++ /dev/null @@ -1,362 +0,0 @@ -part of angular.core.parser; - -typedef dynamic LocalsWrapper(dynamic context, dynamic locals); - -class BoundExpression { - var _context; - LocalsWrapper _localsWrapper; - Expression expression; - - BoundExpression(this._context, this.expression, this._localsWrapper); - _localContext(locals) { - if (locals != null) { - if (_localsWrapper == null) { - throw new StateError("Locals $locals provided, but no LocalsWrapper strategy."); - } - return _localsWrapper(_context, locals); - } - return _context; - } - - call([locals]) => expression.eval(_localContext(locals)); - assign(value, [locals]) => expression.assign(_localContext(locals), value); -} - -class Expression implements ParserAST { - final ParsedGetter eval; - final ParsedSetter assign; - - String exp; - List parts; - - // Expressions that represent field accesses have a couple of - // extra fields. We use that to generate an optimized closure - // for calling fields of objects without having to load the - // field separately. - Expression fieldHolder; - String fieldName; - bool get isFieldAccess => fieldHolder != null; - - Expression(ParsedGetter this.eval, [ParsedSetter this.assign]); - - bind(context, [localsWrapper]) => new BoundExpression(context, this, localsWrapper); - - get assignable => assign != null; -} - -@NgInjectableService() -class GetterSetter { - static stripTrailingNulls(List l) { - while (l.length > 0 && l.last == null) { - l.removeLast(); - } - return l; - } - - static _computeUseInstanceMembers() { - try { - reflect(Object).type.instanceMembers; - return true; - } catch (e) { - return false; - } - } - - final bool _useInstanceMembers = _computeUseInstanceMembers(); - - _containsKey(InstanceMirror instanceMirror, Symbol symbol) { - dynamic type = (instanceMirror.type as dynamic); - var members = _useInstanceMembers ? type.instanceMembers : type.members; - return members.containsKey(symbol); - } - - _maybeInvoke(instanceMirror, symbol) { - if (_containsKey(instanceMirror, symbol)) { - MethodMirror methodMirror = instanceMirror.type.members[symbol]; - return relaxFnArgs(([a0, a1, a2, a3, a4, a5]) { - var args = stripTrailingNulls([a0, a1, a2, a3, a4, a5]); - return instanceMirror.invoke(symbol, args).reflectee; - }); - } - return null; - } - - Map _getter_cache = {}; - - Function getter(String key) { - var value = _getter_cache[key]; - if (value != null) return value; - return _getter_cache[key] = _getter(key); - } - - Function _getter(String key) { - var symbol = new Symbol(key); - Map fieldCache = {}; - return (o) { - InstanceMirror instanceMirror = reflect(o); - ClassMirror classMirror = instanceMirror.type; - Function fn = fieldCache[classMirror]; - if (fn == null) { - try { - return (fieldCache[classMirror] = (instanceMirror) => instanceMirror.getField(symbol).reflectee)(instanceMirror); - } on NoSuchMethodError catch (e) { - var value = (fieldCache[classMirror] = (instanceMirror) => _maybeInvoke(instanceMirror, symbol))(instanceMirror); - if (value == null) { rethrow; } - return value; - } on UnsupportedError catch (e) { - var value = (fieldCache[classMirror] = (instanceMirror) => _maybeInvoke(instanceMirror, symbol))(instanceMirror); - if (value == null) { rethrow; } - return value; - } - } else { - return fn(instanceMirror); - } - }; - } - - Function setter(String key) { - var symbol = new Symbol(key); - return (o, v) { - reflect(o).setField(symbol, v); - return v; - }; - } -} - -var undefined_ = const Symbol("UNDEFINED"); - -@NgInjectableService() -class ParserBackend { - GetterSetter _getterSetter; - FilterMap _filters; - - ParserBackend(GetterSetter this._getterSetter, FilterMap this._filters); - - static Expression ZERO = new Expression((_, [_x]) => 0); - - getter(String path) { - List keys = path.split('.'); - List getters = keys.map(_getterSetter.getter).toList(); - - if (getters.isEmpty) { - return (self) => self; - } else { - return (dynamic self) { - if (self == null) { - return null; - } - - // Cache for local closure access - List _keys = keys; - List _getters = getters; - var _gettersLength = _getters.length; - - num i = 0; - for (; i < _gettersLength; i++) { - if (self is Map) { - self = self[_keys[i]]; - } else { - self = _getters[i](self); - } - if (self == null) { - return null; - } - } - return self; - }; - } - } - - setter(String path) { - List keys = path.split('.'); - List getters = keys.map(_getterSetter.getter).toList(); - List setters = keys.map(_getterSetter.setter).toList(); - return (dynamic self, dynamic value) { - num i = 0; - List _keys = keys; - List _getters = getters; - List _setters = setters; - var setterLengthMinusOne = _keys.length - 1; - - dynamic selfNext; - for (; i < setterLengthMinusOne; i++) { - if (self is Map) { - selfNext = self[_keys[i]]; - } else { - selfNext = _getters[i](self); - } - - if (selfNext == null) { - selfNext = {}; - if (self is Map) { - self[_keys[i]] = selfNext; - } else { - _setters[i](self, selfNext); - } - } - self = selfNext; - } - if (self is Map) { - self[_keys[setterLengthMinusOne]] = value; - } else { - _setters[i](self, value); - } - return value; - }; - } - - List evalList(List list, self) { - int length = list.length; - List result = new List(length); - for (int i = 0; i < length; i++) { - result[i] = list[i].eval(self); - } - return result; - } - - _op(opKey) => OPERATORS[opKey]; - - Expression ternaryFn(Expression cond, Expression _true, Expression _false) => - new Expression((self) => _op('?')( - self, cond, _true, _false)); - - Expression binaryFn(Expression left, String op, Expression right) => - new Expression((self) => _op(op)(self, left, right)); - - Expression unaryFn(String op, Expression right) => - new Expression((self) => _op(op)(self, right, null)); - - Expression assignment(Expression left, Expression right, evalError) => - new Expression((self) { - try { - return left.assign(self, right.eval(self)); - } catch (e, s) { - throw evalError('Caught $e', s); - } - }); - - Expression multipleStatements(statements) => - new Expression((self) { - var value; - for ( var i = 0; i < statements.length; i++) { - var statement = statements[i]; - if (statement != null) - value = statement.eval(self); - } - return value; - }); - - Expression functionCall(fn, fnName, argsFn, evalError) { - if (fn.isFieldAccess) { - Symbol key = new Symbol(fn.fieldName); - return new Expression((self) { - List args = evalList(argsFn, self); - var holder = fn.fieldHolder.eval(self); - InstanceMirror instanceMirror = reflect(holder); - return instanceMirror.invoke(key, args).reflectee; - }); - } else { - return new Expression((self) { - List args = evalList(argsFn, self); - var userFn = safeFunctionCall(fn.eval(self), fnName, evalError); - return relaxFnApply(userFn, args); - }); - } - } - - Expression arrayDeclaration(elementFns) => - new Expression((self){ - var array = []; - for ( var i = 0; i < elementFns.length; i++) { - array.add(elementFns[i].eval(self)); - } - return array; - }); - - Expression objectIndex(obj, indexFn, evalError) => - new Expression((self) { - var i = indexFn.eval(self); - var o = obj.eval(self), - v, p; - - v = objectIndexGetField(o, i, evalError); - - return v; - }, (self, value) => - objectIndexSetField(obj.eval(self), - indexFn.eval(self), value, evalError) - ); - - Expression fieldAccess(object, field) { - var setterFn = setter(field); - var getterFn = getter(field); - return new Expression( - (self) => getterFn(object.eval(self)), - (self, value) => setterFn(object.eval(self), value)) - ..fieldHolder = object - ..fieldName = field; - } - - Expression object(keyValues) => - new Expression((self){ - var object = {}; - for ( var i = 0; i < keyValues.length; i++) { - var keyValue = keyValues[i]; - var value = keyValue["value"].eval(self); - object[keyValue["key"]] = value; - } - return object; - }); - - Expression profiled(value, _perf, text) { - if (value is FilterExpression) return value; - var wrappedGetter = (s) => - _perf.time('angular.parser.getter', () => value.eval(s), text); - var wrappedAssignFn = null; - if (value.assign != null) { - wrappedAssignFn = (s, v) => - _perf.time('angular.parser.assign', - () => value.assign(s, v), text); - } - return new Expression(wrappedGetter, wrappedAssignFn); - } - - Expression fromOperator(String op) => - new Expression((s) => OPERATORS[op](s, null, null)); - - Expression getterSetter(key) => - new Expression(getter(key), setter(key)); - - Expression value(v) => - new Expression((self) => v); - - zero() => ZERO; - - Expression filter(String filterName, - Expression leftHandSide, - List parameters, - Function evalError) { - var filterFn = _filters(filterName); - return new FilterExpression(filterFn, leftHandSide, parameters); - } -} - -class FilterExpression extends Expression { - final Function filterFn; - final Expression leftHandSide; - final List parameters; - - FilterExpression(Function this.filterFn, - Expression this.leftHandSide, - List this.parameters): super(null); - - get eval => _eval; - - dynamic _eval(self) { - var args = [leftHandSide.eval(self)]; - for ( var i = 0; i < parameters.length; i++) { - args.add(parameters[i].eval(self)); - } - return Function.apply(filterFn, args); - } -} diff --git a/lib/core/parser/dynamic_parser.dart b/lib/core/parser/dynamic_parser.dart new file mode 100644 index 000000000..0c657aa8d --- /dev/null +++ b/lib/core/parser/dynamic_parser.dart @@ -0,0 +1,158 @@ +library angular.core.parser.dynamic_parser; + +import 'package:angular/core/module.dart' show FilterMap, NgInjectableService; + +import 'package:angular/core/parser/parser.dart'; +import 'package:angular/core/parser/lexer.dart'; +import 'package:angular/core/parser/dynamic_parser_impl.dart'; + +import 'package:angular/core/parser/eval.dart'; +import 'package:angular/core/parser/utils.dart' show EvalError; + +class ClosureMap { + Getter lookupGetter(String name) => null; + Setter lookupSetter(String name) => null; + Function lookupFunction(String name, int arity) => null; +} + +class DynamicParser implements Parser { + final Lexer _lexer; + final ParserBackend _backend; + final Map _cache = {}; + DynamicParser(this._lexer, this._backend); + + Expression call(String input) { + if (input == null) input = ''; + return _cache.putIfAbsent(input, () => _parse(input)); + } + + Expression _parse(String input) { + DynamicParserImpl parser = new DynamicParserImpl(_lexer, _backend, input); + Expression expression = parser.parseChain(); + return new DynamicExpression(expression); + } +} + +class DynamicExpression extends Expression { + final Expression _expression; + DynamicExpression(this._expression); + + bool get isAssignable => _expression.isAssignable; + bool get isChain => _expression.isChain; + + accept(Visitor visitor) => _expression.accept(visitor); + toString() => _expression.toString(); + + eval(scope) { + try { + return _expression.eval(scope); + } on EvalError catch (e, s) { + throw e.unwrap("$this", s); + } + } + + assign(scope, value) { + try { + return _expression.assign(scope, value); + } on EvalError catch (e, s) { + throw e.unwrap("$this", s); + } + } +} + +class DynamicParserBackend extends ParserBackend { + final FilterMap _filters; + final ClosureMap _closures; + DynamicParserBackend(this._filters, this._closures); + + bool isAssignable(Expression expression) + => expression.isAssignable; + + Expression newFilter(expression, name, arguments) { + Function filter = _filters(name); + List allArguments = new List(arguments.length + 1); + allArguments[0] = expression; + allArguments.setAll(1, arguments); + return new Filter(expression, name, arguments, filter, allArguments); + } + + Expression newChain(expressions) + => new Chain(expressions); + Expression newAssign(target, value) + => new Assign(target, value); + Expression newConditional(condition, yes, no) + => new Conditional(condition, yes, no); + + Expression newAccessKeyed(object, key) + => new AccessKeyed(object, key); + Expression newCallFunction(function, arguments) + => new CallFunction(function, arguments); + + Expression newPrefixNot(expression) + => new PrefixNot(expression); + + Expression newBinary(operation, left, right) + => new Binary(operation, left, right); + + Expression newLiteralPrimitive(value) + => new LiteralPrimitive(value); + Expression newLiteralArray(elements) + => new LiteralArray(elements); + Expression newLiteralObject(keys, values) + => new LiteralObject(keys, values); + Expression newLiteralString(value) + => new LiteralString(value); + + + Expression newAccessScope(name) { + Getter getter = _closures.lookupGetter(name); + Setter setter = _closures.lookupSetter(name); + if (getter != null && setter != null) { + return new AccessScopeFast(name, getter, setter); + } else { + return new AccessScope(name); + } + } + + Expression newAccessMember(object, name) { + Getter getter = _closures.lookupGetter(name); + Setter setter = _closures.lookupSetter(name); + if (getter != null && setter != null) { + return new AccessMemberFast(object, name, getter, setter); + } else { + return new AccessMember(object, name); + } + } + + Expression newCallScope(name, arguments) { + Function constructor = _computeCallConstructor( + _callScopeConstructors, name, arguments.length); + return (constructor != null) + ? constructor(name, arguments, _closures) + : new CallScope(name, arguments); + } + + Expression newCallMember(object, name, arguments) { + Function constructor = _computeCallConstructor( + _callMemberConstructors, name, arguments.length); + return (constructor != null) + ? constructor(object, name, arguments, _closures) + : new CallMember(object, name, arguments); + } + + Function _computeCallConstructor(Map constructors, String name, int arity) { + Function function = _closures.lookupFunction(name, arity); + return (function == null) ? null : constructors[arity]; + } + + static final Map _callScopeConstructors = { + 0: (n, a, c) => new CallScopeFast0(n, a, c.lookupFunction(n, 0)), + 1: (n, a, c) => new CallScopeFast1(n, a, c.lookupFunction(n, 1)), + }; + + static final Map _callMemberConstructors = { + 0: (o, n, a, c) => new CallMemberFast0(o, n, a, c.lookupFunction(n, 0)), + 1: (o, n, a, c) => new CallMemberFast1(o, n, a, c.lookupFunction(n, 1)), + }; +} + diff --git a/lib/core/parser/dynamic_parser_impl.dart b/lib/core/parser/dynamic_parser_impl.dart new file mode 100644 index 000000000..8c0f0c581 --- /dev/null +++ b/lib/core/parser/dynamic_parser_impl.dart @@ -0,0 +1,310 @@ +library angular.core.parser.dynamic_parser_impl; + +import 'package:angular/core/parser/parser.dart' show ParserBackend; +import 'package:angular/core/parser/lexer.dart'; + +class DynamicParserImpl { + static Token EOF = new Token(-1, null); + final ParserBackend backend; + final String input; + final List tokens; + int index = 0; + + DynamicParserImpl(Lexer lexer, this.backend, String input) + : this.input = input, tokens = lexer.call(input); + + Token get peek { + return (index < tokens.length) ? tokens[index] : EOF; + } + + parseChain() { + while (optional(';')); + List expressions = []; + while (index < tokens.length) { + if (peek.text == ')' || peek.text == '}' || peek.text == ']') { + error('Unconsumed token ${peek.text}'); + } + expressions.add(parseFilter()); + while (optional(';')); + } + return (expressions.length == 1) + ? expressions.first + : backend.newChain(expressions); + } + + parseFilter() { + var result = parseExpression(); + while (optional('|')) { + String name = peek.text; // TODO(kasperl): Restrict to identifier? + advance(); + List arguments = []; + while (optional(':')) { + // TODO(kasperl): Is this really supposed to be expressions? + arguments.add(parseExpression()); + } + result = backend.newFilter(result, name, arguments); + } + return result; + } + + parseExpression() { + int start = peek.index; + var result = parseConditional(); + while (peek.text == '=') { + if (!backend.isAssignable(result)) { + int end = (index < tokens.length) ? peek.index : input.length; + String expression = input.substring(start, end); + error('Expression $expression is not assignable'); + } + expect('='); + result = backend.newAssign(result, parseConditional()); + } + return result; + } + + parseConditional() { + int start = peek.index; + var result = parseLogicalOr(); + if (optional('?')) { + var yes = parseExpression(); + if (!optional(':')) { + int end = (index < tokens.length) ? peek.index : input.length; + String expression = input.substring(start, end); + error('Conditional expression $expression requires all 3 expressions'); + } + var no = parseExpression(); + result = backend.newConditional(result, yes, no); + } + return result; + } + + parseLogicalOr() { + // '||' + var result = parseLogicalAnd(); + while (optional('||')) { + result = backend.newBinaryLogicalOr(result, parseLogicalAnd()); + } + return result; + } + + parseLogicalAnd() { + // '&&' + var result = parseEquality(); + while (optional('&&')) { + result = backend.newBinaryLogicalAnd(result, parseEquality()); + } + return result; + } + + parseEquality() { + // '==','!=' + var result = parseRelational(); + while (true) { + if (optional('==')) { + result = backend.newBinaryEqual(result, parseRelational()); + } else if (optional('!=')) { + result = backend.newBinaryNotEqual(result, parseRelational()); + } else { + return result; + } + } + } + + parseRelational() { + // '<', '>', '<=', '>=' + var result = parseAdditive(); + while (true) { + if (optional('<')) { + result = backend.newBinaryLessThan(result, parseAdditive()); + } else if (optional('>')) { + result = backend.newBinaryGreaterThan(result, parseAdditive()); + } else if (optional('<=')) { + result = backend.newBinaryLessThanEqual(result, parseAdditive()); + } else if (optional('>=')) { + result = backend.newBinaryGreaterThanEqual(result, parseAdditive()); + } else { + return result; + } + } + } + + parseAdditive() { + // '+', '-' + var result = parseMultiplicative(); + while (true) { + if (optional('+')) { + result = backend.newBinaryPlus(result, parseMultiplicative()); + } else if (optional('-')) { + result = backend.newBinaryMinus(result, parseMultiplicative()); + } else { + return result; + } + } + } + + parseMultiplicative() { + // '*', '%', '/', '~/' + var result = parsePrefix(); + while (true) { + if (optional('*')) { + result = backend.newBinaryMultiply(result, parsePrefix()); + } else if (optional('%')) { + result = backend.newBinaryModulo(result, parsePrefix()); + } else if (optional('/')) { + result = backend.newBinaryDivide(result, parsePrefix()); + } else if (optional('~/')) { + result = backend.newBinaryTruncatingDivide(result, parsePrefix()); + } else { + return result; + } + } + } + + parsePrefix() { + if (optional('+')) { + // TODO(kasperl): This is different than the original parser. + return backend.newPrefixPlus(parsePrefix()); + } else if (optional('-')) { + return backend.newPrefixMinus(parsePrefix()); + } else if (optional('!')) { + return backend.newPrefixNot(parsePrefix()); + } else { + return parseMemberOrCall(); + } + } + + parseMemberOrCall() { + var result = parsePrimary(); + while (true) { + if (optional('.')) { + String name = peek.text; // TODO(kasperl): Check that this is an identifier + advance(); + if (optional('(')) { + List arguments = parseExpressionList(')'); + expect(')'); + result = backend.newCallMember(result, name, arguments); + } else { + result = backend.newAccessMember(result, name); + } + } else if (optional('[')) { + var key = parseExpression(); + expect(']'); + result = backend.newAccessKeyed(result, key); + } else if (optional('(')) { + List arguments = parseExpressionList(')'); + expect(')'); + result = backend.newCallFunction(result, arguments); + } else { + return result; + } + } + } + + parsePrimary() { + if (optional('(')) { + var result = parseFilter(); + expect(')'); + return result; + } else if (optional('null') || optional('undefined')) { + return backend.newLiteralNull(); + } else if (optional('true')) { + return backend.newLiteralBoolean(true); + } else if (optional('false')) { + return backend.newLiteralBoolean(false); + } else if (optional('[')) { + List elements = parseExpressionList(']'); + expect(']'); + return backend.newLiteralArray(elements); + } else if (peek.text == '{') { + return parseObject(); + } else if (peek.key != null) { + return parseQualified(); + } else if (peek.value != null) { + var value = peek.value; + advance(); + return (value is num) + ? backend.newLiteralNumber(value) + : backend.newLiteralString(value); + } else if (index >= tokens.length) { + throw 'Unexpected end of expression: $input'; + } else { + error('Unexpected token ${peek.text}'); + } + } + + parseQualified() { + List components = peek.key.split('.'); + advance(); + List arguments; + if (optional('(')) { + arguments = parseExpressionList(')'); + expect(')'); + } + var result = (arguments != null) && (components.length == 1) + ? backend.newCallScope(components.first, arguments) + : backend.newAccessScope(components.first); + for (int i = 1; i < components.length; i++) { + result = (arguments != null) && (components.length == i + 1) + ? backend.newCallMember(result, components[i], arguments) + : backend.newAccessMember(result, components[i]); + } + return result; + } + + parseObject() { + List keys = []; + List values = []; + expect('{'); + if (peek.text != '}') { + do { + // TODO(kasperl): Stricter checking. Only allow identifiers + // and strings as keys. Maybe also keywords? + var value = peek.value; + keys.add(value is String ? value : peek.text); + advance(); + expect(':'); + values.add(parseExpression()); + } while (optional(',')); + } + expect('}'); + return backend.newLiteralObject(keys, values); + } + + List parseExpressionList(String terminator) { + List result = []; + if (peek.text != terminator) { + do { + result.add(parseExpression()); + } while (optional(',')); + } + return result; + } + + bool optional(text) { + if (peek.text == text) { + advance(); + return true; + } else { + return false; + } + } + + void expect(text) { + if (peek.text == text) { + advance(); + } else { + error('Missing expected $text'); + } + } + + void advance() { + index++; + } + + void error(message) { + String location = (index < tokens.length) + ? 'at column ${tokens[index].index + 1} in' + : 'the end of the expression'; + throw 'Parser Error: $message $location [$input]'; + } +} diff --git a/lib/core/parser/eval.dart b/lib/core/parser/eval.dart new file mode 100644 index 000000000..0bd1fe2b3 --- /dev/null +++ b/lib/core/parser/eval.dart @@ -0,0 +1,96 @@ +library angular.core.parser.eval; + +import 'package:angular/core/parser/syntax.dart' as syntax; +import 'package:angular/core/parser/utils.dart'; + +export 'package:angular/core/parser/eval_access.dart'; +export 'package:angular/core/parser/eval_calls.dart'; + +class Chain extends syntax.Chain { + Chain(List expressions) : super(expressions); + eval(scope) { + var result; + for (int i = 0, length = expressions.length; i < length; i++) { + var last = expressions[i].eval(scope); + if (last != null) result = last; + } + return result; + } +} + +class Filter extends syntax.Filter { + final Function function; + final List allArguments; + Filter(syntax.Expression expression, String name, List arguments, + Function this.function, List this.allArguments) + : super(expression, name, arguments); + eval(scope) => Function.apply(function, evalList(scope, allArguments)); +} + +class Assign extends syntax.Assign { + Assign(syntax.Expression target, value) : super(target, value); + eval(scope) => target.assign(scope, value.eval(scope)); +} + +class Conditional extends syntax.Conditional { + Conditional(syntax.Expression condition, + syntax.Expression yes, syntax.Expression no): super(condition, yes, no); + eval(scope) => toBool(condition.eval(scope)) + ? yes.eval(scope) + : no.eval(scope); +} + +class PrefixNot extends syntax.Prefix { + PrefixNot(syntax.Expression expression) : super('!', expression); + eval(scope) => !toBool(expression.eval(scope)); +} + +class Binary extends syntax.Binary { + Binary(String operation, syntax.Expression left, syntax.Expression right): + super(operation, left, right); + eval(scope) { + var left = this.left.eval(scope); + switch (operation) { + case '&&': return toBool(left) && toBool(this.right.eval(scope)); + case '||': return toBool(left) || toBool(this.right.eval(scope)); + } + var right = this.right.eval(scope); + switch (operation) { + case '+' : return autoConvertAdd(left, right); + case '-' : return left - right; + case '*' : return left * right; + case '/' : return left / right; + case '~/' : return left ~/ right; + case '%' : return left % right; + case '==' : return left == right; + case '!=' : return left != right; + case '<' : return left < right; + case '>' : return left > right; + case '<=' : return left <= right; + case '>=' : return left >= right; + case '^' : return left ^ right; + case '&' : return left & right; + } + throw new EvalError('Internal error [$operation] not handled'); + } +} + +class LiteralPrimitive extends syntax.LiteralPrimitive { + LiteralPrimitive(dynamic value) : super(value); + eval(scope) => value; +} + +class LiteralString extends syntax.LiteralString { + LiteralString(String value) : super(value); + eval(scope) => value; +} + +class LiteralArray extends syntax.LiteralArray { + LiteralArray(List elements) : super(elements); + eval(scope) => elements.map((e) => e.eval(scope)).toList(); +} + +class LiteralObject extends syntax.LiteralObject { + LiteralObject(List keys, Listvalues) : super(keys, values); + eval(scope) => new Map.fromIterables(keys, values.map((e) => e.eval(scope))); +} diff --git a/lib/core/parser/eval_access.dart b/lib/core/parser/eval_access.dart new file mode 100644 index 000000000..da3b2246d --- /dev/null +++ b/lib/core/parser/eval_access.dart @@ -0,0 +1,179 @@ +library angular.core.parser.eval_access; + +import 'dart:mirrors'; +import 'package:angular/core/parser/parser.dart'; +import 'package:angular/core/parser/syntax.dart' as syntax; +import 'package:angular/core/parser/utils.dart'; + +class AccessScope extends syntax.AccessScope with AccessReflective { + final Symbol symbol; + AccessScope(String name) : super(name), symbol = new Symbol(name); + eval(scope) => _eval(scope); + assign(scope, value) => _assign(scope, scope, value); +} + +class AccessScopeFast extends syntax.AccessScope with AccessFast { + final Getter getter; + final Setter setter; + AccessScopeFast(String name, this.getter, this.setter) : super(name); + eval(scope) => _eval(scope); + assign(scope, value) => _assign(scope, scope, value); +} + +class AccessMember extends syntax.AccessMember with AccessReflective { + final Symbol symbol; + AccessMember(object, String name) : super(object, name), symbol = new Symbol(name); + eval(scope) => _eval(object.eval(scope)); + assign(scope, value) => _assign(scope, object.eval(scope), value); + _assignToNonExisting(scope, value) => object.assign(scope, { name: value }); +} + +class AccessMemberFast extends syntax.AccessMember with AccessFast { + final Getter getter; + final Setter setter; + AccessMemberFast(object, String name, this.getter, this.setter) + : super(object, name); + eval(scope) => _eval(object.eval(scope)); + assign(scope, value) => _assign(scope, object.eval(scope), value); + _assignToNonExisting(scope, value) => object.assign(scope, { name: value }); +} + +class AccessKeyed extends syntax.AccessKeyed { + AccessKeyed(object, key) : super(object, key); + eval(scope) => getKeyed(object.eval(scope), key.eval(scope)); + assign(scope, value) => setKeyed(object.eval(scope), key.eval(scope), value); +} + + +/** + * The [AccessReflective] mixin is used to share code between access expressions + * where we need to use reflection to get or set a field. We optimize for the + * case where we access the same holder repeatedly through caching. + */ +abstract class AccessReflective { + static const int CACHED_FIELD = 0; + static const int CACHED_MAP = 1; + static const int CACHED_VALUE = 2; + + int _cachedKind = 0; + var _cachedHolder = UNINITIALIZED; + var _cachedValue; + + String get name; + Symbol get symbol; + + _eval(holder) { + if (!identical(holder, _cachedHolder)) return _evalUncached(holder); + int cachedKind = _cachedKind; + if (cachedKind == CACHED_MAP) return holder[name]; + var value = _cachedValue; + return (cachedKind == CACHED_FIELD) + ? value.getField(symbol).reflectee + : value; + } + + _evalUncached(holder) { + _cachedHolder = holder; + if (holder == null) { + _cachedKind = CACHED_VALUE; + return _cachedValue = null; + } else if (holder is Map) { + _cachedKind = CACHED_MAP; + _cachedValue = null; + return holder[name]; + } + InstanceMirror mirror = reflect(holder); + try { + var result = mirror.getField(symbol).reflectee; + _cachedKind = CACHED_FIELD; + _cachedValue = mirror; + return result; + } on NoSuchMethodError catch (e) { + var result = createInvokeClosure(mirror, symbol); + if (result == null) rethrow; + _cachedKind = CACHED_VALUE; + return _cachedValue = result; + } on UnsupportedError catch (e) { + var result = createInvokeClosure(mirror, symbol); + if (result == null) rethrow; + _cachedKind = CACHED_VALUE; + return _cachedValue = result; + } + } + + _assign(scope, holder, value) { + if (holder is Map) { + holder[name] = value; + } else if (holder == null) { + _assignToNonExisting(scope, value); + } else { + reflect(holder).setField(symbol, value); + } + return value; + } + + // By default we don't do any assignments to non-existing holders. This + // is overwritten for access to members. + _assignToNonExisting(scope, value) => null; + + static Function createInvokeClosure(InstanceMirror mirror, Symbol symbol) { + if (!hasMember(mirror, symbol)) return null; + return relaxFnArgs(([a0, a1, a2, a3, a4, a5]) { + var arguments = stripTrailingNulls([a0, a1, a2, a3, a4, a5]); + return mirror.invoke(symbol, arguments).reflectee; + }); + } + + static stripTrailingNulls(List list) { + while (list.isNotEmpty && (list.last == null)) { + list.removeLast(); + } + return list; + } + + static bool hasMember(InstanceMirror mirror, Symbol symbol) { + var type = mirror.type as dynamic; + var members = useInstanceMembers ? type.instanceMembers : type.members; + return members.containsKey(symbol); + } + + static final bool useInstanceMembers = computeUseInstanceMembers(); + static bool computeUseInstanceMembers() { + try { + reflect(Object).type.instanceMembers; + return true; + } catch (e) { + return false; + } + } +} + +/** + * The [AccessFast] mixin is used to share code between access expressions + * where we have a pair of pre-compiled getter and setter functions that we + * use to do the access the field. + */ +abstract class AccessFast { + String get name; + Getter get getter; + Setter get setter; + + _eval(holder) { + if (holder == null) return null; + return (holder is Map) ? holder[name] : getter(holder); + } + + _assign(scope, holder, value) { + if (holder == null) { + _assignToNonExisting(scope, value); + return value; + } else { + return (holder is Map) ? (holder[name] = value) : setter(holder, value); + } + } + + // By default we don't do any assignments to non-existing holders. This + // is overwritten for access to members. + _assignToNonExisting(scope, value) => null; +} + diff --git a/lib/core/parser/eval_calls.dart b/lib/core/parser/eval_calls.dart new file mode 100644 index 000000000..efa948bca --- /dev/null +++ b/lib/core/parser/eval_calls.dart @@ -0,0 +1,121 @@ +library angular.core.parser.eval_calls; + +import 'dart:mirrors'; +import 'package:angular/core/parser/syntax.dart' as syntax; +import 'package:angular/core/parser/utils.dart'; + +class CallScope extends syntax.CallScope with CallReflective { + final Symbol symbol; + CallScope(name, arguments) + : super(name, arguments) + , symbol = new Symbol(name); + eval(scope) => _eval(scope, scope); +} + +class CallMember extends syntax.CallMember with CallReflective { + final Symbol symbol; + CallMember(object, name, arguments) + : super(object, name, arguments) + , symbol = new Symbol(name); + eval(scope) => _eval(scope, object.eval(scope)); +} + +class CallScopeFast0 extends syntax.CallScope with CallFast { + final Function function; + CallScopeFast0(name, arguments, this.function) : super(name, arguments); + eval(scope) => _evaluate0(scope); +} + +class CallScopeFast1 extends syntax.CallScope with CallFast { + final Function function; + CallScopeFast1(name, arguments, this.function) : super(name, arguments); + eval(scope) => _evaluate1(scope, arguments[0].eval(scope)); +} + +class CallMemberFast0 extends syntax.CallMember with CallFast { + final Function function; + CallMemberFast0(object, name, arguments, this.function) + : super(object, name, arguments); + eval(scope) => _evaluate0(object.eval(scope)); +} + +class CallMemberFast1 extends syntax.CallMember with CallFast { + final Function function; + CallMemberFast1(object, name, arguments, this.function) + : super(object, name, arguments); + eval(scope) => _evaluate1(object.eval(scope), + arguments[0].eval(scope)); +} + +class CallFunction extends syntax.CallFunction { + CallFunction(function, arguments) : super(function, arguments); + eval(scope) { + var function = this.function.eval(scope); + if (function is !Function) { + throw new EvalError('${this.function} is not a function'); + } else { + return relaxFnApply(function, evalList(scope, arguments)); + } + } +} + + +/** + * The [CallReflective] mixin is used to share code between call expressions + * where we need to use reflection to do the invocation. We optimize for the + * case where we invoke a method on the same holder repeatedly through caching. + */ +abstract class CallReflective { + static const int CACHED_MAP = 0; + static const int CACHED_FUNCTION = 1; + + int _cachedKind = 0; + var _cachedHolder = UNINITIALIZED; + var _cachedValue; + + String get name; + Symbol get symbol; + List get arguments; + + _eval(scope, holder) { + List arguments = evalList(scope, this.arguments); + if (!identical(holder, _cachedHolder)) { + return _evaluteUncached(holder, arguments); + } + return (_cachedKind == CACHED_MAP) + ? relaxFnApply(ensureFunctionFromMap(holder, name), arguments) + : _cachedValue.invoke(symbol, arguments).reflectee; + } + + _evaluteUncached(holder, arguments) { + _cachedHolder = holder; + if (holder is Map) { + _cachedKind = CACHED_MAP; + _cachedValue = null; + return relaxFnApply(ensureFunctionFromMap(holder, name), arguments); + } else { + InstanceMirror mirror = reflect(holder); + _cachedKind = CACHED_FUNCTION; + _cachedValue = mirror; + return mirror.invoke(symbol, arguments).reflectee; + } + } +} + + +/** + * The [CallFast] mixin is used to share code between call expressions + * where we have a pre-compiled helper function that we use to do the + * function invocation. + */ +abstract class CallFast { + String get name; + Function get function; + + _evaluate0(holder) => (holder is Map) + ? ensureFunctionFromMap(holder, name)() + : function(holder); + _evaluate1(holder, a0) => (holder is Map) + ? ensureFunctionFromMap(holder, name)(a0) + : function(holder, a0); +} diff --git a/lib/core/parser/lexer.dart b/lib/core/parser/lexer.dart index fd48fc723..8080d0ccb 100644 --- a/lib/core/parser/lexer.dart +++ b/lib/core/parser/lexer.dart @@ -1,4 +1,32 @@ -part of angular.core.parser; +library angular.core.parser.lexer; + +import 'package:angular/core/module.dart' show NgInjectableService; +import 'package:angular/core/parser/characters.dart'; + +class Token { + final int index; + final String text; + + var value; + // Tokens should have one of these set. + String opKey; + String key; + + Token(this.index, this.text); + + withOp(op) { + this.opKey = op; + } + + withGetterSetter(key) { + this.key = key; + } + + withValue(value) { this.value = value; } + + toString() => "Token($text)"; +} + @NgInjectableService() class Lexer { @@ -112,7 +140,7 @@ class Scanner { Token scanOperator(int start, String string) { assert(peek == string.codeUnitAt(0)); - assert(OPERATORS.containsKey(string)); + assert(OPERATORS.contains(string)); advance(); return new Token(start, string)..withOp(string); } @@ -125,7 +153,7 @@ class Scanner { advance(); string = two; } - assert(OPERATORS.containsKey(string)); + assert(OPERATORS.contains(string)); return new Token(start, string)..withOp(string); } @@ -147,7 +175,7 @@ class Scanner { Token result = new Token(start, string); // TODO(kasperl): Deal with null, undefined, true, and false in // a cleaner and faster way. - if (OPERATORS.containsKey(string)) { + if (OPERATORS.contains(string)) { result.withOp(string); } else { result.withGetterSetter(string); @@ -249,3 +277,31 @@ class Scanner { throw "Lexer Error: $message at column $position in expression [$input]"; } } + +Set OPERATORS = new Set.from([ + 'undefined', + 'null', + 'true', + 'false', + '+', + '-', + '*', + '/', + '~/', + '%', + '^', + '=', + '==', + '!=', + '<', + '>', + '<=', + '>=', + '&&', + '||', + '&', + '|', + '!', + '?', +]); + diff --git a/lib/core/parser/parser.dart b/lib/core/parser/parser.dart index 0de9632fd..74fbb0968 100644 --- a/lib/core/parser/parser.dart +++ b/lib/core/parser/parser.dart @@ -1,485 +1,103 @@ -part of angular.core.parser; - -typedef ParsedGetter(self); -typedef ParsedSetter(self, value); - -typedef Getter([locals]); -typedef Setter(value, [locals]); - -abstract class ParserAST { - bool get assignable; -} - -class Token { - final int index; - final String text; - - var value; - // Tokens should have one of these set. - String opKey; - String key; - - Token(this.index, this.text); - - withOp(op) { - this.opKey = op; - } - - withGetterSetter(key) { - this.key = key; - } - - withValue(value) { this.value = value; } - - toString() => "Token($text)"; -} - -typedef Operator(dynamic self, ParserAST a, ParserAST b); - -Operator NULL_OP = (_, _0, _1) => null; -Operator NOT_IMPL_OP = (_, _0, _1) { throw "Op not implemented"; }; - -// FUNCTIONS USED AT RUNTIME. - -parserEvalError(String s, String text, stack) => - ['Eval Error: $s while evaling [$text]' + - (stack != null ? '\n\nFROM:\n$stack' : '')]; - -// Automatic type conversion. -autoConvertAdd(a, b) { - if (a != null && b != null) { - // TODO(deboer): Support others. - if (a is String && b is! String) { - return a + b.toString(); - } - if (a is! String && b is String) { - return a.toString() + b; - } - return a + b; - } - if (a != null) return a; - if (b != null) return b; - return null; -} - -objectIndexGetField(o, i, evalError) { - if (o == null) throw evalError('Accessing null object'); - - if (o is List) { - return o[i.toInt()]; - } else if (o is Map) { - return o[i.toString()]; // toString dangerous? - } - throw evalError("Attempted field access on a non-list, non-map"); -} - -objectIndexSetField(o, i, v, evalError) { - if (o is List) { - int arrayIndex = i.toInt(); - if (o.length <= arrayIndex) { o.length = arrayIndex + 1; } - o[arrayIndex] = v; - } else if (o is Map) { - o[i.toString()] = v; // toString dangerous? - } else { - throw evalError("Attempting to set a field on a non-list, non-map"); - } - return v; -} - -safeFunctionCall(userFn, fnName, evalError) { - if (userFn == null) { - throw evalError("Undefined function $fnName"); - } - if (userFn is! Function) { - throw evalError("$fnName is not a function"); - } - return userFn; +library angular.core.parser; + +export 'package:angular/core/parser/syntax.dart' + show Visitor, Expression, BoundExpression; +export 'package:angular/core/parser/dynamic_parser.dart' + show DynamicParser, DynamicParserBackend, ClosureMap; +export 'package:angular/core/parser/static_parser.dart' + show StaticParser, StaticParserFunctions; + +typedef LocalsWrapper(context, locals); +typedef Getter(self); +typedef Setter(self, value); + +/// Placeholder for DI. The parser you are looking for is [DynamicParser]. +abstract class Parser { + T call(String input); } -Map OPERATORS = { - 'undefined': NULL_OP, - 'null': NULL_OP, - 'true': (self, a, b) => true, - 'false': (self, a, b) => false, - '+': (self, aFn, bFn) { - var a = aFn.eval(self); - var b = bFn.eval(self); - return autoConvertAdd(a, b); - }, - '-': (self, a, b) { - assert(a != null || b != null); - var aResult = a != null ? a.eval(self) : null; - var bResult = b != null ? b.eval(self) : null; - return (aResult == null ? 0 : aResult) - (bResult == null ? 0 : bResult); - }, - '*': (s, a, b) => a.eval(s) * b.eval(s), - '/': (s, a, b) => a.eval(s) / b.eval(s), - '~/': (s, a, b) => a.eval(s) ~/ b.eval(s), - '%': (s, a, b) => a.eval(s) % b.eval(s), - '^': (s, a, b) => a.eval(s) ^ b.eval(s), - '=': NULL_OP, - '==': (s, a, b) => a.eval(s) == b.eval(s), - '!=': (s, a, b) => a.eval(s) != b.eval(s), - '<': (s, a, b) => a.eval(s) < b.eval(s), - '>': (s, a, b) => a.eval(s) > b.eval(s), - '<=': (s, a, b) => a.eval(s) <= b.eval(s), - '>=': (s, a, b) => a.eval(s) >= b.eval(s), - '&&': (s, a, b) => toBool(a.eval(s)) && toBool(b.eval(s)), - '||': (s, a, b) => toBool(a.eval(s)) || toBool(b.eval(s)), - '&': (s, a, b) => a.eval(s) & b.eval(s), - '|': NOT_IMPL_OP, //b()(a()) - '!': (s, a, b) => !toBool(a.eval(s)), - '?': (s, c, t, f) => toBool(c.eval(s)) ? t.eval(s) : f.eval(s), -}; - -@NgInjectableService() -class DynamicParser implements Parser { - final Lexer _lexer; - final ParserBackend _b; - - DynamicParser(this._lexer, this._b); - - List _tokens; - String _text; - var _evalError; - - Map _cache = {}; - - ParserAST call(String text) { - var value = _cache[text]; - if (value != null) { - return value; - } - return _cache[text] = _call(text); - } - - ParserAST _call(String text) { - try { - if (text == null) text = ''; - _tokenSavers = []; - _text = text; - _tokens = _lexer.call(text); - _evalError = (String s, [stack]) => parserEvalError(s, text, stack); - - ParserAST value = _statements(); - - if (_tokens.length != 0) { - throw _parserError("Unconsumed token ${_tokens[0].text}"); - } - return value; - } finally { - _tokens = null; - _text = null; - _evalError = null; - _tokenSavers = null; - } - } - - primaryFromToken(Token token, parserError) { - if (token.key != null) { - return _b.getterSetter(token.key); - } - if (token.opKey != null) { - return _b.fromOperator(token.opKey); - } - if (token.value != null) { - return _b.value(token.value); - } - if (token.text != null) { - return _b.value(token.text); - } - throw parserError("Internal Angular Error: Tokens should have keys, text or fns"); - } - - _parserError(String s, [Token t]) { - if (t == null && !_tokens.isEmpty) t = _tokens[0]; - String location = t == null ? - 'the end of the expression' : - 'at column ${t.index + 1} in'; - return 'Parser Error: $s $location [$_text]'; - } - - - Token _peekToken() { - if (_tokens.length == 0) - throw "Unexpected end of expression: " + _text; - return _tokens[0]; - } - - Token _peek([String e1, String e2, String e3, String e4]) { - if (_tokens.length > 0) { - Token token = _tokens[0]; - String t = token.text; - if (t==e1 || t==e2 || t==e3 || t==e4 || - (e1 == null && e2 == null && e3 == null && e4 == null)) { - return token; - } - } - return null; - } - - /** - * Token savers are synchronous lists that allows Parser functions to - * access the tokens parsed during some amount of time. They are useful - * for printing helpful debugging messages. - */ - List> _tokenSavers; - List _saveTokens() { var n = []; _tokenSavers.add(n); return n; } - _stopSavingTokens(x) { if (!_tokenSavers.remove(x)) { throw 'bad token saver'; } return x; } - _tokensText(List x) => x.map((x) => x.text).join(); - - - Token _expect([String e1, String e2, String e3, String e4]){ - Token token = _peek(e1, e2, e3, e4); - if (token != null) { - var consumed = _tokens.removeAt(0); - _tokenSavers.forEach((ts) => ts.add(consumed)); - return token; - } - return null; - } - - ParserAST _consume(e1){ - if (_expect(e1) == null) { - throw _parserError("Missing expected $e1"); - } - } - - ParserAST _primary() { - var primary; - var ts = _saveTokens(); - if (_expect('(') != null) { - primary = _filterChain(); - _consume(')'); - } else if (_expect('[') != null) { - primary = _arrayDeclaration(); - } else if (_expect('{') != null) { - primary = _object(); - } else { - Token token = _expect(); - primary = primaryFromToken(token, _parserError); - if (primary == null) { - throw _parserError("Internal Angular Error: Unreachable code A."); - } - } - - var next; - while ((next = _expect('(', '[', '.')) != null) { - if (next.text == '(') { - primary = _functionCall(primary, _tokensText(ts.sublist(0, ts.length - 1))); - } else if (next.text == '[') { - primary = _objectIndex(primary); - } else if (next.text == '.') { - primary = _fieldAccess(primary); - } else { - throw _parserError("Internal Angular Error: Unreachable code B."); - } - } - _stopSavingTokens(ts); - return primary; - } - - ParserAST _binaryFn(ParserAST left, String op, ParserAST right) => - _b.binaryFn(left, op, right); - - ParserAST _unaryFn(String op, ParserAST right) => - _b.unaryFn(op, right); - - ParserAST _unary() { - var token; - if (_expect('+') != null) { - return _primary(); - } else if ((token = _expect('-')) != null) { - return _binaryFn(_b.zero(), token.opKey, _unary()); - } else if ((token = _expect('!')) != null) { - return _unaryFn(token.opKey, _unary()); - } else { - return _primary(); - } - } - - ParserAST _multiplicative() { - var left = _unary(); - var token; - while ((token = _expect('*','%','/','~/')) != null) { - left = _binaryFn(left, token.opKey, _unary()); - } - return left; - } - - ParserAST _additive() { - var left = _multiplicative(); - var token; - while ((token = _expect('+','-')) != null) { - left = _binaryFn(left, token.opKey, _multiplicative()); - } - return left; - } - - ParserAST _relational() { - var left = _additive(); - var token; - if ((token = _expect('<', '>', '<=', '>=')) != null) { - left = _binaryFn(left, token.opKey, _relational()); - } - return left; - } - - ParserAST _equality() { - var left = _relational(); - var token; - if ((token = _expect('==','!=')) != null) { - left = _binaryFn(left, token.opKey, _equality()); - } - return left; - } - - ParserAST _logicalAND() { - var left = _equality(); - var token; - if ((token = _expect('&&')) != null) { - left = _binaryFn(left, token.opKey, _logicalAND()); - } - return left; - } - - ParserAST _logicalOR() { - var left = _logicalAND(); - var token; - while(true) { - if ((token = _expect('||')) != null) { - left = _binaryFn(left, token.opKey, _logicalAND()); - } else { - return left; - } - } - } - - ParserAST _ternary() { - var ts = _saveTokens(); - var cond = _logicalOR(); - var token = _expect('?'); - if (token != null) { - var _true = _expression(); - if ((token = _expect(':')) != null) { - cond = _b.ternaryFn(cond, _true, _expression()); - } else { - throw _parserError('Conditional expression ${_tokensText(ts)} requires ' - 'all 3 expressions'); - } - } - _stopSavingTokens(ts); - return cond; - } - - ParserAST _assignment() { - var ts = _saveTokens(); - var left = _ternary(); - _stopSavingTokens(ts); - var right; - var token; - if ((token = _expect('=')) != null) { - if (!left.assignable) { - throw _parserError('Expression ${_tokensText(ts)} is not assignable', token); - } - right = _ternary(); - return _b.assignment(left, right, _evalError); - } else { - return left; - } - } - - ParserAST _expression() { - return _assignment(); - } - - _filterChain() { - var left = _expression(); - var token; - while(true) { - if ((token = _expect('|')) != null) { - left = _filter(left); - } else { - return left; - } - } - } - - ParserAST _filter(ParserAST left) { - var token = _expect(); - var filterName = token.text; - var argsFn = []; - while(true) { - if ((token = _expect(':')) != null) { - argsFn.add(_expression()); - } else { - return _b.filter(filterName, left, argsFn, _evalError); - } - } - } - - _statements() { - List statements = []; - while (true) { - if (_tokens.length > 0 && _peek('}', ')', ';', ']') == null) - statements.add(_filterChain()); - if (_expect(';') == null) { - return statements.length == 1 - ? statements[0] - : _b.multipleStatements(statements); - } - } - } - - _functionCall(fn, fnName) { - var argsFn = []; - if (_peekToken().text != ')') { - do { - argsFn.add(_expression()); - } while (_expect(',') != null); - } - _consume(')'); - return _b.functionCall(fn, fnName, argsFn, _evalError); - } - - // This is used with json array declaration - _arrayDeclaration() { - var elementFns = []; - if (_peekToken().text != ']') { - do { - elementFns.add(_expression()); - } while (_expect(',') != null); - } - _consume(']'); - return _b.arrayDeclaration(elementFns); - } - - _objectIndex(obj) { - var indexFn = _expression(); - _consume(']'); - return _b.objectIndex(obj, indexFn, _evalError); - } - - _fieldAccess(object) { - var field = _expect().text; - //var getter = getter(field); - return _b.fieldAccess(object, field); - } - - _object() { - var keyValues = []; - if (_peekToken().text != '}') { - do { - var token = _expect(), - key = token.value != null && token.value is String ? token.value : token.text; - _consume(":"); - var value = _expression(); - keyValues.add({"key":key, "value":value}); - } while (_expect(',') != null); - } - _consume('}'); - return _b.object(keyValues); - } - +abstract class ParserBackend { + bool isAssignable(T expression); + + T newChain(List expressions) + => null; + T newFilter(T expression, String name, List arguments) + => null; + + T newAssign(T target, T value) + => null; + T newConditional(T condition, T yes, T no) + => null; + + T newAccessScope(String name) + => null; + T newAccessMember(T object, String name) + => null; + T newAccessKeyed(T object, T key) + => null; + + T newCallScope(String name, List arguments) + => null; + T newCallFunction(T function, List arguments) + => null; + T newCallMember(T object, String name, List arguments) + => null; + + T newPrefix(String operation, T expression) + => null; + T newPrefixPlus(T expression) + => expression; + T newPrefixMinus(T expression) + => newBinaryMinus(newLiteralZero(), expression); + T newPrefixNot(T expression) + => newPrefix('!', expression); + + T newBinary(String operation, T left, T right) + => null; + T newBinaryPlus(T left, T right) + => newBinary('+', left, right); + T newBinaryMinus(T left, T right) + => newBinary('-', left, right); + T newBinaryMultiply(T left, T right) + => newBinary('*', left, right); + T newBinaryDivide(T left, T right) + => newBinary('/', left, right); + T newBinaryModulo(T left, T right) + => newBinary('%', left, right); + T newBinaryTruncatingDivide(T left, T right) + => newBinary('~/', left, right); + T newBinaryLogicalAnd(T left, T right) + => newBinary('&&', left, right); + T newBinaryLogicalOr(T left, T right) + => newBinary('||', left, right); + T newBinaryEqual(T left, T right) + => newBinary('==', left, right); + T newBinaryNotEqual(T left, T right) + => newBinary('!=', left, right); + T newBinaryLessThan(T left, T right) + => newBinary('<', left, right); + T newBinaryGreaterThan(T left, T right) + => newBinary('>', left, right); + T newBinaryLessThanEqual(T left, T right) + => newBinary('<=', left, right); + T newBinaryGreaterThanEqual(T left, T right) + => newBinary('>=', left, right); + + T newLiteralPrimitive(value) + => null; + T newLiteralArray(List elements) + => null; + T newLiteralObject(List keys, List values) + => null; + T newLiteralNull() + => newLiteralPrimitive(null); + T newLiteralZero() + => newLiteralNumber(0); + T newLiteralBoolean(bool value) + => newLiteralPrimitive(value); + T newLiteralNumber(num value) + => newLiteralPrimitive(value); + T newLiteralString(String value) + => null; } diff --git a/lib/core/parser/parser_library.dart b/lib/core/parser/parser_library.dart deleted file mode 100644 index dedce6fd6..000000000 --- a/lib/core/parser/parser_library.dart +++ /dev/null @@ -1,19 +0,0 @@ -library angular.core.parser; - -import 'dart:mirrors'; - -import 'package:angular/utils.dart'; -import 'package:angular/core/module.dart'; -import 'package:angular/core/parser/characters.dart'; - -part 'backend.dart'; -part 'lexer.dart'; -part 'parser.dart'; -part 'static_parser.dart'; - -// Placeholder for DI. -// The parser you are looking for is DynamicParser -abstract class Parser { - Expression call(String text) {} - primaryFromToken(Token token, parserError); -} diff --git a/lib/core/parser/static_parser.dart b/lib/core/parser/static_parser.dart index 5d0db008c..155472108 100644 --- a/lib/core/parser/static_parser.dart +++ b/lib/core/parser/static_parser.dart @@ -1,32 +1,59 @@ -part of angular.core.parser; +library angular.core.parser.static_parser; -class StaticParserFunctions { - StaticParserFunctions(Map this.functions); +import 'package:angular/core/parser/parser.dart'; +import 'package:angular/core/parser/utils.dart' show EvalError; - Map functions; +class StaticParserFunctions { + final Map eval; + final Map assign; + StaticParserFunctions(this.eval, this.assign); } -@NgInjectableService() -class StaticParser implements Parser { - Map _functions; - Parser _fallbackParser; +//@NgInjectableService() +class StaticParser implements Parser { + final StaticParserFunctions _functions; + final DynamicParser _fallbackParser; + final Map _cache = {}; + StaticParser(this._functions, this._fallbackParser); - StaticParser(StaticParserFunctions functions, - DynamicParser this._fallbackParser) { - assert(functions != null); - _functions = functions.functions; + Expression call(String input) { + if (input == null) input = ''; + return _cache.putIfAbsent(input, () => _construct(input)); } - call(String exp) { - if (exp == null) exp = ""; - if (!_functions.containsKey(exp)) { - //print("Expression [$exp] is not supported in static parser"); - return _fallbackParser.call(exp); + Expression _construct(String input) { + var eval = _functions.eval[input]; + if (eval == null) return _fallbackParser(input); + if (eval is !Function) throw eval; + Function assign = _functions.assign[input]; + return new StaticExpression(input, eval, assign); + } +} + +class StaticExpression extends Expression { + final String _input; + final Function _eval; + final Function _assign; + StaticExpression(this._input, this._eval, [this._assign]); + + bool get isAssignable => _assign != null; + accept(Visitor visitor) => throw "Cannot visit static expression $this"; + toString() => _input; + + eval(scope) { + try { + return _eval(scope); + } on EvalError catch (e, s) { + throw e.unwrap("$this", s); } - return _functions[exp]; } - primaryFromToken(Token token, parserError) { - throw 'Not Implemented'; + assign(scope, value) { + try { + if (_assign == null) throw new EvalError("Cannot assign to $this"); + return _assign(scope, value); + } on EvalError catch (e, s) { + throw e.unwrap("$this", s); + } } -} +} \ No newline at end of file diff --git a/lib/core/parser/syntax.dart b/lib/core/parser/syntax.dart new file mode 100644 index 000000000..174c192b6 --- /dev/null +++ b/lib/core/parser/syntax.dart @@ -0,0 +1,202 @@ +library angular.core.parser.syntax; + +import 'package:angular/core/parser/parser.dart' show LocalsWrapper; +import 'package:angular/core/parser/unparser.dart' show Unparser; +import 'package:angular/core/parser/utils.dart' show EvalError; + +abstract class Visitor { + visit(Expression expression) + => expression.accept(this); + + visitExpression(Expression expression) + => null; + visitChain(Chain expression) + => visitExpression(expression); + visitFilter(Filter expression) + => visitExpression(expression); + + visitAssign(Assign expression) + => visitExpression(expression); + visitConditional(Conditional expression) + => visitExpression(expression); + + visitAccessScope(AccessScope expression) + => visitExpression(expression); + visitAccessMember(AccessMember expression) + => visitExpression(expression); + visitAccessKeyed(AccessKeyed expression) + => visitExpression(expression); + + visitCallScope(CallScope expression) + => visitExpression(expression); + visitCallFunction(CallFunction expression) + => visitExpression(expression); + visitCallMember(CallMember expression) + => visitExpression(expression); + + visitBinary(Binary expression) + => visitExpression(expression); + + visitPrefix(Prefix expression) + => visitExpression(expression); + + visitLiteral(Literal expression) + => visitExpression(expression); + visitLiteralPrimitive(LiteralPrimitive expression) + => visitLiteral(expression); + visitLiteralString(LiteralString expression) + => visitLiteral(expression); + visitLiteralArray(LiteralArray expression) + => visitLiteral(expression); + visitLiteralObject(LiteralObject expression) + => visitLiteral(expression); +} + +abstract class Expression { + bool get isAssignable => false; + bool get isChain => false; + + eval(scope) + => throw new EvalError("Cannot evaluate $this"); + assign(scope, value) + => throw new EvalError("Cannot assign to $this"); + bind(context, [LocalsWrapper wrapper]) + => new BoundExpression(this, context, wrapper); + + accept(Visitor visitor); + String toString() => Unparser.unparse(this); +} + +class BoundExpression { + final Expression expression; + final _context; + final LocalsWrapper _wrapper; + BoundExpression(this.expression, this._context, this._wrapper); + + call([locals]) => expression.eval(_computeContext(locals)); + assign(value, [locals]) => expression.assign(_computeContext(locals), value); + + _computeContext(locals) { + if (locals == null) return _context; + if (_wrapper != null) return _wrapper(_context, locals); + throw new StateError("Locals $locals provided, but missing wrapper."); + } +} + +class Chain extends Expression { + final List expressions; + Chain(this.expressions); + bool get isChain => true; + accept(Visitor visitor) => visitor.visitChain(this); +} + +class Filter extends Expression { + final Expression expression; + final String name; + final List arguments; + Filter(this.expression, this.name, this.arguments); + accept(Visitor visitor) => visitor.visitFilter(this); +} + +class Assign extends Expression { + final Expression target; + final Expression value; + Assign(this.target, this.value); + accept(Visitor visitor) => visitor.visitAssign(this); +} + +class Conditional extends Expression { + final Expression condition; + final Expression yes; + final Expression no; + Conditional(this.condition, this.yes, this.no); + accept(Visitor visitor) => visitor.visitConditional(this); +} + +class AccessScope extends Expression { + final String name; + AccessScope(this.name); + bool get isAssignable => true; + accept(Visitor visitor) => visitor.visitAccessScope(this); +} + +class AccessMember extends Expression { + final Expression object; + final String name; + AccessMember(this.object, this.name); + bool get isAssignable => true; + accept(Visitor visitor) => visitor.visitAccessMember(this); +} + +class AccessKeyed extends Expression { + final Expression object; + final Expression key; + AccessKeyed(this.object, this.key); + bool get isAssignable => true; + accept(Visitor visitor) => visitor.visitAccessKeyed(this); +} + +class CallScope extends Expression { + final String name; + final List arguments; + CallScope(this.name, this.arguments); + accept(Visitor visitor) => visitor.visitCallScope(this); +} + +class CallFunction extends Expression { + final Expression function; + final List arguments; + CallFunction(this.function, this.arguments); + accept(Visitor visitor) => visitor.visitCallFunction(this); +} + +class CallMember extends Expression { + final Expression object; + final String name; + final List arguments; + CallMember(this.object, this.name, this.arguments); + accept(Visitor visitor) => visitor.visitCallMember(this); +} + +class Binary extends Expression { + final String operation; + final Expression left; + final Expression right; + Binary(this.operation, this.left, this.right); + accept(Visitor visitor) => visitor.visitBinary(this); +} + +class Prefix extends Expression { + final String operation; + final Expression expression; + Prefix(this.operation, this.expression); + accept(Visitor visitor) => visitor.visitPrefix(this); +} + +abstract class Literal extends Expression { +} + +class LiteralPrimitive extends Literal { + final value; + LiteralPrimitive(this.value); + accept(Visitor visitor) => visitor.visitLiteralPrimitive(this); +} + +class LiteralString extends Literal { + final String value; + LiteralString(this.value); + accept(Visitor visitor) => visitor.visitLiteralString(this); +} + +class LiteralArray extends Literal { + final List elements; + LiteralArray(this.elements); + accept(Visitor visitor) => visitor.visitLiteralArray(this); +} + +class LiteralObject extends Literal { + final List keys; + final List values; + LiteralObject(this.keys, this.values); + accept(Visitor visitor) => visitor.visitLiteralObject(this); +} diff --git a/lib/core/parser/unparser.dart b/lib/core/parser/unparser.dart new file mode 100644 index 000000000..cbf968f2d --- /dev/null +++ b/lib/core/parser/unparser.dart @@ -0,0 +1,135 @@ +library angular.core.parser.unparser; + +import 'package:angular/core/parser/syntax.dart'; + +class Unparser extends Visitor { + final StringBuffer buffer; + Unparser(this.buffer); + + static String unparse(Expression expression) { + StringBuffer buffer = new StringBuffer(); + Unparser unparser = new Unparser(buffer); + unparser.visit(expression); + return "$buffer"; + } + + write(String string) { + buffer.write(string); + } + + writeArguments(List arguments) { + write('('); + for (int i = 0; i < arguments.length; i++) { + if (i != 0) write(','); + visit(arguments[i]); + } + write(')'); + } + + visitChain(Chain chain) { + for (int i = 0; i < chain.expressions.length; i++) { + if (i != 0) write(';'); + visit(chain.expressions[i]); + } + } + + visitFilter(Filter filter) { + write('('); + visit(filter.expression); + write('|${filter.name}'); + for (int i = 0; i < filter.arguments.length; i++) { + write(' :'); + visit(filter.arguments[i]); + } + write(')'); + } + + visitAssign(Assign assign) { + visit(assign.target); + write('='); + visit(assign.value); + } + + visitConditional(Conditional conditional) { + visit(conditional.condition); + write('?'); + visit(conditional.yes); + write(':'); + visit(conditional.no); + } + + visitAccessScope(AccessScope access) { + write(access.name); + } + + visitAccessMember(AccessMember access) { + visit(access.object); + write('.${access.name}'); + } + + visitAccessKeyed(AccessKeyed access) { + visit(access.object); + write('['); + visit(access.key); + write(']'); + } + + visitCallScope(CallScope call) { + write(call.name); + writeArguments(call.arguments); + } + + visitCallFunction(CallFunction call) { + visit(call.function); + writeArguments(call.arguments); + } + + visitCallMember(CallMember call) { + visit(call.object); + write('.${call.name}'); + writeArguments(call.arguments); + } + + visitPrefix(Prefix prefix) { + write('(${prefix.operation}'); + visit(prefix.expression); + write(')'); + } + + visitBinary(Binary binary) { + write('('); + visit(binary.left); + write(binary.operation); + visit(binary.right); + write(')'); + } + + visitLiteralPrimitive(LiteralPrimitive literal) { + write("${literal.value}"); + } + + visitLiteralArray(LiteralArray literal) { + write('['); + for (int i = 0; i < literal.elements.length; i++) { + if (i != 0) write(','); + visit(literal.elements[i]); + } + write(']'); + } + + visitLiteralObject(LiteralObject literal) { + write('{'); + List keys = literal.keys; + for (int i = 0; i < keys.length; i++) { + if (i != 0) write(','); + write("'${keys[i]}':"); + visit(literal.values[i]); + } + write('}'); + } + + visitLiteralString(LiteralString literal) { + String escaped = literal.value.replaceAll("'", "\\'"); + write("'$escaped'"); + } +} diff --git a/lib/core/parser/utils.dart b/lib/core/parser/utils.dart new file mode 100644 index 000000000..5a31e052a --- /dev/null +++ b/lib/core/parser/utils.dart @@ -0,0 +1,96 @@ +library angular.core.parser.utils; + +import 'package:angular/core/parser/syntax.dart' show Expression; +export 'package:angular/utils.dart' show relaxFnApply, relaxFnArgs, toBool; + +/// Marker for an uninitialized value. +const UNINITIALIZED = const _Uninitialized(); +class _Uninitialized { const _Uninitialized(); } + +class EvalError { + final String message; + EvalError(this.message); + + String unwrap(String input, stack) { + String location = (stack == null) ? '' : '\n\nFROM:\n$stack'; + return 'Eval Error: $message while evaling [$input]$location'; + } +} + +/// Evaluate the [list] in context of the [scope]. +List evalList(scope, List list) { + int length = list.length; + for(int cacheLength = _evalListCache.length; cacheLength <= length; cacheLength++) { + _evalListCache.add(new List(cacheLength)); + } + List result = _evalListCache[length]; + for (int i = 0; i < length; i++) { + result[i] = list[i].eval(scope); + } + return result; +} +final List _evalListCache = [[],[0],[0,0],[0,0,0],[0,0,0,0],[0,0,0,0,0]]; + +/// Add the two arguments with automatic type conversion. +autoConvertAdd(a, b) { + if (a != null && b != null) { + // TODO(deboer): Support others. + if (a is String && b is! String) { + return a + b.toString(); + } + if (a is! String && b is String) { + return a.toString() + b; + } + return a + b; + } + if (a != null) return a; + if (b != null) return b; + return null; +} + +/** + * Ensures that the given [function] is a function and return it. Throws + * an [EvalError] if it isn't. + */ +Function ensureFunction(function, String name) { + if (function is Function) return function; + if (function == null) { + throw new EvalError("Undefined function $name"); + } else { + throw new EvalError("$name is not a function"); + } +} + +/** + * Ensures that the map entry with the given [name] is a function and + * return it. Throws an [EvalError] if it isn't. + */ +Function ensureFunctionFromMap(Map map, String name) { + return ensureFunction(map[name], name); +} + +/// Get a keyed element from the given [object]. +getKeyed(object, key) { + if (object is List) { + return object[key.toInt()]; + } else if (object is Map) { + return object["$key"]; // toString dangerous? + } else if (object == null) { + throw new EvalError('Accessing null object'); + } + throw new EvalError("Attempted field access on a non-list, non-map"); +} + +/// Set a keyed element in the given [object]. +setKeyed(object, key, value) { + if (object is List) { + int index = key.toInt(); + if (object.length <= index) object.length = index + 1; + object[index] = value; + } else if (object is Map) { + object["$key"] = value; // toString dangerous? + } else { + throw new EvalError("Attempting to set a field on a non-list, non-map"); + } + return value; +} diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 72637b8bf..928e79ff6 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -767,7 +767,8 @@ class Scope implements Map { if (exp == null) { return () => null; } else if (exp is String) { - return _parser(exp).eval; + Expression expression = _parser(exp); + return expression.eval; } else if (exp is Function) { return exp; } else { diff --git a/lib/core_dom/compiler.dart b/lib/core_dom/compiler.dart index 7ad524e9f..7f11dec49 100644 --- a/lib/core_dom/compiler.dart +++ b/lib/core_dom/compiler.dart @@ -4,7 +4,7 @@ part of angular.core.dom; class Compiler { final DirectiveMap directives; final Profiler _perf; - final Parser _parser; + final Parser _parser; final Expando _expando; DirectiveSelector selector; @@ -141,7 +141,7 @@ class Compiler { var dstPath = match[2]; Expression dstPathFn = _parser(dstPath.isEmpty ? attrName : dstPath); - if (!dstPathFn.assignable) { + if (!dstPathFn.isAssignable) { throw "Expression '$dstPath' is not assignable in mapping '$mapping' for attribute '$attrName'."; } ApplyMapping mappingFn; @@ -160,7 +160,7 @@ class Compiler { () => attrExprFn.eval(scope), (v) => dstPathFn.assign(dst, shadowValue = v), attrs[attrName]); - if (attrExprFn.assignable) { + if (attrExprFn.isAssignable) { scope.$watch( () => dstPathFn.eval(dst), (v) { diff --git a/lib/core_dom/module.dart b/lib/core_dom/module.dart index 70b6fae93..b8a0af936 100644 --- a/lib/core_dom/module.dart +++ b/lib/core_dom/module.dart @@ -7,9 +7,9 @@ import 'dart:html' as dom; import 'package:di/di.dart'; import 'package:perf_api/perf_api.dart'; -import '../core/module.dart'; -import '../core/parser/parser_library.dart'; -import '../utils.dart'; +import 'package:angular/core/module.dart'; +import 'package:angular/core/parser/parser.dart'; +import 'package:angular/utils.dart'; part 'block.dart'; part 'block_factory.dart'; @@ -43,7 +43,6 @@ class NgCoreDomModule extends Module { type(HttpDefaults); type(HttpInterceptors); type(BlockCache); - type(GetterSetter); type(BrowserCookies); type(Cookies); type(LocationWrapper); diff --git a/lib/directive/module.dart b/lib/directive/module.dart index e187bb4e8..2a0d5a097 100644 --- a/lib/directive/module.dart +++ b/lib/directive/module.dart @@ -5,7 +5,7 @@ import 'dart:html' as dom; import 'dart:async' as async; import 'package:intl/intl.dart'; import 'package:angular/core/module.dart'; -import 'package:angular/core/parser/parser_library.dart'; +import 'package:angular/core/parser/parser.dart'; import 'package:angular/core_dom/module.dart'; import 'package:angular/utils.dart'; diff --git a/lib/directive/ng_pluralize.dart b/lib/directive/ng_pluralize.dart index b23f75eaa..5b71a78c5 100644 --- a/lib/directive/ng_pluralize.dart +++ b/lib/directive/ng_pluralize.dart @@ -122,8 +122,7 @@ class NgPluralizeDirective { } catch(e) { try { value = double.parse(value); - } - catch(e) { + } catch(e) { element.text = ''; return; } @@ -131,7 +130,7 @@ class NgPluralizeDirective { } String stringValue = value.toString(); - int intValue = value is double ? value.round() : value; + int intValue = value.toInt(); if (discreteRules[stringValue] != null) { _setAndWatch(discreteRules[stringValue]); diff --git a/lib/filter/filter.dart b/lib/filter/filter.dart index 71ef5db89..c2822d32a 100644 --- a/lib/filter/filter.dart +++ b/lib/filter/filter.dart @@ -111,7 +111,7 @@ typedef bool Equals(a, b); */ @NgFilter(name: 'filter') class FilterFilter { - Parser _parser; + Parser _parser; Equals _comparator; Equals _stringComparator; diff --git a/lib/filter/limit_to.dart b/lib/filter/limit_to.dart index 17d6c35ce..e2889900e 100644 --- a/lib/filter/limit_to.dart +++ b/lib/filter/limit_to.dart @@ -37,9 +37,8 @@ part of angular.filter; @NgFilter(name:'limitTo') class LimitToFilter { Injector _injector; - Parser _parser; - LimitToFilter(this._injector, this._parser); + LimitToFilter(this._injector); dynamic call(dynamic items, [int limit]) { if (items == null) { diff --git a/lib/filter/module.dart b/lib/filter/module.dart index aaecab717..5c90c5af4 100644 --- a/lib/filter/module.dart +++ b/lib/filter/module.dart @@ -4,7 +4,7 @@ import 'dart:convert' show JSON; import 'package:intl/intl.dart'; import 'package:di/di.dart'; import 'package:angular/core/module.dart'; -import 'package:angular/core/parser/parser_library.dart'; +import 'package:angular/core/parser/parser.dart'; part 'currency.dart'; part 'date.dart'; diff --git a/lib/filter/order_by.dart b/lib/filter/order_by.dart index 5685020fa..438162d60 100644 --- a/lib/filter/order_by.dart +++ b/lib/filter/order_by.dart @@ -106,7 +106,7 @@ typedef dynamic Mapper(dynamic e); */ @NgFilter(name: 'orderBy') class OrderByFilter { - Parser _parser; + Parser _parser; OrderByFilter(this._parser); @@ -167,7 +167,7 @@ class OrderByFilter { if (strExp == '') { mappers[i] = _nop; } else { - var parsed = _parser(strExp); + Expression parsed = _parser(strExp); mappers[i] = (e) => parsed.eval(e); } } else if (expression is Mapper) { diff --git a/lib/tools/expression_extractor.dart b/lib/tools/expression_extractor.dart index 8326071f8..b98ea1a35 100644 --- a/lib/tools/expression_extractor.dart +++ b/lib/tools/expression_extractor.dart @@ -10,8 +10,6 @@ import 'package:angular/tools/common.dart'; import 'package:di/di.dart'; import 'package:di/dynamic_injector.dart'; -import 'package:angular/core/parser/parser_library.dart'; -import 'package:angular/tools/parser_generator/dart_code_gen.dart'; import 'package:angular/tools/parser_generator/generator.dart'; main(args) { @@ -37,22 +35,20 @@ main(args) { var headerFile = args[2]; var footerFile = args[3]; var outputFile = args[4]; - SourcePrinter _prt; + SourcePrinter printer; if (outputFile == '--') { - _prt = new SourcePrinter(); + printer = new SourcePrinter(); } else { - _prt = new FileSourcePrinter(outputFile); + printer = new FileSourcePrinter(outputFile); } // Output the header file first. if (headerFile != '') { - _prt.printSrc(_readFile(headerFile)); + printer.printSrc(_readFile(headerFile)); } - _prt.printSrc('// Found ${expressions.length} expressions'); - Module module = new Module() - ..type(ParserBackend, implementedBy: DartCodeGen) - ..value(SourcePrinter, _prt); + printer.printSrc('// Found ${expressions.length} expressions'); + Module module = new Module()..value(SourcePrinter, printer); Injector injector = new DynamicInjector(modules: [module], allowImplicitInjection: true); @@ -61,7 +57,7 @@ main(args) { // Output footer last. if (footerFile != '') { - _prt.printSrc(_readFile(footerFile)); + printer.printSrc(_readFile(footerFile)); } } diff --git a/lib/tools/parser_generator/dart_code_gen.dart b/lib/tools/parser_generator/dart_code_gen.dart index 381234447..8856a003a 100644 --- a/lib/tools/parser_generator/dart_code_gen.dart +++ b/lib/tools/parser_generator/dart_code_gen.dart @@ -1,308 +1,305 @@ library dart_code_gen; import 'package:angular/tools/reserved_dart_keywords.dart'; -import 'package:angular/core/parser/parser_library.dart'; // For ParserBackend. -import 'source.dart'; +import 'package:angular/core/parser/syntax.dart'; + +escape(String s) => s.replaceAllMapped(new RegExp(r'(\"|\$|\n)'), (m) { + var char = m[1]; + if (char == '\n') char = 'n'; + return "\\$char"; +}); + +class DartCodeGen { + final HelperMap getters = new HelperMap('_', + getterTemplate, getterTemplateForReserved); + final HelperMap holders = new HelperMap('_ensure\$', + holderTemplate, holderTemplateForReserved); + final HelperMap setters = new HelperMap('_set\$', + setterTemplate, setterTemplateForReserved); + + String generate(Expression expression, bool assign) { + var v = new DartCodeGenVisitor(getters, holders, setters); + return assign ? v.assign(expression)('value') : v.evaluate(expression); + } +} +class DartCodeGenVisitor extends Visitor { + static const int STATE_EVAL = 0; + static const int STATE_EVAL_HOLDER = 1; + static const int STATE_ASSIGN = 2; + int state = STATE_EVAL; -Code VALUE_CODE = new Code("value"); + final HelperMap getters; + final HelperMap holders; + final HelperMap setters; -typedef CodeAssign(Code c); + DartCodeGenVisitor(this.getters, this.holders, this.setters); -class Code implements ParserAST, Expression { - String id; - String _exp; - String simpleGetter; - CodeAssign _assign; + bool get isEvaluating => state == STATE_EVAL; + bool get isEvaluatingHolder => state == STATE_EVAL_HOLDER; + bool get isAssigning => state == STATE_ASSIGN; - Code(this._exp, [this._assign, this.simpleGetter]) { - id = _exp == null ? simpleGetter : _exp; - if (id == null) { - throw 'id is null'; - } - } + String lookupGetter(String key) => getters.lookup(key); + String lookupHolder(String key) => holders.lookup(key); + String lookupSetter(String key) => setters.lookup(key); - String get exp { - if (_exp == null) { - throw "Can not be used in an expression: $id"; + String lookupAccessor(String key) { + switch (state) { + case STATE_EVAL: return lookupGetter(key); + case STATE_EVAL_HOLDER: return lookupHolder(key); + case STATE_ASSIGN: return lookupSetter(key); } - return _exp; } - get assignable => _assign != null; - - // methods from Expression - Expression fieldHolder; - String fieldName; - bool get isFieldAccess => null; - void set exp(String s) => throw new UnimplementedError(); - ParsedGetter get eval => throw new UnimplementedError(); - ParsedSetter get assign => throw new UnimplementedError(); - List get parts => throw new UnimplementedError(); - set parts(List p) => throw new UnimplementedError(); - bind(context, localsWrapper) => throw new UnimplementedError(); - - Source toSource(SourceBuilder _) { - return _('new Expression', _.parens( - _('(scope)', _.body( - 'return $exp;' - )), - assignable ? _('(scope, value)', _.body( - 'return ${_assign(VALUE_CODE).exp};' - )) : 'null' - )); + String toBool(String value) + => 'toBool($value)'; + String safeCallFunction(String function, String name, String arguments) + => 'ensureFunction($function, "$name")($arguments)'; + + String evaluate(Expression expression, {bool convertToBool: false}) { + int old = state; + try { + state = STATE_EVAL; + String result = visit(expression); + if (convertToBool) result = toBool(result); + return result; + } finally { + state = old; + } } -} -class ThrowCode extends Code { - ThrowCode(code): super('throw $code'); - Source toSource(SourceBuilder _) { - return _('new Expression', _.parens( - _('(scope)', _.body()..source.addAll(exp.split('\n'))), - assignable ? _('(scope, value)', _.body()) : 'null' - )); + String evaluateHolder(Expression expression) { + int old = state; + try { + state = STATE_EVAL_HOLDER; + return visit(expression); + } finally { + state = old; + } } -} -class MultipleStatementCode extends Code { - MultipleStatementCode(code): super(code); - - Source toSource(SourceBuilder _) { - return _('new Expression', _.parens( - _('(scope)', _.body()..source.addAll(exp.split('\n'))), - assignable ? _('(scope, value)', _.body()) : 'null' - )); + Function assign(Expression target) { + int old = state; + try { + state = STATE_ASSIGN; + return visit(target); + } finally { + state = old; + } } -} - -escape(String s) => s.replaceAll('\'', '\\\'').replaceAll(r'$', r'\$'); - -class GetterSetterGenerator { - static RegExp LAST_PATH_PART = new RegExp(r'(.*)\.(.*)'); - static RegExp NON_WORDS = new RegExp(r'\W'); - - - String functions = "// GETTER AND SETTER FUNCTIONS\n\n"; - var _keyToGetterFnName = {}; - var _keyToSetterFnName = {}; - var nextUid = 0; - - _flatten(key) => key.replaceAll(NON_WORDS, '_'); - fieldGetter(String field, String obj) { - var eKey = escape(field); - - var returnValue = isReserved(field) ? "undefined_ /* $field is reserved */" : "$obj.$field"; - - return """ - if ($obj is Map) { - if ($obj.containsKey('$eKey')) { - val = $obj['$eKey']; - } else { - val = undefined_; + visitChain(Chain chain) { + StringBuffer buffer = new StringBuffer(); + buffer.writeln("var result, last;"); + for (int i = 0; i < chain.expressions.length; i++) { + String expression = evaluate(chain.expressions[i]); + buffer.writeln('last = $expression;'); + buffer.writeln('if (last != null) result = last;'); } - } else { - val = $returnValue; + buffer.write('return result;'); + return "$buffer"; } -"""; + visitFilter(Filter filter) { + List expressions = [ filter.expression ]..addAll(filter.arguments); + String arguments = expressions.map((e) => evaluate(e)).join(', '); + String name = escape(filter.name); + return 'filters("$name")($arguments)'; } - fieldSetter(String field, String obj) { - var eKey = escape(field); - - var maybeField = isReserved(field) ? "/* $field is reserved */" : """ - $obj.$field = value; - return value; - """; + visitAssign(Assign expression) { + String value = evaluate(expression.value); + return assign(expression.target)(value); + } - return """ - if ($obj is Map) { - $obj['$eKey'] = value; - return value; + visitConditional(Conditional conditional) { + String condition = evaluate(conditional.condition, convertToBool: true); + String yes = evaluate(conditional.yes); + String no = evaluate(conditional.no); + return "$condition ? $yes : $no"; } - $maybeField -} -"""; + visitAccessScope(AccessScope access) { + String accessor = lookupAccessor(access.name); + return isAssigning + ? (value) => '$accessor(scope, $value)' + : '$accessor(scope)'; } - call(String key) { - if (_keyToGetterFnName.containsKey(key)) { - return _keyToGetterFnName[key]; - } + visitAccessMember(AccessMember access) { + String object = !isEvaluating + ? evaluateHolder(access.object) + : evaluate(access.object); + String accessor = lookupAccessor(access.name); + return isAssigning + ? (value) => '$accessor($object, $value)' + : '$accessor($object)'; + } - var fnName = "_${_flatten(key)}"; - - var keys = key.split('.'); - var lines = [ - "$fnName(s) { // for $key"]; - _(line) => lines.add(' $line'); - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; - var sk = isReserved(k) ? "null" : "s.$k"; - if (i == 0) { - _('if (s != null ) s = s is Map ? s["${escape(k)}"] : $sk;'); - } else { - _('if (s != null ) s = s is Map ? s["${escape(k)}"] : $sk;'); - } - } - _('return s;'); - lines.add('}\n\n'); + visitAccessKeyed(AccessKeyed access) { + String object = evaluate(access.object); + String key = evaluate(access.key); + return (isAssigning) + ? (value) => 'setKeyed($object, $key, $value)' + : 'getKeyed($object, $key)'; + } - functions += lines.join('\n'); + visitCallScope(CallScope call) { + String arguments = call.arguments.map((e) => evaluate(e)).join(', '); + String getter = lookupGetter(call.name); + return safeCallFunction('$getter(scope)', call.name, arguments); + } - _keyToGetterFnName[key] = fnName; - return fnName; + visitCallFunction(CallFunction call) { + String function = evaluate(call.function); + String arguments = call.arguments.map((e) => evaluate(e)).join(', '); + return safeCallFunction(function, "${call.function}", arguments); } - setter(String key) { - if (_keyToSetterFnName.containsKey(key)) { - return _keyToSetterFnName[key]; - } + visitCallMember(CallMember call) { + String object = evaluate(call.object); + String arguments = call.arguments.map((e) => evaluate(e)).join(', '); + String getter = lookupGetter(call.name); + return safeCallFunction('$getter($object)', call.name, arguments); + } - var fnName = "_set_${_flatten(key)}"; - - var lines = [ - "$fnName(s, v) { // for $key"]; - _(line) => lines.add(' $line'); - var keys = key.split('.'); - _(keys.length == 1 ? 'var n = s;' : 'var n;'); - var k = keys[0]; - var sk = isReserved(k) ? "null" : "s.$k"; - var nk = isReserved(k) ? "null" : "n.$k"; - if (keys.length > 1) { - // locals - _('n = s is Map ? s["${escape(k)}"] : $sk;'); - _('if (n == null) n = s is Map ? (s["${escape(k)}"] = {}) : ($sk = {});'); - } - for(var i = 1; i < keys.length - 1; i++) { - k = keys[i]; - sk = isReserved(k) ? "null" : "s.$k"; - nk = isReserved(k) ? "null" : "n.$k"; - // middle - _('s = n; n = n is Map ? n["${escape(k)}"] : $nk;'); - _('if (n == null) n = s is Map ? (s["${escape(k)}"] = {}) : (${isReserved(k) ? "null" : "$sk = {}"});'); + visitBinary(Binary binary) { + String operation = binary.operation; + bool logical = (operation == '||') || (operation == '&&'); + String left = evaluate(binary.left, convertToBool: logical); + String right = evaluate(binary.right, convertToBool: logical); + if (operation == '+') { + return 'autoConvertAdd($left, $right)'; + } else { + return '($left $operation $right)'; } - k = keys[keys.length - 1]; - sk = isReserved(k) ? "null" : "s.$k"; - nk = isReserved(k) ? "null" : "n.$k"; - _('if (n is Map) n["${escape(k)}"] = v; else ${isReserved(k) ? "null" : "$nk = v"};'); - // finish - _('return v;'); - lines.add('}\n\n'); - - functions += lines.join('\n'); - - _keyToSetterFnName[key] = fnName; - return fnName; } -} - - -class DartCodeGen implements ParserBackend { - static Code ZERO = new Code("0"); - - GetterSetterGenerator _getterGen; - - DartCodeGen(this._getterGen); - setter(String path) => throw new UnimplementedError(); - getter(String path) => throw new UnimplementedError(); - - // Returns the Dart code for a particular operator. - _op(fn) => fn == "undefined" ? "null" : fn; + visitPrefix(Prefix prefix) { + String operation = prefix.operation; + bool logical = (operation == '!'); + String expression = evaluate(prefix.expression, convertToBool: logical); + return '$operation$expression'; + } - Code ternaryFn(Code cond, Code trueBranch, Code falseBranch) => - new Code("toBool(${cond.exp}) ? ${trueBranch.exp} : ${falseBranch.exp}"); + visitLiteral(Literal literal) { + return '$literal'; + } - Code binaryFn(Code left, String fn, Code right) { - if (fn == '+') { - return new Code("autoConvertAdd(${left.exp}, ${right.exp})"); - } - var leftExp = left.exp; - var rightExp = right.exp; - if (fn == '&&' || fn == '||') { - leftExp = "toBool($leftExp)"; - rightExp = "toBool($rightExp)"; - } - return new Code("(${leftExp} ${_op(fn)} ${rightExp})"); + visitLiteralString(LiteralString literal) { + return 'r$literal'; } - Code unaryFn(String fn, Code right) { - var rightExp = right.exp; - if (fn == '!') { - rightExp = "toBool($rightExp)"; + visitLiteralArray(LiteralArray literal) { + if (literal.elements.isEmpty) return '[]'; + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < literal.elements.length; i++) { + if (i != 0) buffer.write(', '); + buffer.write(evaluate(literal.elements[i])); } - return new Code("${_op(fn)}${rightExp}"); + return "[ $buffer ]"; } - Code assignment(Code left, Code right, evalError) => - left._assign(right); - - Code multipleStatements(Liststatements) { - var code = "var ret, last;\n"; - code += statements.map((Code s) => - "last = ${s.exp};\nif (last != null) { ret = last; }\n").join('\n'); - code += "return ret;\n"; - return new MultipleStatementCode(code); + visitLiteralObject(LiteralObject literal) { + if (literal.keys.isEmpty) return '{}'; + StringBuffer buffer = new StringBuffer(); + List keys = literal.keys; + for (int i = 0; i < keys.length; i++) { + if (i != 0) buffer.write(', '); + buffer.write("'${keys[i]}': "); + buffer.write(evaluate(literal.values[i])); + } + return "{ $buffer }"; } +} - Code functionCall(Code fn, fnName, List argsFn, evalError) => - new Code("safeFunctionCall(${fn.exp}, \'${escape(fnName)}\', evalError)(${argsFn.map((a) => a.exp).join(', ')})"); +class HelperMap { + final Map helpers = new Map(); + final Map names = new Map(); - Code arrayDeclaration(List elementFns) => - new Code("[${elementFns.map((Code e) => e.exp).join(', ')}]"); + final String prefix; + final Function template; + final Function templateForReserved; - Code objectIndex(Code obj, Code indexFn, evalError) { - var assign = (Code right) => - new Code("objectIndexSetField(${obj.exp}, ${indexFn.exp}, ${right.exp}, evalError)"); + HelperMap(this.prefix, this.template, this.templateForReserved); - return new Code("objectIndexGetField(${obj.exp}, ${indexFn.exp}, evalError)", assign); + String lookup(String key) { + String name = _computeName(key); + if (helpers.containsKey(key)) return name; + helpers[key] = isReserved(key) + ? templateForReserved(name, key) + : template(name, key); + return name; } - Code fieldAccess(Code object, String field) { - var getterFnName = _getterGen(field); - var assign = (Code right) { - var setterFnName = _getterGen.setter(field); - return new Code("$setterFnName(${object.exp}, ${right.exp})"); - }; - return new Code("$getterFnName/*field:$field*/(${object.exp})", assign); + String _computeName(String key) { + String result = names[key]; + if (result != null) return result; + return names[key] = "$prefix$key"; } +} - Code object(List keyValues) => - new Code( - "{${keyValues.map((k) => "${_value(k["key"])}: ${k["value"].exp}").join(', ')}}"); - profiled(value, perf, text) => value; // no profiling for now - Code fromOperator(String op) => new Code(_op(op)); +// ------------------------------------------------------------------ +// Templates for generated getters. +// ------------------------------------------------------------------ +String getterTemplate(String name, String key) => """ +$name(o) { + if (o == null) return null; + return (o is Map) ? o["${escape(key)}"] : o.$key; +} +"""; - Code getterSetter(String key) { - var getterFnName = _getterGen(key); +String getterTemplateForReserved(String name, String key) => """ +$name(o) { + if (o == null) return null; + return (o is Map) ? o["${escape(key)}"] : null; +} +"""; - var assign = (Code right) { - var setterFnName = _getterGen.setter(key); - return new Code("${setterFnName}(scope, ${right.exp})", null, setterFnName); - }; - return new Code("$getterFnName(scope)", assign, getterFnName); +// ------------------------------------------------------------------ +// Templates for generated holders (getters for assignment). +// ------------------------------------------------------------------ +String holderTemplate(String name, String key) => """ +$name(o) { + if (o == null) return null; + if (o is Map) { + var key = "${escape(key)}"; + var result = o[key]; + return (result == null) ? result = o[key] = {} : result; + } else { + var result = o.$key; + return (result == null) ? result = o.$key = {} : result; } +} +"""; - String _value(v) => - v is String ? "r\'${escape(v)}\'" : "$v"; +String holderTemplateForReserved(String name, String key) => """ +$name(o) { + if (o == null) return null; + if (o is !Map) return {}; + var key = "${escape(key)}"; + var result = o[key]; + return (result == null) ? result = o[key] = {} : result; +} +"""; - Code value(v) => new Code(_value(v)); - Code zero() => ZERO; +// ------------------------------------------------------------------ +// Templates for generated setters. +// ------------------------------------------------------------------ +String setterTemplate(String name, String key) => """ +$name(o, v) { + if (o is Map) o["${escape(key)}"] = v; else o.$key = v; + return v; +} +"""; - Code filter(String filterName, - Code leftHandSide, - List parameters, - Function evalError) { - return new Code( - 'filters(\'${filterName}\')(${ - ([leftHandSide]..addAll(parameters)) - .map((Code p) => p.exp).join(', ')})'); - } +String setterTemplateForReserved(String name, String key) => """ +$name(o, v) { + if (o is Map) o["${escape(key)}"] = v; + return v; } +"""; diff --git a/lib/tools/parser_generator/generator.dart b/lib/tools/parser_generator/generator.dart index 1522ac89d..975227e84 100644 --- a/lib/tools/parser_generator/generator.dart +++ b/lib/tools/parser_generator/generator.dart @@ -1,8 +1,7 @@ library generator; import 'dart_code_gen.dart'; -import '../../core/parser/parser_library.dart'; -import 'source.dart'; +import '../../core/parser/parser.dart'; class SourcePrinter { printSrc(src) { @@ -11,50 +10,69 @@ class SourcePrinter { } class ParserGenerator { - DynamicParser _parser; - Map _printedFunctions = {}; - GetterSetterGenerator _getters; - SourceBuilder _ = new SourceBuilder(); - SourcePrinter _prt; + final Parser _parser; + final DartCodeGen _codegen; + final SourcePrinter _printer; + ParserGenerator(this._parser, this._codegen, this._printer); - ParserGenerator(this._parser, this._getters, this._prt); + void print(object) { + _printer.printSrc('$object'); + } generateParser(Iterable expressions) { - _prt..printSrc("genEvalError(msg) { throw msg; }") - ..printSrc("functions(FilterLookup filters) => " - "new StaticParserFunctions(buildExpressions(filters));") - ..printSrc('var evalError = (text, [s]) => text;') - ..printSrc(""); - BodySource body = new BodySource(); - MapSource map = new MapSource(); - - // deterimine the order. + print("StaticParserFunctions functions(FilterLookup filters)"); + print(" => new StaticParserFunctions("); + print(" buildEval(filters), buildAssign(filters));"); + print(""); + + // Compute the function maps. + Map eval = {}; + Map assign = {}; expressions.forEach((exp) { - var code = safeCode(exp); - map('${_.str(exp)}: ${_.ref(code)}'); + generateCode(exp, eval, assign); }); - // now do it in actual order - _.codeRefs.forEach((code) { - body(_.stmt('Expression ${_.ref(code)} = ', code.toSource(_))); - }); - body(_.stmt('return ', map)); - _prt..printSrc("Map buildExpressions(FilterLookup filters) ${body}") - ..printSrc("\n") - ..printSrc(_getters.functions); + + // Generate the code. + generateBuildFunction('buildEval', eval); + generateBuildFunction('buildAssign', assign); + _codegen.getters.helpers.values.forEach(print); + _codegen.holders.helpers.values.forEach(print); + _codegen.setters.helpers.values.forEach(print); } - Code safeCode(String exp) { + void generateBuildFunction(String name, Map map) { + String mapLiteral = map.keys.map((e) => ' "$e": ${map[e]}').join(',\n'); + print("Map $name(FilterLookup filters) {"); + print(" return {\n$mapLiteral\n };"); + print("}"); + print(""); + } + + void generateCode(String exp, Map eval, Map assign) { + String escaped = escape(exp); try { - return _parser(exp); + Expression e = _parser(exp); + if (e.isAssignable) assign[escaped] = getCode(e, true); + eval[escaped] = getCode(e, false); } catch (e) { if ("$e".contains('Parser Error') || - "$e".contains('Lexer Error') || - "$e".contains('Unexpected end of expression')) { - return new ThrowCode("'${escape(e.toString())}';"); + "$e".contains('Lexer Error') || + "$e".contains('Unexpected end of expression')) { + eval[escaped] = '"${escape(e.toString())}"'; } else { rethrow; } } } + String getCode(Expression e, bool assign) { + String args = assign ? "scope, value" : "scope"; + String code = _codegen.generate(e, assign); + if (e.isChain) { + code = code.replaceAll('\n', '\n '); + return "($args) {\n $code\n }"; + } else { + return "($args) => $code"; + } + } } diff --git a/lib/tools/parser_generator/source.dart b/lib/tools/parser_generator/source.dart deleted file mode 100644 index 25b6f67b4..000000000 --- a/lib/tools/parser_generator/source.dart +++ /dev/null @@ -1,92 +0,0 @@ -library dart_code_gen_source; - -import 'dart_code_gen.dart'; - -class SourceBuilder { - static RegExp NON_WORDS = new RegExp(r'\W'); - - Map refs = {}; - List codeRefs = []; - - String str(String s) => '\'' + - s.replaceAll('\'', '\\\'') - .replaceAll('\n', '\\n') - .replaceAll(r'$', r'\$') + '\''; - String ident(String s) => '_${s.replaceAll(NON_WORDS, '_')}_${s.hashCode}'; - - String ref(Code code) { - if (!refs.containsKey(code.id)) { - refs[code.id] = code; - code.toSource(this); // recursively expand; - codeRefs.add(code); - } - return this.ident(code.id); - } - - parens([p1, p2, p3, p4, p5, p6]) => new ParenthesisSource()..call(p1, p2, p3, p4, p5, p6); - body([p1, p2, p3, p4, p5, p6]) => new BodySource()..call(p1, p2, p3, p4, p5, p6); - stmt([p1, p2, p3, p4, p5, p6]) => new StatementSource()..call(p1, p2, p3, p4, p5, p6); - - call([p1, p2, p3, p4, p5, p6]) => new Source()..call(p1, p2, p3, p4, p5, p6); - -} - -class Source { - static String NEW_LINE = '\n'; - List source = []; - - call([p1, p2, p3, p4, p5, p6]) { - if (p1 != null) source.add(p1); - if (p2 != null) source.add(p2); - if (p3 != null) source.add(p3); - if (p4 != null) source.add(p4); - if (p5 != null) source.add(p5); - if (p6 != null) source.add(p6); - } - - toString([String indent='', newLine=false, sep='']) { - var lines = []; - var trailing = sep == ';'; - var _sep = ''; - source.forEach((s) { - if (!trailing) lines.add(_sep); - if (newLine) lines.add('\n' + indent); - if (s is Source) { - lines.add(s.toString(indent)); - } else { - lines.add(s); - } - _sep = sep; - if (trailing) lines.add(_sep); - }); - return lines.join(''); - } -} - - -class ParenthesisSource extends Source { - toString([String indent='', newLine=false, sep='']) { - return '(' + super.toString(indent + ' ', true, ',') + ')'; - } -} - -class MapSource extends Source { - toString([String indent='', newLine=false, sep='']) { - return '{' + super.toString(indent + ' ', true, ',') + '}'; - } -} - -class BodySource extends Source { - BodySource() { - //this(''); - } - toString([String indent='', newLine=false, sep='']) { - return '{${super.toString(indent + ' ', true)}\n$indent}'; - } -} - -class StatementSource extends Source { - toString([String indent='', newLine=false, sep='']) { - return '${super.toString(indent + ' ')};'; - } -} diff --git a/lib/tools/parser_getter_setter/generator.dart b/lib/tools/parser_getter_setter/generator.dart index 5d77e7233..6f3f52763 100644 --- a/lib/tools/parser_getter_setter/generator.dart +++ b/lib/tools/parser_getter_setter/generator.dart @@ -1,90 +1,102 @@ -import 'package:angular/core/parser/parser_library.dart'; +import 'package:angular/core/parser/parser.dart'; import 'package:angular/tools/reserved_dart_keywords.dart'; +import 'dart:math'; -class _AST implements ParserAST { - bool get assignable => true; -} +class DartGetterSetterGen extends ParserBackend { + final Set properties = new Set(); + final Map> calls = new Map>(); + + bool isAssignable(expression) => true; -class DartGetterSetterGen implements ParserBackend { - Map identifiers = {}; - - profiled(value, perf, text) => new _AST(); - - binaryFn(left, String fn, right) => new _AST(); - unaryFn(String fn, right) => new _AST(); - ternaryFn(Expression cond, Expression _true, Expression _false) => new _AST(); - assignment(left, right, evalError) => new _AST(); - multipleStatements(List statements) => new _AST(); - arrayDeclaration(List elementFns) => new _AST(); - objectIndex(obj, indexFn, evalError) => new _AST(); - object(List keyValues) => new _AST(); - fromOperator(String op) => new _AST(); - value(v) => new _AST(); - zero() => new _AST(); - functionCall(fn, fnName, List argsFn, evalError) => new _AST(); - filter(String filterName, - Expression leftHandSide, - List parameters, - Function evalError) => new _AST(); - - setter(String path) => throw new UnimplementedError(); - getter(String path) => throw new UnimplementedError(); - - - fieldAccess(object, String field) { - identifiers[field] = true; - return new _AST(); + registerAccess(String name) { + if (isReserved(name)) return; + properties.add(name); } - getterSetter(String key) { - key.split('.').forEach((i) => identifiers[i] = true); - return new _AST(); + registerCall(String name, List arguments) { + if (isReserved(name)) return; + Set arities = calls.putIfAbsent(name, () => new Set()); + arities.add(arguments.length); } + + newAccessScope(String name) + => registerAccess(name); + newAccessMember(var object, String name) + => registerAccess(name); + newCallScope(String name, List arguments) + => registerCall(name, arguments); + newCallMember(var object, String name, List arguments) + => registerCall(name, arguments); } class ParserGetterSetter { - - DynamicParser parser; - DartGetterSetterGen backend; - + final Parser parser; + final ParserBackend backend; ParserGetterSetter(this.parser, this.backend); generateParser(List exprs) { exprs.forEach((expr) { try { parser(expr); - } catch (e) { } + } catch (e) { + // Ignore exceptions. + } }); - print(generateCode(backend.identifiers.keys.toList())); + DartGetterSetterGen backend = this.backend; + print(generateClosureMap(backend.properties, backend.calls)); } - generateCode(Iterable keys) { - keys = keys.where((key) => !isReserved(key)); + generateClosureMap(Set properties, Map> calls) { return ''' -class StaticGetterSetter extends GetterSetter { - Map _getters = ${generateGetterMap(keys)}; - Map _setters = ${generateSetterMap(keys)}; - - Function getter(String key) { - return _getters.containsKey(key) ? _getters[key] : super.getter(key); - } - - Function setter(String key) { - return _setters.containsKey(key) ? _setters[key] : super.setter(key); - } - +class StaticClosureMap extends ClosureMap { + Map _getters = ${generateGetterMap(properties)}; + Map _setters = ${generateSetterMap(properties)}; + List> _functions = ${generateFunctionMap(calls)}; + + Getter lookupGetter(String name) + => _getters[name]; + Setter lookupSetter(String name) + => _setters[name]; + lookupFunction(String name, int arity) + => (arity < _functions.length) ? _functions[arity][name] : null; } '''; } generateGetterMap(Iterable keys) { - var lines = keys.map((key) => 'r"${key}": (s) => s.$key'); + var lines = keys.map((key) => 'r"${key}": (o) => o.$key'); return '{\n ${lines.join(",\n ")}\n }'; } generateSetterMap(Iterable keys) { - var lines = keys.map((key) => 'r"${key}": (s, v) => s.$key = v'); + var lines = keys.map((key) => 'r"${key}": (o, v) => o.$key = v'); return '{\n ${lines.join(",\n ")}\n }'; } + + generateFunctionMap(Map> calls) { + Map arities = {}; + + calls.forEach((name, callArities) { + callArities.forEach((arity){ + arities.putIfAbsent(arity, () => new Set()).add(name); + }); + }); + + var maxArity = arities.keys.reduce((x, y) => max(x, y)); + + var maps = new Iterable.generate(maxArity, (arity) { + var names = arities[arity]; + if (names == null) { + return '{\n }'; + } else { + var args = new List.generate(arity, (e) => "a$e").join(','); + var p = args.isEmpty ? '' : ', $args'; + var lines = names.map((name) => 'r"$name": (o$p) => o.$name($args)'); + return '{\n ${lines.join(",\n ")}\n }'; + } + }); + + return '[${maps.join(",")}]'; + } } diff --git a/perf/lexer_perf.dart b/perf/lexer_perf.dart index 60345c21f..80aff438d 100644 --- a/perf/lexer_perf.dart +++ b/perf/lexer_perf.dart @@ -1,7 +1,7 @@ library lexer_perf; import '_perf.dart'; -import 'package:angular/core/parser/parser_library.dart'; +import 'package:angular/core/parser/lexer.dart'; main() { Lexer lexer = new Lexer(); diff --git a/perf/parser_perf.dart b/perf/parser_perf.dart index 6f293490d..435047378 100644 --- a/perf/parser_perf.dart +++ b/perf/parser_perf.dart @@ -2,7 +2,7 @@ library parser_perf; import '_perf.dart'; import 'package:angular/core/module.dart'; -import 'package:angular/core/parser/parser_library.dart'; +import 'package:angular/core/parser/parser.dart'; import 'package:angular/filter/module.dart'; import 'package:di/di.dart'; import 'package:di/dynamic_injector.dart'; @@ -14,6 +14,7 @@ import '../gen/generated_getter_setter.dart' as generated_getter_setter; main() { var module = new Module() ..type(Parser, implementedBy: DynamicParser) + ..type(ParserBackend, implementedBy: DynamicParserBackend) ..type(SubstringFilter) ..type(IncrementFilter) ..install(new NgFilterModule()); @@ -33,7 +34,8 @@ main() { var hybridParser = new DynamicInjector( modules: [new Module() ..type(Parser, implementedBy: DynamicParser) - ..type(GetterSetter, implementedBy: generated_getter_setter.StaticGetterSetter)], + ..type(ParserBackend, implementedBy: DynamicParserBackend) + ..type(ClosureMap, implementedBy: generated_getter_setter.StaticClosureMap)], allowImplicitInjection:true).get(Parser); scope['a'] = new ATest(); @@ -42,9 +44,9 @@ main() { compare(expr, idealFn) { var nf = new NumberFormat.decimalPattern(); - var reflectionExpr = reflectiveParser(expr); - var generatedExpr = generatedParser(expr); - var hybridExpr = hybridParser(expr); + Expression reflectionExpr = reflectiveParser(expr); + Expression generatedExpr = generatedParser(expr); + Expression hybridExpr = hybridParser(expr); var measure = (b) => statMeasure(b).mean_ops_sec; var gTime = measure(() => generatedExpr.eval(scope)); var rTime = measure(() => reflectionExpr.eval(scope)); diff --git a/perf/scope_perf.dart b/perf/scope_perf.dart index 888060d10..4d592cb8c 100644 --- a/perf/scope_perf.dart +++ b/perf/scope_perf.dart @@ -2,14 +2,15 @@ library scope_perf; import '_perf.dart'; import 'package:angular/core/module.dart'; -import 'package:angular/core/parser/parser_library.dart'; +import 'package:angular/core/parser/parser.dart'; import 'package:di/di.dart'; import 'package:di/dynamic_injector.dart'; main() { var scope = new DynamicInjector( modules: [new Module() - ..type(Parser, implementedBy: DynamicParser)], + ..type(Parser, implementedBy: DynamicParser) + ..type(ParserBackend, implementedBy: DynamicParserBackend)], allowImplicitInjection:true).get(Scope); var scope2, scope3, scope4, scope5; var fill = (scope) { diff --git a/test/_specs.dart b/test/_specs.dart index a36a0c711..b75571ac6 100644 --- a/test/_specs.dart +++ b/test/_specs.dart @@ -152,6 +152,11 @@ $(selector) { return new JQuery(selector); } + +class GetterSetter { + Getter getter(String key) => null; + Setter setter(String key) => null; +} var getterSetter = new GetterSetter(); class JQuery implements List { diff --git a/test/core/parser/generated_functions.dart b/test/core/parser/generated_functions.dart index 8f4d4eb25..69fbc5a6c 100644 --- a/test/core/parser/generated_functions.dart +++ b/test/core/parser/generated_functions.dart @@ -1,8 +1,8 @@ library angular.service.parser.generated_expressions_template; -import 'package:angular/core/parser/parser_library.dart'; -import 'package:angular/utils.dart'; +import 'package:angular/core/parser/parser.dart' show StaticParserFunctions; +import 'package:angular/core/parser/utils.dart'; -main(){} typedef Function FilterLookup(String filterName); + functions(FilterLookup filterLookup) { throw "This should never be called"; } // REMOVE diff --git a/test/core/parser/generated_getter_setter.dart b/test/core/parser/generated_getter_setter.dart index e0215d8b3..a0424fb64 100644 --- a/test/core/parser/generated_getter_setter.dart +++ b/test/core/parser/generated_getter_setter.dart @@ -1,7 +1,6 @@ library angular.service.parser.generated_getter_setter_template; -import 'package:angular/core/parser/parser_library.dart'; +import 'package:angular/core/parser/parser.dart'; +export'package:angular/core/parser/parser.dart' show ClosureMap; -main() {} - -class StaticGetterSetter {StaticGetterSetter() {throw "Should not be called";} } // REMOVE +class StaticClosureMap extends ClosureMap { } // REMOVE diff --git a/test/core/parser/generated_getter_setter_spec.dart b/test/core/parser/generated_getter_setter_spec.dart index 182c7b5da..75080024e 100644 --- a/test/core/parser/generated_getter_setter_spec.dart +++ b/test/core/parser/generated_getter_setter_spec.dart @@ -2,15 +2,14 @@ library generated_getter_setter_spec; import '../../_specs.dart'; import 'parser_spec.dart' as parser_spec; -import 'generated_getter_setter.dart' as generated; +import 'generated_getter_setter.dart'; main() { describe('hybrid getter-setter', () { beforeEach(module((Module module) { module.type(Parser, implementedBy: DynamicParser); - module.type(GetterSetter, implementedBy: generated.StaticGetterSetter); + module.type(ClosureMap, implementedBy: StaticClosureMap); })); //parser_spec.main(); }); } - diff --git a/test/core/parser/generated_parser_spec.dart b/test/core/parser/generated_parser_spec.dart index 5854c0646..38ceebdc3 100644 --- a/test/core/parser/generated_parser_spec.dart +++ b/test/core/parser/generated_parser_spec.dart @@ -5,11 +5,9 @@ import 'parser_spec.dart' as parser_spec; import 'generated_functions.dart' as generated_functions; class AlwaysThrowError implements DynamicParser { - call(String x) { throw "Fall-thru to DynamicParser disabled [$x]"; } - primaryFromToken(Token token, parserError) => null; + call(String input) => throw "Fall-thru to DynamicParser disabled [$input]"; } - main() { describe('generated parser', () { beforeEach(module((Module module) { diff --git a/test/core/parser/lexer_spec.dart b/test/core/parser/lexer_spec.dart index 4937142e8..35c24950e 100644 --- a/test/core/parser/lexer_spec.dart +++ b/test/core/parser/lexer_spec.dart @@ -14,15 +14,9 @@ expect(actual) => new LexerExpect(actual); main() { describe('lexer', () { - Parser parser; - - // It would be better if we could call Parser.primary() directly. - fn0(Token token) => parser.primaryFromToken(token, (x) => x).eval(null); - Lexer lex; - beforeEach(inject((Lexer lexer, Parser p) { + beforeEach(inject((Lexer lexer) { lex = lexer; - parser = p; })); // New test case @@ -118,7 +112,7 @@ main() { var tokens = lex("undefined"); var i = 0; expect(tokens[i]).toBeToken(0, 'undefined'); - expect(fn0(tokens[i])).toEqual(null); + expect(tokens[i].value).toEqual(null); }); it('should ignore whitespace', () { @@ -142,13 +136,13 @@ main() { var str = '"\\"\\n\\f\\r\\t\\v\\u00A0"'; var tokens = lex(str); - expect(fn0(tokens[0])).toEqual('"\n\f\r\t\v\u00A0'); + expect(tokens[0].value).toEqual('"\n\f\r\t\v\u00A0'); }); it('should tokenize unicode', () { var tokens = lex('"\\u00A0"'); expect(tokens.length).toEqual(1); - expect(fn0(tokens[0])).toEqual('\u00a0'); + expect(tokens[0].value).toEqual('\u00a0'); }); it('should tokenize relation', () { @@ -200,21 +194,21 @@ main() { it('should tokenize number', () { var tokens = lex("0.5"); - expect(fn0(tokens[0])).toEqual(0.5); + expect(tokens[0].value).toEqual(0.5); }); // NOTE(deboer): NOT A LEXER TEST // it('should tokenize negative number', () { // var tokens = lex("-0.5"); - // expect(tokens[0].fn0()).toEqual(-0.5); + // expect(tokens[0].value).toEqual(-0.5); // }); it('should tokenize number with exponent', () { var tokens = lex("0.5E-10"); expect(tokens.length).toEqual(1); - expect(fn0(tokens[0])).toEqual(0.5E-10); + expect(tokens[0].value).toEqual(0.5E-10); tokens = lex("0.5E+10"); - expect(fn0(tokens[0])).toEqual(0.5E+10); + expect(tokens[0].value).toEqual(0.5E+10); }); it('should throws exception for invalid exponent', () { @@ -229,7 +223,7 @@ main() { it('should tokenize number starting with a dot', () { var tokens = lex(".5"); - expect(fn0(tokens[0])).toEqual(0.5); + expect(tokens[0].value).toEqual(0.5); }); it('should throw error on invalid unicode', () { diff --git a/test/core/parser/parser_spec.dart b/test/core/parser/parser_spec.dart index 0e84c6cd6..6578931c3 100644 --- a/test/core/parser/parser_spec.dart +++ b/test/core/parser/parser_spec.dart @@ -44,7 +44,8 @@ toBool(x) => (x is num) ? x != 0 : x == true; main() { describe('parse', () { - var scope, parser; + var scope; + Parser parser; beforeEach(module((Module module) { module.type(IncrementFilter); module.type(SubstringFilter); @@ -141,7 +142,7 @@ main() { }); describe('error handling', () { - var parser; + Parser parser; beforeEach(inject((Parser p) { parser = p; @@ -176,46 +177,92 @@ main() { }); - it('should throw on undefined functions', () { - expectEval("notAFn()").toThrow(errStr('Eval Error: Undefined function notAFn while evaling [notAFn()]')); + it('should throw on incorrect ternary operator syntax', () { + expectEval("true?1").toThrow('Parser Error: Conditional expression true?1 requires all 3 expressions'); }); - it('should throw on not-function function calls', () { - expectEval("4()").toThrow(errStr('Eval Error: 4 is not a function while evaling [4()]')); + it('should throw on non-function function calls', () { + expectEval("4()").toThrow('4 is not a function'); }); - it('should throw on incorrect ternary operator syntax', () { - expectEval("true?1").toThrow(errStr( - 'Conditional expression true?1 requires all 3 expressions')); + it('should fail gracefully when invoking non-function', () { + expect(() { + parser('a[0]()').eval({'a': [4]}); + }).toThrow('a[0] is not a function'); + + expect(() { + parser('a[x()]()').eval({'a': [4], 'x': () => 0}); + }).toThrow('a[x()] is not a function'); + + expect(() { + parser('{}()').eval({}); + }).toThrow('{} is not a function'); }); - it('should fail gracefully when missing a function', () { + it('should throw on undefined functions (relaxed message)', () { + expectEval("notAFn()").toThrow('notAFn'); + }); + + + it('should fail gracefully when missing a function (relaxed message)', () { expect(() { parser('doesNotExist()').eval({}); - }).toThrow('Undefined function doesNotExist'); + }).toThrow('doesNotExist'); expect(() { parser('exists(doesNotExist())').eval({'exists': () => true}); - }).toThrow('Undefined function doesNotExist'); + }).toThrow('doesNotExist'); expect(() { parser('doesNotExists(exists())').eval({'exists': () => true}); - }).toThrow('Undefined function doesNotExist'); + }).toThrow('doesNotExist'); expect(() { - parser('a[0]()').eval({'a': [4]}); - }).toThrow('a[0] is not a function'); + parser('doesNotExist(1)').eval({}); + }).toThrow('doesNotExist'); expect(() { - parser('a[x()]()').eval({'a': [4], 'x': () => 0}); - }).toThrow('a[x()] is not a function'); + parser('doesNotExist(1, 2)').eval({}); + }).toThrow('doesNotExist'); expect(() { - parser('{}()').eval({}); - }).toThrow('{} is not a function'); + parser('doesNotExist()').eval(new TestData()); + }).toThrow('doesNotExist'); + + expect(() { + parser('doesNotExist(1)').eval(new TestData()); + }).toThrow('doesNotExist'); + + expect(() { + parser('doesNotExist(1, 2)').eval(new TestData()); + }).toThrow('doesNotExist'); + + expect(() { + parser('a.doesNotExist()').eval({'a': {}}); + }).toThrow('doesNotExist'); + + expect(() { + parser('a.doesNotExist(1)').eval({'a': {}}); + }).toThrow('doesNotExist'); + + expect(() { + parser('a.doesNotExist(1, 2)').eval({'a': {}}); + }).toThrow('doesNotExist'); + + expect(() { + parser('a.doesNotExist()').eval({'a': new TestData()}); + }).toThrow('doesNotExist'); + + expect(() { + parser('a.doesNotExist(1)').eval({'a': new TestData()}); + }).toThrow('doesNotExist'); + + expect(() { + parser('a.doesNotExist(1, 2)').eval({'a': new TestData()}); + }).toThrow('doesNotExist'); }); @@ -223,7 +270,7 @@ main() { scope['map'] = {}; expect(eval('null')).toBe(null); - //expect(eval('map.null')).toBe(null); + expect(eval('map.null')).toBe(null); }); diff --git a/test/core/parser/static_parser_spec.dart b/test/core/parser/static_parser_spec.dart index 05ab18323..b9a315385 100644 --- a/test/core/parser/static_parser_spec.dart +++ b/test/core/parser/static_parser_spec.dart @@ -2,11 +2,11 @@ library static_parser_spec; import '../../_specs.dart'; -var FUNCTIONS = { '1': 1 }; +var EVAL = { '1': (scope) => 1 }; +var ASSIGN = { }; class AlwaysReturnX implements DynamicParser { - call(String x) => 'x'; - primaryFromToken(Token token, parserError) => null; + call(String input) => throw 'x'; } main() { @@ -14,17 +14,17 @@ main() { beforeEach(module((Module m) { m.type(Parser, implementedBy: StaticParser); m.type(DynamicParser, implementedBy: AlwaysReturnX); - m.value(StaticParserFunctions, new StaticParserFunctions(FUNCTIONS)); + m.value(StaticParserFunctions, new StaticParserFunctions(EVAL, ASSIGN)); })); it('should run a static function', inject((Parser parser) { - expect(parser('1')).toEqual(1); + expect(parser('1').eval(null)).toEqual(1); })); it('should call the fallback if there is not function', inject((Parser parser) { - expect(parser('not 1')).toEqual('x'); + expect(() => parser('not 1')).toThrow('x'); })); }); }