Auditors
Ka-Ping Yee (ping@zesty.ca)

I'm going to try to implement auditors for E.


Features wanted

  1. to audit object implementations
  2. to verify that an instance passes an audit
  3. to let guards adapt instances
  4. to let instances perform voluntary conformance
  5. generalized dispatch on brands

Proposed protocol

The following describes "Non-Gozarian Auditing Protocol 2" (NGAP2).

  1. Object implementations are audited by the auditor's method:
    audit(ast, env) -> boolean
    which gets called when the interpreter loads an object expression with a declared auditor.
  2. Verification of an audit is done by the function:
    audited(auditor, instance) -> boolean
    which is available in the universal scope; a guard can use it in its 'adapt' method if it wants to.
  3. Guards adapt instances through their method:
    adapt(instance, ejector) -> conforming-instance
    which gets called automatically whenever a value is passed into a formal parameter or slot with a declared guard. (This is exactly the current 'coerce' method; i explained previously why i prefer the name 'adapt'.)
  4. Objects can offer to voluntarily conform by providing a method:
    conform(guard) -> allegedly-conforming-instance
    It is up to the guard to decide whether, and when, to call 'conform' during 'adapt'. E provides a Miranda implementation for 'conform' that simply returns the object itself.
  5. Objects can offer to dispatch on brands by providing a method:
    getSecret(brand) -> sealed-box
    The semantics of the contents of the box are entirely up to the object and the brand. If no secret associated with the given brand is available, an exception is thrown. Since this kind of dispatch is done only at the request of other objects, the use of the name 'getSecret' is only a convention, though it may become more entrenched if E introduces shorthand syntax for brand dispatch. It may also be more entrenched if E provides a Miranda implementation for 'getSecret' that always throws a standard "unknown brand" exception.

The Gozarian property

Definition:

A conformance check is Gozarian if the object can tell whether the check occurred.

The getOptMeta protocol is strictly Gozarian; the new NGAP2 (non-Gozarian auditing protocol 2) allows for both Gozarian and non-Gozarian guards.


Naming conventions for auditors and guards

Suppose:

This reduces the number of commonly-seen names per type to two; it doesn't seem that the ability to audit is so special that it needs to be separated from the ability to guard.

Here's an example. The following is what a simple interface and implementation might look like in E 0.8.16, based on my understanding of the release notes on "implements":

    interface Operator guards OperatorAuditor {
        to op(a : integer, b : integer) : integer
    }

    def AdderMaker() : Operator {
        def adder implements OperatorAuditor {
            to op(a : integer, b : integer) : integer { a + b }
        }
    }

    def opuser(op : Operator, a : integer, b : integer) {
        op op(a, b)
    }

Assuming that we standardize on 'OperatorAuditor' so the author of the implementation doesn't have to read the interface in order to find out whether to use 'OperatorAuditor' or 'OperatorStamp', there are still five places where someone has to correctly choose whether to say 'Operator' or 'OperatorAuditor'.

With my hypothetical changes, this becomes the following, which i think is easier to read and understand:

    interface operator {
        to op(a : integer, b : integer) : integer
    }

    def Adder() {
        def self : operator {
            to op(a : integer, b : integer) : integer { a + b }
        }
    }

    def opuser(op : operator, a : integer, b : integer) {
        op op(a, b)
    }

We can tell from the position of ":" in an object expression that it requests auditing; so we don't need to introduce another operator like "::" or "implements" -- we can just use ":".

Also, it's easy to see that Adder() returns something conforming to the 'operator' guard, so we don't have to repeat ": operator" after Adder() as well.


Discriminating or non-discriminating? Closely held or not?

