Last active
December 13, 2024 18:01
-
-
Save thacuber2a03/2de3713169ae25e4722aa5e23066229e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// an excruciatingly overcomplicated calculator with memory written in Wren | |
// (made for learning how to do typechecking) | |
// run with the Wren CLI | |
// https://wren.io/cli | |
//////////////////////////////////////////////////////////////////// | |
class Token { | |
construct new(type, lexeme, line) { | |
_type = type | |
_lexeme = lexeme | |
_line = line | |
} | |
type { _type } | |
lexeme { _lexeme } | |
line { _line } | |
toString { | |
var s = "%(_type) " | |
if (_lexeme.count != 0) s = s + "'%(_lexeme)' " | |
return s + "(line %(_line))" | |
} | |
} | |
class Lexer { | |
construct new(str) { | |
if (!(str is String)) Fiber.abort("expected string as input") | |
_input = str | |
_start = _current = 0 | |
_line = 1 | |
if (!__keywords) __keywords = { | |
"bool": "BOOL", | |
"num": "NUM", | |
"true": "TRUE", | |
"false": "FALSE", | |
} | |
if (!__chars) __chars = { | |
"(": "LPAREN", | |
")": "RPAREN", | |
"+": "PLUS", | |
"-": "MINUS", | |
"*": "STAR", | |
"/": "SLASH", | |
"_": "UNDERSCORE", | |
"!": "BANG", | |
"=": "EQUALS", | |
"^": "CARET", | |
"<": "LESS", | |
">": "GREATER", | |
"@": "AT", | |
} | |
} | |
isDigit_(c) { | |
var b = c.bytes[0] | |
return b >= 48 && b <= 57 | |
} | |
isAlpha_(c) { | |
var b = c.bytes[0] | |
return b >= 97 && b <= 122 | |
} | |
advance_() { | |
var c = _input[_current] | |
_current = _current + 1 | |
if (c == "\n") _line = _line + 1 | |
return c | |
} | |
peek_ { | |
if (_current >= _input.count) return "\0" | |
return _input[_current] | |
} | |
isAtEnd_ { _current >= _input.count } | |
token_(type, text) { Token.new(type, text, _line) } | |
token_(type) { token_(type, _input[_start..._current]) } | |
number_() { | |
while (isDigit_(peek_)) advance_() | |
return token_("NUMBER") | |
} | |
word_() { | |
while (isAlpha_(peek_) || isDigit_(peek_)) { | |
advance_() | |
} | |
var id = _input[_start..._current] | |
var kw = __keywords[id] | |
if (kw) return token_(kw, "") | |
return token_("IDENTIFIER", id) | |
} | |
next() { | |
while (!isAtEnd_ && peek_.trim().count == 0) advance_() | |
if (isAtEnd_) return token_("EOF", "") | |
_start = _current | |
var c = advance_() | |
if (isDigit_(c)) return number_() | |
if (isAlpha_(c)) return word_() | |
if (__chars[c]) return token_(__chars[c]) | |
return token_("error", "unexpected character %(c)") | |
} | |
iterate(unused) { | |
var tok = next() | |
return tok.type != "EOF" ? tok : false | |
} | |
iteratorValue(tok) { tok } | |
} | |
//////////////////////////////////////////////////////////////////// | |
class Node { | |
visit_(v) { Fiber.abort("unimplemented visit_ for %(this.type)") } | |
} | |
class BinOp is Node { | |
construct new(a, op, b) { | |
_left = a | |
_operator = op | |
_right = b | |
} | |
left { _left } | |
operator { _operator } | |
right { _right } | |
toString { "BinOp(%(_left), %(_operator), %(_right))" } | |
visit_(v) { v.visitBinOp_(this) } | |
} | |
class UnOp is Node { | |
construct new(op, b) { | |
_operator = op | |
_value = b | |
} | |
operator { _operator } | |
value { _value } | |
toString { "UnOp(%(_operator), %(_value))" } | |
visit_(v) { v.visitUnOp_(this) } | |
} | |
class Statements is Node { | |
construct new(stmts) { | |
_statements = stmts | |
} | |
statements { _statements } | |
toString { "Statements(%(_statements))" } | |
visit_(v) { v.visitStatements_(this) } | |
} | |
class Assign is Node { | |
construct new(type, id, exp) { | |
_valueType = type | |
_identifier = id | |
_expression = exp | |
} | |
valueType { _valueType } | |
identifier { _identifier } | |
expression { _expression } | |
toString { "Assign(%(_valueType), %(_identifier), %(_expression))"} | |
visit_(v) { v.visitAssign_(this) } | |
} | |
class Variable is Node { | |
construct new(id) { | |
_identifier = id | |
} | |
identifier { _identifier } | |
toString { "Variable(%(_identifier))" } | |
visit_(v) { v.visitVariable_(this) } | |
} | |
class Literal is Node { | |
construct new(value) { _value = value } | |
value { _value } | |
toString { "Literal(%(_value))" } | |
visit_(v) { v.visitLiteral_(this) } | |
} | |
class Meta {} | |
class Type is Meta { | |
construct new(id) { | |
_identifier = id | |
} | |
identifier { _identifier } | |
toString { "Type(%(_identifier))" } | |
==(o) { | |
if (!(o is Type)) Fiber.abort("attempt to compare Type to %(o.type)") | |
return _identifier == o.identifier | |
} | |
!=(o) { !(this == o) } | |
} | |
var BOOLEAN_TYPE = Type.new("BOOLEAN") | |
var NUMBER_TYPE = Type.new("NUMBER") | |
//////////////////////////////////////////////////////////////////// | |
class ParserError { | |
construct new(token, message) { | |
_token = token | |
_message = message | |
} | |
token { _token } | |
message { _message } | |
toString { "error at %(_token.line): %(_message)" } | |
} | |
class Parser { | |
construct new(l) { | |
if (!((_l = l) is Lexer)) Fiber.abort("expected a lexer") | |
_cur = _next = null | |
_errors = [] | |
} | |
error_(t, msg) { | |
_errors.add(ParserError.new(t, msg)) | |
return false | |
} | |
errors { _errors } | |
advance_() { | |
_cur = _next | |
var error = false | |
while (true) { | |
_next = _l.next() | |
if (_next.type != "error") return !error | |
error_(_next, _next.lexeme) | |
error = true | |
} | |
} | |
check_(types) { | |
if (!(types is List)) return _cur.type == types | |
return types.any {|t| _cur.type == t} | |
} | |
isAtEnd_ { check_("EOF") } | |
consume_(t, msg) { | |
var cur = _cur | |
if (match_(t)) return cur | |
return error_(cur, msg) | |
} | |
consume_(t) { consume_(t, "expected %(t) but got %(_cur.type)") } | |
match_(t) { | |
if (check_(t)) { | |
advance_() | |
return true | |
} | |
return false | |
} | |
// primary = IDENTIFIER / NUMBER / TRUE / FALSE / (LPAREN expression RPAREN) | |
primary_() { | |
var cls | |
if (check_("IDENTIFIER")) { | |
cls = Variable | |
} else if (check_(["NUMBER", "TRUE", "FALSE"])) { | |
cls = Literal | |
} else if (match_("LPAREN")) { | |
var n = expr_() | |
if (!consume_("RPAREN", "expected ')' to close expression")) return | |
return n | |
} else { | |
return error_(_cur, "expected expression, got %(_cur.type)") | |
} | |
var n = cls.new(_cur) | |
advance_() | |
return n | |
} | |
// prefix = *1(BANG / AT) primary | |
prefix_() { | |
if (check_(["BANG", "AT"])) { | |
var t = _cur | |
advance_() | |
return UnOp.new(t, prefix_()) | |
} | |
return primary_() | |
} | |
// I can't be bothered to make a Pratt parser | |
// factor = 1*((star / slash) factor) | |
factor_() { | |
var left = prefix_() | |
while (check_(["STAR", "SLASH"])) { | |
var op = _cur | |
advance_() | |
var right = primary_() | |
if (!right) return | |
left = BinOp.new(left, op, right) | |
} | |
return left | |
} | |
// term = 1*((PLUS / MINUS) term) | |
term_() { | |
var left = factor_() | |
while (check_(["PLUS", "MINUS"])) { | |
var op = _cur | |
advance_() | |
var right = factor_() | |
if (!right) return | |
left = BinOp.new(left, op, right) | |
} | |
return left | |
} | |
// comparison = 1*((LESS / GREATER) comparison) | |
comparison_() { | |
var left = term_() | |
while (check_(["LESS", "GREATER"])) { | |
var op = _cur | |
advance_() | |
var right = term_() | |
if (!right) return | |
left = BinOp.new(left, op, right) | |
} | |
return left | |
} | |
// equality = 1*((EQUALS / CARET) equality) | |
equality_() { | |
var left = comparison_() | |
while (check_(["EQUALS", "CARET"])) { | |
var op = _cur | |
advance_() | |
var right = comparison_() | |
if (!right) return | |
left = BinOp.new(left, op, right) | |
} | |
return left | |
} | |
// end of code repetition | |
// expression = equality | |
expr_() { equality_() } | |
// type = BOOL / NUM | |
type_() { | |
var t | |
if (check_("BOOL")) { | |
t = BOOLEAN_TYPE | |
} else if (check_("NUM")) { | |
t = NUMBER_TYPE | |
} else { | |
advance_() | |
return error_(_cur, "expected type but got %(_cur.type)") | |
} | |
advance_() | |
return t | |
} | |
// assign = type (IDENTIFIER/UNDERSCORE) expression | |
assign_() { | |
var t | |
if (!(t = type_())) return | |
var id | |
if (!match_("UNDERSCORE")) { | |
if (!(id = consume_("IDENTIFIER"))) return | |
} | |
var e | |
if (!(e = expr_())) return | |
return Assign.new(t, id, e) | |
} | |
// program = *assign eof | |
program_() { | |
var statements = [] | |
while (!isAtEnd_) { | |
statements.add(assign_()) | |
} | |
return Statements.new(statements) | |
} | |
parse() { | |
if (!advance_()) return | |
if (!advance_()) return | |
var stmts = program_() | |
consume_("EOF") | |
return stmts | |
} | |
} | |
//////////////////////////////////////////////////////////////////// | |
class Visitor { | |
visit_(n) { n.visit_(this) } | |
visitBinOp_(n) { Fiber.abort("no visit_ method for BinOp") } | |
visitUnOp_(n) { Fiber.abort("no visit_ method for UnOp") } | |
visitAssign_(n) { Fiber.abort("no visit_ method for Assign") } | |
visitVariable_(n) { Fiber.abort("no visit_ method for Variable") } | |
visitLiteral_(n) { Fiber.abort("no visit_ method for Literal") } | |
} | |
//////////////////////////////////////////////////////////////////// | |
class SemaError { | |
construct new(node, message) { | |
_node = node | |
_message = message | |
} | |
node { _node } | |
message { _message } | |
toString { "error: %(_message)" } // TODO: use _node in error report somehow? | |
} | |
class Sema is Visitor { | |
construct new() { | |
_variables = {} | |
_errors = [] | |
} | |
error_(n, msg) { | |
_errors.add(SemaError.new(n, msg)) | |
return | |
} | |
errors { _errors } | |
check(ast) { | |
_errors.clear() | |
return visit_(ast) | |
} | |
visitStatements_(n) { | |
for (s in n.statements) if (!visit_(s)) return | |
return true | |
} | |
visitAssign_(n) { | |
var type = n.valueType | |
if (n.identifier) { | |
var id = n.identifier.lexeme | |
if (_variables[id] && _variables[id] != type) { | |
return error_(n, "attempt to reassign to '%(id)' with a declaration of a different type") | |
} | |
_variables[id] = type | |
} | |
var t | |
if (!(t = visit_(n.expression))) return | |
if (type != t) { | |
var s = n.identifier ? | |
"type mismatch in assignment to '%(n.identifier.lexeme)'" : | |
"type assertion failed" | |
return error_(n, "%(s): expected %(type.identifier), got %(t.identifier)") | |
} | |
return type | |
} | |
compatibleOp_(op, v) { compatibleOp_(v, op, null) } | |
compatibleOp_(a, op, b) { | |
// unary operations | |
if (op == "AT" ) return a == NUMBER_TYPE ? BOOLEAN_TYPE : NUMBER_TYPE | |
if (op == "BANG") return BOOLEAN_TYPE | |
if (!b) Fiber.abort("exhausted all unary op possibilities for '%(op)', none matched") | |
// optimization: if the types are different, it must be an equals comparison | |
if (a != b && op == "EQUALS") return BOOLEAN_TYPE | |
// binary operations | |
if (["PLUS", "MINUS", "STAR", "SLASH", "LESS", "GREATER"].contains(op) && | |
a == NUMBER_TYPE) return NUMBER_TYPE | |
// equality is compatible with any types | |
if (["EQUALS", "CARET"].contains(op)) return BOOLEAN_TYPE | |
return null // the operators matched no type | |
} | |
visitUnOp_(n) { | |
var val = visit_(n.value) | |
var t = compatibleOp_(n.operator.type, val) | |
if (!t) return error_(n, "type mismatch in unary '%(n.operator.lexeme)'") | |
return t | |
} | |
visitBinOp_(n) { | |
var a = visit_(n.left) | |
var b = visit_(n.right) | |
var t = compatibleOp_(a, n.operator.type, b) | |
if (!t) return error_(n, "type mismatch in binary '%(n.operator.lexeme)'") | |
return t | |
} | |
visitVariable_(n) { | |
var t = _variables[n.identifier.lexeme] | |
if (!t) return error_(n, "attempt to use undefined variable '%(n.identifier)'") | |
return t | |
} | |
visitLiteral_(n) { | |
if (n.value.type == "NUMBER") return NUMBER_TYPE | |
if (["TRUE", "FALSE"].contains(n.value.type)) return BOOLEAN_TYPE | |
Fiber.abort("unreachable") | |
} | |
} | |
//////////////////////////////////////////////////////////////////// | |
// ...and, as an extra, an interpreter | |
class Value { | |
toString { value.toString } | |
} | |
// itz monad time | |
class Number is Value { | |
construct new(n) { _value = n } | |
value { _value } | |
!=(o) { Number.new(_value != o.value) } | |
==(o) { Number.new(_value == o.value) } | |
+(o) { Number.new(_value + o.value) } | |
-(o) { Number.new(_value - o.value) } | |
*(o) { Number.new(_value * o.value) } | |
/(o) { Number.new(_value / o.value) } | |
>(o) { Number.new(_value > o.value) } | |
<(o) { Number.new(_value < o.value) } | |
! { Boolean.new(false) } | |
applyAt { _value != 0 } | |
} | |
class Boolean is Value { | |
construct new(n) { _value = n } | |
value { _value } | |
==(o) { Boolean.new(_value == o.value) } | |
!=(o) { Boolean.new(_value != o.value) } | |
! { Boolean.new(!_value) } | |
applyAt { _value ? 1 : 0 } | |
} | |
// end of monad time | |
class Interpreter is Visitor { | |
construct new() { | |
_variables = {} | |
_errors = [] // currently useless field is currently useless | |
} | |
errors { _errors } | |
interpret(ast) { | |
_errors.clear() | |
return visit_(ast) | |
} | |
visitStatements_(n) { | |
var final | |
for (s in n.statements) final = visit_(s) | |
return final | |
} | |
visitUnOp_(n) { | |
var value = visit_(n.value) | |
var op = n.operator.type | |
if (op == "BANG") return !value | |
if (op == "AT" ) return value.applyAt | |
Fiber.abort("unreachable") | |
} | |
visitBinOp_(n) { | |
var left = visit_(n.left) | |
var right = visit_(n.right) | |
var op = n.operator.type | |
if (op == "PLUS" ) return left + right | |
if (op == "MINUS" ) return left - right | |
if (op == "STAR" ) return left * right | |
if (op == "SLASH" ) return left / right | |
if (op == "LESS" ) return left < right | |
if (op == "GREATER") return left > right | |
if (op == "CARET" ) return left != right | |
if (op == "EQUALS" ) return left == right | |
Fiber.abort("unreachable") | |
} | |
visitLiteral_(n) { | |
if (n is Boolean) return Boolean.new(n.value.type == "TRUE") | |
return Number.new(Num.fromString(n.value.lexeme)) | |
} | |
visitAssign_(n) { | |
var v = visit_(n.expression) | |
// if not underscore | |
if (n.identifier) _variables[n.identifier.lexeme] = v | |
return v | |
} | |
visitVariable_(n) { _variables[n.identifier.lexeme] } | |
} | |
//////////////////////////////////////////////////////////////////// | |
import "io" for Stdin, Stdout | |
System.print("send 'quit' to... you know...") | |
var i = Interpreter.new() | |
var s = Sema.new() | |
while (true) { | |
System.write("> ") | |
Stdout.flush() | |
var input = Stdin.readLine() | |
if (input.trim() == "quit") break | |
var p = Parser.new(Lexer.new(input)) | |
var n = p.parse() | |
if (p.errors.count > 0) { | |
p.errors.each{|e| System.print(e)} | |
continue | |
} | |
if (!s.check(n)) { | |
s.errors.each{|e| System.print(e)} | |
continue | |
} | |
var v = i.interpret(n) | |
if (i.errors.count > 0) { | |
i.errors.each{|e| System.print(e)} | |
continue | |
} | |
System.print(v) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment