[E-Lang] ERTP-aware MintMaker

Mark S. Miller markm@caplet.com
Thu, 15 Feb 2001 18:32:11 -0800


At 04:23 PM Wednesday 2/14/01, steve jenson wrote:
>According to "E in a Walnut", and MarkM himself, there is going to be
>a new keyword in the language called "describe" which will take care
>of this sort of self-referencing issue. Therefore my code, while 
>stylistically ugly, is the cleanest to fix upon completion of that
>function to the language.
>
>At least, that was my take on it. And I'll certainly concede to you having
>picked a cleaner way of doing it if I hadn't known of forthcoming changes
>to the language.

Actually, the proposed keyword is "declare", although I think either Dean or 
MarcS suggested "forward" and Dean suggested "promise".  Plausible also is 
"introduce".  Now's as good a time as any to publicly propose this.  For the 
moment I'll say "declare", but let's postpone arguing about the keyword until 
we figure out whether it's a good idea (or invent a variant that is a good 
idea).

Let's also ignore the top-level mutable interactive scope issues for now, 
though we must revisit them before we can consider the proposal to be baked.

Generally I like to define the semantics of E simply by defining the 
semantics of Kernel-E, and then defining the syntactic expansion of E to 
Kernel-E.  This approach is often adequate.  But sometimes it's good to also 
try to define semantics directly at the user level as well, to see if the 
rules are regular and predictable enough to be used reliably, without having 
to mentally expand to Kernel-E.  The left-to-right scoping rules, which the 
"declare" proposal extends, was our main example of this, so we continue the 
tradition of defining the scoping rules first at the user level language.


                       Informal User-Level Semantics


"declare foo" introduces "foo" into a scope without defining it.  It's a 
forward declaration.

A forward-declared variable must be defined in the same scope, and must be 
defined as final.

If you use a variable before it's defined, you get a promise for what it 
will be defined as.

The variable definition resolves these promises to the variable's value.

So our original example:

>     var delayedMint := null
>     define issuer {
>         to makePurse() :any {          
>            delayedMint makePurse(0)
>         }
>         ...
>      }
>     define mint { ... }
>     delayedMint := mint

could instead be:

>     declare mint
>     define issuer {
>         to makePurse() :any {          
>            mint makePurse(0)
>         }
>         ...
>      }
>     define mint { ... }

At the time issuer is defined, its "mint" is a promise,  For typical 
patterns (including our MintMaker), by the time the issuer might try to use 
the variable, it will simply be the mint (ie, resolved to the mint).


                          Semi-Formal User-Level Semantics


"declare foo" introduces the name "foo" into a scope-box 
http://www.erights.org/elang/blocks/ifExpr.html .  We say this "foo" is an 
"introducing" occurrence of "foo".  There may not be more than one introducing
occurrence of an identifier directly in any one scope box.  (Another introducing
occurrence of "foo" may still occur in a nested scope box.  Hence "directly" 
above.)

"declare foo" is an expression, but for now let's restrict it to expression
contexts for which it's value is never needed, such as a non-last expression 
in a sequence-expression (a sequence of expressions separated by ";" or "\n").

The name "foo" is "in scope" from an introducing occurrence, left-to-right, 
until the end of the scope box, including within nested scope boxes, except 
where shadowed by introducing occurrences in those nested scope boxes, in 
the usual manner.  A use occurrence of "foo" must occur only where "foo" is 
in scope, and is bound to the corresponding introducing occurrence.

A "defining" occurrence of "foo" is exactly what it is now -- "foo" used as 
a DefinerPattern, possibly preceded by "var", and followed by a guard 
expression (eg, "foo :guardExpr").  There may not be more than one defining 
occurrence of an identifier directly contained in any one scope box.

If a scope box does not directly contain a "declare "foo", then everything 
works as it does now, and is therefore fully upwards compatible.  ("declare" 
is already a reserved keyword.)  In particular, if such a box does directly 
contain a defining occurrence of "foo", then this is also an introducing 
occurrence, and, as always, "foo" is "in scope" from the introducing 
occurrence, left-to-right, until the end of the scope box.


If a scope box directly contains a "declare "foo", then a defining 
occurrence of "foo" must occur left-to-right between the declaring 
occurrence and the end of the scope box, ie, within the area where foo is in 
scope.  The scope box must directly contain the defining occurrence.

Among text directly contained within a scope box (equivalently, the leaves 
of the tree of expressions and patterns not including those in nested scope 
boxes), there is both the left-to-right order (LRO) as used above, as well 
as Static Temporal Order (STO).  Static because it doesn't depend on runtime 
conditions, and so is easily statically apparent.  Note that all flow 
control boundaries are also scope-box boundaries, which is why STO is 
possible so long as we restrict our view to direct containment.  Mostly, STO 
follows LRO, which is why defining a variable textually before (to the left 
of) using it typically implies that the variable will actually be 
initialized temporally before it's used.

