This repository has no description
1import Mlang.Nickel
2import Mlang.Http
3import Mlang.NickelHost
4
5namespace Mlang
6
7open Nickel
8
9abbrev Name := String
10
11inductive Effect where
12 | error : Effect
13 | io : Effect
14 deriving Repr, Inhabited, BEq
15
16abbrev Effects := List Effect
17
18def Effects.contains (effects : Effects) (effect : Effect) : Bool :=
19 effects.any (· == effect)
20
21def Effects.insert (effects : Effects) (effect : Effect) : Effects :=
22 if effects.contains effect then effects else effect :: effects
23
24def Effects.union (lhs rhs : Effects) : Effects :=
25 rhs.foldl Effects.insert lhs
26
27def Effects.erase (effects : Effects) (target : Effect) : Effects :=
28 effects.filter (· != target)
29
30def Effects.render : Effects → String
31 | [] => "{}"
32 | effects =>
33 let names := effects.reverse.map (fun
34 | .error => "Error"
35 | .io => "IO")
36 "{" ++ String.intercalate ", " names ++ "}"
37
38inductive Ty where
39 | int : Ty
40 | bool : Ty
41 | string : Ty
42 | data : Ty
43 | dataInt : Ty
44 | dataBool : Ty
45 | dataString : Ty
46 | dataNull : Ty
47 | dataArray : Ty → Ty
48 | dataRecord : List (String × Ty) → Ty
49 | list : Ty
50 | result : Ty
51 | error : Ty
52 | funTy : Ty → Effects → Ty → Ty
53 deriving Repr, Inhabited, BEq
54
55inductive Data where
56 | null : Data
57 | bool : Bool → Data
58 | int : Int → Data
59 | string : String → Data
60 | array : List Data → Data
61 | record : List (String × Data) → Data
62 deriving Repr, Inhabited, BEq
63
64inductive Expr where
65 | int : Int → Expr
66 | bool : Bool → Expr
67 | string : String → Expr
68 | null : Expr
69 | var : Name → Expr
70 | arrayE : List Expr → Expr
71 | recordE : List (String × Expr) → Expr
72 | add : Expr → Expr → Expr
73 | sub : Expr → Expr → Expr
74 | mul : Expr → Expr → Expr
75 | div : Expr → Expr → Expr
76 | eq : Expr → Expr → Expr
77 | letE : Name → Expr → Expr → Expr
78 | ifE : Expr → Expr → Expr → Expr
79 | tryE : Expr → Name → Expr → Expr
80 | parMapE : Name → Expr → Expr → Expr
81 | lam : Name → Ty → Expr → Expr
82 | app : Expr → Expr → Expr
83 deriving Repr, Inhabited
84
85inductive Value where
86 | int : Int → Value
87 | bool : Bool → Value
88 | string : String → Value
89 | data : Data → Value
90 | list : List Value → Value
91 | resultOk : Value → Value
92 | resultErr : String → Value
93 | error : String → Value
94 | builtinReadFile : Value
95 | builtinHttpGet : Value
96 | builtinReadFileNickel : Value
97 | builtinParseNickel : Value
98 | builtinParseJson : Value
99 | builtinDataGet : Value
100 | builtinDataGetField : Data → Value
101 | builtinDataAt : Value
102 | builtinDataAtIndex : Data → Value
103 | builtinDataAsInt : Value
104 | builtinDataAsString : Value
105 | builtinDataAsBool : Value
106 | builtinDataToJson : Value
107 | builtinDataToNickel : Value
108 | closure : Name → Expr → List (Name × Value) → Value
109 deriving Repr, Inhabited
110
111abbrev Env := List (Name × Value)
112
113inductive RuntimeError where
114 | unboundVariable : Name → RuntimeError
115 | typeError : String → RuntimeError
116 | divisionByZero : RuntimeError
117 | ioError : String → RuntimeError
118 deriving Repr, Inhabited
119
120abbrev EvalM := EIO RuntimeError
121
122def Env.lookup (env : Env) (name : Name) : Option Value :=
123 match env with
124 | [] => none
125 | (n, v) :: rest => if n = name then some v else rest.lookup name
126
127def expectInt : Value → EvalM Int
128 | .int n => pure n
129 | _ => throw (.typeError "expected an integer")
130
131def expectBool : Value → EvalM Bool
132 | .bool b => pure b
133 | _ => throw (.typeError "expected a boolean")
134
135def expectString : Value → EvalM String
136 | .string s => pure s
137 | _ => throw (.typeError "expected a string")
138
139def expectData : Value → EvalM Data
140 | .data term => pure term
141 | _ => throw (.typeError "expected a data value")
142
143def valueToData : Value → EvalM Data
144 | .data term => pure term
145 | .int n => pure (.int n)
146 | .bool b => pure (.bool b)
147 | .string s => pure (.string s)
148 | _ => throw (.typeError "data literals only support Int, Bool, String, and Data values")
149
150partial def dataType : Data → Ty
151 | .null => .dataNull
152 | .bool _ => .dataBool
153 | .int _ => .dataInt
154 | .string _ => .dataString
155 | .array [] => .dataArray .data
156 | .array (x :: xs) =>
157 let itemTy := dataType x
158 if xs.all (fun item => dataType item == itemTy) then
159 .dataArray itemTy
160 else
161 .dataArray .data
162 | .record fields =>
163 .dataRecord (fields.map (fun (name, value) => (name, dataType value)))
164
165def dataRecordField? : List (String × Ty) → String → Option Ty
166 | [], _ => none
167 | (name, ty) :: rest, target =>
168 if name = target then some ty else dataRecordField? rest target
169
170def liftScalarDataTy : Ty → Ty
171 | .dataInt => .int
172 | .dataBool => .bool
173 | .dataString => .string
174 | ty => ty
175
176partial def refineKnownDataTy (ty : Ty) : Ty :=
177 match ty with
178 | .dataInt => .int
179 | .dataBool => .bool
180 | .dataString => .string
181 | .dataNull => .data
182 | .dataArray itemTy => .dataArray (refineKnownDataTy itemTy)
183 | .dataRecord fields =>
184 .dataRecord (fields.map (fun (name, fieldTy) => (name, refineKnownDataTy fieldTy)))
185 | other => other
186
187def dataArrayGet? : List Data → Nat → Option Data
188 | [], _ => none
189 | x :: _, 0 => some x
190 | _ :: xs, n + 1 => dataArrayGet? xs n
191
192def runtimeErrorToValue : RuntimeError → Value
193 | .unboundVariable name => .error s!"unbound variable: {name}"
194 | .typeError msg => .error s!"type error: {msg}"
195 | .divisionByZero => .error "division by zero"
196 | .ioError msg => .error s!"io error: {msg}"
197
198def runtimeErrorToMessage : RuntimeError → String
199 | .unboundVariable name => s!"unbound variable: {name}"
200 | .typeError msg => s!"type error: {msg}"
201 | .divisionByZero => "division by zero"
202 | .ioError msg => s!"io error: {msg}"
203
204partial def Data.ofNickel : Nickel.Term → Data
205 | .null => .null
206 | .bool b => .bool b
207 | .int n => .int n
208 | .string s => .string s
209 | .array xs => .array (xs.map Data.ofNickel)
210 | .record fields => .record (fields.map (fun (k, v) => (k, Data.ofNickel v)))
211
212def parseRenderedData (rendered : String) : EvalM Data := do
213 match Nickel.parse rendered with
214 | .ok term => pure (Data.ofNickel term)
215 | .error err => throw (.typeError s!"host Nickel render parse error: {err}")
216
217def encodeMapResult : Except RuntimeError Value → Value
218 | .ok value => .resultOk value
219 | .error err => .resultErr (runtimeErrorToMessage err)
220
221def escapeJsonChar : Char → String
222 | '"' => "\\\""
223 | '\\' => "\\\\"
224 | '\n' => "\\n"
225 | '\r' => "\\r"
226 | '\t' => "\\t"
227 | c => c.toString
228
229def renderJsonString (s : String) : String :=
230 "\"" ++ String.join (s.toList.map escapeJsonChar) ++ "\""
231
232def isNickelKeyStart (c : Char) : Bool :=
233 c.isAlpha || c = '_'
234
235def isNickelKeyContinue (c : Char) : Bool :=
236 c.isAlpha || c.isDigit || c = '_' || c = '-'
237
238def renderNickelKey (s : String) : String :=
239 match s.toList with
240 | [] => renderJsonString s
241 | c :: cs =>
242 if isNickelKeyStart c && cs.all isNickelKeyContinue then
243 s
244 else
245 renderJsonString s
246
247partial def renderNickelData : Data → String
248 | .null => "null"
249 | .bool b => toString b
250 | .int n => toString n
251 | .string s => renderJsonString s
252 | .array xs => "[" ++ String.intercalate ", " (xs.map renderNickelData) ++ "]"
253 | .record fields =>
254 let rendered := fields.map (fun (k, v) => s!"{renderNickelKey k} = {renderNickelData v}")
255 "{ " ++ String.intercalate ", " rendered ++ " }"
256
257partial def renderJsonData : Data → String
258 | .null => "null"
259 | .bool b => if b then "true" else "false"
260 | .int n => toString n
261 | .string s => renderJsonString s
262 | .array xs => "[" ++ String.intercalate ", " (xs.map renderJsonData) ++ "]"
263 | .record fields =>
264 let rendered := fields.map (fun (k, v) => s!"{renderJsonString k}: {renderJsonData v}")
265 "{" ++ String.intercalate ", " rendered ++ "}"
266
267def builtinType? (name : Name) : Option Ty :=
268 match name with
269 | "readFile" => some (.funTy .string [.error, .io] .string)
270 | "httpGet" => some (.funTy .string [.error, .io] .string)
271 | "readFileNickel" => some (.funTy .string [.error, .io] .data)
272 | "parseNickel" => some (.funTy .string [.error] .data)
273 | "parseJson" => some (.funTy .string [.error] .data)
274 | "get" => some (.funTy .data [] (.funTy .string [.error] .data))
275 | "at" => some (.funTy .data [] (.funTy .int [.error] .data))
276 | "asInt" => some (.funTy .data [.error] .int)
277 | "asString" => some (.funTy .data [.error] .string)
278 | "asBool" => some (.funTy .data [.error] .bool)
279 | "toJson" => some (.funTy .data [] .string)
280 | "toNickel" => some (.funTy .data [] .string)
281 | _ => none
282
283def builtinValue? (name : Name) : Option Value :=
284 match name with
285 | "readFile" => some .builtinReadFile
286 | "httpGet" => some .builtinHttpGet
287 | "readFileNickel" => some .builtinReadFileNickel
288 | "parseNickel" => some .builtinParseNickel
289 | "parseJson" => some .builtinParseJson
290 | "get" => some .builtinDataGet
291 | "at" => some .builtinDataAt
292 | "asInt" => some .builtinDataAsInt
293 | "asString" => some .builtinDataAsString
294 | "asBool" => some .builtinDataAsBool
295 | "toJson" => some .builtinDataToJson
296 | "toNickel" => some .builtinDataToNickel
297 | _ => none
298
299mutual
300 partial def applyValue (fnVal argVal : Value) : EvalM Value := do
301 match fnVal with
302 | .closure param body closureEnv =>
303 eval ((param, argVal) :: closureEnv) body
304 | .builtinReadFile => do
305 let path ← expectString argVal
306 let contents ← IO.toEIO (fun err => .ioError (toString err)) (IO.FS.readFile path)
307 pure (.string contents)
308 | .builtinHttpGet => do
309 let url ← expectString argVal
310 let body ← IO.toEIO (fun err => .ioError (toString err)) (Http.httpGet url)
311 pure (.string body)
312 | .builtinReadFileNickel => do
313 let path ← expectString argVal
314 let rendered ← IO.toEIO (fun err => .ioError (toString err)) (NickelHost.evalFile path)
315 pure (.data (← parseRenderedData rendered))
316 | .builtinParseNickel => do
317 let source ← expectString argVal
318 let rendered ← IO.toEIO (fun err => .ioError (toString err)) (NickelHost.evalString source)
319 pure (.data (← parseRenderedData rendered))
320 | .builtinParseJson => do
321 let source ← expectString argVal
322 let rendered ← IO.toEIO (fun err => .ioError (toString err)) (NickelHost.evalJsonString source)
323 pure (.data (← parseRenderedData rendered))
324 | .builtinDataGet => do
325 let term ← expectData argVal
326 pure (.builtinDataGetField term)
327 | .builtinDataGetField term => do
328 let key ← expectString argVal
329 match term with
330 | .record fields =>
331 match fields.find? (fun (name, _) => name = key) with
332 | some (_, value) => pure (.data value)
333 | none => throw (.typeError s!"missing field: {key}")
334 | _ => throw (.typeError "get expects a record")
335 | .builtinDataAt => do
336 let term ← expectData argVal
337 pure (.builtinDataAtIndex term)
338 | .builtinDataAtIndex term => do
339 let idx ← expectInt argVal
340 if idx < 0 then
341 throw (.typeError s!"negative index: {idx}")
342 else
343 match term with
344 | .array items =>
345 match dataArrayGet? items idx.toNat with
346 | some value => pure (.data value)
347 | none => throw (RuntimeError.typeError s!"index out of bounds: {idx}")
348 | _ => throw (.typeError "at expects an array")
349 | .builtinDataAsInt => do
350 let term ← expectData argVal
351 match term with
352 | .int n => pure (.int n)
353 | _ => throw (.typeError "asInt expects an integer")
354 | .builtinDataAsString => do
355 let term ← expectData argVal
356 match term with
357 | .string s => pure (.string s)
358 | _ => throw (.typeError "asString expects a string")
359 | .builtinDataAsBool => do
360 let term ← expectData argVal
361 match term with
362 | .bool b => pure (.bool b)
363 | _ => throw (.typeError "asBool expects a boolean")
364 | .builtinDataToJson => do
365 let term ← expectData argVal
366 pure (.string (renderJsonData term))
367 | .builtinDataToNickel => do
368 let term ← expectData argVal
369 pure (.string (renderNickelData term))
370 | _ =>
371 match fnVal, argVal with
372 | .data (.record fields), .string key =>
373 match fields.find? (fun (name, _) => name = key) with
374 | some (_, value) => pure (.data value)
375 | none => throw (.typeError s!"missing field: {key}")
376 | .data (.array items), .int idx =>
377 if idx < 0 then
378 throw (.typeError s!"negative index: {idx}")
379 else
380 match dataArrayGet? items idx.toNat with
381 | some value => pure (.data value)
382 | none => throw (RuntimeError.typeError s!"index out of bounds: {idx}")
383 | .data _, .string _ =>
384 throw (.typeError "string dispatch expects a record")
385 | .data _, .int _ =>
386 throw (.typeError "int dispatch expects an array")
387 | _, _ =>
388 throw (.typeError "expected a function")
389
390 partial def eval (env : Env) : Expr → EvalM Value
391 | .int n => pure (.int n)
392 | .bool b => pure (.bool b)
393 | .string s => pure (.string s)
394 | .null => pure (.data .null)
395 | .var name =>
396 match env.lookup name with
397 | some v => pure v
398 | none =>
399 match builtinValue? name with
400 | some v => pure v
401 | none => throw (.unboundVariable name)
402 | .arrayE items => do
403 let values ← items.mapM (eval env)
404 pure (.data (.array (← values.mapM valueToData)))
405 | .recordE fields => do
406 let fields' ← fields.mapM (fun (name, value) => do
407 let value' ← eval env value
408 pure (name, (← valueToData value')))
409 pure (.data (.record fields'))
410 | .add lhs rhs => do
411 let l ← expectInt (← eval env lhs)
412 let r ← expectInt (← eval env rhs)
413 pure (.int (l + r))
414 | .sub lhs rhs => do
415 let l ← expectInt (← eval env lhs)
416 let r ← expectInt (← eval env rhs)
417 pure (.int (l - r))
418 | .mul lhs rhs => do
419 let l ← expectInt (← eval env lhs)
420 let r ← expectInt (← eval env rhs)
421 pure (.int (l * r))
422 | .div lhs rhs => do
423 let l ← expectInt (← eval env lhs)
424 let r ← expectInt (← eval env rhs)
425 if r = 0 then
426 throw .divisionByZero
427 else
428 pure (.int (l / r))
429 | .eq lhs rhs => do
430 let l ← expectInt (← eval env lhs)
431 let r ← expectInt (← eval env rhs)
432 pure (.bool (l = r))
433 | .letE name value body => do
434 let value' ← eval env value
435 eval ((name, value') :: env) body
436 | .ifE cond thenBranch elseBranch => do
437 let c ← expectBool (← eval env cond)
438 if c then
439 eval env thenBranch
440 else
441 eval env elseBranch
442 | .tryE body errName handler => do
443 try
444 eval env body
445 catch err =>
446 eval ((errName, runtimeErrorToValue err) :: env) handler
447 | .parMapE itemName collection body => do
448 let source ← expectData (← eval env collection)
449 match source with
450 | .array items =>
451 let tasks ← items.mapM (fun item =>
452 EIO.asTask (prio := .dedicated) (eval ((itemName, .data item) :: env) body))
453 let results : List (Except RuntimeError Value) := List.map Task.get tasks
454 pure (.list (results.map encodeMapResult))
455 | _ =>
456 throw (.typeError "pmap expects an array value")
457 | .lam param _ body =>
458 pure (.closure param body env)
459 | .app fn arg => do
460 let fnVal ← eval env fn
461 let argVal ← eval env arg
462 applyValue fnVal argVal
463end
464
465def renderData : Data → String := renderNickelData
466
467def renderValue : Value → String
468 | .int n => toString n
469 | .bool b => toString b
470 | .string s => s!"\"{s}\""
471 | .data data => renderData data
472 | .list items => "[" ++ String.intercalate ", " (items.map renderValue) ++ "]"
473 | .resultOk value => s!"ok({renderValue value})"
474 | .resultErr msg => s!"err(\"{msg}\")"
475 | .error msg => s!"<error:{msg}>"
476 | .builtinReadFile => "<builtin:readFile>"
477 | .builtinHttpGet => "<builtin:httpGet>"
478 | .builtinReadFileNickel => "<builtin:readFileNickel>"
479 | .builtinParseNickel => "<builtin:parseNickel>"
480 | .builtinParseJson => "<builtin:parseJson>"
481 | .builtinDataGet => "<builtin:get>"
482 | .builtinDataGetField _ => "<builtin:getField>"
483 | .builtinDataAt => "<builtin:at>"
484 | .builtinDataAtIndex _ => "<builtin:atIndex>"
485 | .builtinDataAsInt => "<builtin:asInt>"
486 | .builtinDataAsString => "<builtin:asString>"
487 | .builtinDataAsBool => "<builtin:asBool>"
488 | .builtinDataToJson => "<builtin:toJson>"
489 | .builtinDataToNickel => "<builtin:toNickel>"
490 | .closure _ _ _ => "<closure>"
491
492abbrev TyEnv := List (Name × Ty)
493
494structure Judgment where
495 ty : Ty
496 effects : Effects
497 deriving Repr, Inhabited
498
499inductive TypeError where
500 | unboundVariable : Name → TypeError
501 | mismatch : Ty → Ty → TypeError
502 | expectedFunction : Ty → TypeError
503 deriving Repr, Inhabited
504
505abbrev CheckM := Except TypeError
506
507def TyEnv.lookup (env : TyEnv) (name : Name) : Option Ty :=
508 match env with
509 | [] => none
510 | (n, ty) :: rest => if n = name then some ty else rest.lookup name
511
512def ensureType (expected actual : Ty) : CheckM Unit :=
513 if expected == actual then
514 pure ()
515 else
516 throw (.mismatch expected actual)
517
518def pureJudgment (ty : Ty) : Judgment :=
519 { ty := ty, effects := [] }
520
521def eraseDataRefinement : Ty → Ty
522 | .dataInt | .dataBool | .dataString | .dataNull | .dataArray _ | .dataRecord _ => .data
523 | ty => ty
524
525def canFlowTo (actual expected : Ty) : Bool :=
526 if actual == expected then
527 true
528 else
529 match actual, expected with
530 | .dataInt, .data => true
531 | .dataBool, .data => true
532 | .dataString, .data => true
533 | .dataNull, .data => true
534 | .dataArray _, .data => true
535 | .dataRecord _, .data => true
536 | .dataArray _, .dataArray .data => true
537 | _, _ => false
538
539partial def constString? : Expr → Option String
540 | .string s => some s
541 | _ => none
542
543unsafe def evalConstData? (env : List (Name × Data)) : Expr → Option Data
544 | .null => some .null
545 | .string s => some (.string s)
546 | .int n => some (.int n)
547 | .bool b => some (.bool b)
548 | .var name => env.lookup name
549 | .arrayE items => do
550 some (.array (← items.mapM (evalConstData? env)))
551 | .recordE fields => do
552 some (.record (← fields.mapM (fun (name, value) => do
553 let value' ← evalConstData? env value
554 pure (name, value'))))
555 | .letE name value body => do
556 let value' ← evalConstData? env value
557 evalConstData? ((name, value') :: env) body
558 | .ifE cond thenBranch elseBranch => do
559 match (← evalConstData? env cond) with
560 | .bool true => evalConstData? env thenBranch
561 | .bool false => evalConstData? env elseBranch
562 | _ => none
563 | .app fn arg => do
564 match fn with
565 | .var "parseNickel" =>
566 let source ← evalConstData? env arg
567 match source with
568 | .string s =>
569 match unsafeIO (NickelHost.evalString s) with
570 | .ok rendered =>
571 match Nickel.parse rendered with
572 | .ok term => some (Data.ofNickel term)
573 | .error _ => none
574 | .error _ => none
575 | _ => none
576 | .var "parseJson" =>
577 let source ← evalConstData? env arg
578 match source with
579 | .string s =>
580 match unsafeIO (NickelHost.evalJsonString s) with
581 | .ok rendered =>
582 match Nickel.parse rendered with
583 | .ok term => some (Data.ofNickel term)
584 | .error _ => none
585 | .error _ => none
586 | _ => none
587 | .var "httpGet" =>
588 let url ← evalConstData? env arg
589 match url with
590 | .string s =>
591 match unsafeIO (Http.httpGet s) with
592 | .ok body => some (.string body)
593 | .error _ => none
594 | _ => none
595 | .var "readFileNickel" =>
596 let path ← evalConstData? env arg
597 match path with
598 | .string p =>
599 match unsafeIO (NickelHost.evalFile p) with
600 | .ok rendered =>
601 match Nickel.parse rendered with
602 | .ok term => some (Data.ofNickel term)
603 | .error _ => none
604 | .error _ => none
605 | _ => none
606 | .app (.var "get") base => do
607 let container ← evalConstData? env base
608 let key ← evalConstData? env arg
609 match container, key with
610 | .record fields, .string field =>
611 match fields.find? (fun (name, _) => name = field) with
612 | some (_, value) => some value
613 | none => none
614 | _, _ => none
615 | .app (.var "at") base => do
616 let container ← evalConstData? env base
617 let index ← evalConstData? env arg
618 match container, index with
619 | .array items, .int idx =>
620 if idx < 0 then none else dataArrayGet? items idx.toNat
621 | _, _ => none
622 | _ => none
623 | _ => none
624
625unsafe def inferType (env : TyEnv) : Expr → CheckM Judgment
626 | .int _ => pure (pureJudgment .int)
627 | .bool _ => pure (pureJudgment .bool)
628 | .string _ => pure (pureJudgment .string)
629 | .null => pure (pureJudgment .dataNull)
630 | .var name =>
631 match env.lookup name with
632 | some ty => pure (pureJudgment ty)
633 | none =>
634 match builtinType? name with
635 | some ty => pure (pureJudgment ty)
636 | none => throw (.unboundVariable name)
637 | .arrayE items => do
638 let itemJs ← items.mapM (inferType env)
639 let dataTys ← itemJs.mapM (fun j =>
640 match j.ty with
641 | .int => pure .dataInt
642 | .bool => pure .dataBool
643 | .string => pure .dataString
644 | .data => pure .data
645 | .dataInt => pure .dataInt
646 | .dataBool => pure .dataBool
647 | .dataString => pure .dataString
648 | .dataNull => pure .dataNull
649 | .dataArray ty => pure (.dataArray ty)
650 | .dataRecord fields => pure (.dataRecord fields)
651 | ty => throw (.mismatch .data ty))
652 let itemTy :=
653 match dataTys with
654 | [] => .data
655 | first :: rest => if rest.all (· == first) then first else .data
656 pure {
657 ty := .dataArray itemTy
658 effects := itemJs.foldl (fun acc j => acc.union j.effects) []
659 }
660 | .recordE fields => do
661 let fieldJs ← fields.mapM (fun (name, value) => do pure (name, ← inferType env value))
662 let fieldTys ← fieldJs.mapM (fun (name, j) => do
663 let ty ← match j.ty with
664 | .int => pure .dataInt
665 | .bool => pure .dataBool
666 | .string => pure .dataString
667 | .data => pure .data
668 | .dataInt => pure .dataInt
669 | .dataBool => pure .dataBool
670 | .dataString => pure .dataString
671 | .dataNull => pure .dataNull
672 | .dataArray ty => pure (.dataArray ty)
673 | .dataRecord fields => pure (.dataRecord fields)
674 | ty => throw (.mismatch .data ty)
675 pure (name, ty, j.effects))
676 pure {
677 ty := .dataRecord (fieldTys.map (fun (name, ty, _) => (name, ty)))
678 effects := fieldTys.foldl (fun acc (_, _, effects) => acc.union effects) []
679 }
680 | .add lhs rhs
681 | .sub lhs rhs
682 | .mul lhs rhs => do
683 let lhsJ ← inferType env lhs
684 let rhsJ ← inferType env rhs
685 ensureType .int lhsJ.ty
686 ensureType .int rhsJ.ty
687 pure { ty := .int, effects := lhsJ.effects.union rhsJ.effects }
688 | .div lhs rhs => do
689 let lhsJ ← inferType env lhs
690 let rhsJ ← inferType env rhs
691 ensureType .int lhsJ.ty
692 ensureType .int rhsJ.ty
693 pure {
694 ty := .int
695 effects := (lhsJ.effects.union rhsJ.effects).insert .error
696 }
697 | .eq lhs rhs => do
698 let lhsJ ← inferType env lhs
699 let rhsJ ← inferType env rhs
700 ensureType .int lhsJ.ty
701 ensureType .int rhsJ.ty
702 pure { ty := .bool, effects := lhsJ.effects.union rhsJ.effects }
703 | .letE name value body => do
704 let valueJ ← inferType env value
705 let bodyJ ← inferType ((name, valueJ.ty) :: env) body
706 pure {
707 ty := bodyJ.ty
708 effects := valueJ.effects.union bodyJ.effects
709 }
710 | .ifE cond thenBranch elseBranch => do
711 let condJ ← inferType env cond
712 ensureType .bool condJ.ty
713 let thenJ ← inferType env thenBranch
714 let elseJ ← inferType env elseBranch
715 ensureType thenJ.ty elseJ.ty
716 pure {
717 ty := thenJ.ty
718 effects := condJ.effects.union (thenJ.effects.union elseJ.effects)
719 }
720 | .tryE body errName handler => do
721 let bodyJ ← inferType env body
722 let handlerJ ← inferType ((errName, .error) :: env) handler
723 ensureType bodyJ.ty handlerJ.ty
724 pure {
725 ty := bodyJ.ty
726 effects := (bodyJ.effects.erase .error).union handlerJ.effects
727 }
728 | .parMapE itemName collection body => do
729 let collectionJ ← inferType env collection
730 match collectionJ.ty with
731 | .data
732 | .dataArray _ =>
733 let itemTy :=
734 match collectionJ.ty with
735 | .dataArray ty => ty
736 | _ => .data
737 let bodyJ ← inferType ((itemName, itemTy) :: env) body
738 pure {
739 ty := .list
740 effects := collectionJ.effects.union bodyJ.effects
741 }
742 | _ =>
743 throw (.mismatch .data collectionJ.ty)
744 | .lam param paramTy body => do
745 let bodyJ ← inferType ((param, paramTy) :: env) body
746 pure (pureJudgment (.funTy paramTy bodyJ.effects bodyJ.ty))
747 | .app fn arg => do
748 let fnJ ← inferType env fn
749 let argJ ← inferType env arg
750 match fnJ.ty with
751 | .funTy paramTy latentEffects resultTy =>
752 if canFlowTo argJ.ty paramTy then
753 let refinedResultTy :=
754 match fn, arg, resultTy with
755 | .app (.var "get") base, .string key, .data =>
756 match inferType env base with
757 | .ok baseJ =>
758 match baseJ.ty with
759 | .dataRecord fields =>
760 match dataRecordField? fields key with
761 | some ty => liftScalarDataTy ty
762 | none => .data
763 | _ =>
764 match evalConstData? [] (.app fn arg) with
765 | some data => liftScalarDataTy (dataType data)
766 | none => .data
767 | .error _ => .data
768 | .app (.var "at") base, _, .data =>
769 match evalConstData? [] (.app fn arg) with
770 | some data => liftScalarDataTy (dataType data)
771 | none =>
772 match inferType env base with
773 | .ok baseJ =>
774 match baseJ.ty with
775 | .dataArray itemTy => liftScalarDataTy itemTy
776 | _ => .data
777 | .error _ => .data
778 | .var "parseNickel", _, .data =>
779 match evalConstData? [] (.app fn arg) with
780 | some data => refineKnownDataTy (dataType data)
781 | none => .data
782 | .var "parseJson", _, .data =>
783 match evalConstData? [] (.app fn arg) with
784 | some data => refineKnownDataTy (dataType data)
785 | none => .data
786 | .var "readFileNickel", _, .data =>
787 match evalConstData? [] (.app fn arg) with
788 | some data => refineKnownDataTy (dataType data)
789 | none => .data
790 | .var "asInt", _, .int =>
791 match argJ.ty with
792 | .dataInt => .int
793 | _ => .int
794 | .var "asString", _, .string =>
795 match argJ.ty with
796 | .dataString => .string
797 | _ => .string
798 | .var "asBool", _, .bool =>
799 match argJ.ty with
800 | .dataBool => .bool
801 | _ => .bool
802 | _, _, _ => resultTy
803 pure {
804 ty := refinedResultTy
805 effects := fnJ.effects.union (argJ.effects.union latentEffects)
806 }
807 else
808 throw (.mismatch paramTy argJ.ty)
809 | _ =>
810 throw (.expectedFunction fnJ.ty)
811
812partial def renderType : Ty → String
813 | .int => "Int"
814 | .bool => "Bool"
815 | .string => "String"
816 | .data => "Data"
817 | .dataInt => "Data:Int"
818 | .dataBool => "Data:Bool"
819 | .dataString => "Data:String"
820 | .dataNull => "Data:Null"
821 | .dataArray itemTy => s!"Data:[{renderType itemTy}]"
822 | .dataRecord fields =>
823 let rendered := fields.map (fun (name, ty) => s!"{name}: {renderType ty}")
824 "Data:{" ++ String.intercalate ", " rendered ++ "}"
825 | .list => "List"
826 | .result => "Result"
827 | .error => "Error"
828 | .funTy lhs effects rhs => s!"({renderType lhs} -> {renderType rhs} ! {effects.render})"
829
830def renderJudgment (judgment : Judgment) : String :=
831 s!"{renderType judgment.ty} ! {judgment.effects.render}"
832
833def sampleProgram : Expr :=
834 .letE "inc" (.lam "x" .int (.add (.var "x") (.int 1)))
835 (.app (.var "inc") (.int 41))
836
837def lexicalScopeProgram : Expr :=
838 .letE "x" (.int 10)
839 (.letE "f" (.lam "y" .int (.add (.var "x") (.var "y")))
840 (.letE "x" (.int 100)
841 (.app (.var "f") (.int 5))))
842
843def conditionalProgram : Expr :=
844 .ifE (.eq (.mul (.int 6) (.int 7)) (.int 42))
845 (.int 1)
846 (.int 0)
847
848end Mlang