[e-lang] [Caja] Functional auditor for Cajita
David-Sarah Hopwood
david-sarah at jacaranda.org
Thu Dec 10 16:46:35 PST 2009
David-Sarah Hopwood wrote:
> David-Sarah Hopwood wrote:
>> It would be possible to make the argument and result checks optional
>> depending on the function annotation: say, /*@pure*/ includes them but
>> /*@functional*/ doesn't. (The environment checks would be the same,
>> and both /*@pure*/ and /*@functional*/ would mark instances as
>> copacetic.)
>>
>> In that case, /*@pure*/ would imply unconditional determinism
>> (referential transparency), whereas /*@functional*/ would only imply
>> determinism for calls for which all arguments are copacetic.
>
> In fact I'm wrong here, as shown by this example:
>
> /*@pure*/ function f() { return cajita.deepFreeze({}); }
>
> const a = f();
> const b = f();
>
> /*@pure*/ function g(x) { return x === a; }
>
> g(a); // true
> g(b); // false
>
> Since a and b are observably different, either f or g must not
> be referentially transparent.
>
> A Cajita that only exposed an egal operator, as defined in
>
> Henry Baker,
> "Equal Rights for Functional Objects, or The More Things Change,
> The More They Are the Same"
> <http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.23.9999>
>
> would solve this problem. (It might be difficult to tame away all
> indirect access to ===, though.)
A more lenient option is to prohibit access to === and !== only to
copacetic functions. So, the above code would be invalid because
g is annotated as @pure, therefore required to be copacetic.
This would transitively deny access to === and !== as long as no
tamed objects are incorrectly marked as being copacetic.
(The fact that some code has access to === and !== precludes the
optimizations I referred to in my previous post, but those optimizations
would not be available in an ECMAScript implementation anyway.)
@pure and @functional functions would still be able to use egal,
if its implementation were "deemed" copacetic (exempted from the
restriction on === and !==).
From now on, I'll rename copacetic to deep-frozen, since it is close
enough to the E concept that I don't think there will be any confusion.
(Bye bye, copacetic. We'll miss you.)
I'll also rename the @functional annotation to @deepFrozen.
I'll write out the differences between the auditing in E, Joe-E, and
my proposal for Cajita in a separate post. In the meantime, here is most
of the run-time support that could be used to implement deep-frozen
auditing in Cajita:
// Using <http://wiki.ecmascript.org/doku.php?id=strawman:weak_references>.
function makeWeakTable() { return new EphemeronTable(true); }
/**
* An auditDeepFrozen entry has the value:
* false or a sour pumpkin, if the key is known not to be deepFrozen
* true or a tasty pumpkin, if the key is known to be deepFrozen.
*
* The objects traversed in a given checkDeepFrozen run are set
* to a unique pumpkin object, which is used to detect loops.
* A "tasty pumpkin" is an object with a 'tasty' field set to
* true, indicating that its run succeeded. A "sour pumpkin" has
* its 'tasty' field set to false. If a run fails, the entries
* pointing to its pumpkin (which has no 'tasty' field) are left
* in the table but are ignored, i.e. treated as equivalent to a
* missing entry, on subsequent runs.
*/
/*const*/ var auditDeepFrozen = makeWeakTable();
/*const*/ var tastyPumpkin = Object.freeze({ tasty: true });
/*const*/ var sourPumpkin = Object.freeze({ tasty: false });
/**
* A functionMarks entry has the value:
* 'deepFrozen', if marked as @deepFrozen
* 'pure', if marked as @pure.
*/
/*const*/ var functionMarks = makeWeakTable();
/**
* Check that obj satisfies the conditions to be deepFrozen, and throw
* AuditError if not. Memoize any results for non-primitive objects in
* the auditDeepFrozen table.
*/
function checkDeepFrozen(obj) {
if (isPrimitive(obj)) return;
/*const*/ var pumpkin = {};
if (isDeepFrozen(obj, pumpkin)) {
// Mark all visited objects that participated in a loop as deepFrozen.
pumpkin.tasty = true;
Object.freeze(pumpkin);
return;
}
Object.freeze(pumpkin);
throw new AuditError();
}
/**
* Return true if obj is definitely deepFrozen, false if it definitely
* isn't, and pumpkin if we encountered a loop.
*/
function isDeepFrozen(obj, pumpkin) {
var t = auditDeepFrozen.get(obj);
if (t) {
/*const*/ var tasty = t.tasty;
if (tasty !== undefined) return tasty;
if (t === pumpkin) return t;
}
test: {
// Function instances must be marked in order to be deepFrozen.
if (typeof obj === 'function' && !functionMarks.get(obj))
break test;
// Mark that we are checking obj, to avoid infinite loops.
// Progress depends on single-threadedness; multiple threads
// could livelock by treading on each other's pumpkins.
auditDeepFrozen.put(obj, pumpkin);
t = isDeepFrozen(Object.getPrototypeOf(obj), pumpkin);
var loopy = t === pumpkin;
if (!t || !Object.isFrozen(obj)) break test;
/*const*/ var ownprops = Object.getOwnPropertyNames(obj);
for (var i = 0; i < ownprops.length; i++) {
/*const*/ var prop = ownprops[i];
/*const*/ var desc = Object.getOwnPropertyDescriptor(obj, prop);
if ('value' in desc) {
t = isDeepFrozen(desc.value, pumpkin);
loopy = loopy || t === pumpkin;
if (!t) break test;
} else {
t = isDeepFrozen(desc.get, pumpkin);
loopy = loopy || t === pumpkin;
// It would be sufficient to check that the setter is deepFrozen,
// but a setter that doesn't actually set anything is misleading.
if (!t || desc.set !== null) break test;
}
}
// If the traversal from this object did not hit a loop, then we can
// memoize it as deepFrozen even if the overall checkDeepFrozen run
// fails.
if (loopy) return pumpkin;
auditDeepFrozen.put(obj, tastyPumpkin);
return true;
}
// break test:
auditDeepFrozen.put(obj, sourPumpkin);
return false;
}
/**
* Check that a chain of property accesses starting from obj, and
* using property names given by subsequent arguments, is guaranteed
* to give a constant result. No getters are invoked during the check.
*
* This can be used to relax the condition that all accesses to captured
* variables of a deepFrozen function are deepFrozen. For instance,
* if a @deepFrozen function uses foo only by accessing foo.bar.baz,
* then rather than requiring foo to be deepFrozen, we can require
* just that foo.bar.baz gives a constant result.
* I.e. we verify that foo is statically const, and generate a call to
* checkConstant(foo, 'bar', 'baz') for each scope in which foo
* might be different.
*/
function checkConstant(obj /*, ...*/) {
var i = 1;
test: while (true) {
if (i >= arguments.length) return;
/*const*/ var t = auditDeepFrozen.get(obj);
if (t && t.tasty) return; // optimization
/*const*/ var prop = arguments[i];
var desc;
try {
desc = Object.getOwnPropertyDescriptor(obj, prop);
} catch (e) {
// getOwnPropertyDescriptor *should* only throw if Type(obj) is not
// Object. However the ES5 spec doesn't explicitly say that
// [[GetOwnProperty]] for a host object can't throw (even though
// that would break stuff).
if (isPrimitive(obj)) return;
break test;
}
if (desc === undefined) {
// If prop doesn't exist as an own-property, then the object must be
// non-Extensible, and we continue using the object's prototype.
// If the property doesn't exist in the prototype chain and all
// objects in the chain are non-Extensible, then obj will eventually
// be null, which is allowed by the primitive case above.
if (Object.isExtensible(obj)) break test;
obj = Object.getPrototypeOf(obj);
continue;
}
// The property must be non-Configurable. If it is a data property,
// it must also be non-Writable, and we continue checking that
// subsequent property accesses on the value are constant. If it
// is an accessor property, we conservatively require its getter
// to be deepFrozen (note that the getter may be null).
if (desc.configurable !== false) break test;
if ('value' in desc) {
if (desc.writable !== false) break test;
obj = desc.value;
i++; // next access
continue;
} else {
checkDeepFrozen(desc.get);
// Since the getter is deepFrozen, there is no need to check
// subsequent properties in the chain (which is lucky, because
// we can't).
return;
}
}
// break test:
auditDeepFrozen.put(obj, sourPumpkin);
throw new AuditError();
}
function isPrimitive(obj) {
if (obj == null) return true; // null and undefined
/*const*/ var t = typeof obj;
return t === 'number' || t === 'string' || t === 'boolean';
}
/**
* This would be called by the rewritten code that instantiates an
* annotated function.
*/
function markFunc(mark, obj) {
if (typeof obj !== 'function' || functionMarks.get(obj) !== undefined)
throw new TypeError();
Object.freeze(obj);
functionMarks.put(obj, mark);
checkDeepFrozen(obj);
}
--
David-Sarah Hopwood ⚥ http://davidsarah.livejournal.com
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 292 bytes
Desc: OpenPGP digital signature
Url : http://www.eros-os.org/pipermail/e-lang/attachments/20091211/089e6a9d/attachment.bin
More information about the e-lang
mailing list