blob: 15941219050ef8fb388531621b741eb52f65d82a [file] [log] [blame]
// 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) {
new Updater(scope).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);
if (_parent != null) {
_parent._invalidate(scope);
}
}
_unobserve() {
if (_subscription != null) {
_subscription.cancel();
_subscription = null;
}
}
_observe(Scope scope) {
_unobserve();
var _oldValue = _value;
// evaluate
_updateSelf(scope);
if (!identical(_value, _oldValue)) {
_controller.add(_value);
}
}
String toString() => _expr.toString();
}
class Updater extends RecursiveVisitor {
final Scope scope;
Updater(this.scope);
visitExpression(ExpressionObserver e) {
e._observe(scope);
}
}
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";
}