feat(*): lazy one-time binding support

Expressions that start with `::` will be binded once. The rule
that binding follows is that the binding will take the first
not-undefined value at the end of a $digest cycle.

Watchers from $watch, $watchCollection and $watchGroup will
automatically stop watching when the expression(s) are bind-once
and fulfill.

Watchers from text and attributes interpolations will
automatically stop watching when the expressions are fulfill.

All directives that use $parse for expressions will automatically
work with bind-once expressions. E.g.

<div ng-bind="::foo"></div>
<li ng-repeat="item in ::items">{{::item.name}};</li>

Paired with: Caitlin and Igor
Design doc: https://docs.google.com/document/d/1fTqaaQYD2QE1rz-OywvRKFSpZirbWUPsnfaZaMq8fWI/edit#
Closes #7486
Closes #5408
This commit is contained in:
Lucas Galfaso
2014-05-14 22:56:12 -03:00
committed by Igor Minar
parent 701ed5fdf6
commit cee429f0aa
13 changed files with 503 additions and 18 deletions

View File

@@ -198,3 +198,122 @@ expose a `$event` object within the scope of that expression.
Note in the example above how we can pass in `$event` to `clickMe`, but how it does not show up
in `{{$event}}`. This is because `$event` is outside the scope of that binding.
## One-time binding
An expression that starts with `::` is considered a one-time expression. One-time expressions
will stop recalculating once they are stable, which happens after the first digest if the expression
result is a non-undefined value (see value stabilization algorithm below).
<example module="oneTimeBidingExampleApp">
<file name="index.html">
<div ng-controller="EventController">
<button ng-click="clickMe($event)">Click Me</button>
<p id="one-time-binding-example">One time binding: {{::name}}</p>
<p id="normal-binding-example">Normal binding: {{name}}</p>
</div>
</file>
<file name="script.js">
angular.module('oneTimeBidingExampleApp', []).
controller('EventController', ['$scope', function($scope) {
var counter = 0;
var names = ['Igor', 'Misko', 'Chirayu', 'Lucas'];
/*
* expose the event object to the scope
*/
$scope.clickMe = function(clickEvent) {
$scope.name = names[counter % names.length];
counter++;
};
}]);
</file>
<file name="protractor.js" type="protractor">
it('should freeze binding after its value has stabilized', function() {
var oneTimeBiding = element(by.id('one-time-binding-example'));
var normalBinding = element(by.id('normal-binding-example'));
expect(oneTimeBiding.getText()).toEqual('One time binding:');
expect(normalBinding.getText()).toEqual('Normal binding:');
element(by.buttonText('Click Me')).click();
expect(oneTimeBiding.getText()).toEqual('One time binding: Igor');
expect(normalBinding.getText()).toEqual('Normal binding: Igor');
element(by.buttonText('Click Me')).click();
expect(oneTimeBiding.getText()).toEqual('One time binding: Igor');
expect(normalBinding.getText()).toEqual('Normal binding: Misko');
element(by.buttonText('Click Me')).click();
element(by.buttonText('Click Me')).click();
expect(oneTimeBiding.getText()).toEqual('One time binding: Igor');
expect(normalBinding.getText()).toEqual('Normal binding: Lucas');
});
</file>
</example>
### Why this feature
The main purpose of one-time binding expression is to provide a way to create a binding
that gets deregistered and frees up resources once the binding is stabilized.
Reducing the number of expressions being watched makes the digest loop faster and allows more
information to be displayed at the same time.
### Value stabilization algorithm
One-time binding expressions will retain the value of the expression at the end of the
digest cycle as long as that value is not undefined. If the value of the expression is set
within the digest loop and later, within the same digest loop, it is set to undefined,
then the expression is not fulfilled and will remain watched.
1. Given an expression that starts with `::` when a digest loop is entered and expression
is dirty-checked store the value as V
2. If V is not undefined mark the result of the expression as stable and schedule a task
to deregister the watch for this expression when we exit the digest loop
3. Process the digest loop as normal
4. When digest loop is done and all the values have settled process the queue of watch
deregistration tasks. For each watch to be deregistered check if it still evaluates
to value that is not `undefined`. If that's the case, deregister the watch. Otherwise
keep dirty-checking the watch in the future digest loops by following the same
algorithm starting from step 1
### How to benefit from one-time binding
When interpolating text or attributes. If the expression, once set, will not change
then it is a candidate for one-time expression.
```html
<div name="attr: {{::color}}">text: {{::name}}</div>
```
When using a directive with bidirectional binding and the parameters will not change
```js
someModule.directive('someDirective', function() {
return {
scope: {
name: '=',
color: '@'
},
template: '{{name}}: {{color}}'
};
});
```
```html
<div some-directive name=“::myName” color=“My color is {{::myColor}}”></div>
```
When using a directive that takes an expression
```html
<ul>
<li ng-repeat="item in ::items">{{item.name}};</li>
</ul>
```

