| Index: observatory_pub_packages/polymer_expressions/eval.dart
|
| ===================================================================
|
| --- observatory_pub_packages/polymer_expressions/eval.dart (revision 0)
|
| +++ observatory_pub_packages/polymer_expressions/eval.dart (working copy)
|
| @@ -0,0 +1,823 @@
|
| +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
|
| +// for details. All rights reserved. Use of this source code is governed by a
|
| +// BSD-style license that can be found in the LICENSE file.
|
| +
|
| +library polymer_expressions.eval;
|
| +
|
| +import 'dart:async';
|
| +import 'dart:collection';
|
| +
|
| +import 'package:observe/observe.dart';
|
| +import 'package:smoke/smoke.dart' as smoke;
|
| +
|
| +import 'async.dart';
|
| +import 'expression.dart';
|
| +import 'filter.dart';
|
| +import 'visitor.dart';
|
| +
|
| +final _BINARY_OPERATORS = {
|
| + '+': (a, b) => a + b,
|
| + '-': (a, b) => a - b,
|
| + '*': (a, b) => a * b,
|
| + '/': (a, b) => a / b,
|
| + '%': (a, b) => a % b,
|
| + '==': (a, b) => a == b,
|
| + '!=': (a, b) => a != b,
|
| + '===': (a, b) => identical(a, b),
|
| + '!==': (a, b) => !identical(a, b),
|
| + '>': (a, b) => a > b,
|
| + '>=': (a, b) => a >= b,
|
| + '<': (a, b) => a < b,
|
| + '<=': (a, b) => a <= b,
|
| + '||': (a, b) => a || b,
|
| + '&&': (a, b) => a && b,
|
| + '|': (a, f) {
|
| + if (f is Transformer) return f.forward(a);
|
| + if (f is Filter) return f(a);
|
| + throw new EvalException("Filters must be a one-argument function.");
|
| + }
|
| +};
|
| +
|
| +final _UNARY_OPERATORS = {
|
| + '+': (a) => a,
|
| + '-': (a) => -a,
|
| + '!': (a) => !a,
|
| +};
|
| +
|
| +final _BOOLEAN_OPERATORS = ['!', '||', '&&'];
|
| +
|
| +/**
|
| + * Evaluation [expr] in the context of [scope].
|
| + */
|
| +Object eval(Expression expr, Scope scope) => new EvalVisitor(scope).visit(expr);
|
| +
|
| +/**
|
| + * Returns an [ExpressionObserver] that evaluates [expr] in the context of
|
| + * scope] and listens for any changes on [Observable] values that are
|
| + * returned from sub-expressions. When a value changes the expression is
|
| + * reevaluated and the new result is sent to the [onUpdate] stream of the
|
| + * [ExpressionObsserver].
|
| + */
|
| +ExpressionObserver observe(Expression expr, Scope scope) {
|
| + var observer = new ObserverBuilder().visit(expr);
|
| + return observer;
|
| +}
|
| +
|
| +/**
|
| + * Causes [expr] to be reevaluated a returns it's value.
|
| + */
|
| +Object update(ExpressionObserver expr, Scope scope, {skipChanges: false}) {
|
| + new Updater(scope, skipChanges).visit(expr);
|
| + return expr.currentValue;
|
| +}
|
| +
|
| +/**
|
| + * Assign [value] to the variable or field referenced by [expr] in the context
|
| + * of [scope].
|
| + *
|
| + * [expr] must be an /assignable/ expression, it must not contain
|
| + * operators or function invocations, and any index operations must use a
|
| + * literal index.
|
| + */
|
| +Object assign(Expression expr, Object value, Scope scope,
|
| + {bool checkAssignability: true}) {
|
| +
|
| + Expression expression;
|
| + var property;
|
| + bool isIndex = false;
|
| + var filters = <Expression>[]; // reversed order for assignment
|
| +
|
| + while (expr is BinaryOperator) {
|
| + BinaryOperator op = expr;
|
| + if (op.operator != '|') {
|
| + break;
|
| + }
|
| + filters.add(op.right);
|
| + expr = op.left;
|
| + }
|
| +
|
| + if (expr is Identifier) {
|
| + expression = empty();
|
| + property = expr.value;
|
| + } else if (expr is Index) {
|
| + expression = expr.receiver;
|
| + property = expr.argument;
|
| + isIndex = true;
|
| + } else if (expr is Getter) {
|
| + expression = expr.receiver;
|
| + property = expr.name;
|
| + } else {
|
| + if (checkAssignability) {
|
| + throw new EvalException("Expression is not assignable: $expr");
|
| + }
|
| + return null;
|
| + }
|
| +
|
| + // transform the values backwards through the filters
|
| + for (var filterExpr in filters) {
|
| + var filter = eval(filterExpr, scope);
|
| + if (filter is! Transformer) {
|
| + if (checkAssignability) {
|
| + throw new EvalException("filter must implement Transformer to be "
|
| + "assignable: $filterExpr");
|
| + } else {
|
| + return null;
|
| + }
|
| + }
|
| + value = filter.reverse(value);
|
| + }
|
| + // evaluate the receiver
|
| + var o = eval(expression, scope);
|
| +
|
| + // can't assign to a property on a null LHS object. Silently fail.
|
| + if (o == null) return null;
|
| +
|
| + if (isIndex) {
|
| + var index = eval(property, scope);
|
| + o[index] = value;
|
| + } else {
|
| + smoke.write(o, smoke.nameToSymbol(property), value);
|
| + }
|
| + return value;
|
| +}
|
| +
|
| +
|
| +/**
|
| + * A scope in polymer expressions that can map names to objects. Scopes contain
|
| + * a set of named variables and a unique model object. The scope structure
|
| + * is then used to lookup names using the `[]` operator. The lookup first
|
| + * searches for the name in local variables, then in global variables,
|
| + * and then finally looks up the name as a property in the model.
|
| + */
|
| +abstract class Scope implements Indexable<String, Object> {
|
| + Scope._();
|
| +
|
| + /** Create a scope containing a [model] and all of [variables]. */
|
| + factory Scope({Object model, Map<String, Object> variables}) {
|
| + var scope = new _ModelScope(model);
|
| + return variables == null ? scope
|
| + : new _GlobalsScope(new Map<String, Object>.from(variables), scope);
|
| + }
|
| +
|
| + /** Return the unique model in this scope. */
|
| + Object get model;
|
| +
|
| + /**
|
| + * Lookup the value of [name] in the current scope. If [name] is 'this', then
|
| + * we return the [model]. For any other name, this finds the first variable
|
| + * matching [name] or, if none exists, the property [name] in the [model].
|
| + */
|
| + Object operator [](String name);
|
| +
|
| + operator []=(String name, Object value) {
|
| + throw new UnsupportedError('[]= is not supported in Scope.');
|
| + }
|
| +
|
| + /**
|
| + * Returns whether [name] is defined in [model], that is, a lookup
|
| + * would not find a variable with that name, but there is a non-null model
|
| + * where we can look it up as a property.
|
| + */
|
| + bool _isModelProperty(String name);
|
| +
|
| + /** Create a new scope extending this scope with an additional variable. */
|
| + Scope childScope(String name, Object value) =>
|
| + new _LocalVariableScope(name, value, this);
|
| +}
|
| +
|
| +/**
|
| + * A scope that looks up names in a model object. This kind of scope has no
|
| + * parent scope because all our lookup operations stop when we reach the model
|
| + * object. Any variables added in scope or global variables are added as child
|
| + * scopes.
|
| + */
|
| +class _ModelScope extends Scope {
|
| + final Object model;
|
| +
|
| + _ModelScope(this.model) : super._();
|
| +
|
| + Object operator[](String name) {
|
| + if (name == 'this') return model;
|
| + var symbol = smoke.nameToSymbol(name);
|
| + if (model == null || symbol == null) {
|
| + throw new EvalException("variable '$name' not found");
|
| + }
|
| + return _convert(smoke.read(model, symbol));
|
| + }
|
| +
|
| + Object _isModelProperty(String name) => name != 'this';
|
| +
|
| + String toString() => "[model: $model]";
|
| +}
|
| +
|
| +/**
|
| + * A scope that holds a reference to a single variable. Polymer expressions
|
| + * introduce variables to the scope one at a time. Each time a variable is
|
| + * added, a new [_LocalVariableScope] is created.
|
| + */
|
| +class _LocalVariableScope extends Scope {
|
| + final Scope parent;
|
| + final String varName;
|
| + // TODO(sigmund,justinfagnani): make this @observable?
|
| + final Object value;
|
| +
|
| + _LocalVariableScope(this.varName, this.value, this.parent) : super._() {
|
| + if (varName == 'this') {
|
| + throw new EvalException("'this' cannot be used as a variable name.");
|
| + }
|
| + }
|
| +
|
| + Object get model => parent != null ? parent.model : null;
|
| +
|
| + Object operator[](String name) {
|
| + if (varName == name) return _convert(value);
|
| + if (parent != null) return parent[name];
|
| + throw new EvalException("variable '$name' not found");
|
| + }
|
| +
|
| + bool _isModelProperty(String name) {
|
| + if (varName == name) return false;
|
| + return parent == null ? false : parent._isModelProperty(name);
|
| + }
|
| +
|
| + String toString() => "$parent > [local: $varName]";
|
| +}
|
| +
|
| +/** A scope that holds a reference to a global variables. */
|
| +class _GlobalsScope extends Scope {
|
| + final _ModelScope parent;
|
| + final Map<String, Object> variables;
|
| +
|
| + _GlobalsScope(this.variables, this.parent) : super._() {
|
| + if (variables.containsKey('this')) {
|
| + throw new EvalException("'this' cannot be used as a variable name.");
|
| + }
|
| + }
|
| +
|
| + Object get model => parent != null ? parent.model : null;
|
| +
|
| + Object operator[](String name) {
|
| + if (variables.containsKey(name)) return _convert(variables[name]);
|
| + if (parent != null) return parent[name];
|
| + throw new EvalException("variable '$name' not found");
|
| + }
|
| +
|
| + bool _isModelProperty(String name) {
|
| + if (variables.containsKey(name)) return false;
|
| + return parent == null ? false : parent._isModelProperty(name);
|
| + }
|
| +
|
| + String toString() => "$parent > [global: ${variables.keys}]";
|
| +}
|
| +
|
| +Object _convert(v) => v is Stream ? new StreamBinding(v) : v;
|
| +
|
| +abstract class ExpressionObserver<E extends Expression> implements Expression {
|
| + final E _expr;
|
| + ExpressionObserver _parent;
|
| +
|
| + StreamSubscription _subscription;
|
| + Object _value;
|
| +
|
| + StreamController _controller = new StreamController.broadcast();
|
| + Stream get onUpdate => _controller.stream;
|
| +
|
| + ExpressionObserver(this._expr);
|
| +
|
| + Expression get expression => _expr;
|
| +
|
| + Object get currentValue => _value;
|
| +
|
| + update(Scope scope) => _updateSelf(scope);
|
| +
|
| + _updateSelf(Scope scope) {}
|
| +
|
| + _invalidate(Scope scope) {
|
| + _observe(scope, false);
|
| + if (_parent != null) {
|
| + _parent._invalidate(scope);
|
| + }
|
| + }
|
| +
|
| + _unobserve() {
|
| + if (_subscription != null) {
|
| + _subscription.cancel();
|
| + _subscription = null;
|
| + }
|
| + }
|
| +
|
| + _observe(Scope scope, skipChanges) {
|
| + _unobserve();
|
| +
|
| + var _oldValue = _value;
|
| +
|
| + // evaluate
|
| + _updateSelf(scope);
|
| +
|
| + if (!skipChanges && !identical(_value, _oldValue)) {
|
| + _controller.add(_value);
|
| + }
|
| + }
|
| +
|
| + String toString() => _expr.toString();
|
| +}
|
| +
|
| +class Updater extends RecursiveVisitor {
|
| + final Scope scope;
|
| + final bool skipChanges;
|
| +
|
| + Updater(this.scope, [this.skipChanges = false]);
|
| +
|
| + visitExpression(ExpressionObserver e) {
|
| + e._observe(scope, skipChanges);
|
| + }
|
| +}
|
| +
|
| +class Closer extends RecursiveVisitor {
|
| + static final _instance = new Closer._();
|
| + factory Closer() => _instance;
|
| + Closer._();
|
| +
|
| + visitExpression(ExpressionObserver e) {
|
| + e._unobserve();
|
| + }
|
| +}
|
| +
|
| +class EvalVisitor extends Visitor {
|
| + final Scope scope;
|
| +
|
| + EvalVisitor(this.scope);
|
| +
|
| + visitEmptyExpression(EmptyExpression e) => scope.model;
|
| +
|
| + visitParenthesizedExpression(ParenthesizedExpression e) => visit(e.child);
|
| +
|
| + visitGetter(Getter g) {
|
| + var receiver = visit(g.receiver);
|
| + if (receiver == null) return null;
|
| + var symbol = smoke.nameToSymbol(g.name);
|
| + return smoke.read(receiver, symbol);
|
| + }
|
| +
|
| + visitIndex(Index i) {
|
| + var receiver = visit(i.receiver);
|
| + if (receiver == null) return null;
|
| + var key = visit(i.argument);
|
| + return receiver[key];
|
| + }
|
| +
|
| + visitInvoke(Invoke i) {
|
| + var receiver = visit(i.receiver);
|
| + if (receiver == null) return null;
|
| + var args = (i.arguments == null)
|
| + ? null
|
| + : i.arguments.map(visit).toList(growable: false);
|
| +
|
| + if (i.method == null) {
|
| + assert(receiver is Function);
|
| + return Function.apply(receiver, args);
|
| + }
|
| +
|
| + var symbol = smoke.nameToSymbol(i.method);
|
| + return smoke.invoke(receiver, symbol, args);
|
| + }
|
| +
|
| + visitLiteral(Literal l) => l.value;
|
| +
|
| + visitListLiteral(ListLiteral l) => l.items.map(visit).toList();
|
| +
|
| + visitMapLiteral(MapLiteral l) {
|
| + var map = {};
|
| + for (var entry in l.entries) {
|
| + var key = visit(entry.key);
|
| + var value = visit(entry.entryValue);
|
| + map[key] = value;
|
| + }
|
| + return map;
|
| + }
|
| +
|
| + visitMapLiteralEntry(MapLiteralEntry e) =>
|
| + throw new UnsupportedError("should never be called");
|
| +
|
| + visitIdentifier(Identifier i) => scope[i.value];
|
| +
|
| + visitBinaryOperator(BinaryOperator o) {
|
| + var operator = o.operator;
|
| + var left = visit(o.left);
|
| + var right = visit(o.right);
|
| +
|
| + var f = _BINARY_OPERATORS[operator];
|
| + if (operator == '&&' || operator == '||') {
|
| + // TODO: short-circuit
|
| + return f(_toBool(left), _toBool(right));
|
| + } else if (operator == '==' || operator == '!=') {
|
| + return f(left, right);
|
| + } else if (left == null || right == null) {
|
| + return null;
|
| + }
|
| + return f(left, right);
|
| + }
|
| +
|
| + visitUnaryOperator(UnaryOperator o) {
|
| + var expr = visit(o.child);
|
| + var f = _UNARY_OPERATORS[o.operator];
|
| + if (o.operator == '!') {
|
| + return f(_toBool(expr));
|
| + }
|
| + return (expr == null) ? null : f(expr);
|
| + }
|
| +
|
| + visitTernaryOperator(TernaryOperator o) =>
|
| + visit(o.condition) == true ? visit(o.trueExpr) : visit(o.falseExpr);
|
| +
|
| + visitInExpression(InExpression i) =>
|
| + throw new UnsupportedError("can't eval an 'in' expression");
|
| +
|
| + visitAsExpression(AsExpression i) =>
|
| + throw new UnsupportedError("can't eval an 'as' expression");
|
| +}
|
| +
|
| +class ObserverBuilder extends Visitor {
|
| + final Queue parents = new Queue();
|
| +
|
| + ObserverBuilder();
|
| +
|
| + visitEmptyExpression(EmptyExpression e) => new EmptyObserver(e);
|
| +
|
| + visitParenthesizedExpression(ParenthesizedExpression e) => visit(e.child);
|
| +
|
| + visitGetter(Getter g) {
|
| + var receiver = visit(g.receiver);
|
| + var getter = new GetterObserver(g, receiver);
|
| + receiver._parent = getter;
|
| + return getter;
|
| + }
|
| +
|
| + visitIndex(Index i) {
|
| + var receiver = visit(i.receiver);
|
| + var arg = visit(i.argument);
|
| + var index = new IndexObserver(i, receiver, arg);
|
| + receiver._parent = index;
|
| + arg._parent = index;
|
| + return index;
|
| + }
|
| +
|
| + visitInvoke(Invoke i) {
|
| + var receiver = visit(i.receiver);
|
| + var args = (i.arguments == null)
|
| + ? null
|
| + : i.arguments.map(visit).toList(growable: false);
|
| + var invoke = new InvokeObserver(i, receiver, args);
|
| + receiver._parent = invoke;
|
| + if (args != null) args.forEach((a) => a._parent = invoke);
|
| + return invoke;
|
| + }
|
| +
|
| + visitLiteral(Literal l) => new LiteralObserver(l);
|
| +
|
| + visitListLiteral(ListLiteral l) {
|
| + var items = l.items.map(visit).toList(growable: false);
|
| + var list = new ListLiteralObserver(l, items);
|
| + items.forEach((e) => e._parent = list);
|
| + return list;
|
| + }
|
| +
|
| + visitMapLiteral(MapLiteral l) {
|
| + var entries = l.entries.map(visit).toList(growable: false);
|
| + var map = new MapLiteralObserver(l, entries);
|
| + entries.forEach((e) => e._parent = map);
|
| + return map;
|
| + }
|
| +
|
| + visitMapLiteralEntry(MapLiteralEntry e) {
|
| + var key = visit(e.key);
|
| + var value = visit(e.entryValue);
|
| + var entry = new MapLiteralEntryObserver(e, key, value);
|
| + key._parent = entry;
|
| + value._parent = entry;
|
| + return entry;
|
| + }
|
| +
|
| + visitIdentifier(Identifier i) => new IdentifierObserver(i);
|
| +
|
| + visitBinaryOperator(BinaryOperator o) {
|
| + var left = visit(o.left);
|
| + var right = visit(o.right);
|
| + var binary = new BinaryObserver(o, left, right);
|
| + left._parent = binary;
|
| + right._parent = binary;
|
| + return binary;
|
| + }
|
| +
|
| + visitUnaryOperator(UnaryOperator o) {
|
| + var expr = visit(o.child);
|
| + var unary = new UnaryObserver(o, expr);
|
| + expr._parent = unary;
|
| + return unary;
|
| + }
|
| +
|
| + visitTernaryOperator(TernaryOperator o) {
|
| + var condition = visit(o.condition);
|
| + var trueExpr = visit(o.trueExpr);
|
| + var falseExpr = visit(o.falseExpr);
|
| + var ternary = new TernaryObserver(o, condition, trueExpr, falseExpr);
|
| + condition._parent = ternary;
|
| + trueExpr._parent = ternary;
|
| + falseExpr._parent = ternary;
|
| + return ternary;
|
| + }
|
| +
|
| + visitInExpression(InExpression i) {
|
| + throw new UnsupportedError("can't eval an 'in' expression");
|
| + }
|
| +
|
| + visitAsExpression(AsExpression i) {
|
| + throw new UnsupportedError("can't eval an 'as' expression");
|
| + }
|
| +}
|
| +
|
| +class EmptyObserver extends ExpressionObserver<EmptyExpression>
|
| + implements EmptyExpression {
|
| +
|
| + EmptyObserver(EmptyExpression value) : super(value);
|
| +
|
| + _updateSelf(Scope scope) {
|
| + _value = scope.model;
|
| + // TODO(justin): listen for scope.model changes?
|
| + }
|
| +
|
| + accept(Visitor v) => v.visitEmptyExpression(this);
|
| +}
|
| +
|
| +class LiteralObserver extends ExpressionObserver<Literal> implements Literal {
|
| +
|
| + LiteralObserver(Literal value) : super(value);
|
| +
|
| + dynamic get value => _expr.value;
|
| +
|
| + _updateSelf(Scope scope) {
|
| + _value = _expr.value;
|
| + }
|
| +
|
| + accept(Visitor v) => v.visitLiteral(this);
|
| +}
|
| +
|
| +class ListLiteralObserver extends ExpressionObserver<ListLiteral>
|
| + implements ListLiteral {
|
| +
|
| + final List<ExpressionObserver> items;
|
| +
|
| + ListLiteralObserver(ListLiteral value, this.items) : super(value);
|
| +
|
| + _updateSelf(Scope scope) {
|
| + _value = items.map((i) => i._value).toList();
|
| + }
|
| +
|
| + accept(Visitor v) => v.visitListLiteral(this);
|
| +}
|
| +
|
| +class MapLiteralObserver extends ExpressionObserver<MapLiteral>
|
| + implements MapLiteral {
|
| +
|
| + final List<MapLiteralEntryObserver> entries;
|
| +
|
| + MapLiteralObserver(MapLiteral value, this.entries) : super(value);
|
| +
|
| + _updateSelf(Scope scope) {
|
| + _value = entries.fold(new Map(),
|
| + (m, e) => m..[e.key._value] = e.entryValue._value);
|
| + }
|
| +
|
| + accept(Visitor v) => v.visitMapLiteral(this);
|
| +}
|
| +
|
| +class MapLiteralEntryObserver extends ExpressionObserver<MapLiteralEntry>
|
| + implements MapLiteralEntry {
|
| +
|
| + final LiteralObserver key;
|
| + final ExpressionObserver entryValue;
|
| +
|
| + MapLiteralEntryObserver(MapLiteralEntry value, this.key, this.entryValue)
|
| + : super(value);
|
| +
|
| + accept(Visitor v) => v.visitMapLiteralEntry(this);
|
| +}
|
| +
|
| +class IdentifierObserver extends ExpressionObserver<Identifier>
|
| + implements Identifier {
|
| +
|
| + IdentifierObserver(Identifier value) : super(value);
|
| +
|
| + String get value => _expr.value;
|
| +
|
| + _updateSelf(Scope scope) {
|
| + _value = scope[value];
|
| + if (!scope._isModelProperty(value)) return;
|
| + var model = scope.model;
|
| + if (model is! Observable) return;
|
| + var symbol = smoke.nameToSymbol(value);
|
| + _subscription = (model as Observable).changes.listen((changes) {
|
| + if (changes.any((c) => c is PropertyChangeRecord && c.name == symbol)) {
|
| + _invalidate(scope);
|
| + }
|
| + });
|
| + }
|
| +
|
| + accept(Visitor v) => v.visitIdentifier(this);
|
| +}
|
| +
|
| +class ParenthesizedObserver extends ExpressionObserver<ParenthesizedExpression>
|
| + implements ParenthesizedExpression {
|
| + final ExpressionObserver child;
|
| +
|
| + ParenthesizedObserver(ParenthesizedExpression expr, this.child) : super(expr);
|
| +
|
| +
|
| + _updateSelf(Scope scope) {
|
| + _value = child._value;
|
| + }
|
| +
|
| + accept(Visitor v) => v.visitParenthesizedExpression(this);
|
| +}
|
| +
|
| +class UnaryObserver extends ExpressionObserver<UnaryOperator>
|
| + implements UnaryOperator {
|
| + final ExpressionObserver child;
|
| +
|
| + UnaryObserver(UnaryOperator expr, this.child) : super(expr);
|
| +
|
| + String get operator => _expr.operator;
|
| +
|
| + _updateSelf(Scope scope) {
|
| + var f = _UNARY_OPERATORS[_expr.operator];
|
| + if (operator == '!') {
|
| + _value = f(_toBool(child._value));
|
| + } else {
|
| + _value = (child._value == null) ? null : f(child._value);
|
| + }
|
| + }
|
| +
|
| + accept(Visitor v) => v.visitUnaryOperator(this);
|
| +}
|
| +
|
| +class BinaryObserver extends ExpressionObserver<BinaryOperator>
|
| + implements BinaryOperator {
|
| +
|
| + final ExpressionObserver left;
|
| + final ExpressionObserver right;
|
| +
|
| + BinaryObserver(BinaryOperator expr, this.left, this.right)
|
| + : super(expr);
|
| +
|
| + String get operator => _expr.operator;
|
| +
|
| + _updateSelf(Scope scope) {
|
| + var f = _BINARY_OPERATORS[operator];
|
| + if (operator == '&&' || operator == '||') {
|
| + _value = f(_toBool(left._value), _toBool(right._value));
|
| + } else if (operator == '==' || operator == '!=') {
|
| + _value = f(left._value, right._value);
|
| + } else if (left._value == null || right._value == null) {
|
| + _value = null;
|
| + } else {
|
| + if (operator == '|' && left._value is ObservableList) {
|
| + _subscription = (left._value as ObservableList).listChanges
|
| + .listen((_) => _invalidate(scope));
|
| + }
|
| + _value = f(left._value, right._value);
|
| + }
|
| + }
|
| +
|
| + accept(Visitor v) => v.visitBinaryOperator(this);
|
| +
|
| +}
|
| +
|
| +class TernaryObserver extends ExpressionObserver<TernaryOperator>
|
| + implements TernaryOperator {
|
| +
|
| + final ExpressionObserver condition;
|
| + final ExpressionObserver trueExpr;
|
| + final ExpressionObserver falseExpr;
|
| +
|
| + TernaryObserver(TernaryOperator expr, this.condition, this.trueExpr,
|
| + this.falseExpr) : super(expr);
|
| +
|
| + _updateSelf(Scope scope) {
|
| + _value = _toBool(condition._value) ? trueExpr._value : falseExpr._value;
|
| + }
|
| +
|
| + accept(Visitor v) => v.visitTernaryOperator(this);
|
| +
|
| +}
|
| +
|
| +class GetterObserver extends ExpressionObserver<Getter> implements Getter {
|
| + final ExpressionObserver receiver;
|
| +
|
| + GetterObserver(Expression expr, this.receiver) : super(expr);
|
| +
|
| + String get name => _expr.name;
|
| +
|
| + _updateSelf(Scope scope) {
|
| + var receiverValue = receiver._value;
|
| + if (receiverValue == null) {
|
| + _value = null;
|
| + return;
|
| + }
|
| + var symbol = smoke.nameToSymbol(_expr.name);
|
| + _value = smoke.read(receiverValue, symbol);
|
| +
|
| + if (receiverValue is Observable) {
|
| + _subscription = (receiverValue as Observable).changes.listen((changes) {
|
| + if (changes.any((c) => c is PropertyChangeRecord && c.name == symbol)) {
|
| + _invalidate(scope);
|
| + }
|
| + });
|
| + }
|
| + }
|
| +
|
| + accept(Visitor v) => v.visitGetter(this);
|
| +}
|
| +
|
| +class IndexObserver extends ExpressionObserver<Index> implements Index {
|
| + final ExpressionObserver receiver;
|
| + final ExpressionObserver argument;
|
| +
|
| + IndexObserver(Expression expr, this.receiver, this.argument) : super(expr);
|
| +
|
| + _updateSelf(Scope scope) {
|
| + var receiverValue = receiver._value;
|
| + if (receiverValue == null) {
|
| + _value = null;
|
| + return;
|
| + }
|
| + var key = argument._value;
|
| + _value = receiverValue[key];
|
| +
|
| + if (receiverValue is ObservableList) {
|
| + _subscription = (receiverValue as ObservableList).listChanges
|
| + .listen((changes) {
|
| + if (changes.any((c) => c.indexChanged(key))) _invalidate(scope);
|
| + });
|
| + } else if (receiverValue is Observable) {
|
| + _subscription = (receiverValue as Observable).changes.listen((changes) {
|
| + if (changes.any((c) => c is MapChangeRecord && c.key == key)) {
|
| + _invalidate(scope);
|
| + }
|
| + });
|
| + }
|
| + }
|
| +
|
| + accept(Visitor v) => v.visitIndex(this);
|
| +}
|
| +
|
| +class InvokeObserver extends ExpressionObserver<Invoke> implements Invoke {
|
| + final ExpressionObserver receiver;
|
| + final List<ExpressionObserver> arguments;
|
| +
|
| + InvokeObserver(Expression expr, this.receiver, this.arguments)
|
| + : super(expr) {
|
| + assert(arguments != null);
|
| + }
|
| +
|
| + String get method => _expr.method;
|
| +
|
| + _updateSelf(Scope scope) {
|
| + var args = arguments.map((a) => a._value).toList();
|
| + var receiverValue = receiver._value;
|
| + if (receiverValue == null) {
|
| + _value = null;
|
| + return;
|
| + }
|
| + if (_expr.method == null) {
|
| + // top-level function or model method
|
| + // TODO(justin): listen to model changes to see if the method has
|
| + // changed? listen to the scope to see if the top-level method has
|
| + // changed?
|
| + assert(receiverValue is Function);
|
| + _value = _convert(Function.apply(receiverValue, args));
|
| + } else {
|
| + var symbol = smoke.nameToSymbol(_expr.method);
|
| + _value = smoke.invoke(receiverValue, symbol, args);
|
| +
|
| + if (receiverValue is Observable) {
|
| + _subscription = (receiverValue as Observable).changes.listen(
|
| + (List<ChangeRecord> changes) {
|
| + if (changes.any(
|
| + (c) => c is PropertyChangeRecord && c.name == symbol)) {
|
| + _invalidate(scope);
|
| + }
|
| + });
|
| + }
|
| + }
|
| + }
|
| +
|
| + accept(Visitor v) => v.visitInvoke(this);
|
| +}
|
| +
|
| +_toBool(v) => (v == null) ? false : v;
|
| +
|
| +class EvalException implements Exception {
|
| + final String message;
|
| + EvalException(this.message);
|
| + String toString() => "EvalException: $message";
|
| +}
|
|
|