Lox interpreter written in Gleam, following Crafting Intepreters
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: strengthen type safety with adts

authored by

beaudidly and committed by
beaudidly
f5081494 5f55109e

+178 -106
+7 -6
src/ast_printer.gleam
··· 1 - import gleam/list 2 - import gleam/string 3 1 import expr.{ 4 2 type Expr, ExprBinary, ExprGrouping, ExprLit, ExprUnary, LitBool, LitNil, 5 3 LitNumber, LitString, 6 4 } 5 + import gleam/list 6 + import gleam/string 7 7 8 8 pub fn print(e: Expr) -> String { 9 9 case e { 10 10 ExprLit(LitNil) -> "nil" 11 - ExprLit(LitBool(tok)) -> tok.lexeme 12 - ExprLit(LitNumber(tok)) -> tok.lexeme 13 - ExprLit(LitString(tok)) -> tok.lexeme 11 + ExprLit(LitBool(tok, _)) -> tok.lexeme 12 + ExprLit(LitNumber(tok, _)) -> tok.lexeme 13 + ExprLit(LitString(tok, _)) -> tok.lexeme 14 14 ExprUnary(op, right) -> parenthesize(op.lexeme, [right]) 15 - ExprBinary(left, op, right) -> parenthesize(op.lexeme, [left, right]) 15 + ExprBinary(left, binop, right) -> 16 + parenthesize(binop.token.lexeme, [left, right]) 16 17 ExprGrouping(inner) -> parenthesize("group", [inner]) 17 18 } 18 19 }
+72 -36
src/expr.gleam
··· 1 1 /////// Binary 2 2 3 + import gleam/dict.{type Dict, from_list} 3 4 import gleam/list 4 5 import gleam/option.{type Option, None, Some} 5 6 import gleam/result ··· 12 13 pub type Expr { 13 14 ExprLit(ExprLit) 14 15 ExprUnary(Token, Expr) 15 - ExprBinary(Expr, Token, Expr) 16 + ExprBinary(Expr, BinOp, Expr) 16 17 ExprGrouping(Expr) 17 18 } 18 19 19 20 pub type ExprLit { 20 - LitString(Token) 21 - LitNumber(Token) 22 - LitBool(Token) 21 + LitString(Token, String) 22 + LitNumber(Token, Float) 23 + LitBool(Token, Bool) 23 24 LitNil 24 25 } 25 26 ··· 29 30 } 30 31 31 32 pub type BinOp { 32 - BinOpEquals 33 - BinOpNotEquals 34 - BinOpGT 35 - BinOpGTE 36 - BinOpLT 37 - BinOpLTE 38 - BinOpPlus 39 - BinOpMinus 40 - BinOpMult 41 - BinOpDiv 42 - BinOpComma 33 + BinOpEquals(token: Token) 34 + BinOpNotEquals(token: Token) 35 + BinOpGT(token: Token) 36 + BinOpGTE(token: Token) 37 + BinOpLT(token: Token) 38 + BinOpLTE(token: Token) 39 + BinOpPlus(token: Token) 40 + BinOpMinus(token: Token) 41 + BinOpMult(token: Token) 42 + BinOpDiv(token: Token) 43 + BinOpComma(token: Token) 43 44 } 44 45 45 46 type ParserResult = ··· 57 58 fn bin_loop_left( 58 59 acc: Expr, 59 60 tokens: List(Token), 60 - ops: List(TokenType), 61 + ops: Dict(TokenType, fn(Token) -> BinOp), 61 62 next: fn(List(Token)) -> ParserResult, 62 63 ) -> ParserResult { 63 64 case tokens { 64 65 [tok, ..rest] -> { 65 - case list.contains(ops, tok.token_type) { 66 - True -> { 66 + case dict.get(ops, tok.token_type) { 67 + Ok(ctor) -> { 67 68 use #(rhs, new_rest) <- result.try(next(rest)) 68 - bin_loop_left(ExprBinary(acc, tok, rhs), new_rest, ops, next) 69 + bin_loop_left(ExprBinary(acc, ctor(tok), rhs), new_rest, ops, next) 69 70 } 70 71 _ -> Ok(#(acc, tokens)) 71 72 } ··· 76 77 77 78 pub fn bin_left( 78 79 tokens: List(Token), 79 - ops: List(TokenType), 80 + ops: Dict(TokenType, fn(Token) -> BinOp), 80 81 next: fn(List(Token)) -> ParserResult, 81 82 ) -> ParserResult { 82 83 use #(expr, rest) <- result.try(next(tokens)) ··· 89 90 } 90 91 91 92 pub fn comma(tokens: List(Token)) -> ParserResult { 92 - bin_left(tokens, [scan.Comma], equality) 93 + let ops = from_list([#(scan.Comma, BinOpComma)]) 94 + bin_left(tokens, ops, equality) 93 95 } 94 96 95 97 pub fn equality(tokens: List(Token)) -> ParserResult { 96 - bin_left(tokens, [scan.BangEqual, scan.EqualEqual], comparison) 98 + let ops = 99 + from_list([ 100 + #(scan.BangEqual, BinOpNotEquals), 101 + #(scan.EqualEqual, BinOpEquals), 102 + ]) 103 + bin_left(tokens, ops, comparison) 97 104 } 98 105 99 106 pub fn comparison(tokens: List(Token)) -> ParserResult { 100 - bin_left( 101 - tokens, 102 - [scan.Greater, scan.GreaterEqual, scan.Less, scan.LessEqual], 103 - term, 104 - ) 107 + let ops = 108 + from_list([ 109 + #(scan.Greater, BinOpGT), 110 + #(scan.GreaterEqual, BinOpGTE), 111 + #(scan.Less, BinOpLT), 112 + #(scan.LessEqual, BinOpLTE), 113 + ]) 114 + bin_left(tokens, ops, term) 105 115 } 106 116 107 117 pub fn term(tokens: List(Token)) -> ParserResult { 108 - bin_left(tokens, [scan.Plus, scan.Minus], factor) 118 + let ops = 119 + from_list([ 120 + #(scan.Plus, BinOpPlus), 121 + #(scan.Minus, BinOpMinus), 122 + ]) 123 + bin_left(tokens, ops, factor) 109 124 } 110 125 111 126 pub fn factor(tokens: List(Token)) -> ParserResult { 112 - bin_left(tokens, [scan.Slash, scan.Star], unary) 127 + let ops = from_list([#(scan.Slash, BinOpDiv), #(scan.Star, BinOpMult)]) 128 + bin_left(tokens, ops, unary) 113 129 } 114 130 115 131 pub fn unary(tokens: List(Token)) -> Result(#(Expr, List(Token)), ParseError) { ··· 123 139 } 124 140 } 125 141 126 - pub fn primary(tokens: List(Token)) -> Result(#(Expr, List(Token)), ParseError) { 142 + pub fn primary( 143 + tokens: List(Token), 144 + ) -> Result(#(Expr, List(Token)), ParseError) { 127 145 case tokens { 128 146 [Token(token_type: scan.FalseKw, ..) as tok, ..rest] -> 129 - Ok(#(ExprLit(LitBool(tok)), rest)) 147 + Ok(#(ExprLit(LitBool(tok, False)), rest)) 130 148 [Token(token_type: scan.TrueKw, ..) as tok, ..rest] -> 131 - Ok(#(ExprLit(LitBool(tok)), rest)) 149 + Ok(#(ExprLit(LitBool(tok, True)), rest)) 132 150 [Token(token_type: scan.NilKw, ..), ..rest] -> Ok(#(ExprLit(LitNil), rest)) 133 - [Token(token_type: scan.StringTok, ..) as tok, ..rest] -> 134 - Ok(#(ExprLit(LitString(tok)), rest)) 135 - [Token(token_type: scan.NumberTok, ..) as tok, ..rest] -> 136 - Ok(#(ExprLit(LitNumber(tok)), rest)) 151 + [ 152 + Token( 153 + token_type: scan.StringTok, 154 + lexeme: _, 155 + literal: scan.LitString(s), 156 + .., 157 + ) as tok, 158 + ..rest 159 + ] -> Ok(#(ExprLit(LitString(tok, s)), rest)) 160 + [Token(token_type: scan.StringTok, ..) as tok, ..] -> 161 + Error(ParseError(Some(tok), "String with non-string literal?")) 162 + [ 163 + Token( 164 + token_type: scan.NumberTok, 165 + lexeme: _, 166 + literal: scan.LitNumber(n), 167 + .., 168 + ) as tok, 169 + ..rest 170 + ] -> Ok(#(ExprLit(LitNumber(tok, n)), rest)) 171 + [Token(token_type: scan.NumberTok, ..) as tok, ..] -> 172 + Error(ParseError(Some(tok), "Number with non-number literal?")) 137 173 [Token(token_type: scan.LeftParen, ..), ..rest] -> { 138 174 use #(expr, new_rest) <- result.try(expression(rest)) 139 175 case new_rest {
+10 -3
src/glox.gleam
··· 2 2 import ast_printer 3 3 import expr 4 4 import gleam/io 5 + import gleam/option.{None, Some} 5 6 import gleam/string 6 7 import input 7 8 import interpreter ··· 45 46 Ok(#(e, _)) -> { 46 47 let repr = ast_printer.print(e) 47 48 let val = interpreter.evaluate(e) 48 - #(repr, val) 49 + #(repr, Some(val)) 49 50 } 50 - Error(e) -> #("Error on parsing." <> e.msg, interpreter.NilVal) 51 + Error(e) -> #("Error on parsing." <> e.msg, None) 51 52 } 52 53 io.println("repr: " <> repr) 53 - io.println("val: " <> string.inspect(val)) 54 + case val { 55 + Some(Ok(val)) -> { 56 + io.println("val: " <> string.inspect(val)) 57 + } 58 + Some(Error(err)) -> io.println(interpreter.error_str(err)) 59 + None -> Nil 60 + } 54 61 Nil 55 62 }
+89 -61
src/interpreter.gleam
··· 1 1 import expr.{type Expr} 2 2 import gleam/float 3 + import gleam/int 4 + import gleam/result 3 5 import scan.{type Token} 4 6 5 7 pub type Value { ··· 9 11 NilVal 10 12 } 11 13 12 - pub fn evaluate(e: Expr) -> Value { 14 + pub type RuntimeError { 15 + RuntimeError(tok: Token, msg: String) 16 + } 17 + 18 + pub fn error_str(err: RuntimeError) -> String { 19 + err.msg <> "\n[line " <> int.to_string(err.tok.line) <> "]" 20 + } 21 + 22 + pub type EvalResult = 23 + Result(Value, RuntimeError) 24 + 25 + pub fn evaluate(e: Expr) -> EvalResult { 13 26 case e { 14 - expr.ExprLit(l) -> evaluate_literal(l) 27 + expr.ExprLit(l) -> Ok(evaluate_literal(l)) 15 28 expr.ExprBinary(l, op, r) -> evaluate_binary(l, op, r) 16 29 expr.ExprUnary(op, e) -> evaluate_unary(op, e) 17 30 expr.ExprGrouping(e) -> evaluate_grouping(e) 18 31 } 19 32 } 20 33 21 - fn evaluate_grouping(e: Expr) { 34 + fn evaluate_grouping(e: Expr) -> EvalResult { 22 35 evaluate(e) 23 36 } 24 37 25 - fn evaluate_unary(op: Token, e: Expr) { 26 - let v = evaluate(e) 38 + fn evaluate_unary(op: Token, e: Expr) -> EvalResult { 39 + use v <- result.try(evaluate(e)) 27 40 28 41 case op.token_type { 29 42 scan.Bang -> { 30 - BoolVal(!is_truthy(v)) 43 + Ok(BoolVal(!is_truthy(v))) 31 44 } 32 45 scan.Minus -> { 33 46 case v { 34 - NumberVal(nv) -> NumberVal(float.negate(nv)) 35 - _ -> todo as "Needs runtime error for non-numeric types" 47 + NumberVal(nv) -> Ok(NumberVal(float.negate(nv))) 48 + _ -> 49 + Error(RuntimeError( 50 + op, 51 + "Operand " <> op.lexeme <> " needs a numeric type.", 52 + )) 36 53 } 37 54 } 38 55 _ -> todo as "Shouldn't our type system restrict these?" 39 56 } 40 57 } 41 58 42 - fn evaluate_binary(l: Expr, op: Token, r: Expr) { 43 - let lv = evaluate(l) 44 - let rv = evaluate(r) 59 + fn evaluate_binary(l: Expr, op: expr.BinOp, r: Expr) -> EvalResult { 60 + use lv <- result.try(evaluate(l)) 61 + use rv <- result.try(evaluate(r)) 45 62 46 63 // I feel like this should be on a more restricted type than all tokens 47 - case op.token_type { 48 - scan.Plus -> { 64 + case op { 65 + expr.BinOpPlus(tok) -> { 49 66 case #(lv, rv) { 50 - #(NumberVal(lnv), NumberVal(rnv)) -> NumberVal(float.add(lnv, rnv)) 51 - #(StringVal(lsv), StringVal(rsv)) -> StringVal(lsv <> rsv) 52 - _ -> todo as "Needs runtime error for invalid types" 67 + #(NumberVal(lnv), NumberVal(rnv)) -> Ok(NumberVal(float.add(lnv, rnv))) 68 + #(StringVal(lsv), StringVal(rsv)) -> Ok(StringVal(lsv <> rsv)) 69 + _ -> 70 + Error(RuntimeError( 71 + op, 72 + "Operand " 73 + <> op.lexeme 74 + <> " requires both values to be both strings or both numbers.", 75 + )) 53 76 } 54 77 } 55 - scan.Minus -> numeric_op(lv, rv, fn(l, r) { float.subtract(l, r) }) 56 - scan.Star -> numeric_op(lv, rv, fn(l, r) { float.multiply(l, r) }) 57 - scan.Slash -> { 58 - numeric_op(lv, rv, fn(l, r) { 78 + expr.BinOpMinus(tok) -> 79 + numeric_op(tok, lv, rv, fn(l, r) { Ok(float.subtract(l, r)) }) 80 + expr.BinOpMult(tok) -> 81 + numeric_op(tok, lv, rv, fn(l, r) { Ok(float.multiply(l, r)) }) 82 + expr.BinOpDiv(tok) -> { 83 + numeric_op(tok, lv, rv, fn(l, r) { 59 84 case float.divide(l, r) { 60 - Ok(v) -> v 61 - _ -> todo as "Handle division errors" 85 + Ok(v) -> Ok(v) 86 + _ -> Error(RuntimeError(tok, "Divide by zero.")) 62 87 } 63 88 }) 64 89 } 65 - scan.Greater -> comparison_op(lv, rv, fn(l, r) { l >. r }) 66 - scan.GreaterEqual -> comparison_op(lv, rv, fn(l, r) { l >=. r }) 67 - scan.Less -> comparison_op(lv, rv, fn(l, r) { l <. r }) 68 - scan.LessEqual -> comparison_op(lv, rv, fn(l, r) { l <=. r }) 90 + expr.BinOpGT(tok) -> comparison_op(tok, lv, rv, fn(l, r) { l >. r }) 91 + expr.BinOpGTE(tok) -> comparison_op(tok, lv, rv, fn(l, r) { l >=. r }) 92 + expr.BinOpLT(tok) -> comparison_op(tok, lv, rv, fn(l, r) { l <. r }) 93 + expr.BinOpLTE(tok) -> comparison_op(tok, lv, rv, fn(l, r) { l <=. r }) 69 94 70 - scan.EqualEqual -> BoolVal(lv == rv) 71 - scan.BangEqual -> BoolVal(lv != rv) 72 - _ -> todo as "Handle other binary operators" 95 + expr.BinOpEquals(_) -> Ok(BoolVal(lv == rv)) 96 + expr.BinOpNotEquals(_) -> Ok(BoolVal(lv != rv)) 97 + 98 + expr.BinOpComma(_) -> Ok(rv) 73 99 } 74 100 } 75 101 76 - fn comparison_op(lv: Value, rv: Value, op: fn(Float, Float) -> Bool) -> Value { 102 + fn comparison_op( 103 + op: Token, 104 + lv: Value, 105 + rv: Value, 106 + f: fn(Float, Float) -> Bool, 107 + ) -> EvalResult { 77 108 case #(lv, rv) { 78 - #(NumberVal(lv), NumberVal(rv)) -> BoolVal(op(lv, rv)) 79 - _ -> todo as "Handle comparison error for non numeric types" 109 + #(NumberVal(lv), NumberVal(rv)) -> Ok(BoolVal(f(lv, rv))) 110 + _ -> 111 + Error(RuntimeError( 112 + op, 113 + "Operand " <> op.lexeme <> " requires both values to be numbers.", 114 + )) 80 115 } 81 116 } 82 117 83 - fn numeric_op(lv: Value, rv: Value, f: fn(Float, Float) -> Float) -> Value { 118 + fn numeric_op( 119 + op: Token, 120 + lv: Value, 121 + rv: Value, 122 + f: fn(Float, Float) -> Result(Float, RuntimeError), 123 + ) -> EvalResult { 84 124 case #(lv, rv) { 85 - #(NumberVal(lnv), NumberVal(rnv)) -> NumberVal(f(lnv, rnv)) 86 - _ -> 87 - todo as "Generic runtime error needs to be filled in here, or parameterized" 125 + #(NumberVal(lnv), NumberVal(rnv)) -> { 126 + use v <- result.try(f(lnv, rnv)) 127 + Ok(NumberVal(v)) 128 + } 129 + _ -> binary_op_error(op) 88 130 } 89 131 } 90 132 133 + fn binary_op_error(op: Token) -> EvalResult { 134 + Error(RuntimeError( 135 + op, 136 + "Operand " <> op.lexeme <> " requires both values to be numbers.", 137 + )) 138 + } 139 + 91 140 fn evaluate_literal(l: expr.ExprLit) -> Value { 92 141 case l { 93 - expr.LitString(t) -> { 94 - case t.literal { 95 - scan.LitString(s) -> StringVal(s) 96 - _ -> todo as "type system should have made this impossible no?" 97 - } 98 - } 99 - expr.LitNumber(t) -> { 100 - case t.literal { 101 - scan.LitNumber(n) -> NumberVal(n) 102 - _ -> todo as "type system should have made this impossible no?" 103 - } 104 - } 105 - expr.LitBool(t) -> { 106 - case t.token_type { 107 - scan.TrueKw -> BoolVal(True) 108 - scan.FalseKw -> BoolVal(False) 109 - // How can we do the typing here better? We can't just say it uses a subset of a type 110 - _ -> todo as "type system should have made this impossible no?" 111 - } 112 - } 142 + expr.LitString(_, s) -> StringVal(s) 143 + expr.LitNumber(_, n) -> NumberVal(n) 144 + expr.LitBool(_, b) -> BoolVal(b) 113 145 expr.LitNil -> NilVal 114 146 } 115 147 } 116 148 117 - fn is_equal(l: Value, r: Value) -> Bool { 118 - todo 119 - } 120 - 121 - fn is_truthy(v: Value) { 149 + fn is_truthy(v: Value) -> Bool { 122 150 case v { 123 151 BoolVal(b) -> b 124 152 NilVal -> False