I see two main attributes to consider:

  1. The auditor is closely held or not closely held. (The decision to stamp is always up to the auditor; see #2 below for how it makes the decision.)
  2. The auditor is discriminating or non-discriminating. (By this i mean whether the auditor is relied upon to ascertain adherence to a particular property before giving its approval, or whether it will freely approve anything.)
Dean Tribble wrote:
> In the protocol you described, the guard authority includes the stamp
> authority, so the stamp authority cannot be separately closely held.

In this taxonomy, i don't need to separate the concept of the stamp from the auditor; attribute #2 is analogous to "stamp closely held" in your description. If the auditor is discriminating, there is no concern; if the auditor is non-discriminating, then we will usually want the auditor to be closely held. As long as we distinguish between the auditor and the guard, i don't think it's necessary to also separate out the stamp.

I think this is a perspective that retains enough distinctions to be useful, while remaining fairly simple on the surface, and offering the simple single-object conceptual model for many forseeable use cases. The simplicity is good because it makes writing and using auditors easier to get right.

The only thing i can see that you do lose is the ability to directly provide multiple auditors that share the same stamp. I would suggest that this is probably a useful restriction. If it's really necessary to accept verification from one of a set of acceptable auditors, this can be implemented in a smarter guard that checks more than one stamp.

Since a hypothetical multiple-auditor problem can be solved either by (a) putting complexity in the auditors, in a world where auditors and stamps are separate, or by (b) putting complexity in the guard, my design intuition tells me that it's better to restrict the complexity to one place by eliminating option (a) than to leave both options open (a.k.a. There's Only One Way To Do It). Option (b) is better than (a) because (a) adds a little complexity to all auditors, whereas (b) adds a little complexity just to the special cases where you need to accept multiple auditors (and i don't even know how often that will turn up, if it ever does).

Here are some examples to illustrate the taxonomy:

auditor is discriminating, closely held

    # public: guard
    # private: auditor

    def auditor {
        to audit(ast, env) : boolean {
            if (...inspect...) { true }
        }
    }

    def guard {
        to adapt(object) : any {
            if (audited(auditor, object)) { object }
            else { ...problem... }
        }
    }

    def object : auditor {
        ...
    }

    def user(object : guard) {
        ...
    }


auditor is non-discriminating, closely held
(the "interface declaration" or "code signing" use case: needs two objects)

    # public: guard
    # private: auditor

    def auditor {
        to audit(ast, env) : boolean { true }
    }

    def guard {
        to adapt(object) : any {
            if (audited(auditor, object)) { object }
            else { ...problem... }
        }
    }

    def object : auditor {
        ...
    }

    def user(object : guard) {
        ...
    }


auditor is discriminating, not closely held
(the "semantic verification" use case: needs just one object)

    # public: auditor

    def auditor {
        to audit(ast, env) : boolean {
            if (...inspect...) { true }
        }
        to adapt(object) : any {
            if (audited(auditor, object)) { object }
            else { ...problem... }
        }
    }

    def object : auditor {
        ...
    }

    def user(object : auditor) {
        ...
    }


auditor is non-discriminating, not closely held

    # public: auditor

    def auditor {
        to audit(ast, env) : boolean { true }
        to adapt(object) : any {
            if (audited(auditor, object)) { object }
            else { ...problem... }
        }
    }

    def object : auditor {
        ...
    }

    def user(object : auditor) {
        ...
    }


two auditors -- one discriminating, not closely held;
                one non-discriminating, closely held
(this requires three objects)

    # public: auditor1, guard
    # private: auditor2

    def auditor1 {
        to audit(ast, env) : boolean {
            if (...inspect...) { true }
        }
    }

    def auditor2 {
        to audit(ast, env) : boolean { true }
    }

    def guard {
        to adapt(object) : any {
            if (audited(auditor1, object) ||
                audited(auditor2, object)) { object }
            else { ...problem... }
        }
    }

    # non-privileged declaration
    def object : auditor1 {
        ...
    }

    # privileged declaration
    def object : auditor2 {
        ...
    }

    def user(object : guard) {
        ...
    }


for comparison, the last example implemented in a
hypothetical scheme with stamps separated from auditors
(this requires four objects)

    # public: auditor1, guard
    # private: stamp, auditor2

    def stamp := Stamp()

    def auditor1 {
        to audit(ast, env) {
            if (...inspect...) { stamp stamp(ast, env) }
        }
    }

    def auditor2 {
        to audit(ast, env) { stamp stamp(ast, env) }
    }

    def guard {
        to adapt(object) : any {
            if (stamp stamped(object)) { object }
            else { ...problem... }
        }
    }

    # non-privileged declaration
    def object : auditor1 {
        ...
    }

    # privileged declaration
    def object : auditor2 {
        ...
    }

    def user(object : guard) {
        ...
    }

Naming conventions

I am now going to use the word "stamp" as an abbreviation for "non-discriminating (i.e. rubber-stamping) auditor".

auditor is non-discriminating

    maker for rubber-stamping guard/auditor pairs: Stamp()

    maker name:    Point, Tree, etc.
    guard name:    point, tree, gpl, etc.
    auditor name:  pointStamp, treeStamp, gplStamp, etc.

    example:

        def [point, pointStamp] := Stamp()

        def Point(x : int, y : int) : point {
            def self : pointStamp {
                to getX : int { x }
                to getY : int { y }
            }
        }

auditor is discriminating

    auditor name:  frozen, confined, etc.

Why we register auditors per instance rather than per vtable

    def Brand() : any {
        def key { }
        def envtype := GuardStamp()
        def Envelope(contents) : any {
            def self : envtype {
                to open(k) : any {
                    if (k == key) {
                        contents
                    }
                }
            }
        }
        def sealer {
            to seal(contents) : any {
                Envelope(contents)
            }
        }
        def unsealer {
            to unseal(envelope : envtype) : any {
                envelope open(key)
            }
        }
        [sealer, unsealer]
    }

I have used "GuardStamp" above to signify a maker that produces a single object willing to serve as both a guard and a rubber stamp.

Passing the key to the envelope's "open" method was the fatal leak; the addition of the "envtype" guard, i believe, closes this leak.