After looking at evaluating statements,
we will look at methods and programs building on the definition of Prog and Method as defined in
earlier.
Moreover will assuming that method bodies use the version of Expr and Stmt from the
previous section
but extended with global contexts and method calls.
Global and Local Contexts.
Remember that with Olaf we have global contexts pointing to methods. Using the following encoding for method types:
data Method = METH (SnocList Ty) Ty
We need to update our existing code for expressions and statements from the previous section.
Our propositions for expressions and statements will have the following datatype type constructors:
data Expr : (gs : SnocList (String, Method))
-> (ls : SnocList (String, Ty))
-> (ty : Ty)
-> (ex : AST.Expr a)
-> Type
where
data Stmt : (he : HowEnds)
-> (gs : SnocList (String, Method))
-> (ls : SnocList (String, Types.Ty))
-> (ty : Types.Ty)
-> (ex : List (Stmt LEAF FileContext))
-> Type
where
and their proofs the following function signatures:
namespace Expr
export
check : (gs : SnocList (String, Method))
-> (ls : SnocList (String, Ty))
-> (ty : Ty)
-> (st : List (AST.Stmt LEAF a))
-> Dec Error
(he ** Stmt he gs ls ty st)
namespace Stmt
export
check : (gs : SnocList (String, Method))
-> (ls : SnocList (String, Ty))
-> (ty : Ty)
-> (ex : AST.Expr FileContext)
-> Dec Error
(Expr gs ls ty ex)
Rewrite the code from the previous section to support global typing contexts for methods.
Evaluating Method Calls
With our new statements and expressions, we need to incorporate evaluation of method calls. Specifically evaluation of the arguments.
We can extend the AST for expressions with the following concrete syntax,
that pairs a variable name and a list of expressions.
Call : (state : a)
-> (expr : String)
-> (args : SnocList (Expr a))
-> Expr a
First, we describe typing the arguments themselves. Rather than define arguments mutually we will use a parameterised datatype, enabling us to define the proposition for typing arguments before we define expressions.
Our datatype Args will be parameterised by a type signature to describe the type of expressions and indexed by:
- global and local contexts;
- two separate lists, one for the arguments types and the other for the arguments themselves;
Args will be a custom All quantifier ensuring each argument has the correct type.
data Args : (exprTy : (gs : SnocList (String, Method))
-> (ls : SnocList (String, Ty))
-> (ty : Ty)
-> (ex : AST.Expr a)
-> Type)
-> (gs : SnocList (String, Method))
-> (ls : SnocList (String, Ty))
-> (ts : SnocList Ty)
-> (as : SnocList (Expr a))
-> Type
where
Our datatype’s constructors will be the same as All:
Lin- a method with no arguments;
Lin : Args expr gs ls Lin Lin
(:<)- ensuring that the last argument has the correct type, and that the front arguments are well-typed too.
(:<) : (front : Args expr gs ls ts es)
-> (last : expr gs ls t e)
-> Args expr gs ls (ts :< t) (es :< e)
Using Args we can describe the typing of method calls similarly to how we did in
lecture 1.
We obtain the method type pointed to by the variable name,
and then ensure the arguments are well-typed.
Call : {tys : _}
-> (prfRef : Exists n (METH tys ty) gs)
-> (args : Args Expr gs ls tys as)
-> Expr gs ls ty (Call state n as)
We can then write monomoprhic functions for extracting intrinsically-typed arguments,
using All.
toTermArgs : Args Expr gs ls ts as
-> All (Expr (map Builtin.snd gs)
(map Builtin.snd ls))
ts
and for checking arguments:
checkArgs : (gs : SnocList (String, Method))
-> (ls : SnocList (String, Ty))
-> (ts : SnocList Ty)
-> (as : SnocList (Expr a))
-> Dec Error (Args Expr gs ls ts as)
Complete the implementations for toTermArgs and checkArgs.
For checkArgs,
your proof will need to take into account that there may be more types than arguments and vice-versa.
These will need to be mutually defined, or declared after the type signature for checking expressions.
Finally, we can then provide the proof for checking method calls. The proof requires that we:
- check if the variable is bound to a method, and then obtain the method type itself;
- we then call
checkArgs, using the method type details to obtain the type for each argument; - finally we check that the return type and the input type are the same;
check gs ls ty (Call fc str sx) with (isBound str gs)
check gs ls ty (Call fc str sx) | (Yes (METH tyAs tyR ** idx)) with (decEq ty tyR)
check gs ls ty (Call fc str sx) | (Yes (METH tyAs ty ** idx)) | (Yes Refl) with (checkArgs gs ls tyAs sx)
check gs ls ty (Call fc str sx) | (Yes (METH tyAs ty ** idx)) | (Yes Refl) | (Yes prf)
= Yes (Call idx prf)
check gs ls ty (Call fc str sx) | (Yes (METH tyAs ty ** idx)) | (Yes Refl) | (No emsg why)
= No (Stack fc emsg)
(\case (Call prfRef args) => case unique idx prfRef of
Refl => why args)
check gs ls ty (Call fc str sx) | (Yes (METH tyAs tyR ** idx)) | (No contra)
= No (Stack fc $ MismatchTy ty tyR)
(\case (Call prfRef args) => case unique prfRef idx of
Refl => contra Refl)
check gs ls ty (Call fc str sx) | (No contra)
= No (Stack fc $ NotBound str)
(\case (Call prfRef args) => contra (_ ** prfRef))
As with previous sections,
we need to use unique to help prove void.
Elaborating Methods
We now look at elaborating methods. We can encode the concrete syntax for methods as a pairing of a method prototype (the arguments as name type pairings) and a single block statement representing the method body.
public export
data Method : (state : Type) -> Type where
M : (state : a)
-> (args : SnocList (String, Types.Ty))
-> (ret : (a, Types.Ty))
-> (scope : Stmt BLOCK a)
-> Method a
Elaborating methods only requires that we have a global context.
data Method : (gs : SnocList (String, Method))
-> (me : AST.Method a)
-> Type
where
We only have a single constructor M that checks our block statement,
ensuring that the body is checked against:
- the current global context;
- a local stack populated by the arguments from the method type; and
- returns an expression of the method’s return type;
Finally,
we need to ensure that the method itself does return a value.
We do so by using decidable equality on the HowEnds result from checking statements.
M : forall he
. (prf : Stmt he gs args ty body)
-> (evi : he = RETURN)
-> Method gs (M fc args (fc',ty) (Block fc'' body))
We can extract intrinsically-typed methods by calling toTerm for statements on the method body.
toTerm : Method gs (M fc args (fc', ty) b)
-> Method (map Builtin.snd gs)
(METH (map Builtin.snd args) ty)
Complete the implementation for toTerm.
We can now look at checking methods.
check : (gs : SnocList (String, Method))
-> (me : AST.Method a)
-> Dec Error
(Method gs me)
Checking methods is a call to check for statements using
- the current global context;
- a local stack populated by the arguments from the method type; and
- returns an expression of the method’s return type;
We then use decEq to ensure that the statement does return some thing.
check gs (M fc args (fc', ty) (Block fc'' xs)) with (check gs args ty xs)
check gs (M fc args (fc', ty) (Block fc'' xs)) | (Yes (he ** ss)) with (decEq he RETURN)
check gs (M fc args (fc', ty) (Block fc'' xs)) | (Yes (RETURN ** ss)) | (Yes Refl)
= Yes (M ss Refl)
check gs (M fc args (fc', ty) (Block fc'' xs)) | (Yes (he ** ss)) | (No contra)
= No (Stack fc $ Stack fc' $ E "Internal Error")
(\case (M prf Refl) => case unique ss prf of
Refl => contra Refl)
check gs (M fc args (fc', ty) (Block fc'' xs)) | (No emsg f)
= No (Stack fc $ Stack fc'' $ emsg )
(\case (M prf Refl) => f (RETURN ** prf))
When proving void for methods we need to use a proof that statements under the same pair of contexts, same type and statements ends the same way.
unique : Stmt a gs ls ty ss
-> Stmt b gs ls ty ss -> a === b
Complete the definition of unique for statements.
Elaborating Programs
Our final tasks is to finish the elaboration of programs themselves.
The concrete syntax for programs will be either a method (i.e. the main method) or a method declaration that binds a method to a name.
data Prog : (state : Type) -> Type where
Main : (state : a)
-> (main : AST.Method a)
-> Prog a
Decl : (state : a)
-> (ident : String)
-> (meth : AST.Method a)
-> (rest : Prog a)
-> Prog a
Our algorithm for checking programs borrows the same ideas for checking let-binders (see expressions and statements).
Declarations extend the global contexts with a name method type paring, remembering to make the argument’s types nameless when extending the context.
For Olaf/Ola,
the main method must have no arguments and return UNIT.
data Prog : (gs : SnocList (String, Method))
-> (pr : AST.Prog a)
-> Type
where
Main : Method gs (M fc Lin (fc', UNIT) body)
-> Prog gs (Main fc'' (M fc Lin (fc', UNIT) body))
Decl : Method gs (M fc args (fc', ty) m)
-> Prog (gs :< (s,METH (map Builtin.snd args) ty)) p
-> Prog gs (Decl fc'' s (M fc args (fc',ty) m) p)
Extracting intrinsically-typed programs is typed as:
toTerm : Prog gs p
-> Prog (map Builtin.snd gs)
Complete the implementation for toTerm.
Checking programs requires a global context.
check : (gs : SnocList (String, Method))
-> (p : Prog a)
-> Dec Error (Prog gs p)
Checking the ‘main’ method requires that we look at the methods arguments,
ensures that there are not arguments and the return type is UNIT.
With the correct main method type,
we can than check the main method itself.
check gs (Main fc (M fc' args (fc'',ret) x)) with (decEq args Lin)
check gs (Main fc (M fc' [<] (fc'',ret) x)) | (Yes Refl) with (decEq ret UNIT)
check gs (Main fc (M fc' [<] (fc'',UNIT) x)) | (Yes Refl) | (Yes Refl) with (check gs (M fc' [<] (fc'',UNIT) x))
check gs (Main fc (M fc' [<] (fc'',UNIT) (Block fc''' body))) | (Yes Refl) | (Yes Refl) | (Yes (M prf evi))
= Yes (Main (M prf evi))
check gs (Main fc (M fc' [<] (fc'',UNIT) x)) | (Yes Refl) | (Yes Refl) | (No emsg f)
= No (Stack fc $ Stack fc' emsg)
(\case (Main y) => f y)
check gs (Main fc (M fc' [<] (fc'',ret) x)) | (Yes Refl) | (No contra)
= No (Stack fc $ Stack fc' $ MismatchTy UNIT ret)
(\case (Main y) => contra Refl)
check gs (Main fc (M fc' args (fc'',ret) x)) | (No contra)
= No (Stack fc $ E "Main takes no args")
(\case (Main y) => contra Refl)
Finally, we can check method declarations and the continuation.
check gs (Decl fc str m rest) with (check gs m)
check gs (Decl fc str (M fc' args (fc'',ret) x) rest) | (Yes d) with (check (gs :< (str, (METH (map snd args) ret))) rest)
check gs (Decl fc str (M fc' args (fc'',ret) x) rest) | (Yes d) | (Yes p)
= Yes (Decl d p)
check gs (Decl fc str (M fc' args (fc'',ret) x) rest) | (Yes d) | (No emsg f)
= No emsg
(\case (Decl y z) => f z)
check gs (Decl fc str m rest) | (No emsg f)
= No (Stack fc emsg)
(\case (Decl x y) => f x)
With elaboration of programs complete, we can write a wrapper function to ensure only closed programs are elaborated.
elab : (fname : String)
-> (ast : AST.Prog a)
-> Either Error (Prog Lin)
Exercises
For the sample programs in
walk-through for Olaf
elaborate them using elab.
Many imperative languages have main methods that take in system arguments and return values of type int.
Rewrite Prog to support evaluation of such main methods.