E has two exceptions to this rule: The DefineExpr ("define pattern := expr") 
and the DefinerPattern ("[var] name :guardExpr").  In both cases, the right 
branch happens first, and then the left branch.  For these LRO and STO are 
different.

A use occurrence of a name may then only occur LRO-after an introducing 
occurrence.  In STO, it may occur before of after the defining occurrence.  

If a use occurrence occurs STO-after the defining occurrence, then it is a 
"direct-use", and refers to an already initialized variable.  As an 
expression, it evaluates to the value of that variable.  

If a use occurrence occurs STO-before the defining occurrence, then it is a 
"promised-use", and may only appear as a name expression (ie, no 
"foo := ..." or "&foo").  A promised-use expression evaluates to a promise 
for the value of the variable.  A variable that has a promised-use may only 
be declared non-var, or equivalently "var foo :final" or 
"var foo :final(valueGuardExpr)".  In other words, only immutable variables 
can be promised.  STO-after the defining occurrence, these promises are 
resolved to the value of the variable.  So if the variable is initialized 
with a NEAR reference, STO-after the definition the promised values are also 
NEAR.

These spec accounts both for the existing to use "define" for circular 
definitions http://www.erights.org/elang/blocks/defVar.html#scopeConsistency 
, as well as the proposed meaning of "declare".


              Expansion to Kernel


This proposal involves no change to Kernel-E, and only an upward compatible 
change to the expansion.  We distinguish two cases: variables for which 
there is a promised-use occurrence, and those for which there is not.  For 
the latter, there is no change to the expansion.  For promised-uses, we 
describe the expansion as two phases.  We believe it can be painlessly 
implemented in one.


      Phase 1:


There are two constructs for which STO != LRO, the DefineExpr and the 
DefinerPattern.  If a promised-use of a variable occurs on the right of one 
of these, for which the introducing use is on the left, then this variable 
use is STO-before its introduction.  Let's call this a cyclic variable use. 
To keep Kernel-E's semantics and implementation simple, Kernel-E rejects any 
programs with cyclic variable uses.  (Kernel-E already does this.  This 
terminology is simply a new way to describe it.)  How do the user-level 
forms of these constructs cope with this restriction?

* The user-level DefineExpr allows cyclic variable use: The expansion of 
these precedes the define with "declare"s of the cyclic variables, to be 
expanded out by Phase 2.  So

    define x := [1, x, 2]

expands to

    declare x; define x := [1, x, 2]

The "x" on the right of the "define" is still a promised-use, but no longer a cyclic-use

* The user-level DefinerPatterns rejects programs with cyclic-use.  There's 
no reason to go out of our way to accept cycles here.


      Phase 2:


The declare turns into an expression for making a promise-pair:

    declare foo

expands to

    define [foo__Promise, foo__Resolver] := PromiseMaker()

A promised-use occurrence (one that is STO-before the defining occurrence) 
refers to the promise facet of the promise pair:

    foo

expands to

    foo__Promise

The defining occurrence also resolves the promise:

    foo :integer

expands to

    foo :integer ? (foo__Resolver resolve(foo); true)

The "?" is read as "such that", meaning that the pattern as a whole succeeds 
at matching a specimen if the left hand pattern matches, and then the right 
hand expression evaluates to true.  Here we are using the such-that pattern 
not to add a test, but to cause an effect at the time we need to cause it.

and non-promised use occurrences are fine as is.



              Minor Fly in the Ointment


This expansion involved turning a definer pattern into a such-that pattern.  
This requires that the grammar be changed to accept a <pattern> (the more 
general production that includes the such-that pattern) everywhere it 
currently accepts <namer> (which corresponds to what I've been calling a 
definer pattern).  I just checked with yacc, and yacc is happy with this 
change.  So the only issue is whether we're happy with it.

The actual requirement is less stringent -- the grammar only has to accept 
patterns for those productions that are also in Kernel-E, in order for 
Kernel-E to remain a subset of E.  However, if we're going to make this 
change, we should do it for the whole language for consistency.  Yacc is 
happy with the change applied to the whole language.

I think this is a good change anyway, as the old grammar's non-uniformity 
wasn't well motivated, and the above case shows that it was less 
compositional than one would like.



                      A Call For Help

There are people on this list who are extraordinary at writing formal 
documentation that is simultaneously clear, unambiguous, readable, and 
non-redundant.  Above I demonstrate my lack of skill in this regard.  But I 
don't know what I'm doing wrong.  If you have any insights that may help, 
please let me know.  Thanks.  As a language designer, this is a skill I need 
to learn.



        Cheers,
        --MarkM