KEY FORMAL SYSTEM IDEAS ======================= HOW TO APPLY A TYPING OR EVALUATION RULE ---------------------------------------- First, pick a judgment that matches what you want to do - typing or evaluation. Then, pick a rule that matches the outermost structure of the thing you have. For example, if you are evaluating, you have a term, and so the outermost part of it must be one of the 5 kinds of terms in Featherweight Java. In the following term: new Pair(new A(), new B()).fst the outermost part is a field access of the form t.fst, where t is the subterm "new Pair(new A(), new B())" So you want to find one of the rules for field terms. There are two rules that have a field term in the conclusion. They are: fields(C) = C.. f.. ----------------------- E-ProjNew (new C(v..)).f_i -> v_i t_0 -> t_0' --------------- E-Field t_0.f -> t_0'.f Note: in the ASCII version of these notes I'm use C.. to mean "a sequence of zero or more Cs." The Featherweight Java (FJ) paper uses an overbar for this purpose. We can try both of them. Let's try E-Field first. Since the term we have is the thing we start evaluating, it goes to the left of the arrow. We want to match the term we have with the term t_0.f on the left of the arrow in the conclusion of the rule. In the rule, t_0 and f are what we call meta-variables: they are variables that stand for pieces of abstract syntax in a program. In this case, we match up t_0 with new Pair(new A(), new B()) and f with fst. We substitute t_0 and f with these things wherever they appear in the E-Field rule; this is called "instantiating" the E-Field rule. This gives us: new Pair(new A(), new B()) -> t_0' ------------------------------------------ E-Field new Pair(new A(), new B()).fst -> t_0'.fst Now, because E-Field has a premise, we need to find a rule that matches the premise. However, that's not going to work out. The only rule that has a "new expression" on the outside is this one: t_i -> t_i' --------------------------------------------- E-New-Arg new C(v.., t_i, t..) -> new C(v.., t_i', t..) This rule itself has a premise that requires one of the arguments to the constructor to evaluate. But in our expression, new Pair(new A(), new B()), all the arguments to the constructor are themselves fully evaluated--they are new statements with no arguments, which are values in FJ. So the whole expression new Pair(new A(), new B()) is a value, too. Since we can't find any rule that works for the premise of E-Field, that means we can't apply E-Field. Let's try the other rule, E-ProjNew. This rule does a field read. This time, the conclusion of the rule says we need to have an expression of the form "(new C(v..)).f_i" This works if we substitute the metavariable C with Pair, the argument sequence v.. with "new A(), new B()" and field f_i with fst. We get: fields(Pair) = C.. f.. --------------------------------------- E-ProjNew (new Pair(new A(), new B())).fst -> v_i Now, we have to figure out what C.. and f.. are. We find a rule for the premise. In this case, it's the field lookup rules, which look like this: CT(C) = class C extends D {C.. f..; K M..} fields(D) = D.. g.. ------------------------------------------------------------------- field-lookup fields(C) = D.. g.., C.. f.. ------------------ field-object fields(Object) = * We're looking to match fields(Pair) = C.. f.. Since Pair is not Object, we use the field-lookup rule, with C substituted with Pair. We look Pair up in the class table CT to get it's definition (the class table just holds all the classes in the FJ source). So we get: CT(Pair) = class Pair extends Object {Object fst; Object snd; K M..} fields(Object) = D.. g.. -------------------------------------------------------------------------------------------------- field-lookup fields(Pair) = D.. g.., Object fst, Object snd Here I've left out the constructor K and methods M from class Pair--it would make the rule too long to write out if I included them, but technically they're there. Now we aren't done applying field-lookup, because it has two premises. One is just a lookup in the class table; we've done that, and there's no rule to apply. The second looks up the fields of Object. We need to derive this premise in order to figure out what the fields D.. g.. are that we get from class Object (I bet you can guess what the answer is!) We use the field-object rule for this: ------------------ field-object fields(Object) = * and we discover that D.. g.. in the instantiation of the field-lookup rule is just the empty list. Thus the entire set of fields we find in Pair is just "Object fst, Object snd" as you would expect. This completes our instantiation of the field-lookup rule: ------------------ field-object CT(Pair) = class Pair extends Object {Object fst; Object snd; K M..} fields(Object) = * -------------------------------------------------------------------------------------------- field-lookup fields(Pair) = *, Object fst, Object snd Notice that I've shown field-lookup with the application of field-object on top of the appropriate premise. This connects the conclusion of field-object with the premise of field-lookup. Now that field-lookup tells us which fields are in Pair, we can finish our instantiation of the E-ProjNew rule: ------------------ field-object CT(Pair) = class Pair extends Object {Object fst; Object snd; K M..} fields(Object) = * -------------------------------------------------------------------------------------------- field-lookup fields(Pair) = *, Object fst, Object snd ---------------------------------------- E-ProjNew (new Pair(new A(), new B())).fst -> v_i What is v_i? Well, the i is supposed to be the same in the f_i and the v_i from the original E-ProjNew rule. The f_i turned out to be first, which is the first of the two fields, i.e. i=1 (if we start counting from 1). That means we are looking for v_1, i.e. the first value that's an argument to Pair. That means new A(). So we plug in "new A()" for v_i and we get: ------------------ field-object CT(Pair) = class Pair extends Object {Object fst; Object snd; K M..} fields(Object) = * -------------------------------------------------------------------------------------------- field-lookup fields(Pair) = *, Object fst, Object snd ------------------------------------------- E-ProjNew (new Pair(new A(), new B())).fst -> new A() We call this a derivation tree, because it shows how we derive the conclusion that (new Pair(new A(), new B())).fst steps to new A(), all the way from first principles. The tree ends with the thing we are deriving at the root; each rule is a node in the tree, with branches above for each premise in the rule (in the example, there's only one rule that branches, field-lookup, and one of the premises is just looking up the class so doesn't have any rule applications above it). The leaves of the tree are rules like field-object that don't have any premises, or have trivial premises like looking something up in a map. NESTED EVALUATION AND CONGRUENCE RULES -------------------------------------- What if we have an expression like this one: (new Pair(new Pair(new A(), new B())), new A()).fst.snd In order to run this, we need to first get the "fst" field of the outer pair, then get the "snd" field of the inner pair. This will be done in two steps, one for each field read. Let's derive the first step. Last time, the E-ProjNew rule worked well. But if we try it now, we can see that the conclusion doesn't fit the expression we have. E-ProjNew's conclusion is "(new C(v..)).f_i -> v_i" So we need an expression of the form (new C(v..)).f_i. But remember, the rule has to match the outermost expression. Our expression is new Pair(...).fst.snd. We can match f_i to snd, but we are reading field snd on "new Pair(...).fst" and that's not a new statement--it's another field read. We have to do the inner field read first and get a new statement (representing an object value) before we can do the outer field read. So rule E-ProjNew can't be applied because the conclusion doesn't match--and that makes sense intuitively because we have to get an object before we can read its field. Let's try rule E-Field, which is once again: t_0 -> t_0' --------------- E-Field t_0.f -> t_0'.f This is what is called a congruence rule. It doesn't actually do something useful itself, like reading a field or calling a method. Instead, it tells us that if we have a field read t_0.f, but the term t_0 isn't a value yet, then we can go inside t_0 and evaluate it to some new term t_0'. Then we get a new program t_0'.f which has executed one step further. Hopefully t_0' is now a value so we can actually do the field read! But if it's not, we can simply continue to apply E-Field in future steps until we do get that value. Let's match up metavariables in the conclusion. Here t_0 will be our nested term, "(new Pair(new Pair(new A(), new B())), new A()).fst" And f will be snd. So we can partially instantiate the rule to get: (new Pair(new Pair(new A(), new B())), new A()).fst -> t_0' ------------------------------------------------------------------- E-Field (new Pair(new Pair(new A(), new B())), new A()).fst.snd -> t_0'.snd Now, to figure out how this term evaluates, we need to figure out what t_0' is. So, we need to apply a rule to the premise. This time E-ProjNew will work. We can apply it as follows: fields(Pair) = C.. f.. ----------------------------------------------------------- E-ProjNew (new Pair(new Pair(new A(), new B())), new A()).fst -> v_i ------------------------------------------------------------------- E-Field (new Pair(new Pair(new A(), new B())), new A()).fst.snd -> v_i.snd Note that the right hand side of the conclusion of E-ProjNew says we get the answer v_i, so I've substituted t_0' with v_i in the derivation above. Next, E-ProjNew has a premise, and we can fill it in exactly the same way we did before: ------------------ field-object CT(Pair) = class Pair extends Object {Object fst; Object snd; K M..} fields(Object) = * -------------------------------------------------------------------------------------------- field-lookup fields(Pair) = *, Object fst, Object snd ----------------------------------------------------------- E-ProjNew (new Pair(new Pair(new A(), new B())), new A()).fst -> v_i ------------------------------------------------------------------- E-Field (new Pair(new Pair(new A(), new B())), new A()).fst.snd -> v_i.snd Now, we know that since f_i is fst, i must be 1 (the first field of Pair), and so v_i is the first argument to the outer new statement--i.e. v_i = new Pair(new A(), new B()). Substituting this in we get our final derivation: ------------------ field-object CT(Pair) = class Pair extends Object {Object fst; Object snd; K M..} fields(Object) = * -------------------------------------------------------------------------------------------- field-lookup fields(Pair) = *, Object fst, Object snd --------------------------------------------------------------------------------- E-ProjNew (new Pair(new Pair(new A(), new B())), new A()).fst -> new Pair(new A(), new B()) ----------------------------------------------------------------------------------------- E-Field (new Pair(new Pair(new A(), new B())), new A()).fst.snd -> new Pair(new A(), new B()).snd SIMULATING MULTIPLE EVALUATION STEPS ------------------------------------ We just showed a derivation tree that explains how we take a first step: (new Pair(new Pair(new A(), new B())), new A()).fst.snd -> new Pair(new A(), new B()).snd Now, what happens next? We have another field read to execute. This is more or less like the first derivation we constructed; we'll get: new Pair(new A(), new B()).snd -> new B() Once you've built a couple of derivation trees like the ones above, you can probably figure out how to simulate execution according to the rules without building the whole thing explicitly. So I haven't drawn the tree; it's identical to the first one I built, except we chose the snd field instead of the fst field. We can write a sequence of steps like this: (new Pair(new Pair(new A(), new B())), new A()).fst.snd -> new Pair(new A(), new B()).snd -> new B() Technically each step is a whole derivation. But once you get how the derivations work, it's easier to understand the step-by-step execution without them, just looking at each step instead. TYPING DERIVATIONS ------------------ Using the typing rules discussed in class, we can also construct typing derivations for expressions and other constructs, such as methods. For example, let's typecheck the body of the Pair.setfst() method described in class. The method typing rule looks like this: x.. : C.., this : C |- t_0 : E_0 E_0 <: C_0 CT(C) = class C extends D {...} override(m, D, C..->C_0) ------------------------------------------------------------------------------------------------------------- C_0 m(C.. x..) { return t_0; } OK in C I'm going to focus mainly on the first premise: "x.. : C.., this : C |- t_0 : E_0" What this premise tells us is that we need to build up a typing context Γ that assigns types to all the arguments of the method and also to the receiver object this. In that context, the method body term t_0 has type E_0. The second premise will check that the type we get from the body, E_0, is a subtype of the return type C_0 that was declared. That is, if we return a Pair but the return type was Object that's OK because Pair is a subtype of Object (written Pair <: Object). The third premise just looks up C in the class table, discovering that C inherits from some other class D. The fourth premise checks that if D has method m, then the signatures match. Let's look at that first premise. Our method definition looks like this: Pair setfst(Object newfst) { return new Pair(newfst, this.snd); } So matching this up with the conclusion of the rule, we have C_0 = Pair, C.. x.. = Object newfst, t_0 = new Pair(newfst, this.snd), and C = Pair. We substitute these into the first premise of the rule, and our typechecking judgment is: newfst:Object, this:Pair |- new Pair(newfst, this.snd) : E_0 Now we need to find a typing rule that matches our top-level term form, which is a new expression. There's only one: fields(C) = D.. f.. Γ |- t.. : C.. C.. <: D.. --------------------------------------------------- T-New Γ |- new C(t..) : C We match up the term in the conclusion of the rule with "new Pair(newfst, this.snd)" to get C=Pair and t.. = "newfst, this.snd" We also have Γ = "newfst:Object, this:Pair" So we can partially instantiate T-New as follows: fields(Pair) = D.. f.. newfst:Object, this:Pair |- newfst, this.snd : C.. C.. <: D.. ------------------------------------------------------------------------------------------ T-New newfst:Object, this:Pair |- new Pair(newfst, this.snd) : Pair This will get too wide to show the entire derivation tree easily in a text file, so I'll cover one premise at a time. The first premise is just like the one we used in E-ProjNew. We look up the fields of Pair and get "Object fst, Object snd" So now we have: (details not shown) ------------------------------------- field-lookup fields(Pair) = Object fst, Object snd newfst:Object, this:Pair |- newfst, this.snd : C.. C.. <: Object, Object -------------------------------------------------------------------------------------------------------------------- T-New newfst:Object, this:Pair |- new Pair(newfst, this.snd) : Pair The second premise is really an abbreviation for two premises, because we have to check each constructor argument. The two premises are: newfst:Object, this:Pair |- newfst : C_1 newfst:Object, this:Pair |- this.snd : C_2 The expression in the first of these is a variable, so we'll use the variable rule: x:C in Γ ---------- T-Var Γ |- x : C We need to match things up. Here, x will be newfst, Γ will be newfst:Object, this:Pair. In the premise, we need to find newfst:C in Γ. Looking at the definition of Γ we discover newfst:Object, so C=Object. That means we get a fully instantiated rule: x:Object in newfst:Object, this:Pair ------------------------------------------- T-Var newfst:Object, this:Pair |- newfst : Object We can likewise get a derivation for the second premise. This one is a couple of levels deep, as we have to use T-Var to figure out the type of this before we can find the type of this.snd: this:Pair in newfst:Object, this:Pair (details not shown--same as before) ------------------------------------- T-Var ------------------------------------- field-lookup newfst:Object, this:Pair |- this:Pair fields(Pair) = Object fst, Object snd ----------------------------------------------------------------------------------- T-Field newfst:Object, this:Pair |- this.snd : Object This derivation nicely illustrates how a typing derivation can be a branching tree--when the T-Field rule is applied, we branch into two subderivations, one of which comes from T-Var and one of which comes from field-lookup.