import Mlang.Nickel import Mlang.Http import Mlang.NickelHost namespace Mlang open Nickel abbrev Name := String inductive Effect where | error : Effect | io : Effect deriving Repr, Inhabited, BEq abbrev Effects := List Effect def Effects.contains (effects : Effects) (effect : Effect) : Bool := effects.any (· == effect) def Effects.insert (effects : Effects) (effect : Effect) : Effects := if effects.contains effect then effects else effect :: effects def Effects.union (lhs rhs : Effects) : Effects := rhs.foldl Effects.insert lhs def Effects.erase (effects : Effects) (target : Effect) : Effects := effects.filter (· != target) def Effects.render : Effects → String | [] => "{}" | effects => let names := effects.reverse.map (fun | .error => "Error" | .io => "IO") "{" ++ String.intercalate ", " names ++ "}" inductive Ty where | int : Ty | bool : Ty | string : Ty | data : Ty | dataInt : Ty | dataBool : Ty | dataString : Ty | dataNull : Ty | dataArray : Ty → Ty | dataRecord : List (String × Ty) → Ty | list : Ty | result : Ty | error : Ty | funTy : Ty → Effects → Ty → Ty deriving Repr, Inhabited, BEq inductive Data where | null : Data | bool : Bool → Data | int : Int → Data | string : String → Data | array : List Data → Data | record : List (String × Data) → Data deriving Repr, Inhabited, BEq inductive Expr where | int : Int → Expr | bool : Bool → Expr | string : String → Expr | null : Expr | var : Name → Expr | arrayE : List Expr → Expr | recordE : List (String × Expr) → Expr | add : Expr → Expr → Expr | sub : Expr → Expr → Expr | mul : Expr → Expr → Expr | div : Expr → Expr → Expr | eq : Expr → Expr → Expr | letE : Name → Expr → Expr → Expr | ifE : Expr → Expr → Expr → Expr | tryE : Expr → Name → Expr → Expr | parMapE : Name → Expr → Expr → Expr | lam : Name → Ty → Expr → Expr | app : Expr → Expr → Expr deriving Repr, Inhabited inductive Value where | int : Int → Value | bool : Bool → Value | string : String → Value | data : Data → Value | list : List Value → Value | resultOk : Value → Value | resultErr : String → Value | error : String → Value | builtinReadFile : Value | builtinHttpGet : Value | builtinReadFileNickel : Value | builtinParseNickel : Value | builtinParseJson : Value | builtinDataGet : Value | builtinDataGetField : Data → Value | builtinDataAt : Value | builtinDataAtIndex : Data → Value | builtinDataAsInt : Value | builtinDataAsString : Value | builtinDataAsBool : Value | builtinDataToJson : Value | builtinDataToNickel : Value | closure : Name → Expr → List (Name × Value) → Value deriving Repr, Inhabited abbrev Env := List (Name × Value) inductive RuntimeError where | unboundVariable : Name → RuntimeError | typeError : String → RuntimeError | divisionByZero : RuntimeError | ioError : String → RuntimeError deriving Repr, Inhabited abbrev EvalM := EIO RuntimeError def Env.lookup (env : Env) (name : Name) : Option Value := match env with | [] => none | (n, v) :: rest => if n = name then some v else rest.lookup name def expectInt : Value → EvalM Int | .int n => pure n | _ => throw (.typeError "expected an integer") def expectBool : Value → EvalM Bool | .bool b => pure b | _ => throw (.typeError "expected a boolean") def expectString : Value → EvalM String | .string s => pure s | _ => throw (.typeError "expected a string") def expectData : Value → EvalM Data | .data term => pure term | _ => throw (.typeError "expected a data value") def valueToData : Value → EvalM Data | .data term => pure term | .int n => pure (.int n) | .bool b => pure (.bool b) | .string s => pure (.string s) | _ => throw (.typeError "data literals only support Int, Bool, String, and Data values") partial def dataType : Data → Ty | .null => .dataNull | .bool _ => .dataBool | .int _ => .dataInt | .string _ => .dataString | .array [] => .dataArray .data | .array (x :: xs) => let itemTy := dataType x if xs.all (fun item => dataType item == itemTy) then .dataArray itemTy else .dataArray .data | .record fields => .dataRecord (fields.map (fun (name, value) => (name, dataType value))) def dataRecordField? : List (String × Ty) → String → Option Ty | [], _ => none | (name, ty) :: rest, target => if name = target then some ty else dataRecordField? rest target def liftScalarDataTy : Ty → Ty | .dataInt => .int | .dataBool => .bool | .dataString => .string | ty => ty partial def refineKnownDataTy (ty : Ty) : Ty := match ty with | .dataInt => .int | .dataBool => .bool | .dataString => .string | .dataNull => .data | .dataArray itemTy => .dataArray (refineKnownDataTy itemTy) | .dataRecord fields => .dataRecord (fields.map (fun (name, fieldTy) => (name, refineKnownDataTy fieldTy))) | other => other def dataArrayGet? : List Data → Nat → Option Data | [], _ => none | x :: _, 0 => some x | _ :: xs, n + 1 => dataArrayGet? xs n def runtimeErrorToValue : RuntimeError → Value | .unboundVariable name => .error s!"unbound variable: {name}" | .typeError msg => .error s!"type error: {msg}" | .divisionByZero => .error "division by zero" | .ioError msg => .error s!"io error: {msg}" def runtimeErrorToMessage : RuntimeError → String | .unboundVariable name => s!"unbound variable: {name}" | .typeError msg => s!"type error: {msg}" | .divisionByZero => "division by zero" | .ioError msg => s!"io error: {msg}" partial def Data.ofNickel : Nickel.Term → Data | .null => .null | .bool b => .bool b | .int n => .int n | .string s => .string s | .array xs => .array (xs.map Data.ofNickel) | .record fields => .record (fields.map (fun (k, v) => (k, Data.ofNickel v))) def parseRenderedData (rendered : String) : EvalM Data := do match Nickel.parse rendered with | .ok term => pure (Data.ofNickel term) | .error err => throw (.typeError s!"host Nickel render parse error: {err}") def encodeMapResult : Except RuntimeError Value → Value | .ok value => .resultOk value | .error err => .resultErr (runtimeErrorToMessage err) def escapeJsonChar : Char → String | '"' => "\\\"" | '\\' => "\\\\" | '\n' => "\\n" | '\r' => "\\r" | '\t' => "\\t" | c => c.toString def renderJsonString (s : String) : String := "\"" ++ String.join (s.toList.map escapeJsonChar) ++ "\"" def isNickelKeyStart (c : Char) : Bool := c.isAlpha || c = '_' def isNickelKeyContinue (c : Char) : Bool := c.isAlpha || c.isDigit || c = '_' || c = '-' def renderNickelKey (s : String) : String := match s.toList with | [] => renderJsonString s | c :: cs => if isNickelKeyStart c && cs.all isNickelKeyContinue then s else renderJsonString s partial def renderNickelData : Data → String | .null => "null" | .bool b => toString b | .int n => toString n | .string s => renderJsonString s | .array xs => "[" ++ String.intercalate ", " (xs.map renderNickelData) ++ "]" | .record fields => let rendered := fields.map (fun (k, v) => s!"{renderNickelKey k} = {renderNickelData v}") "{ " ++ String.intercalate ", " rendered ++ " }" partial def renderJsonData : Data → String | .null => "null" | .bool b => if b then "true" else "false" | .int n => toString n | .string s => renderJsonString s | .array xs => "[" ++ String.intercalate ", " (xs.map renderJsonData) ++ "]" | .record fields => let rendered := fields.map (fun (k, v) => s!"{renderJsonString k}: {renderJsonData v}") "{" ++ String.intercalate ", " rendered ++ "}" def builtinType? (name : Name) : Option Ty := match name with | "readFile" => some (.funTy .string [.error, .io] .string) | "httpGet" => some (.funTy .string [.error, .io] .string) | "readFileNickel" => some (.funTy .string [.error, .io] .data) | "parseNickel" => some (.funTy .string [.error] .data) | "parseJson" => some (.funTy .string [.error] .data) | "get" => some (.funTy .data [] (.funTy .string [.error] .data)) | "at" => some (.funTy .data [] (.funTy .int [.error] .data)) | "asInt" => some (.funTy .data [.error] .int) | "asString" => some (.funTy .data [.error] .string) | "asBool" => some (.funTy .data [.error] .bool) | "toJson" => some (.funTy .data [] .string) | "toNickel" => some (.funTy .data [] .string) | _ => none def builtinValue? (name : Name) : Option Value := match name with | "readFile" => some .builtinReadFile | "httpGet" => some .builtinHttpGet | "readFileNickel" => some .builtinReadFileNickel | "parseNickel" => some .builtinParseNickel | "parseJson" => some .builtinParseJson | "get" => some .builtinDataGet | "at" => some .builtinDataAt | "asInt" => some .builtinDataAsInt | "asString" => some .builtinDataAsString | "asBool" => some .builtinDataAsBool | "toJson" => some .builtinDataToJson | "toNickel" => some .builtinDataToNickel | _ => none mutual partial def applyValue (fnVal argVal : Value) : EvalM Value := do match fnVal with | .closure param body closureEnv => eval ((param, argVal) :: closureEnv) body | .builtinReadFile => do let path ← expectString argVal let contents ← IO.toEIO (fun err => .ioError (toString err)) (IO.FS.readFile path) pure (.string contents) | .builtinHttpGet => do let url ← expectString argVal let body ← IO.toEIO (fun err => .ioError (toString err)) (Http.httpGet url) pure (.string body) | .builtinReadFileNickel => do let path ← expectString argVal let rendered ← IO.toEIO (fun err => .ioError (toString err)) (NickelHost.evalFile path) pure (.data (← parseRenderedData rendered)) | .builtinParseNickel => do let source ← expectString argVal let rendered ← IO.toEIO (fun err => .ioError (toString err)) (NickelHost.evalString source) pure (.data (← parseRenderedData rendered)) | .builtinParseJson => do let source ← expectString argVal let rendered ← IO.toEIO (fun err => .ioError (toString err)) (NickelHost.evalJsonString source) pure (.data (← parseRenderedData rendered)) | .builtinDataGet => do let term ← expectData argVal pure (.builtinDataGetField term) | .builtinDataGetField term => do let key ← expectString argVal match term with | .record fields => match fields.find? (fun (name, _) => name = key) with | some (_, value) => pure (.data value) | none => throw (.typeError s!"missing field: {key}") | _ => throw (.typeError "get expects a record") | .builtinDataAt => do let term ← expectData argVal pure (.builtinDataAtIndex term) | .builtinDataAtIndex term => do let idx ← expectInt argVal if idx < 0 then throw (.typeError s!"negative index: {idx}") else match term with | .array items => match dataArrayGet? items idx.toNat with | some value => pure (.data value) | none => throw (RuntimeError.typeError s!"index out of bounds: {idx}") | _ => throw (.typeError "at expects an array") | .builtinDataAsInt => do let term ← expectData argVal match term with | .int n => pure (.int n) | _ => throw (.typeError "asInt expects an integer") | .builtinDataAsString => do let term ← expectData argVal match term with | .string s => pure (.string s) | _ => throw (.typeError "asString expects a string") | .builtinDataAsBool => do let term ← expectData argVal match term with | .bool b => pure (.bool b) | _ => throw (.typeError "asBool expects a boolean") | .builtinDataToJson => do let term ← expectData argVal pure (.string (renderJsonData term)) | .builtinDataToNickel => do let term ← expectData argVal pure (.string (renderNickelData term)) | _ => match fnVal, argVal with | .data (.record fields), .string key => match fields.find? (fun (name, _) => name = key) with | some (_, value) => pure (.data value) | none => throw (.typeError s!"missing field: {key}") | .data (.array items), .int idx => if idx < 0 then throw (.typeError s!"negative index: {idx}") else match dataArrayGet? items idx.toNat with | some value => pure (.data value) | none => throw (RuntimeError.typeError s!"index out of bounds: {idx}") | .data _, .string _ => throw (.typeError "string dispatch expects a record") | .data _, .int _ => throw (.typeError "int dispatch expects an array") | _, _ => throw (.typeError "expected a function") partial def eval (env : Env) : Expr → EvalM Value | .int n => pure (.int n) | .bool b => pure (.bool b) | .string s => pure (.string s) | .null => pure (.data .null) | .var name => match env.lookup name with | some v => pure v | none => match builtinValue? name with | some v => pure v | none => throw (.unboundVariable name) | .arrayE items => do let values ← items.mapM (eval env) pure (.data (.array (← values.mapM valueToData))) | .recordE fields => do let fields' ← fields.mapM (fun (name, value) => do let value' ← eval env value pure (name, (← valueToData value'))) pure (.data (.record fields')) | .add lhs rhs => do let l ← expectInt (← eval env lhs) let r ← expectInt (← eval env rhs) pure (.int (l + r)) | .sub lhs rhs => do let l ← expectInt (← eval env lhs) let r ← expectInt (← eval env rhs) pure (.int (l - r)) | .mul lhs rhs => do let l ← expectInt (← eval env lhs) let r ← expectInt (← eval env rhs) pure (.int (l * r)) | .div lhs rhs => do let l ← expectInt (← eval env lhs) let r ← expectInt (← eval env rhs) if r = 0 then throw .divisionByZero else pure (.int (l / r)) | .eq lhs rhs => do let l ← expectInt (← eval env lhs) let r ← expectInt (← eval env rhs) pure (.bool (l = r)) | .letE name value body => do let value' ← eval env value eval ((name, value') :: env) body | .ifE cond thenBranch elseBranch => do let c ← expectBool (← eval env cond) if c then eval env thenBranch else eval env elseBranch | .tryE body errName handler => do try eval env body catch err => eval ((errName, runtimeErrorToValue err) :: env) handler | .parMapE itemName collection body => do let source ← expectData (← eval env collection) match source with | .array items => let tasks ← items.mapM (fun item => EIO.asTask (prio := .dedicated) (eval ((itemName, .data item) :: env) body)) let results : List (Except RuntimeError Value) := List.map Task.get tasks pure (.list (results.map encodeMapResult)) | _ => throw (.typeError "pmap expects an array value") | .lam param _ body => pure (.closure param body env) | .app fn arg => do let fnVal ← eval env fn let argVal ← eval env arg applyValue fnVal argVal end def renderData : Data → String := renderNickelData def renderValue : Value → String | .int n => toString n | .bool b => toString b | .string s => s!"\"{s}\"" | .data data => renderData data | .list items => "[" ++ String.intercalate ", " (items.map renderValue) ++ "]" | .resultOk value => s!"ok({renderValue value})" | .resultErr msg => s!"err(\"{msg}\")" | .error msg => s!"" | .builtinReadFile => "" | .builtinHttpGet => "" | .builtinReadFileNickel => "" | .builtinParseNickel => "" | .builtinParseJson => "" | .builtinDataGet => "" | .builtinDataGetField _ => "" | .builtinDataAt => "" | .builtinDataAtIndex _ => "" | .builtinDataAsInt => "" | .builtinDataAsString => "" | .builtinDataAsBool => "" | .builtinDataToJson => "" | .builtinDataToNickel => "" | .closure _ _ _ => "" abbrev TyEnv := List (Name × Ty) structure Judgment where ty : Ty effects : Effects deriving Repr, Inhabited inductive TypeError where | unboundVariable : Name → TypeError | mismatch : Ty → Ty → TypeError | expectedFunction : Ty → TypeError deriving Repr, Inhabited abbrev CheckM := Except TypeError def TyEnv.lookup (env : TyEnv) (name : Name) : Option Ty := match env with | [] => none | (n, ty) :: rest => if n = name then some ty else rest.lookup name def ensureType (expected actual : Ty) : CheckM Unit := if expected == actual then pure () else throw (.mismatch expected actual) def pureJudgment (ty : Ty) : Judgment := { ty := ty, effects := [] } def eraseDataRefinement : Ty → Ty | .dataInt | .dataBool | .dataString | .dataNull | .dataArray _ | .dataRecord _ => .data | ty => ty def canFlowTo (actual expected : Ty) : Bool := if actual == expected then true else match actual, expected with | .dataInt, .data => true | .dataBool, .data => true | .dataString, .data => true | .dataNull, .data => true | .dataArray _, .data => true | .dataRecord _, .data => true | .dataArray _, .dataArray .data => true | _, _ => false partial def constString? : Expr → Option String | .string s => some s | _ => none unsafe def evalConstData? (env : List (Name × Data)) : Expr → Option Data | .null => some .null | .string s => some (.string s) | .int n => some (.int n) | .bool b => some (.bool b) | .var name => env.lookup name | .arrayE items => do some (.array (← items.mapM (evalConstData? env))) | .recordE fields => do some (.record (← fields.mapM (fun (name, value) => do let value' ← evalConstData? env value pure (name, value')))) | .letE name value body => do let value' ← evalConstData? env value evalConstData? ((name, value') :: env) body | .ifE cond thenBranch elseBranch => do match (← evalConstData? env cond) with | .bool true => evalConstData? env thenBranch | .bool false => evalConstData? env elseBranch | _ => none | .app fn arg => do match fn with | .var "parseNickel" => let source ← evalConstData? env arg match source with | .string s => match unsafeIO (NickelHost.evalString s) with | .ok rendered => match Nickel.parse rendered with | .ok term => some (Data.ofNickel term) | .error _ => none | .error _ => none | _ => none | .var "parseJson" => let source ← evalConstData? env arg match source with | .string s => match unsafeIO (NickelHost.evalJsonString s) with | .ok rendered => match Nickel.parse rendered with | .ok term => some (Data.ofNickel term) | .error _ => none | .error _ => none | _ => none | .var "httpGet" => let url ← evalConstData? env arg match url with | .string s => match unsafeIO (Http.httpGet s) with | .ok body => some (.string body) | .error _ => none | _ => none | .var "readFileNickel" => let path ← evalConstData? env arg match path with | .string p => match unsafeIO (NickelHost.evalFile p) with | .ok rendered => match Nickel.parse rendered with | .ok term => some (Data.ofNickel term) | .error _ => none | .error _ => none | _ => none | .app (.var "get") base => do let container ← evalConstData? env base let key ← evalConstData? env arg match container, key with | .record fields, .string field => match fields.find? (fun (name, _) => name = field) with | some (_, value) => some value | none => none | _, _ => none | .app (.var "at") base => do let container ← evalConstData? env base let index ← evalConstData? env arg match container, index with | .array items, .int idx => if idx < 0 then none else dataArrayGet? items idx.toNat | _, _ => none | _ => none | _ => none unsafe def inferType (env : TyEnv) : Expr → CheckM Judgment | .int _ => pure (pureJudgment .int) | .bool _ => pure (pureJudgment .bool) | .string _ => pure (pureJudgment .string) | .null => pure (pureJudgment .dataNull) | .var name => match env.lookup name with | some ty => pure (pureJudgment ty) | none => match builtinType? name with | some ty => pure (pureJudgment ty) | none => throw (.unboundVariable name) | .arrayE items => do let itemJs ← items.mapM (inferType env) let dataTys ← itemJs.mapM (fun j => match j.ty with | .int => pure .dataInt | .bool => pure .dataBool | .string => pure .dataString | .data => pure .data | .dataInt => pure .dataInt | .dataBool => pure .dataBool | .dataString => pure .dataString | .dataNull => pure .dataNull | .dataArray ty => pure (.dataArray ty) | .dataRecord fields => pure (.dataRecord fields) | ty => throw (.mismatch .data ty)) let itemTy := match dataTys with | [] => .data | first :: rest => if rest.all (· == first) then first else .data pure { ty := .dataArray itemTy effects := itemJs.foldl (fun acc j => acc.union j.effects) [] } | .recordE fields => do let fieldJs ← fields.mapM (fun (name, value) => do pure (name, ← inferType env value)) let fieldTys ← fieldJs.mapM (fun (name, j) => do let ty ← match j.ty with | .int => pure .dataInt | .bool => pure .dataBool | .string => pure .dataString | .data => pure .data | .dataInt => pure .dataInt | .dataBool => pure .dataBool | .dataString => pure .dataString | .dataNull => pure .dataNull | .dataArray ty => pure (.dataArray ty) | .dataRecord fields => pure (.dataRecord fields) | ty => throw (.mismatch .data ty) pure (name, ty, j.effects)) pure { ty := .dataRecord (fieldTys.map (fun (name, ty, _) => (name, ty))) effects := fieldTys.foldl (fun acc (_, _, effects) => acc.union effects) [] } | .add lhs rhs | .sub lhs rhs | .mul lhs rhs => do let lhsJ ← inferType env lhs let rhsJ ← inferType env rhs ensureType .int lhsJ.ty ensureType .int rhsJ.ty pure { ty := .int, effects := lhsJ.effects.union rhsJ.effects } | .div lhs rhs => do let lhsJ ← inferType env lhs let rhsJ ← inferType env rhs ensureType .int lhsJ.ty ensureType .int rhsJ.ty pure { ty := .int effects := (lhsJ.effects.union rhsJ.effects).insert .error } | .eq lhs rhs => do let lhsJ ← inferType env lhs let rhsJ ← inferType env rhs ensureType .int lhsJ.ty ensureType .int rhsJ.ty pure { ty := .bool, effects := lhsJ.effects.union rhsJ.effects } | .letE name value body => do let valueJ ← inferType env value let bodyJ ← inferType ((name, valueJ.ty) :: env) body pure { ty := bodyJ.ty effects := valueJ.effects.union bodyJ.effects } | .ifE cond thenBranch elseBranch => do let condJ ← inferType env cond ensureType .bool condJ.ty let thenJ ← inferType env thenBranch let elseJ ← inferType env elseBranch ensureType thenJ.ty elseJ.ty pure { ty := thenJ.ty effects := condJ.effects.union (thenJ.effects.union elseJ.effects) } | .tryE body errName handler => do let bodyJ ← inferType env body let handlerJ ← inferType ((errName, .error) :: env) handler ensureType bodyJ.ty handlerJ.ty pure { ty := bodyJ.ty effects := (bodyJ.effects.erase .error).union handlerJ.effects } | .parMapE itemName collection body => do let collectionJ ← inferType env collection match collectionJ.ty with | .data | .dataArray _ => let itemTy := match collectionJ.ty with | .dataArray ty => ty | _ => .data let bodyJ ← inferType ((itemName, itemTy) :: env) body pure { ty := .list effects := collectionJ.effects.union bodyJ.effects } | _ => throw (.mismatch .data collectionJ.ty) | .lam param paramTy body => do let bodyJ ← inferType ((param, paramTy) :: env) body pure (pureJudgment (.funTy paramTy bodyJ.effects bodyJ.ty)) | .app fn arg => do let fnJ ← inferType env fn let argJ ← inferType env arg match fnJ.ty with | .funTy paramTy latentEffects resultTy => if canFlowTo argJ.ty paramTy then let refinedResultTy := match fn, arg, resultTy with | .app (.var "get") base, .string key, .data => match inferType env base with | .ok baseJ => match baseJ.ty with | .dataRecord fields => match dataRecordField? fields key with | some ty => liftScalarDataTy ty | none => .data | _ => match evalConstData? [] (.app fn arg) with | some data => liftScalarDataTy (dataType data) | none => .data | .error _ => .data | .app (.var "at") base, _, .data => match evalConstData? [] (.app fn arg) with | some data => liftScalarDataTy (dataType data) | none => match inferType env base with | .ok baseJ => match baseJ.ty with | .dataArray itemTy => liftScalarDataTy itemTy | _ => .data | .error _ => .data | .var "parseNickel", _, .data => match evalConstData? [] (.app fn arg) with | some data => refineKnownDataTy (dataType data) | none => .data | .var "parseJson", _, .data => match evalConstData? [] (.app fn arg) with | some data => refineKnownDataTy (dataType data) | none => .data | .var "readFileNickel", _, .data => match evalConstData? [] (.app fn arg) with | some data => refineKnownDataTy (dataType data) | none => .data | .var "asInt", _, .int => match argJ.ty with | .dataInt => .int | _ => .int | .var "asString", _, .string => match argJ.ty with | .dataString => .string | _ => .string | .var "asBool", _, .bool => match argJ.ty with | .dataBool => .bool | _ => .bool | _, _, _ => resultTy pure { ty := refinedResultTy effects := fnJ.effects.union (argJ.effects.union latentEffects) } else throw (.mismatch paramTy argJ.ty) | _ => throw (.expectedFunction fnJ.ty) partial def renderType : Ty → String | .int => "Int" | .bool => "Bool" | .string => "String" | .data => "Data" | .dataInt => "Data:Int" | .dataBool => "Data:Bool" | .dataString => "Data:String" | .dataNull => "Data:Null" | .dataArray itemTy => s!"Data:[{renderType itemTy}]" | .dataRecord fields => let rendered := fields.map (fun (name, ty) => s!"{name}: {renderType ty}") "Data:{" ++ String.intercalate ", " rendered ++ "}" | .list => "List" | .result => "Result" | .error => "Error" | .funTy lhs effects rhs => s!"({renderType lhs} -> {renderType rhs} ! {effects.render})" def renderJudgment (judgment : Judgment) : String := s!"{renderType judgment.ty} ! {judgment.effects.render}" def sampleProgram : Expr := .letE "inc" (.lam "x" .int (.add (.var "x") (.int 1))) (.app (.var "inc") (.int 41)) def lexicalScopeProgram : Expr := .letE "x" (.int 10) (.letE "f" (.lam "y" .int (.add (.var "x") (.var "y"))) (.letE "x" (.int 100) (.app (.var "f") (.int 5)))) def conditionalProgram : Expr := .ifE (.eq (.mul (.int 6) (.int 7)) (.int 42)) (.int 1) (.int 0) end Mlang