View File

@@ -1485,6 +1485,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
parentSet(scope, parentValue = isolateScope[scopeName]);
}
}
parentValueWatch.$$unwatch = parentGet.$$unwatch;
return lastValue = parentValue;
}, null, parentGet.literal);
break;
@@ -1813,6 +1814,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
compile: valueFn(function textInterpolateLinkFn(scope, node) {
var parent = node.parent(),
bindings = parent.data('$binding') || [];
// Need to interpolate again in case this is using one-time bindings in multiple clones
// of transcluded templates.
interpolateFn = $interpolate(text);
bindings.push(interpolateFn);
safeAddClass(parent.data('$binding', bindings), 'ng-binding');
scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {

View File

@@ -174,7 +174,11 @@ var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) {
element.addClass('ng-binding').data('$binding', attr.ngBindHtml);
var parsed = $parse(attr.ngBindHtml);
function getStringValue() { return (parsed(scope) || '').toString(); }
function getStringValue() {
var value = parsed(scope);
getStringValue.$$unwatch = parsed.$$unwatch;
return (value || '').toString();
}
scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) {
element.html($sce.getTrustedHtml(parsed(scope)) || '');

View File

@@ -309,9 +309,11 @@ function $InterpolateProvider() {
try {
interpolationFn.$$unwatch = true;
for (; i < ii; i++) {
val = getValue(parseFns[i](context));
if (allOrNothing && isUndefined(val)) {
interpolationFn.$$unwatch = undefined;
return;
}
val = stringify(val);
@@ -319,6 +321,7 @@ function $InterpolateProvider() {
inputsChanged = true;
}
values[i] = val;
interpolationFn.$$unwatch = interpolationFn.$$unwatch && parseFns[i].$$unwatch;
}
if (inputsChanged) {

View File

@@ -1018,13 +1018,19 @@ function $ParseProvider() {
$parseOptions.csp = $sniffer.csp;
return function(exp) {
var parsedExpression;
var parsedExpression,
oneTime;
switch (typeof exp) {
case 'string':
if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
oneTime = true;
exp = exp.substring(2);
}
if (cache.hasOwnProperty(exp)) {
return cache[exp];
return oneTime ? oneTimeWrapper(cache[exp]) : cache[exp];
}
var lexer = new Lexer($parseOptions);
@@ -1037,7 +1043,11 @@ function $ParseProvider() {
cache[exp] = parsedExpression;
}
return parsedExpression;
if (parsedExpression.constant) {
parsedExpression.$$unwatch = true;
}
return oneTime ? oneTimeWrapper(parsedExpression) : parsedExpression;
case 'function':
return exp;
@@ -1045,6 +1055,31 @@ function $ParseProvider() {
default:
return noop;
}
function oneTimeWrapper(expression) {
var stable = false,
lastValue;
oneTimeParseFn.literal = expression.literal;
oneTimeParseFn.constant = expression.constant;
oneTimeParseFn.assign = expression.assign;
return oneTimeParseFn;
function oneTimeParseFn(self, locals) {
if (!stable) {
lastValue = expression(self, locals);
oneTimeParseFn.$$unwatch = isDefined(lastValue);
if (oneTimeParseFn.$$unwatch && self && self.$$postDigestQueue) {
self.$$postDigestQueue.push(function () {
// create a copy if the value is defined and it is not a $sce value
if ((stable = isDefined(lastValue)) && !lastValue.$$unwrapTrustedValue) {
lastValue = copy(lastValue);
}
});
}
}
return lastValue;
}
}
};
}];
}

View File

@@ -338,14 +338,6 @@ function $RootScopeProvider(){
watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);};
}
if (typeof watchExp == 'string' && get.constant) {
var originalFn = watcher.fn;
watcher.fn = function(newVal, oldVal, scope) {
originalFn.call(this, newVal, oldVal, scope);
arrayRemove(array, watcher);
};
}
if (!array) {
array = scope.$$watchers = [];
}
@@ -391,17 +383,28 @@ function $RootScopeProvider(){
var deregisterFns = [];
var changeCount = 0;
var self = this;
var unwatchFlags = new Array(watchExpressions.length);
var unwatchCount = watchExpressions.length;
forEach(watchExpressions, function (expr, i) {
deregisterFns.push(self.$watch(expr, function (value, oldValue) {
var exprFn = $parse(expr);
deregisterFns.push(self.$watch(exprFn, function (value, oldValue) {
newValues[i] = value;
oldValues[i] = oldValue;
changeCount++;
if (unwatchFlags[i] && !exprFn.$$unwatch) unwatchCount++;
if (!unwatchFlags[i] && exprFn.$$unwatch) unwatchCount--;
unwatchFlags[i] = exprFn.$$unwatch;
}));
}, this);
deregisterFns.push(self.$watch(function () {return changeCount;}, function () {
deregisterFns.push(self.$watch(watchGroupFn, function () {
listener(newValues, oldValues, self);
if (unwatchCount === 0) {
watchGroupFn.$$unwatch = true;
} else {
watchGroupFn.$$unwatch = false;
}
}));
return function deregisterWatchGroup() {
@@ -409,6 +412,8 @@ function $RootScopeProvider(){
fn();
});
};
function watchGroupFn() {return changeCount;}
},
@@ -553,6 +558,7 @@ function $RootScopeProvider(){
}
}
}
$watchCollectionWatch.$$unwatch = objGetter.$$unwatch;
return changeDetected;
}
@@ -644,6 +650,7 @@ function $RootScopeProvider(){
dirty, ttl = TTL,
next, current, target = this,
watchLog = [],
stableWatchesCandidates = [],
logIdx, logMsg, asyncTask;
beginPhase('$digest');
@@ -694,6 +701,7 @@ function $RootScopeProvider(){
logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
watchLog[logIdx].push(logMsg);
}
if (watch.get.$$unwatch) stableWatchesCandidates.push({watch: watch, array: watchers});
} else if (watch === lastDirtyWatch) {
// If the most recently dirty watcher is now clean, short circuit since the remaining watchers
// have already been tested.
@@ -740,6 +748,13 @@ function $RootScopeProvider(){
$exceptionHandler(e);
}
}
for (length = stableWatchesCandidates.length - 1; length >= 0; --length) {
var candidate = stableWatchesCandidates[length];
if (candidate.watch.get.$$unwatch) {
arrayRemove(candidate.array, candidate.watch);
}
}
},

View File

@@ -787,7 +787,9 @@ function $SceProvider() {
return parsed;
} else {
return function sceParseAsTrusted(self, locals) {
return sce.getTrusted(type, parsed(self, locals));
var result = sce.getTrusted(type, parsed(self, locals));
sceParseAsTrusted.$$unwatch = parsed.$$unwatch;
return result;
};
}
};

View File

@@ -2180,6 +2180,23 @@ describe('$compile', function() {
);
it('should one-time bind if the expression starts with two colons', inject(
function($rootScope, $compile) {
$rootScope.name = 'angular';
element = $compile('<div name="attr: {{::name}}">text: {{::name}}</div>')($rootScope);
expect($rootScope.$$watchers.length).toBe(2);
$rootScope.$digest();
expect(element.text()).toEqual('text: angular');
expect(element.attr('name')).toEqual('attr: angular');
expect($rootScope.$$watchers.length).toBe(0);
$rootScope.name = 'not-angular';
$rootScope.$digest();
expect(element.text()).toEqual('text: angular');
expect(element.attr('name')).toEqual('attr: angular');
})
);
it('should process attribute interpolation in pre-linking phase at priority 100', function() {
module(function() {
directive('attrLog', function(log) {
@@ -2792,6 +2809,83 @@ describe('$compile', function() {
});
it('should be possible to one-time bind a parameter on a component with a template', function() {
module(function() {
directive('otherTplDir', function() {
return {
scope: {param: '@', anotherParam: '=' },
template: 'value: {{param}}, another value {{anotherParam}}'
};
});
});
function countWatches(scope) {
var result = 0;
while (scope !== null) {
result += (scope.$$watchers && scope.$$watchers.length) || 0;
result += countWatches(scope.$$childHead);
scope = scope.$$nextSibling;
}
return result;
}
inject(function($rootScope) {
compile('<div other-tpl-dir param="{{::foo}}" another-param="::bar"></div>');
expect(countWatches($rootScope)).toEqual(3);
$rootScope.$digest();
expect(element.html()).toBe('value: , another value ');
expect(countWatches($rootScope)).toEqual(3);
$rootScope.foo = 'from-parent';
$rootScope.$digest();
expect(element.html()).toBe('value: from-parent, another value ');
expect(countWatches($rootScope)).toEqual(2);
$rootScope.foo = 'not-from-parent';
$rootScope.bar = 'some value';
$rootScope.$digest();
expect(element.html()).toBe('value: from-parent, another value some value');
expect(countWatches($rootScope)).toEqual(1);
$rootScope.bar = 'some new value';
$rootScope.$digest();
expect(element.html()).toBe('value: from-parent, another value some value');
});
});
it('should be possible to one-time bind a parameter on a component with a templateUrl', function() {
module(function() {
directive('otherTplDir', function() {
return {
scope: {param: '@', anotherParam: '=' },
templateUrl: 'other.html'
};
});
});
inject(function($rootScope, $templateCache) {
$templateCache.put('other.html', 'value: {{param}}, another value {{anotherParam}}');
compile('<div other-tpl-dir param="{{::foo}}" another-param="::bar"></div>');
$rootScope.$digest();
expect(element.html()).toBe('value: , another value ');
$rootScope.foo = 'from-parent';
$rootScope.$digest();
expect(element.html()).toBe('value: from-parent, another value ');
$rootScope.foo = 'not-from-parent';
$rootScope.bar = 'some value';
$rootScope.$digest();
expect(element.html()).toBe('value: from-parent, another value some value');
$rootScope.bar = 'some new value';
$rootScope.$digest();
expect(element.html()).toBe('value: from-parent, another value some value');
});
});
describe('attribute', function() {
it('should copy simple attribute', inject(function() {
compile('<div><span my-component attr="some text">');

View File

@@ -45,6 +45,43 @@ describe('ngBind*', function() {
$rootScope.$digest();
expect(element.text()).toEqual('-0false');
}));
it('should one-time bind if the expression starts with two colons', inject(function($rootScope, $compile) {
element = $compile('<div ng-bind="::a"></div>')($rootScope);
$rootScope.a = 'lucas';
expect($rootScope.$$watchers.length).toEqual(1);
$rootScope.$digest();
expect(element.text()).toEqual('lucas');
expect($rootScope.$$watchers.length).toEqual(0);
$rootScope.a = undefined;
$rootScope.$digest();
expect(element.text()).toEqual('lucas');
}));
it('should be possible to bind to a new value within the same $digest', inject(function($rootScope, $compile) {
element = $compile('<div ng-bind="::a"></div>')($rootScope);
$rootScope.$watch('a', function(newVal) { if (newVal === 'foo') { $rootScope.a = 'bar'; } });
$rootScope.a = 'foo';
$rootScope.$digest();
expect(element.text()).toEqual('bar');
$rootScope.a = undefined;
$rootScope.$digest();
expect(element.text()).toEqual('bar');
}));
it('should remove the binding if the value is defined at the end of a $digest loop', inject(function($rootScope, $compile) {
element = $compile('<div ng-bind="::a"></div>')($rootScope);
$rootScope.$watch('a', function(newVal) { if (newVal === 'foo') { $rootScope.a = undefined; } });
$rootScope.a = 'foo';
$rootScope.$digest();
expect(element.text()).toEqual('');
$rootScope.a = 'bar';
$rootScope.$digest();
expect(element.text()).toEqual('bar');
$rootScope.a = 'man';
$rootScope.$digest();
expect(element.text()).toEqual('bar');
}));
});
@@ -59,6 +96,22 @@ describe('ngBind*', function() {
}));
it('should one-time bind the expressions that start with ::', inject(function($rootScope, $compile) {
element = $compile('<div ng-bind-template="{{::hello}} {{::name}}!"></div>')($rootScope);
$rootScope.name = 'Misko';
expect($rootScope.$$watchers.length).toEqual(1);
$rootScope.$digest();
expect(element.hasClass('ng-binding')).toEqual(true);
expect(element.text()).toEqual(' Misko!');
expect($rootScope.$$watchers.length).toEqual(1);
$rootScope.hello = 'Hello';
$rootScope.name = 'Lucas';
$rootScope.$digest();
expect(element.text()).toEqual('Hello Misko!');
expect($rootScope.$$watchers.length).toEqual(0);
}));
it('should render object as JSON ignore $$', inject(function($rootScope, $compile) {
element = $compile('<pre>{{ {key:"value", $$key:"hide"} }}</pre>')($rootScope);
$rootScope.$digest();
@@ -79,6 +132,18 @@ describe('ngBind*', function() {
$rootScope.$digest();
expect(angular.lowercase(element.html())).toEqual('<div onclick="">hello</div>');
}));
it('should one-time bind if the expression starts with two colons', inject(function($rootScope, $compile) {
element = $compile('<div ng-bind-html="::html"></div>')($rootScope);
$rootScope.html = '<div onclick="">hello</div>';
expect($rootScope.$$watchers.length).toEqual(1);
$rootScope.$digest();
expect(element.text()).toEqual('hello');
expect($rootScope.$$watchers.length).toEqual(0);
$rootScope.html = '<div onclick="">hello</div>';
$rootScope.$digest();
expect(element.text()).toEqual('hello');
}));
});

View File

@@ -58,6 +58,42 @@ describe('ngRepeat', function() {
expect(element.text()).toEqual('shyam;');
});
it('should be possible to use one-time bindings on the collection', function() {
element = $compile(
'<ul>' +
'<li ng-repeat="item in ::items">{{item.name}};</li>' +
'</ul>')(scope);
scope.$digest();
scope.items = [{name: 'misko'}, {name:'shyam'}];
scope.$digest();
expect(element.find('li').length).toEqual(2);
expect(element.text()).toEqual('misko;shyam;');
scope.items.push({name: 'adam'});
scope.$digest();
expect(element.find('li').length).toEqual(2);
expect(element.text()).toEqual('misko;shyam;');
});
it('should be possible to use one-time bindings on the content', function() {
element = $compile(
'<ul>' +
'<li ng-repeat="item in items">{{::item.name}};</li>' +
'</ul>')(scope);
scope.$digest();
scope.items = [{name: 'misko'}, {name:'shyam'}];
scope.$digest();
expect(element.find('li').length).toEqual(2);
expect(element.text()).toEqual('misko;shyam;');
scope.items.push({name: 'adam'});
scope.$digest();
expect(element.find('li').length).toEqual(3);
expect(element.text()).toEqual('misko;shyam;adam;');
});
it('should iterate over an array-like object', function() {
element = $compile(

View File

@@ -760,9 +760,6 @@ describe('parser', function() {
'disallowed! Expression: wrap["d"]');
}));
it('should NOT allow access to the Window or DOM returned from a function', inject(function($window, $document) {
scope.getWin = valueFn($window);
scope.getDoc = valueFn($document);
expect(function() {
scope.$eval('getWin()', scope);
@@ -953,6 +950,69 @@ describe('parser', function() {
});
describe('one-time binding', function() {
it('should only use the cache when it is not a one-time binding', inject(function($parse) {
expect($parse('foo')).toBe($parse('foo'));
expect($parse('::foo')).not.toBe($parse('::foo'));
}));
it('should stay stable once the value defined', inject(function($parse, $rootScope) {
var fn = $parse('::foo');
expect(fn.$$unwatch).not.toBe(true);
$rootScope.$watch(fn);
$rootScope.$digest();
expect(fn.$$unwatch).not.toBe(true);
$rootScope.foo = 'bar';
$rootScope.$digest();
expect(fn.$$unwatch).toBe(true);
expect(fn($rootScope)).toBe('bar');
expect(fn()).toBe('bar');
$rootScope.foo = 'man';
$rootScope.$digest();
expect(fn.$$unwatch).toBe(true);
expect(fn($rootScope)).toBe('bar');
expect(fn()).toBe('bar');
}));
it('should have a stable value if at the end of a $digest it has a defined value', inject(function($parse, $rootScope) {
var fn = $parse('::foo');
$rootScope.$watch(fn);
$rootScope.$watch('foo', function() { if ($rootScope.foo === 'bar') {$rootScope.foo = undefined; } });
$rootScope.foo = 'bar';
$rootScope.$digest();
expect(fn.$$unwatch).toBe(false);
$rootScope.foo = 'man';
$rootScope.$digest();
expect(fn.$$unwatch).toBe(true);
expect(fn($rootScope)).toBe('man');
expect(fn()).toBe('man');
$rootScope.foo = 'shell';
$rootScope.$digest();
expect(fn.$$unwatch).toBe(true);
expect(fn($rootScope)).toBe('man');
expect(fn()).toBe('man');
}));
it('should keep a copy of the stable element', inject(function($parse, $rootScope) {
var fn = $parse('::foo'),
value = {bar: 'bar'};
$rootScope.$watch(fn);
$rootScope.foo = value;
$rootScope.$digest();
value.baz = 'baz';
expect(fn()).toEqual({bar: 'bar'});
}));
});
describe('locals', function() {
it('should expose local variables', inject(function($parse) {
expect($parse('a')({a: 0}, {a: 1})).toEqual(1);

View File

@@ -114,6 +114,45 @@ describe('Scope', function() {
expect($rootScope.$$watchers.length).toEqual(0);
}));
it('should clean up stable watches on the watch queue', inject(function($rootScope, $parse) {
$rootScope.$watch($parse('::foo'), function() {});
expect($rootScope.$$watchers.length).toEqual(1);
$rootScope.$digest();
expect($rootScope.$$watchers.length).toEqual(1);
$rootScope.foo = 'foo';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toEqual(0);
}));
it('should claen up stable watches from $watchCollection', inject(function($rootScope, $parse) {
$rootScope.$watchCollection('::foo', function() {});
expect($rootScope.$$watchers.length).toEqual(1);
$rootScope.$digest();
expect($rootScope.$$watchers.length).toEqual(1);
$rootScope.foo = [];
$rootScope.$digest();
expect($rootScope.$$watchers.length).toEqual(0);
}));
it('should clean up stable watches from $watchGroup', inject(function($rootScope, $parse) {
$rootScope.$watchGroup(['::foo', '::bar'], function() {});
expect($rootScope.$$watchers.length).toEqual(3);
$rootScope.$digest();
expect($rootScope.$$watchers.length).toEqual(3);
$rootScope.foo = 'foo';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toEqual(2);
$rootScope.bar = 'bar';
$rootScope.$digest();
expect($rootScope.$$watchers.length).toEqual(0);
}));
it('should delegate exceptions', function() {
module(function($exceptionHandlerProvider) {

View File

@@ -209,6 +209,15 @@ describe('SCE', function() {
expect($sce.parseAsJs('"string"')()).toBe("string");
}));
it('should be possible to do one-time binding', inject(function($sce, $rootScope) {
var exprFn = $sce.parseAsHtml('::foo');
expect(exprFn($rootScope, {'foo': $sce.trustAs($sce.HTML, 'trustedValue')})).toBe('trustedValue');
expect(exprFn($rootScope, {'foo': $sce.trustAs($sce.HTML, 'anotherTrustedValue')})).toBe('anotherTrustedValue');
$rootScope.$digest();
expect(exprFn($rootScope, {'foo': $sce.trustAs($sce.HTML, 'somethingElse')})).toBe('anotherTrustedValue');
expect(exprFn.$$unwatch).toBe(true);
}));
it('should NOT parse constant non-literals', inject(function($sce) {
// Until there's a real world use case for this, we're disallowing
// constant non-literals. See $SceParseProvider.