From b21ed593f5fe0a076a6456db2b9638638cdc52d5 Mon Sep 17 00:00:00 2001 From: "Brook, Daniel" Date: Sat, 9 Mar 2019 15:13:02 +0000 Subject: [PATCH 01/48] Clear out previous Kotlin implementation But keep the bones so I don't have to reinvent the build end of things. --- kotlin/src/mal/.dir-locals.el | 3 + kotlin/src/mal/core.kt | 238 ----------------------------- kotlin/src/mal/env.kt | 35 ----- kotlin/src/mal/printer.kt | 26 ---- kotlin/src/mal/reader.kt | 155 ------------------- kotlin/src/mal/readline.kt | 7 - kotlin/src/mal/step0_repl.kt | 19 --- kotlin/src/mal/step1_read_print.kt | 21 --- kotlin/src/mal/step2_eval.kt | 44 ------ kotlin/src/mal/step3_env.kt | 60 -------- kotlin/src/mal/step4_if_fn_do.kt | 102 ------------- kotlin/src/mal/step5_tco.kt | 102 ------------- kotlin/src/mal/step6_file.kt | 113 -------------- kotlin/src/mal/step7_quote.kt | 147 ------------------ kotlin/src/mal/step8_macros.kt | 179 ---------------------- kotlin/src/mal/step9_try.kt | 195 ----------------------- kotlin/src/mal/stepA_mal.kt | 198 ------------------------ kotlin/src/mal/types.kt | 221 --------------------------- 18 files changed, 3 insertions(+), 1862 deletions(-) create mode 100644 kotlin/src/mal/.dir-locals.el diff --git a/kotlin/src/mal/.dir-locals.el b/kotlin/src/mal/.dir-locals.el new file mode 100644 index 00000000..7a1f2069 --- /dev/null +++ b/kotlin/src/mal/.dir-locals.el @@ -0,0 +1,3 @@ +((nil . ((indent-tabs-mode . nil) + (tab-width . 4) + ))) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index bc312c8b..8b137891 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -1,239 +1 @@ -package mal -import java.io.File -import java.util.* - -val ns = hashMapOf( - envPair("+", { a: ISeq -> a.seq().reduce({ x, y -> x as MalInteger + y as MalInteger }) }), - envPair("-", { a: ISeq -> a.seq().reduce({ x, y -> x as MalInteger - y as MalInteger }) }), - envPair("*", { a: ISeq -> a.seq().reduce({ x, y -> x as MalInteger * y as MalInteger }) }), - envPair("/", { a: ISeq -> a.seq().reduce({ x, y -> x as MalInteger / y as MalInteger }) }), - - envPair("list", { a: ISeq -> MalList(a) }), - envPair("list?", { a: ISeq -> if (a.first() is MalList) TRUE else FALSE }), - envPair("empty?", { a: ISeq -> if (a.first() !is ISeq || !(a.first() as ISeq).seq().any()) TRUE else FALSE }), - envPair("count", { a: ISeq -> - if (a.first() is ISeq) MalInteger((a.first() as ISeq).count().toLong()) else MalInteger(0) - }), - - envPair("=", { a: ISeq -> pairwiseEquals(a) }), - envPair("<", { a: ISeq -> pairwiseCompare(a, { x, y -> x.value < y.value }) }), - envPair("<=", { a: ISeq -> pairwiseCompare(a, { x, y -> x.value <= y.value }) }), - envPair(">", { a: ISeq -> pairwiseCompare(a, { x, y -> x.value > y.value }) }), - envPair(">=", { a: ISeq -> pairwiseCompare(a, { x, y -> x.value >= y.value }) }), - - envPair("pr-str", { a: ISeq -> - MalString(a.seq().map({ it -> pr_str(it, print_readably = true) }).joinToString(" ")) - }), - envPair("str", { a: ISeq -> - MalString(a.seq().map({ it -> pr_str(it, print_readably = false) }).joinToString("")) - }), - envPair("prn", { a: ISeq -> - println(a.seq().map({ it -> pr_str(it, print_readably = true) }).joinToString(" ")) - NIL - }), - envPair("println", { a: ISeq -> - println(a.seq().map({ it -> pr_str(it, print_readably = false) }).joinToString(" ")) - NIL - }), - - envPair("read-string", { a: ISeq -> - val string = a.first() as? MalString ?: throw MalException("slurp requires a string parameter") - read_str(string.value) - }), - envPair("slurp", { a: ISeq -> - val name = a.first() as? MalString ?: throw MalException("slurp requires a filename parameter") - val text = File(name.value).readText() - MalString(text) - }), - - envPair("cons", { a: ISeq -> - val list = a.nth(1) as? ISeq ?: throw MalException("cons requires a list as its second parameter") - val mutableList = list.seq().toCollection(LinkedList()) - mutableList.addFirst(a.nth(0)) - MalList(mutableList) - }), - envPair("concat", { a: ISeq -> MalList(a.seq().flatMap({ it -> (it as ISeq).seq() }).toCollection(LinkedList())) }), - - envPair("nth", { a: ISeq -> - val list = a.nth(0) as? ISeq ?: throw MalException("nth requires a list as its first parameter") - val index = a.nth(1) as? MalInteger ?: throw MalException("nth requires an integer as its second parameter") - if (index.value >= list.count()) throw MalException("index out of bounds") - list.nth(index.value.toInt()) - }), - envPair("first", { a: ISeq -> - if (a.nth(0) == NIL) NIL - else { - val list = a.nth(0) as? ISeq ?: throw MalException("first requires a list parameter") - if (list.seq().any()) list.first() else NIL - } - }), - envPair("rest", { a: ISeq -> - if (a.nth(0) == NIL) MalList() - else { - val list = a.nth(0) as? ISeq ?: throw MalException("rest requires a list parameter") - MalList(list.rest()) - } - }), - - envPair("throw", { a: ISeq -> - val throwable = a.nth(0) - throw MalCoreException(pr_str(throwable), throwable) - }), - - envPair("apply", { a: ISeq -> - val function = a.nth(0) as MalFunction - val params = MalList() - a.seq().drop(1).forEach({ it -> - if (it is ISeq) { - it.seq().forEach({ x -> params.conj_BANG(x) }) - } else { - params.conj_BANG(it) - } - }) - function.apply(params) - }), - - envPair("map", { a: ISeq -> - val function = a.nth(0) as MalFunction - MalList((a.nth(1) as ISeq).seq().map({ it -> - val params = MalList() - params.conj_BANG(it) - function.apply(params) - }).toCollection(LinkedList())) - }), - - envPair("nil?", { a: ISeq -> if (a.nth(0) == NIL) TRUE else FALSE }), - envPair("true?", { a: ISeq -> if (a.nth(0) == TRUE) TRUE else FALSE }), - envPair("false?", { a: ISeq -> if (a.nth(0) == FALSE) TRUE else FALSE }), - envPair("string?", { a: ISeq -> - if (a.nth(0) is MalString && !(a.nth(0) is MalKeyword)) TRUE else FALSE - }), - envPair("symbol?", { a: ISeq -> if (a.nth(0) is MalSymbol) TRUE else FALSE }), - - envPair("symbol", { a: ISeq -> MalSymbol((a.nth(0) as MalString).value) }), - envPair("keyword", { a: ISeq -> - val param = a.nth(0) - if (param is MalKeyword) param else MalKeyword((a.nth(0) as MalString).value) - }), - envPair("keyword?", { a: ISeq -> if (a.nth(0) is MalKeyword) TRUE else FALSE }), - envPair("number?", { a: ISeq -> if (a.nth(0) is MalInteger) TRUE else FALSE }), - envPair("fn?", { a: ISeq -> if ((a.nth(0) as? MalFunction)?.is_macro ?: true) FALSE else TRUE }), - envPair("macro?", { a: ISeq -> if ((a.nth(0) as? MalFunction)?.is_macro ?: false) TRUE else FALSE }), - - envPair("vector", { a: ISeq -> MalVector(a) }), - envPair("vector?", { a: ISeq -> if (a.nth(0) is MalVector) TRUE else FALSE }), - - envPair("hash-map", { a: ISeq -> - val map = MalHashMap() - pairwise(a).forEach({ it -> map.assoc_BANG(it.first as MalString, it.second) }) - map - }), - envPair("map?", { a: ISeq -> if (a.nth(0) is MalHashMap) TRUE else FALSE }), - envPair("assoc", { a: ISeq -> - val map = MalHashMap(a.first() as MalHashMap) - pairwise(a.rest()).forEach({ it -> map.assoc_BANG(it.first as MalString, it.second) }) - map - }), - envPair("dissoc", { a: ISeq -> - val map = MalHashMap(a.first() as MalHashMap) - a.rest().seq().forEach({ it -> map.dissoc_BANG(it as MalString) }) - map - }), - envPair("get", { a: ISeq -> - val map = a.nth(0) as? MalHashMap - val key = a.nth(1) as MalString - map?.elements?.get(key) ?: NIL - }), - envPair("contains?", { a: ISeq -> - val map = a.nth(0) as? MalHashMap - val key = a.nth(1) as MalString - if (map?.elements?.get(key) != null) TRUE else FALSE - }), - envPair("keys", { a: ISeq -> - val map = a.nth(0) as MalHashMap - MalList(map.elements.keys.toCollection(LinkedList())) - }), - envPair("vals", { a: ISeq -> - val map = a.nth(0) as MalHashMap - MalList(map.elements.values.toCollection(LinkedList())) - }), - envPair("count", { a: ISeq -> - val seq = a.nth(0) as? ISeq - if (seq != null) MalInteger(seq.count().toLong()) else ZERO - }), - envPair("sequential?", { a: ISeq -> if (a.nth(0) is ISeq) TRUE else FALSE }), - - envPair("with-meta", { a: ISeq -> - val obj = a.nth(0) - val metadata = a.nth(1) - obj.with_meta(metadata) - }), - envPair("meta", { a: ISeq -> a.first().metadata }), - - envPair("conj", { a: ISeq -> (a.first() as ISeq).conj(a.rest()) }), - envPair("seq", { a: ISeq -> - val obj = a.nth(0) - if (obj is ISeq) { - if (obj.count() == 0) NIL - else MalList(obj.seq().toCollection(LinkedList())) - } else if (obj is MalString && !(obj is MalKeyword)) { - if (obj.value.length == 0) NIL - else { - var strs = obj.value.map({ c -> MalString(c.toString()) }) - MalList(strs.toCollection(LinkedList())) - } - } else { - NIL - } - }), - - envPair("atom", { a: ISeq -> MalAtom(a.first()) }), - envPair("atom?", { a: ISeq -> if (a.first() is MalAtom) TRUE else FALSE }), - envPair("deref", { a: ISeq -> (a.first() as MalAtom).value }), - envPair("reset!", { a: ISeq -> - val atom = a.nth(0) as MalAtom - val value = a.nth(1) - atom.value = value - value - }), - envPair("swap!", { a: ISeq -> - val atom = a.nth(0) as MalAtom - val function = a.nth(1) as MalFunction - - val params = MalList() - params.conj_BANG(atom.value) - a.seq().drop(2).forEach({ it -> params.conj_BANG(it) }) - - val value = function.apply(params) - atom.value = value - - value - }), - - envPair("readline", { a: ISeq -> - val prompt = a.first() as MalString - try { - MalString(readline(prompt.value)) - } catch (e: java.io.IOException) { - throw MalException(e.message) - } catch (e: EofException) { - NIL - } - }), - - envPair("time-ms", { a: ISeq -> MalInteger(System.currentTimeMillis()) }) -) - -private fun envPair(k: String, v: (ISeq) -> MalType): Pair = Pair(MalSymbol(k), MalFunction(v)) - -private fun pairwise(s: ISeq): List> { - val (keys, vals) = s.seq().withIndex().partition({ it -> it.index % 2 == 0 }) - return keys.map({ it -> it.value }).zip(vals.map({ it -> it.value })) -} - -private fun pairwiseCompare(s: ISeq, pred: (MalInteger, MalInteger) -> Boolean): MalConstant = - if (pairwise(s).all({ it -> pred(it.first as MalInteger, it.second as MalInteger) })) TRUE else FALSE - -private fun pairwiseEquals(s: ISeq): MalConstant = - if (pairwise(s).all({ it -> it.first == it.second })) TRUE else FALSE diff --git a/kotlin/src/mal/env.kt b/kotlin/src/mal/env.kt index fa7b5991..8b137891 100644 --- a/kotlin/src/mal/env.kt +++ b/kotlin/src/mal/env.kt @@ -1,36 +1 @@ -package mal -import java.util.* - -class Env(val outer: Env?, binds: Sequence?, exprs: Sequence?) { - val data = HashMap() - - init { - if (binds != null && exprs != null) { - val itb = binds.iterator() - val ite = exprs.iterator() - while (itb.hasNext()) { - val b = itb.next() - if (b.value != "&") { - set(b, if (ite.hasNext()) ite.next() else NIL) - } else { - if (!itb.hasNext()) throw MalException("expected a symbol name for varargs") - set(itb.next(), MalList(ite.asSequence().toCollection(LinkedList()))) - break - } - } - } - } - - constructor() : this(null, null, null) - constructor(outer: Env?) : this(outer, null, null) - - fun set(key: MalSymbol, value: MalType): MalType { - data.put(key.value, value) - return value - } - - fun find(key: MalSymbol): MalType? = data[key.value] ?: outer?.find(key) - - fun get(key: MalSymbol): MalType = find(key) ?: throw MalException("'${key.value}' not found") -} diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index 25a2233a..8b137891 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -1,27 +1 @@ -package mal -fun pr_str(malType: MalType, print_readably: Boolean = false): String = - when (malType) { - is MalInteger -> malType.value.toString() - is MalKeyword -> ":" + malType.value.substring(1) - is MalString -> - if (print_readably) { - "\"" + malType.value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + "\"" - } else malType.value - is MalConstant -> malType.value - is MalSymbol -> malType.value - is MalFunction -> "#" + malType - is MalCoreException -> pr_str(malType.value, print_readably) - is MalException -> "\"" + (malType.message ?: "exception") + "\"" - is MalList -> pr_str(malType.elements, "(", ")", print_readably) - is MalVector -> pr_str(malType.elements, "[", "]", print_readably) - is MalHashMap -> malType.elements.map({ it -> pr_str(it, print_readably) }).joinToString(" ", "{", "}") - is MalAtom -> "(atom " + pr_str(malType.value, print_readably) + ")" - else -> throw MalPrinterException("Unrecognized MalType: " + malType) - } - -private fun pr_str(coll: Collection, start: String, end: String, print_readably: Boolean = false): String = - coll.map({ it -> pr_str(it, print_readably) }).joinToString(" ", start, end) - -private fun pr_str(mapEntry: Map.Entry, print_readably: Boolean = false): String = - pr_str(mapEntry.key, print_readably) + " " + pr_str(mapEntry.value, print_readably) diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index 48b258e3..8b137891 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -1,156 +1 @@ -package mal -import kotlin.text.Regex - -val TOKEN_REGEX = Regex("[\\s,]*(~@|[\\[\\]{}()'`~^@]|\"(?:\\\\.|[^\\\\\"])*\"?|;.*|[^\\s\\[\\]{}('\"`,;)]*)") -val ATOM_REGEX = Regex("(^-?[0-9]+$)|(^nil$)|(^true$)|(^false$)|^\"(.*)\"$|^\"(.*)$|:(.*)|(^[^\"]*$)") - -class Reader(sequence: Sequence) { - val tokens = sequence.iterator() - var current = advance() - - fun next(): String? { - var result = current - current = advance() - return result - } - - fun peek(): String? = current - - private fun advance(): String? = if (tokens.hasNext()) tokens.next() else null -} - -fun read_str(input: String?): MalType { - val tokens = tokenizer(input) ?: return NIL - return read_form(Reader(tokens)) -} - -fun tokenizer(input: String?): Sequence? { - if (input == null) return null - - return TOKEN_REGEX.findAll(input) - .map({ it -> it.groups[1]?.value as String }) - .filter({ it != "" && !it.startsWith(";")}) -} - -fun read_form(reader: Reader): MalType = - when (reader.peek()) { - null -> throw MalContinue() - "(" -> read_list(reader) - ")" -> throw MalReaderException("expected form, got ')'") - "[" -> read_vector(reader) - "]" -> throw MalReaderException("expected form, got ']'") - "{" -> read_hashmap(reader) - "}" -> throw MalReaderException("expected form, got '}'") - "'" -> read_shorthand(reader, "quote") - "`" -> read_shorthand(reader, "quasiquote") - "~" -> read_shorthand(reader, "unquote") - "~@" -> read_shorthand(reader, "splice-unquote") - "^" -> read_with_meta(reader) - "@" -> read_shorthand(reader, "deref") - else -> read_atom(reader) - } - -fun read_list(reader: Reader): MalType = read_sequence(reader, MalList(), ")") -fun read_vector(reader: Reader): MalType = read_sequence(reader, MalVector(), "]") - -private fun read_sequence(reader: Reader, sequence: IMutableSeq, end: String): MalType { - reader.next() - - do { - val form = when (reader.peek()) { - null -> throw MalReaderException("expected '$end', got EOF") - end -> { reader.next(); null } - else -> read_form(reader) - } - - if (form != null) { - sequence.conj_BANG(form) - } - } while (form != null) - - return sequence -} - -fun read_hashmap(reader: Reader): MalType { - reader.next() - val hashMap = MalHashMap() - - do { - var value : MalType? = null; - val key = when (reader.peek()) { - null -> throw MalReaderException("expected '}', got EOF") - "}" -> { reader.next(); null } - else -> { - var key = read_form(reader) - if (key !is MalString) { - throw MalReaderException("hash-map keys must be strings or keywords") - } - value = when (reader.peek()) { - null -> throw MalReaderException("expected form, got EOF") - else -> read_form(reader) - } - key - } - } - - if (key != null) { - hashMap.assoc_BANG(key, value as MalType) - } - } while (key != null) - - return hashMap -} - -fun read_shorthand(reader: Reader, symbol: String): MalType { - reader.next() - - val list = MalList() - list.conj_BANG(MalSymbol(symbol)) - list.conj_BANG(read_form(reader)) - - return list -} - -fun read_with_meta(reader: Reader): MalType { - reader.next() - - val meta = read_form(reader) - val obj = read_form(reader) - - val list = MalList() - list.conj_BANG(MalSymbol("with-meta")) - list.conj_BANG(obj) - list.conj_BANG(meta) - - return list -} - -fun read_atom(reader: Reader): MalType { - val next = reader.next() ?: throw MalReaderException("Unexpected null token") - val groups = ATOM_REGEX.find(next)?.groups ?: throw MalReaderException("Unrecognized token: " + next) - - return if (groups[1]?.value != null) { - MalInteger(groups[1]?.value?.toLong() ?: throw MalReaderException("Error parsing number: " + next)) - } else if (groups[2]?.value != null) { - NIL - } else if (groups[3]?.value != null) { - TRUE - } else if (groups[4]?.value != null) { - FALSE - } else if (groups[5]?.value != null) { - MalString((groups[5]?.value as String).replace(Regex("""\\(.)""")) - { m: MatchResult -> - if (m.groups[1]?.value == "n") "\n" - else m.groups[1]?.value.toString() - }) - } else if (groups[6]?.value != null) { - throw MalReaderException("expected '\"', got EOF") - } else if (groups[7]?.value != null) { - MalKeyword(groups[7]?.value as String) - } else if (groups[8]?.value != null) { - MalSymbol(groups[8]?.value as String) - } else { - throw MalReaderException("Unrecognized token: " + next) - } -} diff --git a/kotlin/src/mal/readline.kt b/kotlin/src/mal/readline.kt index 97902386..8b137891 100644 --- a/kotlin/src/mal/readline.kt +++ b/kotlin/src/mal/readline.kt @@ -1,8 +1 @@ -package mal -class EofException : Exception("EOF") - -fun readline(prompt: String): String { - print(prompt) - return readLine() ?: throw EofException() -} diff --git a/kotlin/src/mal/step0_repl.kt b/kotlin/src/mal/step0_repl.kt index 1ced37d3..e69de29b 100644 --- a/kotlin/src/mal/step0_repl.kt +++ b/kotlin/src/mal/step0_repl.kt @@ -1,19 +0,0 @@ -package mal - -fun main(args: Array) { - fun read(input: String?): String? = input - fun eval(expression: String?): String? = expression - fun print(result: String?): String? = result - - while (true) { - val input = readline("user> ") - - try { - println(print(eval(read(input)))) - } catch (e: EofException) { - break - } catch (t: Throwable) { - println("Uncaught " + t + ": " + t.message) - } - } -} diff --git a/kotlin/src/mal/step1_read_print.kt b/kotlin/src/mal/step1_read_print.kt index 18ee081d..8b137891 100644 --- a/kotlin/src/mal/step1_read_print.kt +++ b/kotlin/src/mal/step1_read_print.kt @@ -1,22 +1 @@ -package mal -fun main(args: Array) { - fun read(input: String?): MalType = read_str(input) - fun eval(expression: MalType): MalType = expression - fun print(result: MalType) = pr_str(result, print_readably = true) - - while (true) { - val input = readline("user> ") - - try { - println(print(eval(read(input)))) - } catch (e: EofException) { - break - } catch (e: MalContinue) { - } catch (e: MalException) { - println("Error: " + e.message) - } catch (t: Throwable) { - println("Uncaught " + t + ": " + t.message) - } - } -} diff --git a/kotlin/src/mal/step2_eval.kt b/kotlin/src/mal/step2_eval.kt index 630745a1..8b137891 100644 --- a/kotlin/src/mal/step2_eval.kt +++ b/kotlin/src/mal/step2_eval.kt @@ -1,45 +1 @@ -package mal -fun read(input: String?): MalType = read_str(input) - -fun eval(ast: MalType, env: Map): MalType = - if (ast is MalList && ast.count() > 0) { - val evaluated = eval_ast(ast, env) as ISeq - if (evaluated.first() !is MalFunction) throw MalException("cannot execute non-function") - (evaluated.first() as MalFunction).apply(evaluated.rest()) - } else eval_ast(ast, env) - -fun eval_ast(ast: MalType, env: Map): MalType = - when (ast) { - is MalSymbol -> env[ast.value] ?: throw MalException("'${ast.value}' not found") - is MalList -> ast.elements.fold(MalList(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalVector -> ast.elements.fold(MalVector(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalHashMap -> ast.elements.entries.fold(MalHashMap(), { a, b -> a.assoc_BANG(b.key, eval(b.value, env)); a }) - else -> ast - } - -fun print(result: MalType) = pr_str(result, print_readably = true) - -fun main(args: Array) { - val env = hashMapOf( - Pair("+", MalFunction({ a: ISeq -> a.seq().reduce({ x, y -> x as MalInteger + y as MalInteger }) })), - Pair("-", MalFunction({ a: ISeq -> a.seq().reduce({ x, y -> x as MalInteger - y as MalInteger }) })), - Pair("*", MalFunction({ a: ISeq -> a.seq().reduce({ x, y -> x as MalInteger * y as MalInteger }) })), - Pair("/", MalFunction({ a: ISeq -> a.seq().reduce({ x, y -> x as MalInteger / y as MalInteger }) })) - ) - - while (true) { - val input = readline("user> ") - - try { - println(print(eval(read(input), env))) - } catch (e: EofException) { - break - } catch (e: MalContinue) { - } catch (e: MalException) { - println("Error: " + e.message) - } catch (t: Throwable) { - println("Uncaught " + t + ": " + t.message) - } - } -} diff --git a/kotlin/src/mal/step3_env.kt b/kotlin/src/mal/step3_env.kt index 021ac487..8b137891 100644 --- a/kotlin/src/mal/step3_env.kt +++ b/kotlin/src/mal/step3_env.kt @@ -1,61 +1 @@ -package mal -fun read(input: String?): MalType = read_str(input) - -fun eval(ast: MalType, env: Env): MalType = - if (ast is MalList && ast.count() > 0) { - val first = ast.first() - if (first is MalSymbol && first.value == "def!") { - env.set(ast.nth(1) as MalSymbol, eval(ast.nth(2), env)) - } else if (first is MalSymbol && first.value == "let*") { - val child = Env(env) - val bindings = ast.nth(1) - if (bindings !is ISeq) throw MalException("expected sequence as the first parameter to let*") - val it = bindings.seq().iterator() - while (it.hasNext()) { - val key = it.next() - if (!it.hasNext()) throw MalException("odd number of binding elements in let*") - val value = eval(it.next(), child) - child.set(key as MalSymbol, value) - } - eval(ast.nth(2), child) - } else { - val evaluated = eval_ast(ast, env) as ISeq - if (evaluated.first() !is MalFunction) throw MalException("cannot execute non-function") - (evaluated.first() as MalFunction).apply(evaluated.rest()) - } - } else eval_ast(ast, env) - -fun eval_ast(ast: MalType, env: Env): MalType = - when (ast) { - is MalSymbol -> env.get(ast) - is MalList -> ast.elements.fold(MalList(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalVector -> ast.elements.fold(MalVector(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalHashMap -> ast.elements.entries.fold(MalHashMap(), { a, b -> a.assoc_BANG(b.key, eval(b.value, env)); a }) - else -> ast - } - -fun print(result: MalType) = pr_str(result, print_readably = true) - -fun main(args: Array) { - val env = Env() - env.set(MalSymbol("+"), MalFunction({ a: ISeq -> a.seq().reduce({ x, y -> x as MalInteger + y as MalInteger }) })) - env.set(MalSymbol("-"), MalFunction({ a: ISeq -> a.seq().reduce({ x, y -> x as MalInteger - y as MalInteger }) })) - env.set(MalSymbol("*"), MalFunction({ a: ISeq -> a.seq().reduce({ x, y -> x as MalInteger * y as MalInteger }) })) - env.set(MalSymbol("/"), MalFunction({ a: ISeq -> a.seq().reduce({ x, y -> x as MalInteger / y as MalInteger }) })) - - while (true) { - val input = readline("user> ") - - try { - println(print(eval(read(input), env))) - } catch (e: EofException) { - break - } catch (e: MalContinue) { - } catch (e: MalException) { - println("Error: " + e.message) - } catch (t: Throwable) { - println("Uncaught " + t + ": " + t.message) - } - } -} diff --git a/kotlin/src/mal/step4_if_fn_do.kt b/kotlin/src/mal/step4_if_fn_do.kt index ff7ae5c5..8b137891 100644 --- a/kotlin/src/mal/step4_if_fn_do.kt +++ b/kotlin/src/mal/step4_if_fn_do.kt @@ -1,103 +1 @@ -package mal -fun read(input: String?): MalType = read_str(input) - -fun eval(ast: MalType, env: Env): MalType = - if (ast is MalList && ast.count() > 0) { - val first = ast.first() - if (first is MalSymbol) { - when (first.value) { - "def!" -> eval_def_BANG(ast, env) - "let*" -> eval_let_STAR(ast, env) - "fn*" -> eval_fn_STAR(ast, env) - "do" -> eval_do(ast, env) - "if" -> eval_if(ast, env) - else -> eval_function_call(ast, env) - } - } else eval_function_call(ast, env) - } else eval_ast(ast, env) - -private fun eval_def_BANG(ast: ISeq, env: Env): MalType = - env.set(ast.nth(1) as MalSymbol, eval(ast.nth(2), env)) - -private fun eval_let_STAR(ast: ISeq, env: Env): MalType { - val child = Env(env) - val bindings = ast.nth(1) as? ISeq ?: throw MalException("expected sequence as the first parameter to let*") - - val it = bindings.seq().iterator() - while (it.hasNext()) { - val key = it.next() - if (!it.hasNext()) throw MalException("odd number of binding elements in let*") - - val value = eval(it.next(), child) - child.set(key as MalSymbol, value) - } - - return eval(ast.nth(2), child) -} - -private fun eval_fn_STAR(ast: ISeq, env: Env): MalType { - val binds = ast.nth(1) as? ISeq ?: throw MalException("fn* requires a binding list as first parameter") - val symbols = binds.seq().filterIsInstance() - val body = ast.nth(2) - - return MalFunction({ s: ISeq -> - eval(body, Env(env, symbols, s.seq())) - }) -} - -private fun eval_do(ast: ISeq, env: Env): MalType = - (eval_ast(MalList(ast.rest()), env) as ISeq).seq().last() - -private fun eval_if(ast: ISeq, env: Env): MalType { - val check = eval(ast.nth(1), env) - - return if (check != NIL && check != FALSE) { - eval(ast.nth(2), env) - } else if (ast.count() > 3) { - eval(ast.nth(3), env) - } else NIL -} - -private fun eval_function_call(ast: ISeq, env: Env): MalType { - val evaluated = eval_ast(ast, env) as ISeq - val first = evaluated.first() as? MalFunction ?: throw MalException("cannot execute non-function") - return first.apply(evaluated.rest()) -} - -fun eval_ast(ast: MalType, env: Env): MalType = - when (ast) { - is MalSymbol -> env.get(ast) - is MalList -> ast.elements.fold(MalList(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalVector -> ast.elements.fold(MalVector(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalHashMap -> ast.elements.entries.fold(MalHashMap(), { a, b -> a.assoc_BANG(b.key, eval(b.value, env)); a }) - else -> ast - } - -fun print(result: MalType) = pr_str(result, print_readably = true) - -fun rep(input: String, env: Env): String = - print(eval(read(input), env)) - -fun main(args: Array) { - val repl_env = Env() - ns.forEach({ it -> repl_env.set(it.key, it.value) }) - - rep("(def! not (fn* (a) (if a false true)))", repl_env) - - while (true) { - val input = readline("user> ") - - try { - println(rep(input, repl_env)) - } catch (e: EofException) { - break - } catch (e: MalContinue) { - } catch (e: MalException) { - println("Error: " + e.message) - } catch (t: Throwable) { - println("Uncaught " + t + ": " + t.message) - t.printStackTrace() - } - } -} diff --git a/kotlin/src/mal/step5_tco.kt b/kotlin/src/mal/step5_tco.kt index cfc750f3..8b137891 100644 --- a/kotlin/src/mal/step5_tco.kt +++ b/kotlin/src/mal/step5_tco.kt @@ -1,103 +1 @@ -package mal -fun read(input: String?): MalType = read_str(input) - -fun eval(_ast: MalType, _env: Env): MalType { - var ast = _ast - var env = _env - - while (true) { - if (ast is MalList) { - if (ast.count() == 0) return ast - when ((ast.first() as? MalSymbol)?.value) { - "def!" -> return env.set(ast.nth(1) as MalSymbol, eval(ast.nth(2), env)) - "let*" -> { - val childEnv = Env(env) - val bindings = ast.nth(1) as? ISeq ?: throw MalException("expected sequence as the first parameter to let*") - - val it = bindings.seq().iterator() - while (it.hasNext()) { - val key = it.next() - if (!it.hasNext()) throw MalException("odd number of binding elements in let*") - childEnv.set(key as MalSymbol, eval(it.next(), childEnv)) - } - - env = childEnv - ast = ast.nth(2) - } - "fn*" -> return fn_STAR(ast, env) - "do" -> { - eval_ast(ast.slice(1, ast.count() - 1), env) - ast = ast.seq().last() - } - "if" -> { - val check = eval(ast.nth(1), env) - - if (check !== NIL && check !== FALSE) { - ast = ast.nth(2) - } else if (ast.count() > 3) { - ast = ast.nth(3) - } else return NIL - } - else -> { - val evaluated = eval_ast(ast, env) as ISeq - val firstEval = evaluated.first() - - when (firstEval) { - is MalFnFunction -> { - ast = firstEval.ast - env = Env(firstEval.env, firstEval.params, evaluated.rest().seq()) - } - is MalFunction -> return firstEval.apply(evaluated.rest()) - else -> throw MalException("cannot execute non-function") - } - } - } - } else return eval_ast(ast, env) - } -} - -fun eval_ast(ast: MalType, env: Env): MalType = - when (ast) { - is MalSymbol -> env.get(ast) - is MalList -> ast.elements.fold(MalList(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalVector -> ast.elements.fold(MalVector(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalHashMap -> ast.elements.entries.fold(MalHashMap(), { a, b -> a.assoc_BANG(b.key, eval(b.value, env)); a }) - else -> ast - } - -private fun fn_STAR(ast: MalList, env: Env): MalType { - val binds = ast.nth(1) as? ISeq ?: throw MalException("fn* requires a binding list as first parameter") - val params = binds.seq().filterIsInstance() - val body = ast.nth(2) - - return MalFnFunction(body, params, env, { s: ISeq -> eval(body, Env(env, params, s.seq())) }) -} - -fun print(result: MalType) = pr_str(result, print_readably = true) - -fun rep(input: String, env: Env): String = - print(eval(read(input), env)) - -fun main(args: Array) { - val repl_env = Env() - ns.forEach({ it -> repl_env.set(it.key, it.value) }) - - rep("(def! not (fn* (a) (if a false true)))", repl_env) - - while (true) { - val input = readline("user> ") - - try { - println(rep(input, repl_env)) - } catch (e: EofException) { - break - } catch (e: MalContinue) { - } catch (e: MalException) { - println("Error: " + e.message) - } catch (t: Throwable) { - println("Uncaught " + t + ": " + t.message) - t.printStackTrace() - } - } -} diff --git a/kotlin/src/mal/step6_file.kt b/kotlin/src/mal/step6_file.kt index bbb24a07..8b137891 100644 --- a/kotlin/src/mal/step6_file.kt +++ b/kotlin/src/mal/step6_file.kt @@ -1,114 +1 @@ -package mal -import java.util.* - -fun read(input: String?): MalType = read_str(input) - -fun eval(_ast: MalType, _env: Env): MalType { - var ast = _ast - var env = _env - - while (true) { - if (ast is MalList) { - if (ast.count() == 0) return ast - when ((ast.first() as? MalSymbol)?.value) { - "def!" -> return env.set(ast.nth(1) as MalSymbol, eval(ast.nth(2), env)) - "let*" -> { - val childEnv = Env(env) - val bindings = ast.nth(1) as? ISeq ?: throw MalException("expected sequence as the first parameter to let*") - - val it = bindings.seq().iterator() - while (it.hasNext()) { - val key = it.next() - if (!it.hasNext()) throw MalException("odd number of binding elements in let*") - childEnv.set(key as MalSymbol, eval(it.next(), childEnv)) - } - - env = childEnv - ast = ast.nth(2) - } - "fn*" -> return fn_STAR(ast, env) - "do" -> { - eval_ast(ast.slice(1, ast.count() - 1), env) - ast = ast.seq().last() - } - "if" -> { - val check = eval(ast.nth(1), env) - - if (check !== NIL && check !== FALSE) { - ast = ast.nth(2) - } else if (ast.count() > 3) { - ast = ast.nth(3) - } else return NIL - } - else -> { - val evaluated = eval_ast(ast, env) as ISeq - val firstEval = evaluated.first() - - when (firstEval) { - is MalFnFunction -> { - ast = firstEval.ast - env = Env(firstEval.env, firstEval.params, evaluated.rest().seq()) - } - is MalFunction -> return firstEval.apply(evaluated.rest()) - else -> throw MalException("cannot execute non-function") - } - } - } - } else return eval_ast(ast, env) - } -} - -fun eval_ast(ast: MalType, env: Env): MalType = - when (ast) { - is MalSymbol -> env.get(ast) - is MalList -> ast.elements.fold(MalList(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalVector -> ast.elements.fold(MalVector(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalHashMap -> ast.elements.entries.fold(MalHashMap(), { a, b -> a.assoc_BANG(b.key, eval(b.value, env)); a }) - else -> ast - } - -private fun fn_STAR(ast: MalList, env: Env): MalType { - val binds = ast.nth(1) as? ISeq ?: throw MalException("fn* requires a binding list as first parameter") - val params = binds.seq().filterIsInstance() - val body = ast.nth(2) - - return MalFnFunction(body, params, env, { s: ISeq -> eval(body, Env(env, params, s.seq())) }) -} - -fun print(result: MalType) = pr_str(result, print_readably = true) - -fun rep(input: String, env: Env): String = - print(eval(read(input), env)) - -fun main(args: Array) { - val repl_env = Env() - ns.forEach({ it -> repl_env.set(it.key, it.value) }) - - repl_env.set(MalSymbol("*ARGV*"), MalList(args.drop(1).map({ it -> MalString(it) }).toCollection(LinkedList()))) - repl_env.set(MalSymbol("eval"), MalFunction({ a: ISeq -> eval(a.first(), repl_env) })) - - rep("(def! not (fn* (a) (if a false true)))", repl_env) - rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env) - - if (args.any()) { - rep("(load-file \"${args[0]}\")", repl_env) - return - } - - while (true) { - val input = readline("user> ") - - try { - println(rep(input, repl_env)) - } catch (e: EofException) { - break - } catch (e: MalContinue) { - } catch (e: MalException) { - println("Error: " + e.message) - } catch (t: Throwable) { - println("Uncaught " + t + ": " + t.message) - t.printStackTrace() - } - } -} diff --git a/kotlin/src/mal/step7_quote.kt b/kotlin/src/mal/step7_quote.kt index b8d4ab46..8b137891 100644 --- a/kotlin/src/mal/step7_quote.kt +++ b/kotlin/src/mal/step7_quote.kt @@ -1,148 +1 @@ -package mal -import java.util.* - -fun read(input: String?): MalType = read_str(input) - -fun eval(_ast: MalType, _env: Env): MalType { - var ast = _ast - var env = _env - - while (true) { - if (ast is MalList) { - if (ast.count() == 0) return ast - when ((ast.first() as? MalSymbol)?.value) { - "def!" -> return env.set(ast.nth(1) as MalSymbol, eval(ast.nth(2), env)) - "let*" -> { - val childEnv = Env(env) - val bindings = ast.nth(1) as? ISeq ?: throw MalException("expected sequence as the first parameter to let*") - - val it = bindings.seq().iterator() - while (it.hasNext()) { - val key = it.next() - if (!it.hasNext()) throw MalException("odd number of binding elements in let*") - childEnv.set(key as MalSymbol, eval(it.next(), childEnv)) - } - - env = childEnv - ast = ast.nth(2) - } - "fn*" -> return fn_STAR(ast, env) - "do" -> { - eval_ast(ast.slice(1, ast.count() - 1), env) - ast = ast.seq().last() - } - "if" -> { - val check = eval(ast.nth(1), env) - - if (check !== NIL && check !== FALSE) { - ast = ast.nth(2) - } else if (ast.count() > 3) { - ast = ast.nth(3) - } else return NIL - } - "quote" -> return ast.nth(1) - "quasiquote" -> ast = quasiquote(ast.nth(1)) - else -> { - val evaluated = eval_ast(ast, env) as ISeq - val firstEval = evaluated.first() - - when (firstEval) { - is MalFnFunction -> { - ast = firstEval.ast - env = Env(firstEval.env, firstEval.params, evaluated.rest().seq()) - } - is MalFunction -> return firstEval.apply(evaluated.rest()) - else -> throw MalException("cannot execute non-function") - } - } - } - } else return eval_ast(ast, env) - } -} - -fun eval_ast(ast: MalType, env: Env): MalType = - when (ast) { - is MalSymbol -> env.get(ast) - is MalList -> ast.elements.fold(MalList(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalVector -> ast.elements.fold(MalVector(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalHashMap -> ast.elements.entries.fold(MalHashMap(), { a, b -> a.assoc_BANG(b.key, eval(b.value, env)); a }) - else -> ast - } - -private fun fn_STAR(ast: MalList, env: Env): MalType { - val binds = ast.nth(1) as? ISeq ?: throw MalException("fn* requires a binding list as first parameter") - val params = binds.seq().filterIsInstance() - val body = ast.nth(2) - - return MalFnFunction(body, params, env, { s: ISeq -> eval(body, Env(env, params, s.seq())) }) -} - -private fun is_pair(ast: MalType): Boolean = ast is ISeq && ast.seq().any() - -private fun quasiquote(ast: MalType): MalType { - if (!is_pair(ast)) { - val quoted = MalList() - quoted.conj_BANG(MalSymbol("quote")) - quoted.conj_BANG(ast) - return quoted - } - - val seq = ast as ISeq - var first = seq.first() - - if ((first as? MalSymbol)?.value == "unquote") { - return seq.nth(1) - } - - if (is_pair(first) && ((first as ISeq).first() as? MalSymbol)?.value == "splice-unquote") { - val spliced = MalList() - spliced.conj_BANG(MalSymbol("concat")) - spliced.conj_BANG(first.nth(1)) - spliced.conj_BANG(quasiquote(MalList(seq.seq().drop(1).toCollection(LinkedList())))) - return spliced - } - - val consed = MalList() - consed.conj_BANG(MalSymbol("cons")) - consed.conj_BANG(quasiquote(ast.first())) - consed.conj_BANG(quasiquote(MalList(seq.seq().drop(1).toCollection(LinkedList())))) - return consed -} - -fun print(result: MalType) = pr_str(result, print_readably = true) - -fun rep(input: String, env: Env): String = - print(eval(read(input), env)) - -fun main(args: Array) { - val repl_env = Env() - ns.forEach({ it -> repl_env.set(it.key, it.value) }) - - repl_env.set(MalSymbol("*ARGV*"), MalList(args.drop(1).map({ it -> MalString(it) }).toCollection(LinkedList()))) - repl_env.set(MalSymbol("eval"), MalFunction({ a: ISeq -> eval(a.first(), repl_env) })) - - rep("(def! not (fn* (a) (if a false true)))", repl_env) - rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env) - - if (args.any()) { - rep("(load-file \"${args[0]}\")", repl_env) - return - } - - while (true) { - val input = readline("user> ") - - try { - println(rep(input, repl_env)) - } catch (e: EofException) { - break - } catch (e: MalContinue) { - } catch (e: MalException) { - println("Error: " + e.message) - } catch (t: Throwable) { - println("Uncaught " + t + ": " + t.message) - t.printStackTrace() - } - } -} diff --git a/kotlin/src/mal/step8_macros.kt b/kotlin/src/mal/step8_macros.kt index 929ccfb2..8b137891 100644 --- a/kotlin/src/mal/step8_macros.kt +++ b/kotlin/src/mal/step8_macros.kt @@ -1,180 +1 @@ -package mal -import java.util.* - -fun read(input: String?): MalType = read_str(input) - -fun eval(_ast: MalType, _env: Env): MalType { - var ast = _ast - var env = _env - - while (true) { - ast = macroexpand(ast, env) - - if (ast is MalList) { - if (ast.count() == 0) return ast - when ((ast.first() as? MalSymbol)?.value) { - "def!" -> return env.set(ast.nth(1) as MalSymbol, eval(ast.nth(2), env)) - "let*" -> { - val childEnv = Env(env) - val bindings = ast.nth(1) as? ISeq ?: throw MalException("expected sequence as the first parameter to let*") - - val it = bindings.seq().iterator() - while (it.hasNext()) { - val key = it.next() - if (!it.hasNext()) throw MalException("odd number of binding elements in let*") - childEnv.set(key as MalSymbol, eval(it.next(), childEnv)) - } - - env = childEnv - ast = ast.nth(2) - } - "fn*" -> return fn_STAR(ast, env) - "do" -> { - eval_ast(ast.slice(1, ast.count() - 1), env) - ast = ast.seq().last() - } - "if" -> { - val check = eval(ast.nth(1), env) - - if (check !== NIL && check !== FALSE) { - ast = ast.nth(2) - } else if (ast.count() > 3) { - ast = ast.nth(3) - } else return NIL - } - "quote" -> return ast.nth(1) - "quasiquote" -> ast = quasiquote(ast.nth(1)) - "defmacro!" -> return defmacro(ast, env) - "macroexpand" -> return macroexpand(ast.nth(1), env) - else -> { - val evaluated = eval_ast(ast, env) as ISeq - val firstEval = evaluated.first() - - when (firstEval) { - is MalFnFunction -> { - ast = firstEval.ast - env = Env(firstEval.env, firstEval.params, evaluated.rest().seq()) - } - is MalFunction -> return firstEval.apply(evaluated.rest()) - else -> throw MalException("cannot execute non-function") - } - } - } - } else return eval_ast(ast, env) - } -} - -fun eval_ast(ast: MalType, env: Env): MalType = - when (ast) { - is MalSymbol -> env.get(ast) - is MalList -> ast.elements.fold(MalList(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalVector -> ast.elements.fold(MalVector(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalHashMap -> ast.elements.entries.fold(MalHashMap(), { a, b -> a.assoc_BANG(b.key, eval(b.value, env)); a }) - else -> ast - } - -private fun fn_STAR(ast: MalList, env: Env): MalType { - val binds = ast.nth(1) as? ISeq ?: throw MalException("fn* requires a binding list as first parameter") - val params = binds.seq().filterIsInstance() - val body = ast.nth(2) - - return MalFnFunction(body, params, env, { s: ISeq -> eval(body, Env(env, params, s.seq())) }) -} - -private fun is_pair(ast: MalType): Boolean = ast is ISeq && ast.seq().any() - -private fun quasiquote(ast: MalType): MalType { - if (!is_pair(ast)) { - val quoted = MalList() - quoted.conj_BANG(MalSymbol("quote")) - quoted.conj_BANG(ast) - return quoted - } - - val seq = ast as ISeq - var first = seq.first() - - if ((first as? MalSymbol)?.value == "unquote") { - return seq.nth(1) - } - - if (is_pair(first) && ((first as ISeq).first() as? MalSymbol)?.value == "splice-unquote") { - val spliced = MalList() - spliced.conj_BANG(MalSymbol("concat")) - spliced.conj_BANG(first.nth(1)) - spliced.conj_BANG(quasiquote(MalList(seq.seq().drop(1).toCollection(LinkedList())))) - return spliced - } - - val consed = MalList() - consed.conj_BANG(MalSymbol("cons")) - consed.conj_BANG(quasiquote(ast.first())) - consed.conj_BANG(quasiquote(MalList(seq.seq().drop(1).toCollection(LinkedList())))) - return consed -} - -private fun is_macro_call(ast: MalType, env: Env): Boolean { - val ast_list = ast as? MalList ?: return false - if (ast_list.count() == 0) return false - val symbol = ast_list.first() as? MalSymbol ?: return false - val function = env.find(symbol) as? MalFunction ?: return false - - return function.is_macro -} - -private fun macroexpand(_ast: MalType, env: Env): MalType { - var ast = _ast - while (is_macro_call(ast, env)) { - val symbol = (ast as MalList).first() as MalSymbol - val function = env.find(symbol) as MalFunction - ast = function.apply(ast.rest()) - } - return ast -} - -private fun defmacro(ast: MalList, env: Env): MalType { - val macro = eval(ast.nth(2), env) as MalFunction - macro.is_macro = true - - return env.set(ast.nth(1) as MalSymbol, macro) -} - -fun print(result: MalType) = pr_str(result, print_readably = true) - -fun rep(input: String, env: Env): String = - print(eval(read(input), env)) - -fun main(args: Array) { - val repl_env = Env() - ns.forEach({ it -> repl_env.set(it.key, it.value) }) - - repl_env.set(MalSymbol("*ARGV*"), MalList(args.drop(1).map({ it -> MalString(it) }).toCollection(LinkedList()))) - repl_env.set(MalSymbol("eval"), MalFunction({ a: ISeq -> eval(a.first(), repl_env) })) - - rep("(def! not (fn* (a) (if a false true)))", repl_env) - rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env) - rep("(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))", repl_env) - rep("(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) `(let* (or_FIXME ~(first xs)) (if or_FIXME or_FIXME (or ~@(rest xs))))))))", repl_env) - - if (args.any()) { - rep("(load-file \"${args[0]}\")", repl_env) - return - } - - while (true) { - val input = readline("user> ") - - try { - println(rep(input, repl_env)) - } catch (e: EofException) { - break - } catch (e: MalContinue) { - } catch (e: MalException) { - println("Error: " + e.message) - } catch (t: Throwable) { - println("Uncaught " + t + ": " + t.message) - t.printStackTrace() - } - } -} diff --git a/kotlin/src/mal/step9_try.kt b/kotlin/src/mal/step9_try.kt index 03d44f4e..8b137891 100644 --- a/kotlin/src/mal/step9_try.kt +++ b/kotlin/src/mal/step9_try.kt @@ -1,196 +1 @@ -package mal -import java.util.* - -fun read(input: String?): MalType = read_str(input) - -fun eval(_ast: MalType, _env: Env): MalType { - var ast = _ast - var env = _env - - while (true) { - ast = macroexpand(ast, env) - - if (ast is MalList) { - if (ast.count() == 0) return ast - when ((ast.first() as? MalSymbol)?.value) { - "def!" -> return env.set(ast.nth(1) as MalSymbol, eval(ast.nth(2), env)) - "let*" -> { - val childEnv = Env(env) - val bindings = ast.nth(1) as? ISeq ?: throw MalException("expected sequence as the first parameter to let*") - - val it = bindings.seq().iterator() - while (it.hasNext()) { - val key = it.next() - if (!it.hasNext()) throw MalException("odd number of binding elements in let*") - childEnv.set(key as MalSymbol, eval(it.next(), childEnv)) - } - - env = childEnv - ast = ast.nth(2) - } - "fn*" -> return fn_STAR(ast, env) - "do" -> { - eval_ast(ast.slice(1, ast.count() - 1), env) - ast = ast.seq().last() - } - "if" -> { - val check = eval(ast.nth(1), env) - - if (check !== NIL && check !== FALSE) { - ast = ast.nth(2) - } else if (ast.count() > 3) { - ast = ast.nth(3) - } else return NIL - } - "quote" -> return ast.nth(1) - "quasiquote" -> ast = quasiquote(ast.nth(1)) - "defmacro!" -> return defmacro(ast, env) - "macroexpand" -> return macroexpand(ast.nth(1), env) - "try*" -> return try_catch(ast, env) - else -> { - val evaluated = eval_ast(ast, env) as ISeq - val firstEval = evaluated.first() - - when (firstEval) { - is MalFnFunction -> { - ast = firstEval.ast - env = Env(firstEval.env, firstEval.params, evaluated.rest().seq()) - } - is MalFunction -> return firstEval.apply(evaluated.rest()) - else -> throw MalException("cannot execute non-function") - } - } - } - } else return eval_ast(ast, env) - } -} - -fun eval_ast(ast: MalType, env: Env): MalType = - when (ast) { - is MalSymbol -> env.get(ast) - is MalList -> ast.elements.fold(MalList(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalVector -> ast.elements.fold(MalVector(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalHashMap -> ast.elements.entries.fold(MalHashMap(), { a, b -> a.assoc_BANG(b.key, eval(b.value, env)); a }) - else -> ast - } - -private fun fn_STAR(ast: MalList, env: Env): MalType { - val binds = ast.nth(1) as? ISeq ?: throw MalException("fn* requires a binding list as first parameter") - val params = binds.seq().filterIsInstance() - val body = ast.nth(2) - - return MalFnFunction(body, params, env, { s: ISeq -> eval(body, Env(env, params, s.seq())) }) -} - -private fun is_pair(ast: MalType): Boolean = ast is ISeq && ast.seq().any() - -private fun quasiquote(ast: MalType): MalType { - if (!is_pair(ast)) { - val quoted = MalList() - quoted.conj_BANG(MalSymbol("quote")) - quoted.conj_BANG(ast) - return quoted - } - - val seq = ast as ISeq - var first = seq.first() - - if ((first as? MalSymbol)?.value == "unquote") { - return seq.nth(1) - } - - if (is_pair(first) && ((first as ISeq).first() as? MalSymbol)?.value == "splice-unquote") { - val spliced = MalList() - spliced.conj_BANG(MalSymbol("concat")) - spliced.conj_BANG(first.nth(1)) - spliced.conj_BANG(quasiquote(MalList(seq.seq().drop(1).toCollection(LinkedList())))) - return spliced - } - - val consed = MalList() - consed.conj_BANG(MalSymbol("cons")) - consed.conj_BANG(quasiquote(ast.first())) - consed.conj_BANG(quasiquote(MalList(seq.seq().drop(1).toCollection(LinkedList())))) - return consed -} - -private fun is_macro_call(ast: MalType, env: Env): Boolean { - val ast_list = ast as? MalList ?: return false - if (ast_list.count() == 0) return false - val symbol = ast_list.first() as? MalSymbol ?: return false - val function = env.find(symbol) as? MalFunction ?: return false - - return function.is_macro -} - -private fun macroexpand(_ast: MalType, env: Env): MalType { - var ast = _ast - while (is_macro_call(ast, env)) { - val symbol = (ast as MalList).first() as MalSymbol - val function = env.find(symbol) as MalFunction - ast = function.apply(ast.rest()) - } - return ast -} - -private fun defmacro(ast: MalList, env: Env): MalType { - val macro = eval(ast.nth(2), env) as MalFunction - macro.is_macro = true - - return env.set(ast.nth(1) as MalSymbol, macro) -} - -private fun try_catch(ast: MalList, env: Env): MalType = - try { - eval(ast.nth(1), env) - } catch (e: Exception) { - if (ast.count() < 3) { throw e } - val thrown = if (e is MalException) e else MalException(e.message) - val symbol = (ast.nth(2) as MalList).nth(1) as MalSymbol - - val catchBody = (ast.nth(2) as MalList).nth(2) - val catchEnv = Env(env) - catchEnv.set(symbol, thrown) - - eval(catchBody, catchEnv) - } - -fun print(result: MalType) = pr_str(result, print_readably = true) - -fun rep(input: String, env: Env): String = - print(eval(read(input), env)) - -fun main(args: Array) { - val repl_env = Env() - ns.forEach({ it -> repl_env.set(it.key, it.value) }) - - repl_env.set(MalSymbol("*ARGV*"), MalList(args.drop(1).map({ it -> MalString(it) }).toCollection(LinkedList()))) - repl_env.set(MalSymbol("eval"), MalFunction({ a: ISeq -> eval(a.first(), repl_env) })) - - rep("(def! not (fn* (a) (if a false true)))", repl_env) - rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env) - rep("(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))", repl_env) - rep("(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) `(let* (or_FIXME ~(first xs)) (if or_FIXME or_FIXME (or ~@(rest xs))))))))", repl_env) - - if (args.any()) { - rep("(load-file \"${args[0]}\")", repl_env) - return - } - - while (true) { - val input = readline("user> ") - - try { - println(rep(input, repl_env)) - } catch (e: EofException) { - break - } catch (e: MalContinue) { - } catch (e: MalException) { - println("Error: " + e.message) - } catch (t: Throwable) { - println("Uncaught " + t + ": " + t.message) - t.printStackTrace() - } - } -} diff --git a/kotlin/src/mal/stepA_mal.kt b/kotlin/src/mal/stepA_mal.kt index 5bf73632..8b137891 100644 --- a/kotlin/src/mal/stepA_mal.kt +++ b/kotlin/src/mal/stepA_mal.kt @@ -1,199 +1 @@ -package mal -import java.util.* - -fun read(input: String?): MalType = read_str(input) - -fun eval(_ast: MalType, _env: Env): MalType { - var ast = _ast - var env = _env - - while (true) { - ast = macroexpand(ast, env) - - if (ast is MalList) { - if (ast.count() == 0) return ast - when ((ast.first() as? MalSymbol)?.value) { - "def!" -> return env.set(ast.nth(1) as MalSymbol, eval(ast.nth(2), env)) - "let*" -> { - val childEnv = Env(env) - val bindings = ast.nth(1) as? ISeq ?: throw MalException("expected sequence as the first parameter to let*") - - val it = bindings.seq().iterator() - while (it.hasNext()) { - val key = it.next() - if (!it.hasNext()) throw MalException("odd number of binding elements in let*") - childEnv.set(key as MalSymbol, eval(it.next(), childEnv)) - } - - env = childEnv - ast = ast.nth(2) - } - "fn*" -> return fn_STAR(ast, env) - "do" -> { - eval_ast(ast.slice(1, ast.count() - 1), env) - ast = ast.seq().last() - } - "if" -> { - val check = eval(ast.nth(1), env) - - if (check !== NIL && check !== FALSE) { - ast = ast.nth(2) - } else if (ast.count() > 3) { - ast = ast.nth(3) - } else return NIL - } - "quote" -> return ast.nth(1) - "quasiquote" -> ast = quasiquote(ast.nth(1)) - "defmacro!" -> return defmacro(ast, env) - "macroexpand" -> return macroexpand(ast.nth(1), env) - "try*" -> return try_catch(ast, env) - else -> { - val evaluated = eval_ast(ast, env) as ISeq - val firstEval = evaluated.first() - - when (firstEval) { - is MalFnFunction -> { - ast = firstEval.ast - env = Env(firstEval.env, firstEval.params, evaluated.rest().seq()) - } - is MalFunction -> return firstEval.apply(evaluated.rest()) - else -> throw MalException("cannot execute non-function") - } - } - } - } else return eval_ast(ast, env) - } -} - -fun eval_ast(ast: MalType, env: Env): MalType = - when (ast) { - is MalSymbol -> env.get(ast) - is MalList -> ast.elements.fold(MalList(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalVector -> ast.elements.fold(MalVector(), { a, b -> a.conj_BANG(eval(b, env)); a }) - is MalHashMap -> ast.elements.entries.fold(MalHashMap(), { a, b -> a.assoc_BANG(b.key, eval(b.value, env)); a }) - else -> ast - } - -private fun fn_STAR(ast: MalList, env: Env): MalType { - val binds = ast.nth(1) as? ISeq ?: throw MalException("fn* requires a binding list as first parameter") - val params = binds.seq().filterIsInstance() - val body = ast.nth(2) - - return MalFnFunction(body, params, env, { s: ISeq -> eval(body, Env(env, params, s.seq())) }) -} - -private fun is_pair(ast: MalType): Boolean = ast is ISeq && ast.seq().any() - -private fun quasiquote(ast: MalType): MalType { - if (!is_pair(ast)) { - val quoted = MalList() - quoted.conj_BANG(MalSymbol("quote")) - quoted.conj_BANG(ast) - return quoted - } - - val seq = ast as ISeq - var first = seq.first() - - if ((first as? MalSymbol)?.value == "unquote") { - return seq.nth(1) - } - - if (is_pair(first) && ((first as ISeq).first() as? MalSymbol)?.value == "splice-unquote") { - val spliced = MalList() - spliced.conj_BANG(MalSymbol("concat")) - spliced.conj_BANG(first.nth(1)) - spliced.conj_BANG(quasiquote(MalList(seq.seq().drop(1).toCollection(LinkedList())))) - return spliced - } - - val consed = MalList() - consed.conj_BANG(MalSymbol("cons")) - consed.conj_BANG(quasiquote(ast.first())) - consed.conj_BANG(quasiquote(MalList(seq.seq().drop(1).toCollection(LinkedList())))) - return consed -} - -private fun is_macro_call(ast: MalType, env: Env): Boolean { - val ast_list = ast as? MalList ?: return false - if (ast_list.count() == 0) return false - val symbol = ast_list.first() as? MalSymbol ?: return false - val function = env.find(symbol) as? MalFunction ?: return false - - return function.is_macro -} - -private fun macroexpand(_ast: MalType, env: Env): MalType { - var ast = _ast - while (is_macro_call(ast, env)) { - val symbol = (ast as MalList).first() as MalSymbol - val function = env.find(symbol) as MalFunction - ast = function.apply(ast.rest()) - } - return ast -} - -private fun defmacro(ast: MalList, env: Env): MalType { - val macro = eval(ast.nth(2), env) as MalFunction - macro.is_macro = true - - return env.set(ast.nth(1) as MalSymbol, macro) -} - -private fun try_catch(ast: MalList, env: Env): MalType = - try { - eval(ast.nth(1), env) - } catch (e: Exception) { - if (ast.count() < 3) { throw e } - val thrown = if (e is MalException) e else MalException(e.message) - val symbol = (ast.nth(2) as MalList).nth(1) as MalSymbol - - val catchBody = (ast.nth(2) as MalList).nth(2) - val catchEnv = Env(env) - catchEnv.set(symbol, thrown) - - eval(catchBody, catchEnv) - } - -fun print(result: MalType) = pr_str(result, print_readably = true) - -fun rep(input: String, env: Env): String = - print(eval(read(input), env)) - -fun main(args: Array) { - val repl_env = Env() - ns.forEach({ it -> repl_env.set(it.key, it.value) }) - - repl_env.set(MalSymbol("*host-language*"), MalString("kotlin")) - repl_env.set(MalSymbol("*ARGV*"), MalList(args.drop(1).map({ it -> MalString(it) }).toCollection(LinkedList()))) - repl_env.set(MalSymbol("eval"), MalFunction({ a: ISeq -> eval(a.first(), repl_env) })) - - rep("(def! not (fn* (a) (if a false true)))", repl_env) - rep("(def! load-file (fn* (f) (eval (read-string (str \"(do \" (slurp f) \")\")))))", repl_env) - rep("(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw \"odd number of forms to cond\")) (cons 'cond (rest (rest xs)))))))", repl_env) - rep("(def! *gensym-counter* (atom 0))", repl_env) - rep("(def! gensym (fn* [] (symbol (str \"G__\" (swap! *gensym-counter* (fn* [x] (+ 1 x)))))))", repl_env) - rep("(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) (let* (condvar (gensym)) `(let* (~condvar ~(first xs)) (if ~condvar ~condvar (or ~@(rest xs)))))))))", repl_env) - - if (args.any()) { - rep("(load-file \"${args[0]}\")", repl_env) - return - } - - rep("(println (str \"Mal [\" *host-language* \"]\"))", repl_env) - while (true) { - val input = readline("user> ") - try { - println(rep(input, repl_env)) - } catch (e: EofException) { - break - } catch (e: MalContinue) { - } catch (e: MalException) { - println("Error: " + e.message) - } catch (t: Throwable) { - println("Uncaught " + t + ": " + t.message) - t.printStackTrace() - } - } -} diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index a981f4ec..8b137891 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -1,222 +1 @@ -package mal -import java.util.* - -open class MalException(message: String?) : Exception(message), MalType { - override var metadata: MalType = NIL - override fun with_meta(meta: MalType): MalType { - val exception = MalException(message) - exception.metadata = meta - return exception - } -} - -class MalContinue() : MalException("continue") -class MalReaderException(message: String) : MalException(message) -class MalPrinterException(message: String) : MalException(message) - -class MalCoreException(message: String, val value: MalType) : MalException(message) { - override fun with_meta(meta: MalType): MalType { - val exception = MalCoreException(message as String, value) - exception.metadata = meta - return exception - } -} - -interface MalType { - var metadata: MalType - fun with_meta(meta: MalType): MalType -} - -open class MalConstant(val value: String) : MalType { - override var metadata: MalType = NIL - - override fun equals(other: Any?): Boolean = other is MalConstant && value.equals(other.value) - override fun hashCode(): Int = value.hashCode() - - override fun with_meta(meta: MalType): MalType { - val obj = MalConstant(value) - obj.metadata = meta - return obj - } -} - -class MalInteger(val value: Long) : MalType { - override var metadata: MalType = NIL - - operator fun plus(a: MalInteger): MalInteger = MalInteger(value + a.value) - operator fun minus(a: MalInteger): MalInteger = MalInteger(value - a.value) - operator fun times(a: MalInteger): MalInteger = MalInteger(value * a.value) - operator fun div(a: MalInteger): MalInteger = MalInteger(value / a.value) - operator fun compareTo(a: MalInteger): Int = value.compareTo(a.value) - - override fun equals(other: Any?): Boolean = other is MalInteger && value.equals(other.value) - - override fun with_meta(meta: MalType): MalType { - val obj = MalInteger(value) - obj.metadata = meta - return obj - } -} - -class MalSymbol(val value: String) : MalType { - override var metadata: MalType = NIL - - override fun equals(other: Any?): Boolean = other is MalSymbol && value.equals(other.value) - - override fun with_meta(meta: MalType): MalType { - val obj = MalSymbol(value) - obj.metadata = meta - return obj - } -} - -open class MalString(value: String) : MalConstant(value) { - override fun with_meta(meta: MalType): MalType { - val obj = MalString(value) - obj.metadata = meta - return obj - } -} - -class MalKeyword(value: String) : MalString("\u029E" + value) { - override fun with_meta(meta: MalType): MalType { - val obj = MalKeyword(value) - obj.metadata = meta - return obj - } -} - -interface ILambda : MalType { - fun apply(seq: ISeq): MalType -} - -open class MalFunction(val lambda: (ISeq) -> MalType) : MalType, ILambda { - var is_macro: Boolean = false - override var metadata: MalType = NIL - - override fun apply(seq: ISeq): MalType = lambda(seq) - - override fun with_meta(meta: MalType): MalType { - val obj = MalFunction(lambda) - obj.metadata = meta - return obj - } -} - -class MalFnFunction(val ast: MalType, val params: Sequence, val env: Env, lambda: (ISeq) -> MalType) : MalFunction(lambda) { - override fun with_meta(meta: MalType): MalType { - val obj = MalFnFunction(ast, params, env, lambda) - obj.metadata = meta - return obj - } -} - -interface ISeq : MalType { - fun seq(): Sequence - fun first(): MalType - fun rest(): ISeq - fun nth(n: Int): MalType - fun count(): Int - fun slice(fromIndex: Int, toIndex: Int): ISeq - fun conj(s: ISeq): ISeq -} - -interface IMutableSeq : ISeq { - fun conj_BANG(form: MalType) -} - -abstract class MalSequence(val elements: MutableList) : MalType, IMutableSeq { - override var metadata: MalType = NIL - - override fun seq(): Sequence = elements.asSequence() - override fun first(): MalType = elements.first() - override fun nth(n: Int): MalType = elements.elementAt(n) - override fun count(): Int = elements.count() - - override fun conj_BANG(form: MalType) { - elements.add(form) - } - - override fun equals(other: Any?): Boolean = - (other is ISeq) - && elements.size == other.count() - && elements.asSequence().zip(other.seq()).all({ it -> it.first == it.second }) -} - -class MalList(elements: MutableList) : MalSequence(elements) { - constructor() : this(LinkedList()) - constructor(s: ISeq) : this(s.seq().toCollection(LinkedList())) - - override fun rest(): ISeq = MalList(elements.drop(1).toCollection(LinkedList())) - - override fun slice(fromIndex: Int, toIndex: Int): MalList = - MalList(elements.subList(fromIndex, toIndex)) - - override fun conj(s: ISeq): ISeq { - val list = LinkedList(elements) - s.seq().forEach({ it -> list.addFirst(it) }) - return MalList(list) - } - - override fun with_meta(meta: MalType): MalType { - val obj = MalList(elements) - obj.metadata = meta - return obj - } -} - -class MalVector(elements: MutableList) : MalSequence(elements) { - override var metadata: MalType = NIL - - constructor() : this(ArrayList()) - constructor(s: ISeq) : this(s.seq().toCollection(ArrayList())) - - override fun rest(): ISeq = MalVector(elements.drop(1).toCollection(ArrayList())) - - override fun slice(fromIndex: Int, toIndex: Int): MalVector = - MalVector(elements.subList(fromIndex, toIndex)) - - override fun conj(s: ISeq): ISeq = MalVector(elements.plus(s.seq()).toCollection(ArrayList())) - - override fun with_meta(meta: MalType): MalType { - val obj = MalVector(elements) - obj.metadata = meta - return obj - } -} - -class MalHashMap() : MalType { - override var metadata: MalType = NIL - - val elements = HashMap() - - constructor(other: MalHashMap) : this() { - other.elements.forEach({ it -> assoc_BANG(it.key, it.value) }) - } - - fun assoc_BANG(key: MalString, value: MalType) = elements.put(key, value) - - fun dissoc_BANG(key: MalString) { - elements.remove(key) - } - - override fun with_meta(meta: MalType): MalType { - val obj = MalHashMap(this) - obj.metadata = meta - return obj - } - - override fun equals(other: Any?): Boolean = - (other is MalHashMap) && elements.equals(other.elements) -} - -class MalAtom(var value: MalType) : MalType { - override var metadata: MalType = NIL - override fun with_meta(meta: MalType): MalType = throw UnsupportedOperationException() -} - -val NIL = MalConstant("nil") -val TRUE = MalConstant("true") -val FALSE = MalConstant("false") -val ZERO = MalInteger(0) From 0d44331ca63290a7c7e8c7ef8e73b276e7ccf350 Mon Sep 17 00:00:00 2001 From: "Brook, Daniel" Date: Sat, 9 Mar 2019 15:13:51 +0000 Subject: [PATCH 02/48] Implement step0 --- kotlin/src/mal/step0_repl.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/kotlin/src/mal/step0_repl.kt b/kotlin/src/mal/step0_repl.kt index e69de29b..c7ae70c1 100644 --- a/kotlin/src/mal/step0_repl.kt +++ b/kotlin/src/mal/step0_repl.kt @@ -0,0 +1,14 @@ +fun READ(s: String) = s + +fun EVAL(s: String) = s + +fun PRINT(s: String) = println(s) + +fun rep(s: String) = PRINT(EVAL(READ(s))) + +fun main(args: Array) { + while(true) { + print("user> ") + readLine()?.let { rep(it) } + } +} From b20f07ae789a9acdd824f9a33de85d5bf22841c6 Mon Sep 17 00:00:00 2001 From: "Brook, Daniel" Date: Sat, 9 Mar 2019 16:11:51 +0000 Subject: [PATCH 03/48] Fix indenting --- kotlin/src/mal/.dir-locals.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlin/src/mal/.dir-locals.el b/kotlin/src/mal/.dir-locals.el index 7a1f2069..2201cbcf 100644 --- a/kotlin/src/mal/.dir-locals.el +++ b/kotlin/src/mal/.dir-locals.el @@ -1,3 +1,3 @@ ((nil . ((indent-tabs-mode . nil) - (tab-width . 4) + (kotlin-tab-width . 4) ))) From 5eb74c7f8d0a40dee8d76a389c63addd7230bd4c Mon Sep 17 00:00:00 2001 From: "Brook, Daniel" Date: Sat, 9 Mar 2019 16:12:38 +0000 Subject: [PATCH 04/48] Implement step1 [x] The tokenizing works so far. [x] Basic printing is working, but all tests aren't passing. [x] Useful tests are passing, unspecified ones aren't. So everything mentioned in step 1 is done, the deferable (and otherwise unspecified) ones aren't. It'll do. --- kotlin/src/mal/printer.kt | 13 +++- kotlin/src/mal/reader.kt | 107 +++++++++++++++++++++++++++++ kotlin/src/mal/step1_read_print.kt | 22 ++++++ kotlin/src/mal/types.kt | 16 +++++ 4 files changed, 157 insertions(+), 1 deletion(-) diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index 8b137891..03d303fb 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -1 +1,12 @@ - +// … does the opposite of read_str: take a mal data structure and +// return a string representation of it. +fun pr_str(v: MalType) : String { + return when(v) { + is MalList -> { + "(" + v.atoms.map { pr_str(it) }.joinToString(" ") + ")" + } + is MalNumber -> v.number.toString() + is MalSymbol -> v.sym + else -> "" + } +} diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index 8b137891..783b6c27 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -1 +1,108 @@ +// For each match captured within the parenthesis starting at char 6 +// of the regular expression a new token will be created. +var tokenizer = Regex(""" +# Matches any number of whitespaces or commas +[\s,]* +(?: + # Captures the special two-characters ~@ (tokenized). + ~@ | + # Captures any special single character, one of []{}()'`~^@ (tokenized). + [\[\]{}()'`~^@] | + # Starts capturing at a double-quote and stops at the next double-quote unless + # it was preceded by a backslash in which case it includes it until the next + # double-quote (tokenized). It will also match unbalanced strings (no ending + # double-quote) which should be reported as an error. + "(?:.|[^"])*"? | + # Captures any sequence of characters starting with ; (tokenized). + ;.* | + # Captures a sequence of zero or more non special characters (e.g. symbols, + # numbers, "true", "false", and "nil") and is sort of the inverse of the one + # above that captures special characters (tokenized). + [^\s\[\]{}('"`,;)]* +) +""", +RegexOption.COMMENTS +) +// This function will take a single string and return an array/list of all the tokens (strings) in it. +fun tokenize(s: String) : List { + // For some reason this fails where findAll doesn't. + // if (!tokenizer.matches(s)) { + // throw Exception("Failed tokenizing") + // } + return tokenizer.findAll(s) + .map { it.value.trim() } + .filter { it.length > 0 } + .toList() +} + +// This object will store the tokens and a position. +class Reader(val tokens: List) { + var pos = 0 + // returns the token at the current position and increments the position + fun next() : String { + val s = tokens[pos] + pos += 1 + return s + } + + // just returns the token at the current position. + fun peek() = tokens[pos] +} + +var readLimit = 0 + +// This function will peek at the first token in the Reader object and +// switch on the first character of that token. If the character is a +// left paren then read_list is called with the Reader +// object. Otherwise, read_atom is called with the Reader Object. The +// return value from read_form is a mal data type. +fun read_form(r: Reader) : MalType { + try { + return when(r.peek()) { + "(" -> read_list(r) + else -> read_atom(r) + } + } + catch(e: IndexOutOfBoundsException) { + throw Exception("Ran out of tokens, missing right paren?") + } + finally { + readLimit = 0 + } +} + +fun read_list(r: Reader) : MalList { +// println("Reading list: " + r) + r.next() // Move past the opening paren. + val list : MutableList = mutableListOf() + while(r.peek() != ")") { + list.add(read_form(r)) + // Safety limit to prevent the REPL never coming back. + readLimit += 1 + if (readLimit > 200) { + throw Exception("Parser found no end :(") + } + } + return MalList(list) +} + +fun is_number(s: String) = Regex("\\d+").matches(s) + +// This function will look at the contents of the token and return the +// appropriate scalar (simple/single) data type value. +fun read_atom(r: Reader) : MalAtom { +// println("Reading atom: " + r) + val t = r.next() + return if (is_number(t)) { + MalNumber(t.toInt()) + } + else { + MalSymbol(t) + } +} + +// This function will call tokenize and then create a new Reader +// object instance with the tokens. Then it will call read_form with +// the Reader instance. +fun read_str(s: String) = read_form(Reader(tokenize(s))) diff --git a/kotlin/src/mal/step1_read_print.kt b/kotlin/src/mal/step1_read_print.kt index 8b137891..1b3b1765 100644 --- a/kotlin/src/mal/step1_read_print.kt +++ b/kotlin/src/mal/step1_read_print.kt @@ -1 +1,23 @@ +fun READ(s: String) = read_str(s) + +fun EVAL(s: MalType) = s + +fun PRINT(v: MalType) = pr_str(v) + +fun rep(s: String) { + println(PRINT(EVAL(READ(s)))) +} + +fun main(args: Array) { + while(true) { + print("user> ") + + try { + readLine()?.let { rep(it) } + } + catch(e: Exception) { + println("Oh dear:" + e.toString()) + } + } +} diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index 8b137891..0d539534 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -1 +1,17 @@ +// If your target language is statically typed then you will need some +// way for read_form to return a variant or subclass type. For example, +// if your language is object oriented, then you can define a top level +// MalType (in types.qx) that all your mal data types inherit from. The +// MalList type (which also inherits from MalType) will contain a +// list/array of other MalTypes. If your language is dynamically typed +// then you can likely just return a plain list/array of other mal types. +interface MalType {} + +data class MalList(val atoms : List) : MalType + +interface MalAtom : MalType {} + +data class MalNumber(val number : Number) : MalAtom + +data class MalSymbol(val sym : String) : MalAtom From 05a56c3f7f1b1e4ef906664fbb8e11e71362a926 Mon Sep 17 00:00:00 2001 From: "Brook, Daniel" Date: Thu, 11 Jul 2019 21:58:27 +0100 Subject: [PATCH 05/48] Implement step2 Not all tests are passing but crucially a valid AST is produced! --- kotlin/src/mal/eval.kt | 57 ++++++++++++++++++++++++++ kotlin/src/mal/printer.kt | 3 +- kotlin/src/mal/reader.kt | 79 ++++++++++++++++++++++-------------- kotlin/src/mal/step2_eval.kt | 78 +++++++++++++++++++++++++++++++++++ kotlin/src/mal/types.kt | 12 +++++- 5 files changed, 195 insertions(+), 34 deletions(-) create mode 100644 kotlin/src/mal/eval.kt diff --git a/kotlin/src/mal/eval.kt b/kotlin/src/mal/eval.kt new file mode 100644 index 00000000..040092a6 --- /dev/null +++ b/kotlin/src/mal/eval.kt @@ -0,0 +1,57 @@ +val repl_env : Map = mapOf( + "+" to MalFunc({ a, b -> MalNumber((a as MalNumber).num + (b as MalNumber).num) }), + "-" to MalFunc({ a, b -> MalNumber((a as MalNumber).num - (b as MalNumber).num) }), + "*" to MalFunc({ a, b -> MalNumber((a as MalNumber).num * (b as MalNumber).num) }), + "/" to MalFunc({ a, b -> MalNumber((a as MalNumber).num / (b as MalNumber).num) }) +) + +// Create a new function eval_ast which takes ast (mal data type) and +// an associative structure (the environment from above). eval_ast +// switches on the type of ast as follows: +// +// * symbol: lookup the symbol in the environment structure and return the value or raise an error if no value is found +// * list: return a new list that is the result of calling EVAL on each of the members of the list +// * otherwise just return the original ast value + +fun eval_ast(ast: MalType, env: Map, depth: Int) : MalType { +// println("eval_ast: ".repeat(depth) + pr_str(ast)) + return when(ast) { + is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth) }.toList()) + is MalSymbol -> env[ast.sym] ?: throw Exception("Unknown symbol '${ast.sym}'") + else -> ast + } +} + +// Modify EVAL to check if the first parameter ast is a list. +// * ast is not a list: then return the result of calling eval_ast on it. +// * ast is a empty list: return ast unchanged. +// * ast is a list: call eval_ast to get a new evaluated list. Take the first +// item of the evaluated list and call it as function using the rest of the +// evaluated list as its arguments. + +var eval_count = 0 +fun EVAL(ast: MalType, env: Map, depth: Int) : MalType { + println(" EVAL: ".repeat(depth) + pr_str(ast)) + + eval_count += 1 + if (depth > 200 || eval_count > 500) { + throw Exception("Recursion fail :(") + } + + if (ast !is MalList) { + return eval_ast(ast, env, depth) + } + else { + if (ast.atoms.isEmpty()) { + return ast + } + else { + println("calling "+ast.head()+" with "+ast.tail()) + val l = eval_ast(ast, env, depth + 1) + val f = ((l as MalList).head() as MalFunc) + val r = f(l.tail()) + println("result: ".repeat(depth) + pr_str(r)) + return r + } + } +} diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index 03d303fb..546b8830 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -5,8 +5,9 @@ fun pr_str(v: MalType) : String { is MalList -> { "(" + v.atoms.map { pr_str(it) }.joinToString(" ") + ")" } - is MalNumber -> v.number.toString() + is MalNumber -> v.num.toString() is MalSymbol -> v.sym + is MalFunc -> "MalFunc(...)" else -> "" } } diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index 783b6c27..a1b1a070 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -42,67 +42,84 @@ class Reader(val tokens: List) { // returns the token at the current position and increments the position fun next() : String { val s = tokens[pos] - pos += 1 + pos++ return s } + // Check whether we're at the end. + fun isLast() = pos == (tokens.size - 1) // just returns the token at the current position. fun peek() = tokens[pos] } + +fun is_number(s: String) = Regex("\\d+").matches(s) + +// This function will look at the contents of the token and return the +// appropriate scalar (simple/single) data type value. +fun read_atom(r: Reader) : MalAtom { +// println("Reading atom: " + r) + val t = r.next() + return if (is_number(t)) { + MalNumber(t.toInt()) + } + else { + MalSymbol(t) + } +} + var readLimit = 0 -// This function will peek at the first token in the Reader object and -// switch on the first character of that token. If the character is a -// left paren then read_list is called with the Reader -// object. Otherwise, read_atom is called with the Reader Object. The -// return value from read_form is a mal data type. -fun read_form(r: Reader) : MalType { +fun check_limit() { + readLimit++ + if (readLimit > 200) { + throw Exception("Parser found no end :/") + } +} + +fun read_form(r: Reader, n: Int) : MalType { +// println("v8> " + " ".repeat(n) + "read_form") try { return when(r.peek()) { - "(" -> read_list(r) + "(" -> read_list(r, n + 1) // ) else -> read_atom(r) } } catch(e: IndexOutOfBoundsException) { throw Exception("Ran out of tokens, missing right paren?") } - finally { - readLimit = 0 - } } -fun read_list(r: Reader) : MalList { -// println("Reading list: " + r) +fun read_list(r: Reader, n: Int) : MalList { r.next() // Move past the opening paren. +// val say = { m: String -> println("v8> " + " ".repeat(n) + m) } val list : MutableList = mutableListOf() - while(r.peek() != ")") { - list.add(read_form(r)) + while(r.peek() != ")") { // balance parens x_x +// say("at char: " + r.peek()) + list.add(read_form(r, n)) + check_limit() // Safety limit to prevent the REPL never coming back. - readLimit += 1 - if (readLimit > 200) { - throw Exception("Parser found no end :(") - } } + if(!r.isLast()) r.next() +// say("returning list!") return MalList(list) } -fun is_number(s: String) = Regex("\\d+").matches(s) - -// This function will look at the contents of the token and return the -// appropriate scalar (simple/single) data type value. -fun read_atom(r: Reader) : MalAtom { -// println("Reading atom: " + r) - val t = r.next() - return if (is_number(t)) { - MalNumber(t.toInt()) +// This function will peek at the first token in the Reader object and +// switch on the first character of that token. If the character is a +// left paren then read_list is called with the Reader +// object. Otherwise, read_atom is called with the Reader Object. The +// return value from read_form is a mal data type. +fun read_form_safely(r: Reader) : MalType { + try { + return read_form(r, 0); } - else { - MalSymbol(t) + finally { + readLimit = 0 } } // This function will call tokenize and then create a new Reader // object instance with the tokens. Then it will call read_form with // the Reader instance. -fun read_str(s: String) = read_form(Reader(tokenize(s))) +fun read_str(s: String) = read_form_safely(Reader(tokenize(s))) diff --git a/kotlin/src/mal/step2_eval.kt b/kotlin/src/mal/step2_eval.kt index 8b137891..5fddd96e 100644 --- a/kotlin/src/mal/step2_eval.kt +++ b/kotlin/src/mal/step2_eval.kt @@ -1 +1,79 @@ +fun READ(s: String) = read_str(s) + +val repl_env : Map = mapOf( + "+" to MalFunc({ a, b -> MalNumber((a as MalNumber).num + (b as MalNumber).num) }), + "-" to MalFunc({ a, b -> MalNumber((a as MalNumber).num - (b as MalNumber).num) }), + "*" to MalFunc({ a, b -> MalNumber((a as MalNumber).num * (b as MalNumber).num) }), + "/" to MalFunc({ a, b -> MalNumber((a as MalNumber).num / (b as MalNumber).num) }) +) + +// Create a new function eval_ast which takes ast (mal data type) and +// an associative structure (the environment from above). eval_ast +// switches on the type of ast as follows: +// +// * symbol: lookup the symbol in the environment structure and return the value or raise an error if no value is found +// * list: return a new list that is the result of calling EVAL on each of the members of the list +// * otherwise just return the original ast value + +fun eval_ast(ast: MalType, env: Map, depth: Int) : MalType { +// print("eval_ast: ".repeat(depth)) +// println(PRINT(ast)) + return when(ast) { + is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth) }.toList()) + is MalSymbol -> env[ast.sym] ?: throw Exception("Unknown symbol '${ast.sym}'") + else -> ast + } +} + +// Modify EVAL to check if the first parameter ast is a list. +// * ast is not a list: then return the result of calling eval_ast on it. +// * ast is a empty list: return ast unchanged. +// * ast is a list: call eval_ast to get a new evaluated list. Take the first +// item of the evaluated list and call it as function using the rest of the +// evaluated list as its arguments. + +var eval_count = 0 +fun EVAL(ast: MalType, env: Map, depth: Int) : MalType { +// print("EVAL____: ".repeat(depth)) +// println(PRINT(ast)) + + eval_count += 1 + if (depth > 200 || eval_count > 500) { + throw Exception("Recursion fail :(") + } + + if (ast !is MalList) { + return eval_ast(ast, env, depth) + } + else { + if (ast.atoms.isEmpty()) { + return ast + } + else { + val l = eval_ast(ast, env, depth + 1) + val f = ((l as MalList).head() as MalFunc) + return f(l.tail()) + } + } +} + +fun PRINT(v: MalType) = pr_str(v) + +fun rep(s: String) { + println(PRINT(EVAL(READ(s), repl_env, 0))) +} + +fun main(args: Array) { + while(true) { + print("user> ") + + try { + readLine()?.let { rep(it) } + } + catch(e: Exception) { + println("Oh dear:" + e.toString()) + } + } +} + diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index 0d539534..97ef2db1 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -8,10 +8,18 @@ interface MalType {} -data class MalList(val atoms : List) : MalType +data class MalList(val atoms : List) : MalType { + fun head() = atoms[0] + fun tail() = atoms.slice(1 .. atoms.size - 1) +} interface MalAtom : MalType {} -data class MalNumber(val number : Number) : MalAtom +data class MalNumber(val num : Int) : MalAtom data class MalSymbol(val sym : String) : MalAtom + +class MalFunc(val func : (MalType, MalType) -> MalType) : MalType { + operator fun invoke(args: List) : MalType = + args.reduce { acc: MalType, v: MalType -> func(acc, v) } +} From adaac1b28a3c43238314f1d4e6789afd5b743160 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sun, 8 Mar 2020 00:31:03 +0000 Subject: [PATCH 06/48] Implement step 3 Relatively painless apart from the rather obtuse error: error: expecting a top level declaration Which was the result of having calls to `repl_env.set(...)` in the file. Moving it to `apply` fixed it, not sure why having statements at file level isn't kosher, maybe a JVM hangover? --- kotlin/src/mal/env.kt | 20 ++++++ kotlin/src/mal/step3_env.kt | 125 ++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) diff --git a/kotlin/src/mal/env.kt b/kotlin/src/mal/env.kt index 8b137891..ad0244e3 100644 --- a/kotlin/src/mal/env.kt +++ b/kotlin/src/mal/env.kt @@ -1 +1,21 @@ +// Define an Env object that is instantiated with a single outer parameter and starts with an empty associative data structure property data. +class Env(val outer: Env?) { + val data : MutableMap = mutableMapOf() + // Define three methods for the Env object: + // set: takes a symbol key and a mal value and adds to the data structure + fun set(sym: MalSymbol, value: MalType) : MalType { + data.set(sym, value) + return value + } + + // find: takes a symbol key and if the current environment contains that key then return the environment. If no key is found and outer is not nil then call find (recurse) on the outer environment. + fun find(sym: MalSymbol) : Env? = + if(data.contains(sym)) this else outer?.find(sym) + + // get: takes a symbol key and uses the find method to locate the environment with the key, then returns the matching value. If no key is found up the outer chain, then throws/raises a "not found" error. + fun get(key: MalSymbol) : MalType { + val env = find(key) ?: throw Exception("Could not find '${key.sym}' in env") + return env.data.getValue(key) + } +} diff --git a/kotlin/src/mal/step3_env.kt b/kotlin/src/mal/step3_env.kt index 8b137891..4b8f7186 100644 --- a/kotlin/src/mal/step3_env.kt +++ b/kotlin/src/mal/step3_env.kt @@ -1 +1,126 @@ +fun READ(s: String) = read_str(s) + +// Create a new function eval_ast which takes ast (mal data type) and +// an associative structure (the environment from above). eval_ast +// switches on the type of ast as follows: +// +// * symbol: lookup the symbol in the environment structure and return the value or raise an error if no value is found +// * list: return a new list that is the result of calling EVAL on each of the members of the list +// * otherwise just return the original ast value + +fun eval_ast(ast: MalType, env: Env, depth: Int) : MalType { +// print("eval_ast: ".repeat(depth)) +// println(PRINT(ast)) + return when(ast) { + is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth) }.toList()) + is MalSymbol -> env.get(ast) + else -> ast + } +} + +// Modify EVAL to check if the first parameter ast is a list. +// * ast is not a list: then return the result of calling eval_ast on it. +// * ast is a empty list: return ast unchanged. +// * ast is a list: call eval_ast to get a new evaluated list. Take the first +// item of the evaluated list and call it as function using the rest of the +// evaluated list as its arguments. + +/* +Modify the apply section of EVAL to switch on the first element of the +list: + + symbol "def!": call the set method of the current environment +(second parameter of EVAL called env) using the unevaluated first +parameter (second list element) as the symbol key and the evaluated +second parameter as the value. + + symbol "let*": create a new environment using the current +environment as the outer value and then use the first parameter as a +list of new bindings in the "let*" environment. Take the second +element of the binding list, call EVAL using the new "let*" +environment as the evaluation environment, then call set on the "let*" +environment using the first binding list element as the key and the +evaluated second element as the value. This is repeated for each +odd/even pair in the binding list. Note in particular, the bindings +earlier in the list can be referred to by later bindings. Finally, the +second parameter (third element) of the original let* form is +evaluated using the new "let*" environment and the result is returned +as the result of the let* (the new let environment is discarded upon +completion). + + otherwise: call eval_ast on the list and apply the first element to the rest as before. + +*/ + +fun make_env(pairs: MalList, outer_env: Env, depth: Int) : Env { + val new_env = Env(outer_env) + for (idx in pairs.atoms.indices step 2) { + val k = pairs.atoms[idx] as MalSymbol + val v = pairs.atoms[idx + 1] + new_env.set(k, EVAL(v, new_env, depth + 1)) + } + return new_env +} + +var eval_count = 0 +fun EVAL(ast: MalType, env: Env, depth: Int) : MalType { +// print("EVAL____: ".repeat(depth)) +// println(PRINT(ast)) + + eval_count += 1 + if (depth > 200 || eval_count > 500) { + throw Exception("Recursion fail :(") + } + + if (ast !is MalList) { + return eval_ast(ast, env, depth) + } + else { + if (ast.atoms.isEmpty()) { + return ast + } + else { + val op = ast.head() + val args = ast.tail() + if(op is MalSymbol) { + when(op.sym) { + "def!" -> { + val v = EVAL(args[1], env, depth + 1) + return env.set((args[0] as MalSymbol), v) + } + "let*" -> return EVAL(args[1], make_env((args[0] as MalList), env, depth), depth + 1) + } + } + val l = eval_ast(ast, env, depth + 1) + val f = ((l as MalList).head() as MalFunc) + return f(l.tail()) + } + } +} + +fun PRINT(v: MalType) = pr_str(v) + +val repl_env = Env(null).apply { + set(MalSymbol("+"), MalFunc({ a, b -> MalNumber((a as MalNumber).num + (b as MalNumber).num) })) + set(MalSymbol("-"), MalFunc({ a, b -> MalNumber((a as MalNumber).num - (b as MalNumber).num) })) + set(MalSymbol("*"), MalFunc({ a, b -> MalNumber((a as MalNumber).num * (b as MalNumber).num) })) + set(MalSymbol("/"), MalFunc({ a, b -> MalNumber((a as MalNumber).num / (b as MalNumber).num) })) +} + +fun rep(s: String) { + println(PRINT(EVAL(READ(s), repl_env, 0))) +} + +fun main(args: Array) { + while(true) { + print("user> ") + + try { + readLine()?.let { rep(it) } + } + catch(e: Exception) { + println("Oh dear:" + e.toString()) + } + } +} From bba81522fe76f21b3d5beebde460f864d17ec174 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sun, 8 Mar 2020 11:46:26 +0000 Subject: [PATCH 07/48] Implement rest of step 4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now all but the deferable stuff works! The non–deferable stuff is a little fragile (lots of assumptions about types) but the happy path is happy enough for me! --- kotlin/src/mal/core.kt | 79 ++++++++++++++++++++ kotlin/src/mal/env.kt | 11 ++- kotlin/src/mal/printer.kt | 9 ++- kotlin/src/mal/reader.kt | 37 ++++++++-- kotlin/src/mal/step3_env.kt | 2 +- kotlin/src/mal/step4_if_fn_do.kt | 123 +++++++++++++++++++++++++++++++ kotlin/src/mal/types.kt | 34 +++++++-- 7 files changed, 277 insertions(+), 18 deletions(-) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 8b137891..330517f2 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -1 +1,80 @@ +fun int_ops_reducer(f: (Int, Int) -> Int, args: MalList): MalNumber = + args.atoms.map { v: MalType -> v as MalNumber } + .reduce { acc, v -> MalNumber(f(acc.num, v.num)) } +fun to_fun(name: String, f: (MalList) -> MalType) : Pair = + malSym(name) to malFun(name, f) + +// =: compare the first two parameters and return true if they are the same type and contain the same value. +fun is_equal(a: MalType, b: MalType) = + if(a::class == b::class) { + when(a) { + is MalNumber -> a.num == (b as MalNumber).num + is MalString -> a.str == (b as MalString).str + is MalSymbol -> a.sym == (b as MalSymbol).sym + is MalBoolean -> a.bool == (b as MalBoolean).bool + is MalNil -> true + is MalList -> compare_lists(a, (b as MalList)) + is MalFunc -> a.func == (b as MalFunc).func + else -> throw Exception("Unknown type $a in is_equal (aka =)") + } + } + else { + false + } +// In the case of equal length lists, each element of the list should be compared for equality and if they are the same return true, otherwise false. +fun compare_lists(a: MalList, b: MalList) : Boolean { + if(a.atoms.count() == b.atoms.count()) + return a.atoms.indices.all { v: Int -> is_equal(a.atoms[v], b.atoms[v]) } + else + return false +} + +object core { + val ns : Map = mutableMapOf( + // Basic number ops. + malSym("+") to malFun("plus") { int_ops_reducer(Int::plus, it) }, + malSym("-") to malFun("minus") { int_ops_reducer(Int::minus, it) }, + malSym("*") to malFun("times") { int_ops_reducer(Int::times, it) }, + malSym("/") to malFun("div") { int_ops_reducer(Int::div, it) }, + + // prn: call pr_str on the first parameter with print_readably set to true, prints the result to the screen and then return nil. Note that the full version of prn is a deferrable below. + to_fun("prn") { pr_str(it[0]); MalNil() }, + // list: take the parameters and return them as a list. + to_fun("list") { it }, // we always get a list at this point + // list?: return true if the first parameter is a list, false otherwise. + to_fun("list?") { MalBoolean(it[0] is MalList) }, + // empty?: treat the first parameter as a list and return true if the list is empty and false if it contains any elements. + to_fun("empty?") { MalBoolean(it[0] is MalList && (it[0] as MalList).atoms.isEmpty()) }, + // count: treat the first parameter as a list and return the number of elements that it contains. + to_fun("count") { MalNumber((it[0] as MalList).atoms.count()) }, + + // =: see is_equal + malSym("=") to malFun("equals?") { + val a = it[0] + val b = it[1] + MalBoolean(is_equal(a, b)) + }, + // <, <=, >, and >=: treat the first two parameters as numbers and do the corresponding numeric comparison, returning either true or false. + malSym("<") to malFun("lt") { + val a = it[0] as MalNumber + val b = it[1] as MalNumber + MalBoolean(a.num < b.num) + }, + malSym("<=") to malFun("lt-eq") { + val a = it[0] as MalNumber + val b = it[1] as MalNumber + MalBoolean(a.num <= b.num) + }, + malSym(">") to malFun("gt") { + val a = it[0] as MalNumber + val b = it[1] as MalNumber + MalBoolean(a.num > b.num) + }, + malSym(">=") to malFun("gt-eq") { + val a = it[0] as MalNumber + val b = it[1] as MalNumber + MalBoolean(a.num >= b.num) + } + ) +} diff --git a/kotlin/src/mal/env.kt b/kotlin/src/mal/env.kt index ad0244e3..f38ef8fd 100644 --- a/kotlin/src/mal/env.kt +++ b/kotlin/src/mal/env.kt @@ -1,7 +1,16 @@ // Define an Env object that is instantiated with a single outer parameter and starts with an empty associative data structure property data. -class Env(val outer: Env?) { +class Env(val outer: Env? = null, + val binds : MalList = emptyMalList(), + val exprs : MalList = emptyMalList()) { + val data : MutableMap = mutableMapOf() + init { + for(idx in binds.atoms.indices) { + set(binds.atoms[idx] as MalSymbol, exprs.atoms[idx]) + } + } + // Define three methods for the Env object: // set: takes a symbol key and a mal value and adds to the data structure fun set(sym: MalSymbol, value: MalType) : MalType { diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index 546b8830..1f2a7faa 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -5,9 +5,12 @@ fun pr_str(v: MalType) : String { is MalList -> { "(" + v.atoms.map { pr_str(it) }.joinToString(" ") + ")" } - is MalNumber -> v.num.toString() - is MalSymbol -> v.sym - is MalFunc -> "MalFunc(...)" + is MalNumber -> v.num.toString() + is MalString -> v.str // TODO Support escapes + is MalSymbol -> v.sym + is MalBoolean -> v.bool.toString() + is MalNil -> "nil" + is MalFunc -> "#<${v.name}>" else -> "" } } diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index a1b1a070..a4a6dc9d 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -1,5 +1,7 @@ // For each match captured within the parenthesis starting at char 6 // of the regular expression a new token will be created. + +// TODO Support escaped double quotes e.g "foo \"bar\" baz" -> """foo "bar" baz""" var tokenizer = Regex(""" # Matches any number of whitespaces or commas [\s,]* @@ -12,7 +14,7 @@ var tokenizer = Regex(""" # it was preceded by a backslash in which case it includes it until the next # double-quote (tokenized). It will also match unbalanced strings (no ending # double-quote) which should be reported as an error. - "(?:.|[^"])*"? | + "(?:.|[^"])*?" | # Captures any sequence of characters starting with ; (tokenized). ;.* | # Captures a sequence of zero or more non special characters (e.g. symbols, @@ -63,6 +65,15 @@ fun read_atom(r: Reader) : MalAtom { return if (is_number(t)) { MalNumber(t.toInt()) } + else if (t[0] == '"') { + MalString(t.substring(1 .. t.length - 2)) + } + else if (t == "true" || t == "false") { + MalBoolean(t == "true") + } + else if (t == "nil") { + MalNil() + } else { MalSymbol(t) } @@ -78,10 +89,11 @@ fun check_limit() { } fun read_form(r: Reader, n: Int) : MalType { -// println("v8> " + " ".repeat(n) + "read_form") +// println("v9> " + " ".repeat(n) + "read_form") try { return when(r.peek()) { "(" -> read_list(r, n + 1) // ) + "[" -> read_vec(r, n + 1) // ] else -> read_atom(r) } } @@ -92,10 +104,10 @@ fun read_form(r: Reader, n: Int) : MalType { fun read_list(r: Reader, n: Int) : MalList { r.next() // Move past the opening paren. -// val say = { m: String -> println("v8> " + " ".repeat(n) + m) } +// val say = { m: String -> println("v9> " + " ".repeat(n) + m) } val list : MutableList = mutableListOf() while(r.peek() != ")") { // balance parens x_x -// say("at char: " + r.peek()) +// say("at token: " + r.peek()) list.add(read_form(r, n)) check_limit() // Safety limit to prevent the REPL never coming back. @@ -105,6 +117,21 @@ fun read_list(r: Reader, n: Int) : MalList { return MalList(list) } +fun read_vec(r: Reader, n: Int) : MalList { + r.next() // Move past the opening paren. +// val say = { m: String -> println("v9> " + " ".repeat(n) + m) } + val vec : MutableList = mutableListOf() + while(r.peek() != "]") { // balance parens x_x +// say("at token: " + r.peek()) + vec.add(read_form(r, n)) + check_limit() + // Safety limit to prevent the REPL never coming back. + } + if(!r.isLast()) r.next() +// say("returning vec!") + return MalVector(vec) +} + // This function will peek at the first token in the Reader object and // switch on the first character of that token. If the character is a // left paren then read_list is called with the Reader @@ -112,7 +139,7 @@ fun read_list(r: Reader, n: Int) : MalList { // return value from read_form is a mal data type. fun read_form_safely(r: Reader) : MalType { try { - return read_form(r, 0); + return read_form(r, 0) } finally { readLimit = 0 diff --git a/kotlin/src/mal/step3_env.kt b/kotlin/src/mal/step3_env.kt index 4b8f7186..7eaf6106 100644 --- a/kotlin/src/mal/step3_env.kt +++ b/kotlin/src/mal/step3_env.kt @@ -100,7 +100,7 @@ fun EVAL(ast: MalType, env: Env, depth: Int) : MalType { fun PRINT(v: MalType) = pr_str(v) -val repl_env = Env(null).apply { +val repl_env = Env().apply { set(MalSymbol("+"), MalFunc({ a, b -> MalNumber((a as MalNumber).num + (b as MalNumber).num) })) set(MalSymbol("-"), MalFunc({ a, b -> MalNumber((a as MalNumber).num - (b as MalNumber).num) })) set(MalSymbol("*"), MalFunc({ a, b -> MalNumber((a as MalNumber).num * (b as MalNumber).num) })) diff --git a/kotlin/src/mal/step4_if_fn_do.kt b/kotlin/src/mal/step4_if_fn_do.kt index 8b137891..3e37d491 100644 --- a/kotlin/src/mal/step4_if_fn_do.kt +++ b/kotlin/src/mal/step4_if_fn_do.kt @@ -1 +1,124 @@ +fun READ(s: String) = read_str(s) + +// Create a new function eval_ast which takes ast (mal data type) and +// an associative structure (the environment from above). eval_ast +// switches on the type of ast as follows: +// +// * symbol: lookup the symbol in the environment structure and return the value or raise an error if no value is found +// * list: return a new list that is the result of calling EVAL on each of the members of the list +// * otherwise just return the original ast value + +fun eval_ast(ast: MalType, env: Env, depth: Int) : MalType { +// print("eval_ast: ".repeat(depth)) +// println(PRINT(ast)) + return when(ast) { + is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalSymbol -> env.get(ast) + else -> ast + } +} + +/* + + do: Evaluate all the elements of the list using eval_ast and return the final evaluated element. + if: Evaluate the first parameter (second element). If the result (condition) is anything other than nil or false, then evaluate the second parameter (third element of the list) and return the result. Otherwise, evaluate the third parameter (fourth element) and return the result. If condition is false and there is no third parameter, then just return nil. + fn*: Return a new function closure. The body of that closure does the following: + Create a new environment using env (closed over from outer scope) as the outer parameter, the first parameter (second list element of ast from the outer scope) as the binds parameter, and the parameters to the closure as the exprs parameter. + Call EVAL on the second parameter (third list element of ast from outer scope), using the new environment. Use the result as the return value of the closure. + +*/ + +fun make_env(pairs: MalList, outer_env: Env, depth: Int) : Env { + val new_env = Env(outer_env) + for (idx in pairs.atoms.indices step 2) { + val k = pairs.atoms[idx] as MalSymbol + val v = pairs.atoms[idx + 1] + new_env.set(k, EVAL(v, new_env, depth + 1)) + } + return new_env +} + +fun is_true(cond : MalType) = + when(cond) { + is MalNil -> false + is MalBoolean -> cond.bool + else -> true + } + +var eval_count = 0 +fun EVAL(ast: MalType, env: Env, depth: Int) : MalType { +// print("EVAL____: ".repeat(depth)) +// println(PRINT(ast)) + + // Only use n when recursing into EVAL + val n = depth + 1 + eval_count += 1 + if (depth > 10000 || eval_count > 50000) { + throw Exception("Recursion fail :(") + } + + if (ast !is MalList) { + return eval_ast(ast, env, depth) + } + else { + if (ast.atoms.isEmpty()) { + return ast + } + else { + val op = ast.head() + val args = ast.tail() + if(op is MalSymbol) { + when(op.sym) { + "def!" -> { + val v = EVAL(args[1], env, n) + return env.set((args[0] as MalSymbol), v) + } + "let*" -> return EVAL(args[1], make_env((args[0] as MalList), env, depth), n) + "do" -> return (eval_ast(args, env, depth) as MalList).last() + "if" -> { + val body = if(is_true(EVAL(args[0], env, n))) args[1] else args[2] + return EVAL(body, env, n) + } + "fn*" -> { + val binds = args[0] as MalList + val body = args[1] + return malFun("funccall") { EVAL(body, Env(env, binds, it), n) } + } + } + } + val l = eval_ast(ast, env, depth) as MalList + val f = l.head() as MalFunc + return f(l.tail()) + } + } +} + +fun PRINT(v: MalType) = pr_str(v) + +fun reduce_ops(f: (Int, Int) -> Int, args: MalList): MalNumber { + return args.atoms.map { v: MalType -> v as MalNumber } + .reduce { acc, v -> MalNumber(f(acc.num, v.num)) } +} + +val repl_env = Env().apply { + core.ns.forEach { (k,v) -> set(k, v) } +} + +fun rep(s: String) { + println(PRINT(EVAL(READ(s), repl_env, 0))) +} + +fun main(args: Array) { + while(true) { + print("user> ") + + try { + readLine()?.let { rep(it) } + } + catch(e: Exception) { + println("Oh dear:" + e.toString()) + eval_count = 0 + } + } +} diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index 97ef2db1..f168d033 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -8,18 +8,36 @@ interface MalType {} -data class MalList(val atoms : List) : MalType { - fun head() = atoms[0] - fun tail() = atoms.slice(1 .. atoms.size - 1) -} - interface MalAtom : MalType {} data class MalNumber(val num : Int) : MalAtom +data class MalString(val str : String) : MalAtom + data class MalSymbol(val sym : String) : MalAtom -class MalFunc(val func : (MalType, MalType) -> MalType) : MalType { - operator fun invoke(args: List) : MalType = - args.reduce { acc: MalType, v: MalType -> func(acc, v) } +data class MalBoolean(val bool : Boolean) : MalAtom + +class MalNil() : MalAtom + +open class MalList(val atoms : List) : MalType { + fun head() = atoms[0] + fun tail() = MalList(atoms.slice(1 .. atoms.size - 1)) + fun last() = atoms.last() + operator fun get(index: Int): MalType = atoms[index] + // TODO Maybe implement complementN too. } + +class MalVector(atoms : List) : MalList(atoms) + +class MalFunc(val func : (MalList) -> MalType, val name : String = "anon") : MalType { + operator fun invoke(args: MalList) : MalType { + return func(args) + } +} + +// Helper functions. +fun emptyMalList() = MalList(listOf()) +fun malListOf(vararg elems: MalType) = MalList(elems.asList()) +fun malSym(sym: String) = MalSymbol(sym) +fun malFun(name: String, f: (MalList) -> MalType) = MalFunc(f, name) From 0cb55999a71f89327aadc06e3aa2aa78364e08e6 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Tue, 10 Mar 2020 07:23:24 +0000 Subject: [PATCH 08/48] Implement "not" in mal --- kotlin/src/mal/step4_if_fn_do.kt | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/kotlin/src/mal/step4_if_fn_do.kt b/kotlin/src/mal/step4_if_fn_do.kt index 3e37d491..4b2986e4 100644 --- a/kotlin/src/mal/step4_if_fn_do.kt +++ b/kotlin/src/mal/step4_if_fn_do.kt @@ -95,25 +95,19 @@ fun EVAL(ast: MalType, env: Env, depth: Int) : MalType { fun PRINT(v: MalType) = pr_str(v) -fun reduce_ops(f: (Int, Int) -> Int, args: MalList): MalNumber { - return args.atoms.map { v: MalType -> v as MalNumber } - .reduce { acc, v -> MalNumber(f(acc.num, v.num)) } -} +fun rep(s: String) = PRINT(EVAL(READ(s), repl_env, 0)) val repl_env = Env().apply { core.ns.forEach { (k,v) -> set(k, v) } } -fun rep(s: String) { - println(PRINT(EVAL(READ(s), repl_env, 0))) -} - fun main(args: Array) { + rep("(def! not (fn* [v] (if v false true)))") while(true) { print("user> ") try { - readLine()?.let { rep(it) } + readLine()?.let { println(rep(it)) } } catch(e: Exception) { println("Oh dear:" + e.toString()) From 46c0b7421d6a00739296a154653b83a0aeb90664 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Fri, 13 Mar 2020 00:21:26 +0000 Subject: [PATCH 09/48] Implement step 5 Trickiest part was figuring out why the test was hanging: because the code kept hitting StackOverflow error :/ --- kotlin/src/mal/step5_tco.kt | 141 ++++++++++++++++++++++++++++++++++++ kotlin/src/mal/types.kt | 14 +++- 2 files changed, 152 insertions(+), 3 deletions(-) diff --git a/kotlin/src/mal/step5_tco.kt b/kotlin/src/mal/step5_tco.kt index 8b137891..164158ad 100644 --- a/kotlin/src/mal/step5_tco.kt +++ b/kotlin/src/mal/step5_tco.kt @@ -1 +1,142 @@ +fun READ(s: String) = read_str(s) + +fun eval_ast(ast: MalType, env: Env, depth: Int) : MalType { + return when(ast) { + is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalSymbol -> env.get(ast) + else -> ast + } +} + +fun make_env(pairs: MalList, outer_env: Env, depth: Int) : Env { + val new_env = Env(outer_env) + for (idx in pairs.atoms.indices step 2) { + val k = pairs.atoms[idx] as MalSymbol + val v = pairs.atoms[idx + 1] + new_env.set(k, EVAL(v, new_env, depth + 1)) + } + return new_env +} + +fun is_true(cond : MalType) = + when(cond) { + is MalNil -> false + is MalBoolean -> cond.bool + else -> true + } + +var eval_count = 0 +fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { + // Only use n when recursing into EVAL + val n = depth + 1 + // Allow modification of where in the ast is being evaluated. + var ast = cur_ast + // Allow modification of which env is pointed at while evaluating. + var env = cur_env + eval_loop@ while(true) { + eval_count++ + // The depth check is low enough so as not to hit a terminal StackOverflow error. + // But eval_count is just an arbitrary limit. + if (depth > 1012 || eval_count > 654321) { + throw Exception("Recursion/eval limit hit: depth ${depth} / evalled ${eval_count}!") + } + + if (ast !is MalList) { + return eval_ast(ast, env, depth) + } + else { + if (ast.atoms.isEmpty()) { + return ast + } + else { + val next = ast.head() + val rest = ast.tail() + if(next is MalSymbol) { + when(next.sym) { + "def!" -> { + val v = EVAL(rest[1], env, n) + return env.set((rest[0] as MalSymbol), v) + } + "let*" -> { + // Set env (i.e. the local variable passed in as second parameter of EVAL) to the new let environment. + env = make_env((rest[0] as MalList), env, depth) + // Set ast (i.e. the local variable passed in as first parameter of EVAL) to be the second ast argument. + ast = ast[2] + continue@eval_loop // TCO + } + "do" -> { + // change the eval_ast call to evaluate all the parameters except for the last (2nd list element up to but not including last). + eval_ast(rest.butlast(), env, depth) + // Set ast to the last element of ast. Continue at the beginning of the loop (env stays unchanged). + ast = rest.tail() + continue@eval_loop // TCO + } + "if" -> { + // the condition continues to be evaluated, however, rather than evaluating the true or false branch, ast is set to the unevaluated value of the chosen branch. + ast = if(is_true(EVAL(rest[0], env, n))) rest[1] else + if(rest.atoms.count() == 3) rest[2] else ast + continue@eval_loop // TCO + } + "fn*" -> { + // The return value from the fn* special form will now become an object/structure with attributes that allow the default invoke case of EVAL to do TCO on mal functions. Those attributes are: + val binds = rest[0] as MalList + val body = rest[1] + val func = malFun("funccall") { + EVAL(body, Env(env, binds, it), n) + } + return MalUserFunc(body, binds, env, func) + } + } + } + + val op = eval_ast(ast, env, depth) as MalList + val func = op.head() + val args = op.tail() + + if(func is MalUserFunc) { + // set ast to the ast attribute of f. + // Generate a new environment using the env and params attributes of f as the outer and binds arguments and rest ast arguments (list elements 2 through the end) as the exprs argument. Set env to the new environment. Continue at the beginning of the loop. + ast = func.ast + // set func.params -> args somehow? + val binds = func.params.atoms.mapIndexed { index, atom -> + val k = atom as MalSymbol + val v = args[index] + listOf(k,v) + }.flatten().let { MalList(it) } + env = make_env(binds, func.env, depth) + continue@eval_loop // TCO + } + else if(func is MalFunc) { + return func(args) + } + else { + throw Exception("Don't know what to do with " + func) + } + } + } + } +} + +fun PRINT(v: MalType) = pr_str(v) + +fun rep(s: String) = PRINT(EVAL(READ(s), repl_env, 0)) + +val repl_env = Env().apply { + core.ns.forEach { (k,v) -> set(k, v) } +} + +fun main(args: Array) { + rep("(def! not (fn* [v] (if v false true)))") + while(true) { + print("user> ") + + try { + readLine()?.let { println(rep(it)) } + } + catch(e: Exception) { + println("Oh dear:" + e.toString()) + eval_count = 0 + } + } +} diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index f168d033..94e48c28 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -21,9 +21,10 @@ data class MalBoolean(val bool : Boolean) : MalAtom class MalNil() : MalAtom open class MalList(val atoms : List) : MalType { - fun head() = atoms[0] - fun tail() = MalList(atoms.slice(1 .. atoms.size - 1)) - fun last() = atoms.last() + fun head() = atoms[0] + fun tail() = MalList(atoms.slice(1 .. atoms.size - 1)) + fun last() = atoms.last() + fun butlast() = MalList(atoms.slice(0 .. atoms.size - 2)) operator fun get(index: Int): MalType = atoms[index] // TODO Maybe implement complementN too. } @@ -36,6 +37,13 @@ class MalFunc(val func : (MalList) -> MalType, val name : String = "anon") : Mal } } +class MalUserFunc( + val ast : MalType, + val params : MalList, + val env : Env, + val fn : MalFunc +) : MalType + // Helper functions. fun emptyMalList() = MalList(listOf()) fun malListOf(vararg elems: MalType) = MalList(elems.asList()) From e7a11b42eea5820cdc50a17edc59670dbf062213 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sat, 14 Mar 2020 09:29:29 +0000 Subject: [PATCH 10/48] Implement step 6 Fixed the inevitable bug involving newlines not being handled correctly during tokenization. Mostly puzzled at eval failing when calling (do ...) as calling EVAL meant the final form would be evaluated again instead of returned. Still haven't quite figured out why that happens but meh tests are passing. --- kotlin/src/mal/core.kt | 39 +++++++++ kotlin/src/mal/printer.kt | 15 ++-- kotlin/src/mal/reader.kt | 31 ++++--- kotlin/src/mal/step6_file.kt | 164 +++++++++++++++++++++++++++++++++++ kotlin/src/mal/types.kt | 9 +- 5 files changed, 238 insertions(+), 20 deletions(-) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 330517f2..fe137424 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -49,6 +49,11 @@ object core { // count: treat the first parameter as a list and return the number of elements that it contains. to_fun("count") { MalNumber((it[0] as MalList).atoms.count()) }, + // str: calls pr_str on each argument with print_readably set to false, concatenates the results together ("" separator), and returns the new string. + to_fun("str") { + MalString(it.atoms.map { pr_str(it) }.joinToString("")) + }, + // =: see is_equal malSym("=") to malFun("equals?") { val a = it[0] @@ -75,6 +80,40 @@ object core { val a = it[0] as MalNumber val b = it[1] as MalNumber MalBoolean(a.num >= b.num) + }, + + to_fun("read-string") { + read_str((it[0] as MalString).str) + }, + + to_fun("slurp") { + MalString(java.io.File((it[0] as MalString).str).readText()) + }, + + to_fun("atom") { + MalCljAtom(it[0]) + }, + to_fun("atom?") { + MalBoolean(it[0] is MalCljAtom) + }, + to_fun("deref") { + (it[0] as MalCljAtom).value + }, + to_fun("reset!") { + val a = it[0] as MalCljAtom + a.value = it[1] + a.value + }, + to_fun("swap!") { + val atom = it[0] as MalCljAtom + val callable = it[1] + val fn = if(callable is MalUserFunc) callable.fn else callable as MalFunc + // Pull out args if there are any. + val args = it.atoms.slice(2 .. (if(it.size > 2) it.size - 1 else 1)) + // Call the function with atom value + any args. + val res = fn(malListOf(listOf(atom.value) + args)) + atom.value = res + res } ) } diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index 1f2a7faa..0e055e15 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -5,12 +5,15 @@ fun pr_str(v: MalType) : String { is MalList -> { "(" + v.atoms.map { pr_str(it) }.joinToString(" ") + ")" } - is MalNumber -> v.num.toString() - is MalString -> v.str // TODO Support escapes - is MalSymbol -> v.sym - is MalBoolean -> v.bool.toString() - is MalNil -> "nil" - is MalFunc -> "#<${v.name}>" + is MalNumber -> v.num.toString() + is MalString -> v.str // TODO Support escapes + is MalSymbol -> v.sym + is MalBoolean -> v.bool.toString() + // Use this specific format to make tests pass :/ + is MalCljAtom -> "(atom ${pr_str(v.value)})" + is MalNil -> "nil" + is MalUserFunc -> "#<${v.fn.name}>" + is MalFunc -> "#<${v.name}>" else -> "" } } diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index a4a6dc9d..62666038 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -23,7 +23,7 @@ var tokenizer = Regex(""" [^\s\[\]{}('"`,;)]* ) """, -RegexOption.COMMENTS +setOf(RegexOption.COMMENTS, RegexOption.MULTILINE) ) // This function will take a single string and return an array/list of all the tokens (strings) in it. @@ -33,8 +33,9 @@ fun tokenize(s: String) : List { // throw Exception("Failed tokenizing") // } return tokenizer.findAll(s) - .map { it.value.trim() } + .map { it.value.trim() } .filter { it.length > 0 } + .filter { !it.startsWith(";") } .toList() } @@ -81,15 +82,21 @@ fun read_atom(r: Reader) : MalAtom { var readLimit = 0 +// Safety limit to prevent the REPL never coming back. fun check_limit() { readLimit++ - if (readLimit > 200) { + if (readLimit > 1024) { throw Exception("Parser found no end :/") } } +// This function will peek at the first token in the Reader object and +// switch on the first character of that token. If the character is a +// left paren then read_list is called with the Reader +// object. Otherwise, read_atom is called with the Reader Object. The +// return value from read_form is a mal data type. fun read_form(r: Reader, n: Int) : MalType { -// println("v9> " + " ".repeat(n) + "read_form") +// println("v1> " + " ".repeat(n) + "read_form") try { return when(r.peek()) { "(" -> read_list(r, n + 1) // ) @@ -104,13 +111,12 @@ fun read_form(r: Reader, n: Int) : MalType { fun read_list(r: Reader, n: Int) : MalList { r.next() // Move past the opening paren. -// val say = { m: String -> println("v9> " + " ".repeat(n) + m) } +// val say = { m: String -> println("v1> " + " ".repeat(n) + m) } val list : MutableList = mutableListOf() while(r.peek() != ")") { // balance parens x_x // say("at token: " + r.peek()) list.add(read_form(r, n)) check_limit() - // Safety limit to prevent the REPL never coming back. } if(!r.isLast()) r.next() // say("returning list!") @@ -125,21 +131,20 @@ fun read_vec(r: Reader, n: Int) : MalList { // say("at token: " + r.peek()) vec.add(read_form(r, n)) check_limit() - // Safety limit to prevent the REPL never coming back. } if(!r.isLast()) r.next() // say("returning vec!") return MalVector(vec) } -// This function will peek at the first token in the Reader object and -// switch on the first character of that token. If the character is a -// left paren then read_list is called with the Reader -// object. Otherwise, read_atom is called with the Reader Object. The -// return value from read_form is a mal data type. fun read_form_safely(r: Reader) : MalType { try { - return read_form(r, 0) + return if(r.tokens.isEmpty()) { + emptyMalList() + } + else { + read_form(r, 0) + } } finally { readLimit = 0 diff --git a/kotlin/src/mal/step6_file.kt b/kotlin/src/mal/step6_file.kt index 8b137891..3ea5a370 100644 --- a/kotlin/src/mal/step6_file.kt +++ b/kotlin/src/mal/step6_file.kt @@ -1 +1,165 @@ +fun READ(s: String) = read_str(s) + +fun eval_ast(ast: MalType, env: Env, depth: Int) : MalType { + return when(ast) { + is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalSymbol -> env.get(ast) + else -> ast + } +} + +fun make_env(pairs: MalList, outer_env: Env, depth: Int) : Env { + val new_env = Env(outer_env) + for (idx in pairs.atoms.indices step 2) { + val k = pairs.atoms[idx] as MalSymbol + val v = pairs.atoms[idx + 1] + new_env.set(k, EVAL(v, new_env, depth + 1)) + } + return new_env +} + +fun is_true(cond : MalType) = + when(cond) { + is MalNil -> false + is MalBoolean -> cond.bool + else -> true + } + +var eval_count = 0 +fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { +// val say = { m: String -> println(" ".repeat(depth) + m) } + // Only use n when recursing into EVAL + val n = depth + 1 + // Allow modification of where in the ast is being evaluated. + var ast = cur_ast + // Allow modification of which env is pointed at while evaluating. + var env = cur_env + eval_loop@ while(true) { + eval_count++ + // The depth check is low enough so as not to hit a terminal StackOverflow error. + // But eval_count is just an arbitrary limit. + if (depth > 1012 || eval_count > 654321) { + throw Exception("Recursion/eval limit hit: depth ${depth} / evalled ${eval_count}!") + } + + if (ast !is MalList) { + return eval_ast(ast, env, depth) + } + else { + if (ast.atoms.isEmpty()) { + return ast + } + else { +// say(pr_str(ast[0])) + val next = ast.head() + val rest = ast.tail() + if(next is MalSymbol) { + when(next.sym) { + "def!" -> { + val v = EVAL(rest[1], env, n) + val name = (rest[0] as MalSymbol) + if (v is MalUserFunc) { + v.fn.name = name.sym + } + return env.set(name, v) + } + "let*" -> { + // Set env (i.e. the local variable passed in as second parameter of EVAL) to the new let environment. + env = make_env((rest[0] as MalList), env, depth) + // Set ast (i.e. the local variable passed in as first parameter of EVAL) to be the second ast argument. + ast = ast[2] + continue@eval_loop // TCO + } + "do" -> { + // change the eval_ast call to evaluate all the parameters except for the last (2nd list element up to but not including last). + eval_ast(rest.butlast(), env, depth) + // Set ast to the last element of ast. Continue at the beginning of the loop (env stays unchanged). + ast = rest.last() + continue@eval_loop // TCO + } + "if" -> { + // the condition continues to be evaluated, however, rather than evaluating the true or false branch, ast is set to the unevaluated value of the chosen branch. + ast = if(is_true(EVAL(rest[0], env, n))) rest[1] else + if(rest.atoms.count() == 3) rest[2] else ast + continue@eval_loop // TCO + } + "fn*" -> { + // The return value from the fn* special form will now become an object/structure with attributes that allow the default invoke case of EVAL to do TCO on mal functions. Those attributes are: + val binds = rest[0] as MalList + val body = rest[1] + val func = malFun("funccall") { + EVAL(body, Env(env, binds, it), n) + } + return MalUserFunc(body, binds, env, func) + } + } + } + + val op = eval_ast(ast, env, depth) as MalList + val func = op.head() + val args = op.tail() + + if(func is MalUserFunc) { + // set ast to the ast attribute of f. + // Generate a new environment using the env and params attributes of f as the outer and binds arguments and rest ast arguments (list elements 2 through the end) as the exprs argument. Set env to the new environment. Continue at the beginning of the loop. + ast = func.ast + val binds = func.params.atoms.mapIndexed { index, atom -> + val k = atom as MalSymbol + val v = args[index] + listOf(k,v) + }.flatten().let { MalList(it) } + env = make_env(binds, func.env, depth) + continue@eval_loop // TCO + } + else if(func is MalFunc) { + return func(args) + } + else { + return func +// throw Exception("Don't know what to do with " + func) + } + } + } + } +} + +fun PRINT(v: MalType) = pr_str(v) + +fun rep(s: String) = PRINT(EVAL(READ(s), repl_env, 0)) + +val repl_env = Env().apply { + core.ns.forEach { (k,v) -> set(k, v) } +} + +fun main(args: Array) { + repl_env.set(malSym("eval"), malFun("eval") { + val res = eval_ast(it, repl_env, 0) + when(res) { + is MalList -> res.last() + else -> res + } + }) + repl_env.set(malSym("*ARGV*"), malListOf(args.map(::MalString))) + + rep("""(def! load-file (fn* [f] (eval (read-string (str "(do " (slurp f) ")")))))""") + rep("(def! not (fn* [v] (if v false true)))") + + repl@ while(true) { + print("user> ") + + try { + val line = readLine() ?: continue@repl + if (line.trim() == "quit") { + println("Bye!") + break@repl + } + println(rep(line)) + } + catch(e: Exception) { + println("Oh dear:" + e.toString()) + e.printStackTrace() + eval_count = 0 + } + } +} diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index 94e48c28..85004471 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -18,20 +18,26 @@ data class MalSymbol(val sym : String) : MalAtom data class MalBoolean(val bool : Boolean) : MalAtom +// Would use MalAtom but that's already a thing :/ +data class MalCljAtom(var value : MalType) : MalType + class MalNil() : MalAtom open class MalList(val atoms : List) : MalType { + val size = atoms.size fun head() = atoms[0] fun tail() = MalList(atoms.slice(1 .. atoms.size - 1)) fun last() = atoms.last() fun butlast() = MalList(atoms.slice(0 .. atoms.size - 2)) + operator fun get(index: Int): MalType = atoms[index] // TODO Maybe implement complementN too. } class MalVector(atoms : List) : MalList(atoms) -class MalFunc(val func : (MalList) -> MalType, val name : String = "anon") : MalType { +// Allow name to be set after the fact so functions in Env are named. +class MalFunc(val func : (MalList) -> MalType, var name : String = "anon") : MalType { operator fun invoke(args: MalList) : MalType { return func(args) } @@ -47,5 +53,6 @@ class MalUserFunc( // Helper functions. fun emptyMalList() = MalList(listOf()) fun malListOf(vararg elems: MalType) = MalList(elems.asList()) +fun malListOf(elems: List) = MalList(elems) fun malSym(sym: String) = MalSymbol(sym) fun malFun(name: String, f: (MalList) -> MalType) = MalFunc(f, name) From 08762d9dc48b77a9dff4c30efb7152d3fcbef236 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sun, 15 Mar 2020 17:16:24 +0000 Subject: [PATCH 11/48] Implement atom deref syntax --- kotlin/src/mal/reader.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index 62666038..2e838720 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -58,9 +58,11 @@ class Reader(val tokens: List) { fun is_number(s: String) = Regex("\\d+").matches(s) +fun make_atom(token: String) = MalList(listOf("deref", token).map(::malSym)) + // This function will look at the contents of the token and return the // appropriate scalar (simple/single) data type value. -fun read_atom(r: Reader) : MalAtom { +fun read_atom(r: Reader) : MalType { // println("Reading atom: " + r) val t = r.next() return if (is_number(t)) { @@ -75,6 +77,9 @@ fun read_atom(r: Reader) : MalAtom { else if (t == "nil") { MalNil() } + else if (t == "@") { + make_atom(r.next()) + } else { MalSymbol(t) } From 6cc5699bc0d43031e3527b818cc783f50553362b Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Mon, 16 Mar 2020 00:00:25 +0000 Subject: [PATCH 12/48] WIP Implement step 7 cons & concat are working, so far so good quote now implemented --- kotlin/src/mal/core.kt | 8 ++ kotlin/src/mal/step7_quote.kt | 167 ++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index fe137424..a36d674c 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -114,6 +114,14 @@ object core { val res = fn(malListOf(listOf(atom.value) + args)) atom.value = res res + }, + + to_fun("cons") { + val rest = if(it.size > 1) it[1] as MalList else emptyMalList() + malListOf(listOf(it.head()) + rest.atoms) + }, + to_fun("concat") { + malListOf(it.atoms.flatMap { (it as MalList).atoms }) } ) } diff --git a/kotlin/src/mal/step7_quote.kt b/kotlin/src/mal/step7_quote.kt index 8b137891..356c6808 100644 --- a/kotlin/src/mal/step7_quote.kt +++ b/kotlin/src/mal/step7_quote.kt @@ -1 +1,168 @@ +fun READ(s: String) = read_str(s) + +fun eval_ast(ast: MalType, env: Env, depth: Int) : MalType { + return when(ast) { + is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalSymbol -> env.get(ast) + else -> ast + } +} + +fun make_env(pairs: MalList, outer_env: Env, depth: Int) : Env { + val new_env = Env(outer_env) + for (idx in pairs.atoms.indices step 2) { + val k = pairs.atoms[idx] as MalSymbol + val v = pairs.atoms[idx + 1] + new_env.set(k, EVAL(v, new_env, depth + 1)) + } + return new_env +} + +fun is_true(cond : MalType) = + when(cond) { + is MalNil -> false + is MalBoolean -> cond.bool + else -> true + } + +var eval_count = 0 +fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { +// val say = { m: String -> println(" ".repeat(depth) + m) } + // Only use n when recursing into EVAL + val n = depth + 1 + // Allow modification of where in the ast is being evaluated. + var ast = cur_ast + // Allow modification of which env is pointed at while evaluating. + var env = cur_env + eval_loop@ while(true) { + eval_count++ + // The depth check is low enough so as not to hit a terminal StackOverflow error. + // But eval_count is just an arbitrary limit. + if (depth > 1012 || eval_count > 654321) { + throw Exception("Recursion/eval limit hit: depth ${depth} / evalled ${eval_count}!") + } + + if (ast !is MalList) { + return eval_ast(ast, env, depth) + } + else { + if (ast.atoms.isEmpty()) { + return ast + } + else { +// say(pr_str(ast[0])) + val next = ast.head() + val rest = ast.tail() + if(next is MalSymbol) { + when(next.sym) { + "def!" -> { + val v = EVAL(rest[1], env, n) + val name = (rest[0] as MalSymbol) + if (v is MalUserFunc) { + v.fn.name = name.sym + } + return env.set(name, v) + } + "let*" -> { + // Set env (i.e. the local variable passed in as second parameter of EVAL) to the new let environment. + env = make_env((rest[0] as MalList), env, depth) + // Set ast (i.e. the local variable passed in as first parameter of EVAL) to be the second ast argument. + ast = ast[2] + continue@eval_loop // TCO + } + "do" -> { + // change the eval_ast call to evaluate all the parameters except for the last (2nd list element up to but not including last). + eval_ast(rest.butlast(), env, depth) + // Set ast to the last element of ast. Continue at the beginning of the loop (env stays unchanged). + ast = rest.last() + continue@eval_loop // TCO + } + "if" -> { + // the condition continues to be evaluated, however, rather than evaluating the true or false branch, ast is set to the unevaluated value of the chosen branch. + ast = if(is_true(EVAL(rest[0], env, n))) rest[1] else + if(rest.atoms.count() == 3) rest[2] else ast + continue@eval_loop // TCO + } + "fn*" -> { + // The return value from the fn* special form will now become an object/structure with attributes that allow the default invoke case of EVAL to do TCO on mal functions. Those attributes are: + val binds = rest[0] as MalList + val body = rest[1] + val func = malFun("funccall") { + EVAL(body, Env(env, binds, it), n) + } + return MalUserFunc(body, binds, env, func) + } + "quote" -> { + return rest.head() + } + } + } + + val op = eval_ast(ast, env, depth) as MalList + val func = op.head() + val args = op.tail() + + if(func is MalUserFunc) { + // set ast to the ast attribute of f. + // Generate a new environment using the env and params attributes of f as the outer and binds arguments and rest ast arguments (list elements 2 through the end) as the exprs argument. Set env to the new environment. Continue at the beginning of the loop. + ast = func.ast + val binds = func.params.atoms.mapIndexed { index, atom -> + val k = atom as MalSymbol + val v = args[index] + listOf(k,v) + }.flatten().let { MalList(it) } + env = make_env(binds, func.env, depth) + continue@eval_loop // TCO + } + else if(func is MalFunc) { + return func(args) + } + else { + return func +// throw Exception("Don't know what to do with " + func) + } + } + } + } +} + +fun PRINT(v: MalType) = pr_str(v) + +fun rep(s: String) = PRINT(EVAL(READ(s), repl_env, 0)) + +val repl_env = Env().apply { + core.ns.forEach { (k,v) -> set(k, v) } +} + +fun main(args: Array) { + repl_env.set(malSym("eval"), malFun("eval") { + val res = eval_ast(it, repl_env, 0) + when(res) { + is MalList -> res.last() + else -> res + } + }) + repl_env.set(malSym("*ARGV*"), malListOf(args.map(::MalString))) + + rep("""(def! load-file (fn* [f] (eval (read-string (str "(do " (slurp f) ")")))))""") + rep("(def! not (fn* [v] (if v false true)))") + + repl@ while(true) { + print("user> ") + + try { + val line = readLine() ?: continue@repl + if (line.trim() == "quit") { + println("Bye!") + break@repl + } + println(rep(line)) + } + catch(e: Exception) { + println("Oh dear:" + e.toString()) + e.printStackTrace() + eval_count = 0 + } + } +} From c7b1f452669f865f044bc958896f109d4617c049 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Tue, 17 Mar 2020 08:54:58 +0000 Subject: [PATCH 13/48] WIP Step 7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reimplementing quasiquote AIUI, the recommended approach was … confusing. --- kotlin/src/mal/step7_quote.kt | 67 ++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/kotlin/src/mal/step7_quote.kt b/kotlin/src/mal/step7_quote.kt index 356c6808..4609b3ce 100644 --- a/kotlin/src/mal/step7_quote.kt +++ b/kotlin/src/mal/step7_quote.kt @@ -18,13 +18,65 @@ fun make_env(pairs: MalList, outer_env: Env, depth: Int) : Env { return new_env } -fun is_true(cond : MalType) = - when(cond) { +fun is_true(cond: MalType) = + when (cond) { is MalNil -> false is MalBoolean -> cond.bool else -> true } +// First implement a helper function `is_pair` that returns true if the +// parameter is a non-empty list. +fun is_pair(p: MalType) = p is MalList && p.size > 0 + +fun quasiquote(ast: List) = quasiquote(*ast.toTypedArray()) + +fun quasiquote(vararg ast: MalType): MalType { + println("Quasiquoting: " + ast.map(::pr_str).joinToString(" ")) + // if `is_pair` of `ast` is false: return a new list containing: + // a symbol named "quote" and `ast`. + if (ast[0] !is MalList || !is_pair(ast[0])) { + // XXX Should the symbol and ast be joined? + return malListOf(malSym("quote"), ast[0]) + } + // We know ast[0] is now a list with stuff in it. + val fst = ast[0] + val rest = ast.drop(1) + // else if the first element of `ast` is a symbol named "unquote": + // return the second element of `ast`. + return if (fst == malSym("unquote")) { + ast[1] + } + // if `is_pair` of the first element of `ast` is true and the first + // element of first element of `ast` (`ast[0][0]`) is a symbol named + // "splice-unquote": return a new list containing: a symbol named + // "concat", the second element of first element of `ast` + // (`ast[0][1]`), and the result of calling `quasiquote` with the + // second through last element of `ast`. + else if (is_pair(fst) && fst is MalList && fst.head() == malSym("splice-unquote")) { + malListOf(listOf(malSym("concat"), fst[1]) + quasiquote(rest)) + } + // otherwise: return a new list containing: a symbol named "cons", the + // result of calling `quasiquote` on first element of `ast` + // (`ast[0]`), and the result of calling `quasiquote` with the second + // through last element of `ast`. + else { + val head = if (fst is MalList) fst.head() else fst + val tail = if (fst is MalList) fst.tail().atoms else rest + // XXX Assumes we'll only get a list i.e (qq (1 2 (3 4))) not (qq (1 2) 3 4) + malListOf(malSym("cons"), quasiquote(head), quasiquote(tail)) + } +} + +fun newqq(ast: MalType) : MalType { + return if (ast is MalList && ast.size > 0) { + malListOf(malSym("cons"), newqq(ast.head()), newqq(ast.tail())) + } + else { + malListOf(malSym("quote"), ast) + } +} + var eval_count = 0 fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { // val say = { m: String -> println(" ".repeat(depth) + m) } @@ -34,7 +86,7 @@ fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { var ast = cur_ast // Allow modification of which env is pointed at while evaluating. var env = cur_env - eval_loop@ while(true) { + eval_loop@ while (true) { eval_count++ // The depth check is low enough so as not to hit a terminal StackOverflow error. // But eval_count is just an arbitrary limit. @@ -95,6 +147,13 @@ fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { "quote" -> { return rest.head() } + // This is called from `EVAL` with the first `ast` argument (second list element) and then `ast` is set to the result and execution continues at the top of the loop (TCO). + "quasiquote" -> { + // TODO TCO + // XXX Support >1 args? + ast = newqq(rest[0]) + continue@eval_loop // TCO + } } } @@ -152,7 +211,7 @@ fun main(args: Array) { try { val line = readLine() ?: continue@repl - if (line.trim() == "quit") { + if (setOf("quit","exit").contains(line.trim())) { println("Bye!") break@repl } From 3762285fcd2bc3976fe23125bf89d737a90b3bb2 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Wed, 18 Mar 2020 23:02:02 +0000 Subject: [PATCH 14/48] WIP step 7 Implement unquote Also fix string repr so tests pass --- kotlin/src/mal/printer.kt | 2 +- kotlin/src/mal/step7_quote.kt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index 0e055e15..1bed4732 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -6,7 +6,7 @@ fun pr_str(v: MalType) : String { "(" + v.atoms.map { pr_str(it) }.joinToString(" ") + ")" } is MalNumber -> v.num.toString() - is MalString -> v.str // TODO Support escapes + is MalString -> "\"${v.str}\"" // TODO Support escapes is MalSymbol -> v.sym is MalBoolean -> v.bool.toString() // Use this specific format to make tests pass :/ diff --git a/kotlin/src/mal/step7_quote.kt b/kotlin/src/mal/step7_quote.kt index 4609b3ce..14ddb6fc 100644 --- a/kotlin/src/mal/step7_quote.kt +++ b/kotlin/src/mal/step7_quote.kt @@ -70,7 +70,10 @@ fun quasiquote(vararg ast: MalType): MalType { fun newqq(ast: MalType) : MalType { return if (ast is MalList && ast.size > 0) { - malListOf(malSym("cons"), newqq(ast.head()), newqq(ast.tail())) + if (ast.head() == malSym("unquote")) + ast.tail().head() + else + malListOf(malSym("cons"), newqq(ast.head()), newqq(ast.tail())) } else { malListOf(malSym("quote"), ast) From 50700f0e5ff8d4f4e6149102f4dce571bd3d45c5 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Wed, 18 Mar 2020 23:26:58 +0000 Subject: [PATCH 15/48] WIP Implement step 7 splice-unquote --- kotlin/src/mal/step7_quote.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/kotlin/src/mal/step7_quote.kt b/kotlin/src/mal/step7_quote.kt index 14ddb6fc..c994777f 100644 --- a/kotlin/src/mal/step7_quote.kt +++ b/kotlin/src/mal/step7_quote.kt @@ -70,10 +70,14 @@ fun quasiquote(vararg ast: MalType): MalType { fun newqq(ast: MalType) : MalType { return if (ast is MalList && ast.size > 0) { - if (ast.head() == malSym("unquote")) - ast.tail().head() + val fst = ast.head() + val rest = ast.tail() + if (fst == malSym("unquote")) + rest.head() + else if (fst is MalList && fst.head() == malSym("splice-unquote")) + malListOf(malSym("concat"), fst.tail().head(), newqq(rest)) else - malListOf(malSym("cons"), newqq(ast.head()), newqq(ast.tail())) + malListOf(malSym("cons"), newqq(fst), newqq(rest)) } else { malListOf(malSym("quote"), ast) From fb631b3005afda64ab2a2faa7bf8faf3c25eb1bb Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Wed, 18 Mar 2020 23:47:19 +0000 Subject: [PATCH 16/48] WIP Step 7 implement quote reader macros --- kotlin/src/mal/reader.kt | 16 ++++++++++-- kotlin/src/mal/step7_quote.kt | 47 +++-------------------------------- 2 files changed, 18 insertions(+), 45 deletions(-) diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index 2e838720..a19b2102 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -62,7 +62,7 @@ fun make_atom(token: String) = MalList(listOf("deref", token).map(::malSym)) // This function will look at the contents of the token and return the // appropriate scalar (simple/single) data type value. -fun read_atom(r: Reader) : MalType { +fun read_atom(r: Reader, n: Int) : MalType { // println("Reading atom: " + r) val t = r.next() return if (is_number(t)) { @@ -80,6 +80,18 @@ fun read_atom(r: Reader) : MalType { else if (t == "@") { make_atom(r.next()) } + else if (t == "'") { + malListOf(malSym("quote"), read_form(r, n)) + } + else if (t == "`") { + malListOf(malSym("quasiquote"), read_form(r, n)) + } + else if (t == "~") { + malListOf(malSym("unquote"), read_form(r, n)) + } + else if (t == "~@") { + malListOf(malSym("splice-unquote"), read_form(r, n)) + } else { MalSymbol(t) } @@ -106,7 +118,7 @@ fun read_form(r: Reader, n: Int) : MalType { return when(r.peek()) { "(" -> read_list(r, n + 1) // ) "[" -> read_vec(r, n + 1) // ] - else -> read_atom(r) + else -> read_atom(r, n + 1) } } catch(e: IndexOutOfBoundsException) { diff --git a/kotlin/src/mal/step7_quote.kt b/kotlin/src/mal/step7_quote.kt index c994777f..ef5affca 100644 --- a/kotlin/src/mal/step7_quote.kt +++ b/kotlin/src/mal/step7_quote.kt @@ -29,55 +29,16 @@ fun is_true(cond: MalType) = // parameter is a non-empty list. fun is_pair(p: MalType) = p is MalList && p.size > 0 -fun quasiquote(ast: List) = quasiquote(*ast.toTypedArray()) - -fun quasiquote(vararg ast: MalType): MalType { - println("Quasiquoting: " + ast.map(::pr_str).joinToString(" ")) - // if `is_pair` of `ast` is false: return a new list containing: - // a symbol named "quote" and `ast`. - if (ast[0] !is MalList || !is_pair(ast[0])) { - // XXX Should the symbol and ast be joined? - return malListOf(malSym("quote"), ast[0]) - } - // We know ast[0] is now a list with stuff in it. - val fst = ast[0] - val rest = ast.drop(1) - // else if the first element of `ast` is a symbol named "unquote": - // return the second element of `ast`. - return if (fst == malSym("unquote")) { - ast[1] - } - // if `is_pair` of the first element of `ast` is true and the first - // element of first element of `ast` (`ast[0][0]`) is a symbol named - // "splice-unquote": return a new list containing: a symbol named - // "concat", the second element of first element of `ast` - // (`ast[0][1]`), and the result of calling `quasiquote` with the - // second through last element of `ast`. - else if (is_pair(fst) && fst is MalList && fst.head() == malSym("splice-unquote")) { - malListOf(listOf(malSym("concat"), fst[1]) + quasiquote(rest)) - } - // otherwise: return a new list containing: a symbol named "cons", the - // result of calling `quasiquote` on first element of `ast` - // (`ast[0]`), and the result of calling `quasiquote` with the second - // through last element of `ast`. - else { - val head = if (fst is MalList) fst.head() else fst - val tail = if (fst is MalList) fst.tail().atoms else rest - // XXX Assumes we'll only get a list i.e (qq (1 2 (3 4))) not (qq (1 2) 3 4) - malListOf(malSym("cons"), quasiquote(head), quasiquote(tail)) - } -} - -fun newqq(ast: MalType) : MalType { +fun quasiquote(ast: MalType) : MalType { return if (ast is MalList && ast.size > 0) { val fst = ast.head() val rest = ast.tail() if (fst == malSym("unquote")) rest.head() else if (fst is MalList && fst.head() == malSym("splice-unquote")) - malListOf(malSym("concat"), fst.tail().head(), newqq(rest)) + malListOf(malSym("concat"), fst.tail().head(), quasiquote(rest)) else - malListOf(malSym("cons"), newqq(fst), newqq(rest)) + malListOf(malSym("cons"), quasiquote(fst), quasiquote(rest)) } else { malListOf(malSym("quote"), ast) @@ -158,7 +119,7 @@ fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { "quasiquote" -> { // TODO TCO // XXX Support >1 args? - ast = newqq(rest[0]) + ast = quasiquote(rest[0]) continue@eval_loop // TCO } } From 78888f80115c72fbd2ed0275408418e95dde9bce Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Thu, 19 Mar 2020 00:01:19 +0000 Subject: [PATCH 17/48] WIP Implement step 7 fix vector implementation The lazy hack to treat vectors like lists hit its limit with the deferrable tests. --- kotlin/src/mal/core.kt | 16 ++++++++-------- kotlin/src/mal/env.kt | 4 ++-- kotlin/src/mal/printer.kt | 3 +++ kotlin/src/mal/reader.kt | 2 +- kotlin/src/mal/step7_quote.kt | 6 +++--- kotlin/src/mal/types.kt | 19 +++++++++++-------- 6 files changed, 28 insertions(+), 22 deletions(-) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index a36d674c..422c11f1 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -1,8 +1,8 @@ -fun int_ops_reducer(f: (Int, Int) -> Int, args: MalList): MalNumber = +fun int_ops_reducer(f: (Int, Int) -> Int, args: MalSeq): MalNumber = args.atoms.map { v: MalType -> v as MalNumber } .reduce { acc, v -> MalNumber(f(acc.num, v.num)) } -fun to_fun(name: String, f: (MalList) -> MalType) : Pair = +fun to_fun(name: String, f: (MalSeq) -> MalType) : Pair = malSym(name) to malFun(name, f) // =: compare the first two parameters and return true if they are the same type and contain the same value. @@ -14,7 +14,7 @@ fun is_equal(a: MalType, b: MalType) = is MalSymbol -> a.sym == (b as MalSymbol).sym is MalBoolean -> a.bool == (b as MalBoolean).bool is MalNil -> true - is MalList -> compare_lists(a, (b as MalList)) + is MalSeq -> compare_lists(a, (b as MalSeq)) is MalFunc -> a.func == (b as MalFunc).func else -> throw Exception("Unknown type $a in is_equal (aka =)") } @@ -23,7 +23,7 @@ fun is_equal(a: MalType, b: MalType) = false } // In the case of equal length lists, each element of the list should be compared for equality and if they are the same return true, otherwise false. -fun compare_lists(a: MalList, b: MalList) : Boolean { +fun compare_lists(a: MalSeq, b: MalSeq) : Boolean { if(a.atoms.count() == b.atoms.count()) return a.atoms.indices.all { v: Int -> is_equal(a.atoms[v], b.atoms[v]) } else @@ -45,9 +45,9 @@ object core { // list?: return true if the first parameter is a list, false otherwise. to_fun("list?") { MalBoolean(it[0] is MalList) }, // empty?: treat the first parameter as a list and return true if the list is empty and false if it contains any elements. - to_fun("empty?") { MalBoolean(it[0] is MalList && (it[0] as MalList).atoms.isEmpty()) }, + to_fun("empty?") { MalBoolean(it[0] is MalSeq && (it[0] as MalSeq).atoms.isEmpty()) }, // count: treat the first parameter as a list and return the number of elements that it contains. - to_fun("count") { MalNumber((it[0] as MalList).atoms.count()) }, + to_fun("count") { MalNumber((it[0] as MalSeq).atoms.count()) }, // str: calls pr_str on each argument with print_readably set to false, concatenates the results together ("" separator), and returns the new string. to_fun("str") { @@ -117,11 +117,11 @@ object core { }, to_fun("cons") { - val rest = if(it.size > 1) it[1] as MalList else emptyMalList() + val rest = if(it.size > 1) it[1] as MalSeq else emptyMalList() malListOf(listOf(it.head()) + rest.atoms) }, to_fun("concat") { - malListOf(it.atoms.flatMap { (it as MalList).atoms }) + malListOf(it.atoms.flatMap { (it as MalSeq).atoms }) } ) } diff --git a/kotlin/src/mal/env.kt b/kotlin/src/mal/env.kt index f38ef8fd..9b5fba7a 100644 --- a/kotlin/src/mal/env.kt +++ b/kotlin/src/mal/env.kt @@ -1,7 +1,7 @@ // Define an Env object that is instantiated with a single outer parameter and starts with an empty associative data structure property data. class Env(val outer: Env? = null, - val binds : MalList = emptyMalList(), - val exprs : MalList = emptyMalList()) { + val binds : MalSeq = emptyMalList(), + val exprs : MalSeq = emptyMalList()) { val data : MutableMap = mutableMapOf() diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index 1bed4732..e875e290 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -5,6 +5,9 @@ fun pr_str(v: MalType) : String { is MalList -> { "(" + v.atoms.map { pr_str(it) }.joinToString(" ") + ")" } + is MalVector -> { + "[" + v.atoms.map { pr_str(it) }.joinToString(" ") + "]" + } is MalNumber -> v.num.toString() is MalString -> "\"${v.str}\"" // TODO Support escapes is MalSymbol -> v.sym diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index a19b2102..25ef397f 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -140,7 +140,7 @@ fun read_list(r: Reader, n: Int) : MalList { return MalList(list) } -fun read_vec(r: Reader, n: Int) : MalList { +fun read_vec(r: Reader, n: Int) : MalVector { r.next() // Move past the opening paren. // val say = { m: String -> println("v9> " + " ".repeat(n) + m) } val vec : MutableList = mutableListOf() diff --git a/kotlin/src/mal/step7_quote.kt b/kotlin/src/mal/step7_quote.kt index ef5affca..d1ae46d8 100644 --- a/kotlin/src/mal/step7_quote.kt +++ b/kotlin/src/mal/step7_quote.kt @@ -8,7 +8,7 @@ fun eval_ast(ast: MalType, env: Env, depth: Int) : MalType { } } -fun make_env(pairs: MalList, outer_env: Env, depth: Int) : Env { +fun make_env(pairs: MalSeq, outer_env: Env, depth: Int) : Env { val new_env = Env(outer_env) for (idx in pairs.atoms.indices step 2) { val k = pairs.atoms[idx] as MalSymbol @@ -85,7 +85,7 @@ fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { } "let*" -> { // Set env (i.e. the local variable passed in as second parameter of EVAL) to the new let environment. - env = make_env((rest[0] as MalList), env, depth) + env = make_env((rest[0] as MalSeq), env, depth) // Set ast (i.e. the local variable passed in as first parameter of EVAL) to be the second ast argument. ast = ast[2] continue@eval_loop // TCO @@ -105,7 +105,7 @@ fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { } "fn*" -> { // The return value from the fn* special form will now become an object/structure with attributes that allow the default invoke case of EVAL to do TCO on mal functions. Those attributes are: - val binds = rest[0] as MalList + val binds = rest[0] as MalSeq val body = rest[1] val func = malFun("funccall") { EVAL(body, Env(env, binds, it), n) diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index 85004471..f82110bf 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -23,29 +23,32 @@ data class MalCljAtom(var value : MalType) : MalType class MalNil() : MalAtom -open class MalList(val atoms : List) : MalType { - val size = atoms.size +interface MalSeq : MalType { + val atoms : List + + val size: Int get() = atoms.size fun head() = atoms[0] fun tail() = MalList(atoms.slice(1 .. atoms.size - 1)) fun last() = atoms.last() fun butlast() = MalList(atoms.slice(0 .. atoms.size - 2)) operator fun get(index: Int): MalType = atoms[index] - // TODO Maybe implement complementN too. + // TODO Maybe implement complementN too? } -class MalVector(atoms : List) : MalList(atoms) +class MalList(override val atoms : List) : MalSeq +class MalVector(override val atoms : List) : MalSeq // Allow name to be set after the fact so functions in Env are named. -class MalFunc(val func : (MalList) -> MalType, var name : String = "anon") : MalType { - operator fun invoke(args: MalList) : MalType { +class MalFunc(val func : (MalSeq) -> MalType, var name : String = "anon") : MalType { + operator fun invoke(args: MalSeq) : MalType { return func(args) } } class MalUserFunc( val ast : MalType, - val params : MalList, + val params : MalSeq, val env : Env, val fn : MalFunc ) : MalType @@ -55,4 +58,4 @@ fun emptyMalList() = MalList(listOf()) fun malListOf(vararg elems: MalType) = MalList(elems.asList()) fun malListOf(elems: List) = MalList(elems) fun malSym(sym: String) = MalSymbol(sym) -fun malFun(name: String, f: (MalList) -> MalType) = MalFunc(f, name) +fun malFun(name: String, f: (MalSeq) -> MalType) = MalFunc(f, name) From bc5fedc7198e3a800fa60186177b2203f655f669 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Thu, 19 Mar 2020 23:08:30 +0000 Subject: [PATCH 18/48] Reify MalFunc and MalUserFunc The previous implementation was clunky, now both act like callable things. --- kotlin/src/mal/core.kt | 7 +++---- kotlin/src/mal/env.kt | 10 +++++----- kotlin/src/mal/printer.kt | 2 +- kotlin/src/mal/types.kt | 26 ++++++++++++++++---------- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 422c11f1..454354b3 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -2,7 +2,7 @@ fun int_ops_reducer(f: (Int, Int) -> Int, args: MalSeq): MalNumber = args.atoms.map { v: MalType -> v as MalNumber } .reduce { acc, v -> MalNumber(f(acc.num, v.num)) } -fun to_fun(name: String, f: (MalSeq) -> MalType) : Pair = +fun to_fun(name: String, f: MalFn) : Pair = malSym(name) to malFun(name, f) // =: compare the first two parameters and return true if they are the same type and contain the same value. @@ -105,9 +105,8 @@ object core { a.value }, to_fun("swap!") { - val atom = it[0] as MalCljAtom - val callable = it[1] - val fn = if(callable is MalUserFunc) callable.fn else callable as MalFunc + val atom = it[0] as MalCljAtom + val fn = it[1] as MalCallable // Pull out args if there are any. val args = it.atoms.slice(2 .. (if(it.size > 2) it.size - 1 else 1)) // Call the function with atom value + any args. diff --git a/kotlin/src/mal/env.kt b/kotlin/src/mal/env.kt index 9b5fba7a..ee8e3600 100644 --- a/kotlin/src/mal/env.kt +++ b/kotlin/src/mal/env.kt @@ -1,7 +1,7 @@ // Define an Env object that is instantiated with a single outer parameter and starts with an empty associative data structure property data. class Env(val outer: Env? = null, - val binds : MalSeq = emptyMalList(), - val exprs : MalSeq = emptyMalList()) { + val binds: MalSeq = emptyMalList(), + val exprs: MalSeq = emptyMalList()) { val data : MutableMap = mutableMapOf() @@ -13,17 +13,17 @@ class Env(val outer: Env? = null, // Define three methods for the Env object: // set: takes a symbol key and a mal value and adds to the data structure - fun set(sym: MalSymbol, value: MalType) : MalType { + fun set(sym: MalSymbol, value: MalType): MalType { data.set(sym, value) return value } // find: takes a symbol key and if the current environment contains that key then return the environment. If no key is found and outer is not nil then call find (recurse) on the outer environment. - fun find(sym: MalSymbol) : Env? = + fun find(sym: MalSymbol): Env? = if(data.contains(sym)) this else outer?.find(sym) // get: takes a symbol key and uses the find method to locate the environment with the key, then returns the matching value. If no key is found up the outer chain, then throws/raises a "not found" error. - fun get(key: MalSymbol) : MalType { + fun get(key: MalSymbol): MalType { val env = find(key) ?: throw Exception("Could not find '${key.sym}' in env") return env.data.getValue(key) } diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index e875e290..434b3be3 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -15,7 +15,7 @@ fun pr_str(v: MalType) : String { // Use this specific format to make tests pass :/ is MalCljAtom -> "(atom ${pr_str(v.value)})" is MalNil -> "nil" - is MalUserFunc -> "#<${v.fn.name}>" + is MalUserFunc -> "#<${v.name}>" is MalFunc -> "#<${v.name}>" else -> "" } diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index f82110bf..84e8161a 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -36,26 +36,32 @@ interface MalSeq : MalType { // TODO Maybe implement complementN too? } -class MalList(override val atoms : List) : MalSeq -class MalVector(override val atoms : List) : MalSeq +class MalList(override val atoms: List) : MalSeq +class MalVector(override val atoms: List) : MalSeq -// Allow name to be set after the fact so functions in Env are named. -class MalFunc(val func : (MalSeq) -> MalType, var name : String = "anon") : MalType { +typealias MalFn = (MalSeq) -> MalType + +open class MalCallable(val func: MalFn, var name: String) : MalType { + var isMacro = false operator fun invoke(args: MalSeq) : MalType { return func(args) } } +// Allow name to be set after the fact so functions in Env are named. +class MalFunc(func: MalFn, name: String = "anon") : MalCallable(func, name) + class MalUserFunc( - val ast : MalType, - val params : MalSeq, - val env : Env, - val fn : MalFunc -) : MalType + val ast: MalType, + val params: MalSeq, + val env: Env, + name: String = "anon", + func: MalFn +) : MalCallable(func, name) // Helper functions. fun emptyMalList() = MalList(listOf()) fun malListOf(vararg elems: MalType) = MalList(elems.asList()) fun malListOf(elems: List) = MalList(elems) fun malSym(sym: String) = MalSymbol(sym) -fun malFun(name: String, f: (MalSeq) -> MalType) = MalFunc(f, name) +fun malFun(name: String, f: MalFn) = MalFunc(f, name) From 35862a7cf347229efcfba4b1f5b5943abe73dc7a Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Thu, 19 Mar 2020 23:09:19 +0000 Subject: [PATCH 19/48] Implement step 8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After it compiled the non–deferrable tests passed off the bat. Pleasantly surprised. --- kotlin/src/mal/step8_macros.kt | 234 +++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) diff --git a/kotlin/src/mal/step8_macros.kt b/kotlin/src/mal/step8_macros.kt index 8b137891..1631af7f 100644 --- a/kotlin/src/mal/step8_macros.kt +++ b/kotlin/src/mal/step8_macros.kt @@ -1 +1,235 @@ +fun READ(s: String) = read_str(s) + +fun eval_ast(ast: MalType, env: Env, depth: Int) : MalType { + return when(ast) { + is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalSymbol -> env.get(ast) + else -> ast + } +} + +fun make_env(pairs: MalSeq, outer_env: Env, depth: Int) : Env { + val new_env = Env(outer_env) + for (idx in pairs.atoms.indices step 2) { + val k = pairs.atoms[idx] as MalSymbol + val v = pairs.atoms[idx + 1] + new_env.set(k, EVAL(v, new_env, depth + 1)) + } + return new_env +} + +fun is_true(cond: MalType) = + when (cond) { + is MalNil -> false + is MalBoolean -> cond.bool + else -> true + } + +// Not implemented as safe-casing is gooder. +// fun is_pair(p: MalType) = p is MalList && p.size > 0 + +fun quasiquote(ast: MalType) : MalType { + return if (ast is MalList && ast.size > 0) { + val fst = ast.head() + val rest = ast.tail() + if (fst == malSym("unquote")) + rest.head() + else if (fst is MalList && fst.head() == malSym("splice-unquote")) + malListOf(malSym("concat"), fst.tail().head(), quasiquote(rest)) + else + malListOf(malSym("cons"), quasiquote(fst), quasiquote(rest)) + } + else { + malListOf(malSym("quote"), ast) + } +} + +fun is_macro_call(ast: MalType, env: Env) : Boolean { + if(ast is MalList && ast[0] is MalSymbol) { + val sym = ast[0] as MalSymbol + val e = env.find(sym) ?: return false + val v = e.get(sym) + return v is MalCallable && v.isMacro + } + else { + return false + } +} + +fun macroexpand(orig_ast: MalType, env: Env) : MalType { + var ast = orig_ast + while(is_macro_call(ast, env)) { + val l = ast as MalList + // Inside the loop, the first element of the ast list (a symbol), is looked up in the environment to get the macro function + val f = env.get(l.head() as MalSymbol) as MalUserFunc + // This macro function is then called/applied with the rest of the ast elements (2nd through the last) as arguments. + // The return value of the macro call becomes the new value of ast. + ast = f(l.tail()) + } + return ast +} + +var eval_count = 0 +fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { +// val say = { m: String -> println(" ".repeat(depth) + m) } + // Only use n when recursing into EVAL + val n = depth + 1 + // Allow modification of where in the ast is being evaluated. + var ast = cur_ast + // Allow modification of which env is pointed at while evaluating. + var env = cur_env + eval_loop@ while (true) { + eval_count++ + // The depth check is low enough so as not to hit a terminal StackOverflow error. + // But eval_count is just an arbitrary limit. + if (depth > 1012 || eval_count > 654321) { + throw Exception("Recursion/eval limit hit: depth ${depth} / evalled ${eval_count}!") + } + + ast = macroexpand(ast, env) + + if (ast !is MalList) { + return eval_ast(ast, env, depth) + } + else { + if (ast.atoms.isEmpty()) { + return ast + } + else { + val next = ast.head() + val rest = ast.tail() + if(next is MalSymbol) { + when(next.sym) { + "def!" -> { + val v = EVAL(rest[1], env, n) + val name = (rest[0] as MalSymbol) + if (v is MalUserFunc) { + v.name = name.sym + } + return env.set(name, v) + } + "defmacro!" -> { + val v = EVAL(rest[1], env, n) + if (v !is MalUserFunc) { + throw Exception("Can't defmacro! with a: "+pr_str(v)) + } + else { + v.isMacro = true + } + val name = (rest[0] as MalSymbol) + return env.set(name, v) + + } + "macroexpand" -> { + return macroexpand(rest[0], env) + } + "let*" -> { + // Set env (i.e. the local variable passed in as second parameter of EVAL) to the new let environment. + env = make_env((rest[0] as MalSeq), env, depth) + // Set ast (i.e. the local variable passed in as first parameter of EVAL) to be the second ast argument. + ast = ast[2] + continue@eval_loop // TCO + } + "do" -> { + // change the eval_ast call to evaluate all the parameters except for the last (2nd list element up to but not including last). + eval_ast(rest.butlast(), env, depth) + // Set ast to the last element of ast. Continue at the beginning of the loop (env stays unchanged). + ast = rest.last() + continue@eval_loop // TCO + } + "if" -> { + // the condition continues to be evaluated, however, rather than evaluating the true or false branch, ast is set to the unevaluated value of the chosen branch. + ast = if(is_true(EVAL(rest[0], env, n))) rest[1] else + if(rest.atoms.count() == 3) rest[2] else ast + continue@eval_loop // TCO + } + "fn*" -> { + // The return value from the fn* special form will now become an object/structure with attributes that allow the default invoke case of EVAL to do TCO on mal functions. Those attributes are: + val binds = rest[0] as MalSeq + val body = rest[1] +// val func = malFun("funccall") + return MalUserFunc(body, binds, env) { + EVAL(body, Env(env, binds, it), n) + } + } + "quote" -> { + return rest.head() + } + // This is called from `EVAL` with the first `ast` argument (second list element) and then `ast` is set to the result and execution continues at the top of the loop (TCO). + "quasiquote" -> { + // TODO TCO + // XXX Support >1 args? + ast = quasiquote(rest[0]) + continue@eval_loop // TCO + } + } + } + + val op = eval_ast(ast, env, depth) as MalList + val func = op.head() + val args = op.tail() + + if(func is MalUserFunc) { + // set ast to the ast attribute of f. + // Generate a new environment using the env and params attributes of f as the outer and binds arguments and rest ast arguments (list elements 2 through the end) as the exprs argument. Set env to the new environment. Continue at the beginning of the loop. + ast = func.ast + val binds = func.params.atoms.mapIndexed { index, atom -> + val k = atom as MalSymbol + val v = args[index] + listOf(k,v) + }.flatten().let { MalList(it) } + env = make_env(binds, func.env, depth) + continue@eval_loop // TCO + } + else if(func is MalFunc) { + return func(args) + } + else { + return func +// throw Exception("Don't know what to do with " + func) + } + } + } + } +} + +fun PRINT(v: MalType) = pr_str(v) + +fun rep(s: String) = PRINT(EVAL(READ(s), repl_env, 0)) + +val repl_env = Env().apply { + core.ns.forEach { (k,v) -> set(k, v) } +} + +fun main(args: Array) { + repl_env.set(malSym("eval"), malFun("eval") { + val res = eval_ast(it, repl_env, 0) + when(res) { + is MalList -> res.last() + else -> res + } + }) + repl_env.set(malSym("*ARGV*"), malListOf(args.map(::MalString))) + + rep("""(def! load-file (fn* [f] (eval (read-string (str "(do " (slurp f) ")")))))""") + rep("(def! not (fn* [v] (if v false true)))") + + repl@ while(true) { + print("user> ") + + try { + val line = readLine() ?: continue@repl + if (setOf("quit","exit").contains(line.trim())) { + println("Bye!") + break@repl + } + println(rep(line)) + } + catch(e: Exception) { + println("Oh dear:" + e.toString()) + e.printStackTrace() + eval_count = 0 + } + } +} From 9859a6dd87cbbc0bfb55fa90729bef76544d6494 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Thu, 19 Mar 2020 23:23:43 +0000 Subject: [PATCH 20/48] Implement step 8 deferrable nth, first & rest --- kotlin/src/mal/core.kt | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 454354b3..075a7288 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -121,6 +121,33 @@ object core { }, to_fun("concat") { malListOf(it.atoms.flatMap { (it as MalSeq).atoms }) + }, + + to_fun("nth") { + val seq = it[0] as MalSeq + val idx = it[1] as MalNumber + seq[idx.num] + }, + to_fun("first") { + val v = it[0] + if (v is MalNil) { + MalNil() + } + else if (v is MalSeq) { + if(v.size == 0) MalNil() else v.head() + } + else { + throw Exception("Can't fall 'first' on " + pr_str(v)) + } + }, + to_fun("rest") { + val v = it[0] + if (v is MalSeq) { + v.tail() + } + else { + throw Exception("Can't fall 'rest' on " + pr_str(v)) + } } ) } From 799cc1041370a5dec9f278e9687e1255dca996aa Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Thu, 19 Mar 2020 23:43:51 +0000 Subject: [PATCH 21/48] Simplify list and vector reading Initial impl was _very_ lazy. --- kotlin/src/mal/reader.kt | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index 25ef397f..52942ffc 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -116,8 +116,8 @@ fun read_form(r: Reader, n: Int) : MalType { // println("v1> " + " ".repeat(n) + "read_form") try { return when(r.peek()) { - "(" -> read_list(r, n + 1) // ) - "[" -> read_vec(r, n + 1) // ] + "(" -> MalList(read_seq(")", r, n + 1)) + "[" -> MalVector(read_seq("]", r, n + 1)) else -> read_atom(r, n + 1) } } @@ -126,32 +126,18 @@ fun read_form(r: Reader, n: Int) : MalType { } } -fun read_list(r: Reader, n: Int) : MalList { +fun read_seq(endTok: String, r: Reader, n: Int) : List { r.next() // Move past the opening paren. // val say = { m: String -> println("v1> " + " ".repeat(n) + m) } val list : MutableList = mutableListOf() - while(r.peek() != ")") { // balance parens x_x + while(r.peek() != endTok) { // say("at token: " + r.peek()) list.add(read_form(r, n)) check_limit() } if(!r.isLast()) r.next() // say("returning list!") - return MalList(list) -} - -fun read_vec(r: Reader, n: Int) : MalVector { - r.next() // Move past the opening paren. -// val say = { m: String -> println("v9> " + " ".repeat(n) + m) } - val vec : MutableList = mutableListOf() - while(r.peek() != "]") { // balance parens x_x -// say("at token: " + r.peek()) - vec.add(read_form(r, n)) - check_limit() - } - if(!r.isLast()) r.next() -// say("returning vec!") - return MalVector(vec) + return list } fun read_form_safely(r: Reader) : MalType { From cf0382ea02421d40ae6a2f8b54b53d18e0cb7d2c Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Fri, 20 Mar 2020 00:21:29 +0000 Subject: [PATCH 22/48] Implement keyword & hashmap types --- kotlin/src/mal/core.kt | 3 ++- kotlin/src/mal/printer.kt | 8 ++++++-- kotlin/src/mal/reader.kt | 14 ++++++++++++++ kotlin/src/mal/step8_macros.kt | 2 ++ kotlin/src/mal/types.kt | 21 +++++++++++++++------ 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 075a7288..d7e404aa 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -12,6 +12,7 @@ fun is_equal(a: MalType, b: MalType) = is MalNumber -> a.num == (b as MalNumber).num is MalString -> a.str == (b as MalString).str is MalSymbol -> a.sym == (b as MalSymbol).sym + is MalKeyword -> a.kw == (b as MalKeyword).kw is MalBoolean -> a.bool == (b as MalBoolean).bool is MalNil -> true is MalSeq -> compare_lists(a, (b as MalSeq)) @@ -126,7 +127,7 @@ object core { to_fun("nth") { val seq = it[0] as MalSeq val idx = it[1] as MalNumber - seq[idx.num] + seq[idx] }, to_fun("first") { val v = it[0] diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index 434b3be3..d3c23e2a 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -2,13 +2,17 @@ // return a string representation of it. fun pr_str(v: MalType) : String { return when(v) { - is MalList -> { + is MalList -> { "(" + v.atoms.map { pr_str(it) }.joinToString(" ") + ")" } - is MalVector -> { + is MalVector -> { "[" + v.atoms.map { pr_str(it) }.joinToString(" ") + "]" } + is MalMap -> { + "{" + v.pairs.map { (k,v) -> pr_str(k) + " " + pr_str(v) }.joinToString(" ") + "}" + } is MalNumber -> v.num.toString() + is MalKeyword -> ":${v.kw}" is MalString -> "\"${v.str}\"" // TODO Support escapes is MalSymbol -> v.sym is MalBoolean -> v.bool.toString() diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index 52942ffc..1d744026 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -77,6 +77,9 @@ fun read_atom(r: Reader, n: Int) : MalType { else if (t == "nil") { MalNil() } + else if (t[0] == ':') { + MalKeyword(t.substring(1 .. t.length - 1)) + } else if (t == "@") { make_atom(r.next()) } @@ -97,6 +100,16 @@ fun read_atom(r: Reader, n: Int) : MalType { } } +fun read_map(pairs: List) : MalMap { + val map : MutableMap = mutableMapOf() + for (idx in pairs.indices step 2) { + val k = pairs[idx] as MalString + val v = pairs[idx + 1] + map[k] = v + } + return MalMap(map) +} + var readLimit = 0 // Safety limit to prevent the REPL never coming back. @@ -118,6 +131,7 @@ fun read_form(r: Reader, n: Int) : MalType { return when(r.peek()) { "(" -> MalList(read_seq(")", r, n + 1)) "[" -> MalVector(read_seq("]", r, n + 1)) + "{" -> read_map(read_seq("}", r, n + 1)) else -> read_atom(r, n + 1) } } diff --git a/kotlin/src/mal/step8_macros.kt b/kotlin/src/mal/step8_macros.kt index 1631af7f..54b2b2d6 100644 --- a/kotlin/src/mal/step8_macros.kt +++ b/kotlin/src/mal/step8_macros.kt @@ -3,6 +3,8 @@ fun READ(s: String) = read_str(s) fun eval_ast(ast: MalType, env: Env, depth: Int) : MalType { return when(ast) { is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalVector -> MalVector(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalMap -> malMapOf(ast.pairs.map { (k,v) -> k to EVAL(v, env, depth + 1) }) is MalSymbol -> env.get(ast) else -> ast } diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index 84e8161a..dca2766b 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -6,17 +6,20 @@ // list/array of other MalTypes. If your language is dynamically typed // then you can likely just return a plain list/array of other mal types. -interface MalType {} +interface MalType -interface MalAtom : MalType {} +interface MalAtom : MalType -data class MalNumber(val num : Int) : MalAtom +data class MalNumber(val num: Int) : MalAtom -data class MalString(val str : String) : MalAtom +data class MalSymbol(val sym: String) : MalAtom -data class MalSymbol(val sym : String) : MalAtom +data class MalBoolean(val bool: Boolean) : MalAtom -data class MalBoolean(val bool : Boolean) : MalAtom +open class MalString(val str: String) : MalAtom + +// XXX Inheriting from MalString is a bit shonky. +data class MalKeyword(val kw: String) : MalString(kw) // Would use MalAtom but that's already a thing :/ data class MalCljAtom(var value : MalType) : MalType @@ -33,12 +36,17 @@ interface MalSeq : MalType { fun butlast() = MalList(atoms.slice(0 .. atoms.size - 2)) operator fun get(index: Int): MalType = atoms[index] + operator fun get(index: MalNumber): MalType = atoms[index.num] // TODO Maybe implement complementN too? } class MalList(override val atoms: List) : MalSeq class MalVector(override val atoms: List) : MalSeq +class MalMap(val pairs: Map) : MalType { + operator fun get(k: MalString): MalType = pairs[k] ?: MalNil() +} + typealias MalFn = (MalSeq) -> MalType open class MalCallable(val func: MalFn, var name: String) : MalType { @@ -63,5 +71,6 @@ class MalUserFunc( fun emptyMalList() = MalList(listOf()) fun malListOf(vararg elems: MalType) = MalList(elems.asList()) fun malListOf(elems: List) = MalList(elems) +fun malMapOf(elems: List>) = MalMap(mapOf(*elems.toTypedArray())) fun malSym(sym: String) = MalSymbol(sym) fun malFun(name: String, f: MalFn) = MalFunc(f, name) From 7999dcedc42f67c8b58ab08137de1d84b70ec677 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sat, 21 Mar 2020 16:58:39 +0000 Subject: [PATCH 23/48] Implement deferrable rest parameter functionality --- kotlin/src/mal/env.kt | 14 ++++++++++++-- kotlin/src/mal/step8_macros.kt | 9 +-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/kotlin/src/mal/env.kt b/kotlin/src/mal/env.kt index ee8e3600..02c4dbc6 100644 --- a/kotlin/src/mal/env.kt +++ b/kotlin/src/mal/env.kt @@ -6,8 +6,18 @@ class Env(val outer: Env? = null, val data : MutableMap = mutableMapOf() init { - for(idx in binds.atoms.indices) { - set(binds.atoms[idx] as MalSymbol, exprs.atoms[idx]) + // Modify the constructor/initializer for environments, so that if a "&" symbol is encountered in the binds list, the next symbol in the binds list after the "&" is bound to the rest of the exprs list that has not been bound yet. + var vals = exprs.atoms + bind_loop@ for(idx in binds.atoms.indices) { + val bind = binds.atoms[idx] as MalSymbol + if (bind == malSym("&")) { + val lastBind = binds.atoms[idx + 1] as MalSymbol + set(lastBind, malListOf(vals.slice(idx .. vals.size - 1))) + break@bind_loop + } + else { + set(bind, vals[idx]) + } } } diff --git a/kotlin/src/mal/step8_macros.kt b/kotlin/src/mal/step8_macros.kt index 54b2b2d6..7c931b45 100644 --- a/kotlin/src/mal/step8_macros.kt +++ b/kotlin/src/mal/step8_macros.kt @@ -172,15 +172,8 @@ fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { val args = op.tail() if(func is MalUserFunc) { - // set ast to the ast attribute of f. - // Generate a new environment using the env and params attributes of f as the outer and binds arguments and rest ast arguments (list elements 2 through the end) as the exprs argument. Set env to the new environment. Continue at the beginning of the loop. ast = func.ast - val binds = func.params.atoms.mapIndexed { index, atom -> - val k = atom as MalSymbol - val v = args[index] - listOf(k,v) - }.flatten().let { MalList(it) } - env = make_env(binds, func.env, depth) + env = Env(func.env, func.params, args) continue@eval_loop // TCO } else if(func is MalFunc) { From 7f7bf04c0181ce798048fc68b7fa8a6048cfe5da Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sat, 21 Mar 2020 23:04:23 +0000 Subject: [PATCH 24/48] Support escaping in strings And the related functions. --- kotlin/src/mal/core.kt | 40 ++++++++++++++++++++++++++++----------- kotlin/src/mal/printer.kt | 28 +++++++++++++++++++++++++-- kotlin/src/mal/reader.kt | 17 +++++++++++++++-- 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index d7e404aa..3da01356 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -1,12 +1,12 @@ -fun int_ops_reducer(f: (Int, Int) -> Int, args: MalSeq): MalNumber = +private fun int_ops_reducer(f: (Int, Int) -> Int, args: MalSeq): MalNumber = args.atoms.map { v: MalType -> v as MalNumber } .reduce { acc, v -> MalNumber(f(acc.num, v.num)) } -fun to_fun(name: String, f: MalFn) : Pair = +private fun to_fun(name: String, f: MalFn) : Pair = malSym(name) to malFun(name, f) // =: compare the first two parameters and return true if they are the same type and contain the same value. -fun is_equal(a: MalType, b: MalType) = +private fun is_equal(a: MalType, b: MalType) = if(a::class == b::class) { when(a) { is MalNumber -> a.num == (b as MalNumber).num @@ -24,13 +24,19 @@ fun is_equal(a: MalType, b: MalType) = false } // In the case of equal length lists, each element of the list should be compared for equality and if they are the same return true, otherwise false. -fun compare_lists(a: MalSeq, b: MalSeq) : Boolean { +private fun compare_lists(a: MalSeq, b: MalSeq) : Boolean { if(a.atoms.count() == b.atoms.count()) return a.atoms.indices.all { v: Int -> is_equal(a.atoms[v], b.atoms[v]) } else return false } +private fun pr_str_core(seq: MalSeq) = + seq.atoms.map { as_str(it, readable=true) }.joinToString(" ") + +private fun str_core(seq: MalSeq) = + seq.atoms.map { as_str(it, readable=false) }.joinToString("") + object core { val ns : Map = mutableMapOf( // Basic number ops. @@ -39,8 +45,25 @@ object core { malSym("*") to malFun("times") { int_ops_reducer(Int::times, it) }, malSym("/") to malFun("div") { int_ops_reducer(Int::div, it) }, - // prn: call pr_str on the first parameter with print_readably set to true, prints the result to the screen and then return nil. Note that the full version of prn is a deferrable below. - to_fun("prn") { pr_str(it[0]); MalNil() }, + // pr-str: calls `pr_str` on each argument with `print_readably` set to true, joins the results with " " and returns the new string. + to_fun("pr-str") { + MalString(pr_str_core(it)) + }, + // `str`: calls `pr_str` on each argument with `print_readably` set to false, concatenates the results together ("" separator), and returns the new string. + to_fun("str") { + MalString(str_core(it)) + }, + // prn: calls `pr_str` on each argument with `print_readably` set to true, joins the results with " ", prints the string to the screen and then returns `nil`. + to_fun("prn") { + println(pr_str_core(it)) + MalNil() + }, + // `println`: calls `pr_str` on each argument with `print_readably` set to false, joins the results with " ", prints the string to the screen and then returns `nil`. + to_fun("println") { + println(str_core(it)) + MalNil() + }, + // list: take the parameters and return them as a list. to_fun("list") { it }, // we always get a list at this point // list?: return true if the first parameter is a list, false otherwise. @@ -50,11 +73,6 @@ object core { // count: treat the first parameter as a list and return the number of elements that it contains. to_fun("count") { MalNumber((it[0] as MalSeq).atoms.count()) }, - // str: calls pr_str on each argument with print_readably set to false, concatenates the results together ("" separator), and returns the new string. - to_fun("str") { - MalString(it.atoms.map { pr_str(it) }.joinToString("")) - }, - // =: see is_equal malSym("=") to malFun("equals?") { val a = it[0] diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index d3c23e2a..37237416 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -1,6 +1,23 @@ +// Use named variables otherwise we have to escape escaping x_x +private val q = "\u0022" // '"' U+0022 " QUOTATION MARK (Other_Punctuation) +private val bs = "\u005C" // '\' U+005C \ REVERSE SOLIDUS (Other_Punctuation) + +// Reflect any changes in reader.kt +private val printEscapeMap = mapOf( + "$q" to "$bs$q", + "\n" to "${bs}n", + "$bs" to "$bs$bs" +) +// Bleurgh, the escapes need escapes as they become interpolated into Regex ;_; +// So we need to manage three levels escaping different >_< +private val printEscapes = Regex(listOf(q, "${bs}n", "$bs$bs(?![n$q])").joinToString("|", "(", ")")) + +private fun malstr_as_string(s: String) = + s.replace(printEscapes) { printEscapeMap.get(it.value) ?: it.value } + // … does the opposite of read_str: take a mal data structure and // return a string representation of it. -fun pr_str(v: MalType) : String { +fun as_str(v: MalType, readable: Boolean) : String { return when(v) { is MalList -> { "(" + v.atoms.map { pr_str(it) }.joinToString(" ") + ")" @@ -13,7 +30,7 @@ fun pr_str(v: MalType) : String { } is MalNumber -> v.num.toString() is MalKeyword -> ":${v.kw}" - is MalString -> "\"${v.str}\"" // TODO Support escapes + is MalString -> if(readable) malstr_as_string(v.str) else v.str is MalSymbol -> v.sym is MalBoolean -> v.bool.toString() // Use this specific format to make tests pass :/ @@ -24,3 +41,10 @@ fun pr_str(v: MalType) : String { else -> "" } } + +// Only add quotes to strings at the point of output, otherwise functions like +// str end up with multiply quoted strings. +fun pr_str(v: MalType, readable: Boolean = true) : String { + val s = as_str(v, readable) + return if(v is MalString) "\"$s\"" else s +} diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index 1d744026..e4aec035 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -14,7 +14,7 @@ var tokenizer = Regex(""" # it was preceded by a backslash in which case it includes it until the next # double-quote (tokenized). It will also match unbalanced strings (no ending # double-quote) which should be reported as an error. - "(?:.|[^"])*?" | + "(?:\\"|[^"])*?" | # Captures any sequence of characters starting with ; (tokenized). ;.* | # Captures a sequence of zero or more non special characters (e.g. symbols, @@ -60,6 +60,19 @@ fun is_number(s: String) = Regex("\\d+").matches(s) fun make_atom(token: String) = MalList(listOf("deref", token).map(::malSym)) +// Reflect any changes in printer.kt +val readEscapeMap = mapOf( + "\\\"" to "\"", + "\\n" to "\n", + "\\\\" to "\\" +) +// Bleurgh, the escapes need escapes as they become interpolated into Regex ;_; +// So we need to manage three levels escaping different >_< +val readEscapes = Regex(listOf("\\\\\"", "\\\\n", "\\\\\\\\").joinToString("|", "(", ")")) + +fun make_string(s: String) = + MalString(s.replace(readEscapes) { readEscapeMap.get(it.value) ?: it.value }) + // This function will look at the contents of the token and return the // appropriate scalar (simple/single) data type value. fun read_atom(r: Reader, n: Int) : MalType { @@ -69,7 +82,7 @@ fun read_atom(r: Reader, n: Int) : MalType { MalNumber(t.toInt()) } else if (t[0] == '"') { - MalString(t.substring(1 .. t.length - 2)) + make_string(t.substring(1 .. t.length - 2)) } else if (t == "true" || t == "false") { MalBoolean(t == "true") From 952aa6efe3e82f3f39956de6c306ea49a170bf0e Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Mon, 23 Mar 2020 23:21:40 +0000 Subject: [PATCH 25/48] Implement loading of mal files --- kotlin/src/mal/step8_macros.kt | 36 ++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/kotlin/src/mal/step8_macros.kt b/kotlin/src/mal/step8_macros.kt index 7c931b45..15cd7d84 100644 --- a/kotlin/src/mal/step8_macros.kt +++ b/kotlin/src/mal/step8_macros.kt @@ -204,26 +204,32 @@ fun main(args: Array) { else -> res } }) - repl_env.set(malSym("*ARGV*"), malListOf(args.map(::MalString))) rep("""(def! load-file (fn* [f] (eval (read-string (str "(do " (slurp f) ")")))))""") rep("(def! not (fn* [v] (if v false true)))") - repl@ while(true) { - print("user> ") - - try { - val line = readLine() ?: continue@repl - if (setOf("quit","exit").contains(line.trim())) { - println("Bye!") - break@repl + if(args.size > 0) { + repl_env.set(malSym("*ARGV*"), malListOf(args.drop(1).map(::MalString))) + rep("""(load-file "${args[0]}")""") + } + else { + repl_env.set(malSym("*ARGV*"), emptyMalList()) + repl@ while(true) { + print("user> ") + + try { + val line = readLine() ?: continue@repl + if (setOf("quit","exit").contains(line.trim())) { + println("Bye!") + break@repl + } + println(rep(line)) + } + catch(e: Exception) { + println("Oh dear:" + e.toString()) + e.printStackTrace() + eval_count = 0 } - println(rep(line)) - } - catch(e: Exception) { - println("Oh dear:" + e.toString()) - e.printStackTrace() - eval_count = 0 } } } From bf84ea6ead3da98899742ef7fac1c29063af0aef Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Mon, 23 Mar 2020 23:22:42 +0000 Subject: [PATCH 26/48] Add (deferrable) macros from step 8 Now most of the tests pass! A minor bug with the `cond` macro where the evaluator goes into an infinite loop when there's no valid condition. Meh. --- kotlin/src/mal/step8_macros.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kotlin/src/mal/step8_macros.kt b/kotlin/src/mal/step8_macros.kt index 15cd7d84..eda82443 100644 --- a/kotlin/src/mal/step8_macros.kt +++ b/kotlin/src/mal/step8_macros.kt @@ -206,6 +206,8 @@ fun main(args: Array) { }) rep("""(def! load-file (fn* [f] (eval (read-string (str "(do " (slurp f) ")")))))""") + rep("""(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw "odd number of forms to cond")) (cons 'cond (rest (rest xs)))))))""") + rep("""(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) `(let* (or_FIXME ~(first xs)) (if or_FIXME or_FIXME (or ~@(rest xs))))))))""") rep("(def! not (fn* [v] (if v false true)))") if(args.size > 0) { From de71bc893d56e9003a6a76ffbc256766536b2406 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Thu, 26 Mar 2020 23:17:29 +0000 Subject: [PATCH 27/48] Implement step 9 try*/catch* Passes the basic tests, start to trip up on intentional misuses of functions as their failure modes weren't specified. --- kotlin/src/mal/core.kt | 21 ++- kotlin/src/mal/env.kt | 2 +- kotlin/src/mal/printer.kt | 5 +- kotlin/src/mal/reader.kt | 6 +- kotlin/src/mal/step9_try.kt | 267 ++++++++++++++++++++++++++++++++++++ kotlin/src/mal/types.kt | 9 +- 6 files changed, 297 insertions(+), 13 deletions(-) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 3da01356..2d0e4f61 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -16,21 +16,28 @@ private fun is_equal(a: MalType, b: MalType) = is MalBoolean -> a.bool == (b as MalBoolean).bool is MalNil -> true is MalSeq -> compare_lists(a, (b as MalSeq)) + is MalMap -> compare_maps(a, (b as MalMap)) is MalFunc -> a.func == (b as MalFunc).func - else -> throw Exception("Unknown type $a in is_equal (aka =)") + // XXX Not particularly useful but is_equal(a.src, b.src) hits: + // error: type checking has run into a recursive problem + is MalUserEx -> a.src == (b as MalUserEx).src + else -> throw MalCoreEx("Unknown type $a in is_equal (aka =)") } } else { false } // In the case of equal length lists, each element of the list should be compared for equality and if they are the same return true, otherwise false. -private fun compare_lists(a: MalSeq, b: MalSeq) : Boolean { +private fun compare_lists(a: MalSeq, b: MalSeq): Boolean { if(a.atoms.count() == b.atoms.count()) return a.atoms.indices.all { v: Int -> is_equal(a.atoms[v], b.atoms[v]) } else return false } +// TODO Implement! +private fun compare_maps(a: MalMap, b: MalMap) = a == b + private fun pr_str_core(seq: MalSeq) = seq.atoms.map { as_str(it, readable=true) }.joinToString(" ") @@ -156,7 +163,7 @@ object core { if(v.size == 0) MalNil() else v.head() } else { - throw Exception("Can't fall 'first' on " + pr_str(v)) + throw MalCoreEx("Can't fall 'first' on " + pr_str(v)) } }, to_fun("rest") { @@ -165,7 +172,13 @@ object core { v.tail() } else { - throw Exception("Can't fall 'rest' on " + pr_str(v)) + throw MalCoreEx("Can't fall 'rest' on " + pr_str(v)) + } + }, + to_fun("throw") { + throw when(it.size) { + 0 -> MalUserEx(MalString("error raised anon")) + else -> MalUserEx(it[0]) } } ) diff --git a/kotlin/src/mal/env.kt b/kotlin/src/mal/env.kt index 02c4dbc6..2907a59c 100644 --- a/kotlin/src/mal/env.kt +++ b/kotlin/src/mal/env.kt @@ -34,7 +34,7 @@ class Env(val outer: Env? = null, // get: takes a symbol key and uses the find method to locate the environment with the key, then returns the matching value. If no key is found up the outer chain, then throws/raises a "not found" error. fun get(key: MalSymbol): MalType { - val env = find(key) ?: throw Exception("Could not find '${key.sym}' in env") + val env = find(key) ?: throw MalCoreEx("Could not find '${key.sym}' in env") return env.data.getValue(key) } } diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index 37237416..b5a927f1 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -38,7 +38,8 @@ fun as_str(v: MalType, readable: Boolean) : String { is MalNil -> "nil" is MalUserFunc -> "#<${v.name}>" is MalFunc -> "#<${v.name}>" - else -> "" + is MalUserEx -> "Exception raised: "+as_str(v, readable) + else -> "Can't stringify a "+v::class } } @@ -46,5 +47,5 @@ fun as_str(v: MalType, readable: Boolean) : String { // str end up with multiply quoted strings. fun pr_str(v: MalType, readable: Boolean = true) : String { val s = as_str(v, readable) - return if(v is MalString) "\"$s\"" else s + return if(v is MalString && v !is MalKeyword) "\"$s\"" else s } diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index e4aec035..faad8cce 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -30,7 +30,7 @@ setOf(RegexOption.COMMENTS, RegexOption.MULTILINE) fun tokenize(s: String) : List { // For some reason this fails where findAll doesn't. // if (!tokenizer.matches(s)) { - // throw Exception("Failed tokenizing") + // throw MalCoreEx("Failed tokenizing") // } return tokenizer.findAll(s) .map { it.value.trim() } @@ -129,7 +129,7 @@ var readLimit = 0 fun check_limit() { readLimit++ if (readLimit > 1024) { - throw Exception("Parser found no end :/") + throw MalCoreEx("Parser found no end :/") } } @@ -149,7 +149,7 @@ fun read_form(r: Reader, n: Int) : MalType { } } catch(e: IndexOutOfBoundsException) { - throw Exception("Ran out of tokens, missing right paren?") + throw MalCoreEx("Ran out of tokens, missing right paren?") } } diff --git a/kotlin/src/mal/step9_try.kt b/kotlin/src/mal/step9_try.kt index 8b137891..913557df 100644 --- a/kotlin/src/mal/step9_try.kt +++ b/kotlin/src/mal/step9_try.kt @@ -1 +1,268 @@ +fun READ(s: String) = read_str(s) + +fun eval_ast(ast: MalType, env: Env, depth: Int) : MalType { + return when(ast) { + is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalVector -> MalVector(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalMap -> malMapOf(ast.pairs.map { (k,v) -> k to EVAL(v, env, depth + 1) }) + is MalSymbol -> env.get(ast) + else -> ast + } +} + +fun make_env(pairs: MalSeq, outer_env: Env, depth: Int) : Env { + val new_env = Env(outer_env) + for (idx in pairs.atoms.indices step 2) { + val k = pairs.atoms[idx] as MalSymbol + val v = pairs.atoms[idx + 1] + new_env.set(k, EVAL(v, new_env, depth + 1)) + } + return new_env +} + +fun is_true(cond: MalType) = + when (cond) { + is MalNil -> false + is MalBoolean -> cond.bool + else -> true + } + +// Not implemented as safe-casing is gooder. +// fun is_pair(p: MalType) = p is MalList && p.size > 0 + +fun quasiquote(ast: MalType) : MalType { + return if (ast is MalList && ast.size > 0) { + val fst = ast.head() + val rest = ast.tail() + if (fst == malSym("unquote")) + rest.head() + else if (fst is MalList && fst.head() == malSym("splice-unquote")) + malListOf(malSym("concat"), fst.tail().head(), quasiquote(rest)) + else + malListOf(malSym("cons"), quasiquote(fst), quasiquote(rest)) + } + else { + malListOf(malSym("quote"), ast) + } +} + +fun is_macro_call(ast: MalType, env: Env) : Boolean { + if(ast is MalList && ast[0] is MalSymbol) { + val sym = ast[0] as MalSymbol + val e = env.find(sym) ?: return false + val v = e.get(sym) + return v is MalCallable && v.isMacro + } + else { + return false + } +} + +fun macroexpand(orig_ast: MalType, env: Env) : MalType { + var ast = orig_ast + while(is_macro_call(ast, env)) { + val l = ast as MalList + // Inside the loop, the first element of the ast list (a symbol), is looked up in the environment to get the macro function + val f = env.get(l.head() as MalSymbol) as MalUserFunc + // This macro function is then called/applied with the rest of the ast elements (2nd through the last) as arguments. + // The return value of the macro call becomes the new value of ast. + ast = f(l.tail()) + } + return ast +} + +var eval_count = 0 +fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { +// val say = { m: String -> println(" ".repeat(depth) + m) } + // Only use n when recursing into EVAL + val n = depth + 1 + // Allow modification of where in the ast is being evaluated. + var ast = cur_ast + // Allow modification of which env is pointed at while evaluating. + var env = cur_env + eval_loop@ while (true) { + eval_count++ + // The depth check is low enough so as not to hit a terminal StackOverflow error. + // But eval_count is just an arbitrary limit. + if (depth > 1012 || eval_count > 654321) { + throw MalCoreEx("Recursion/eval limit hit: depth ${depth} / evalled ${eval_count}!") + } + + ast = macroexpand(ast, env) + + if (ast !is MalList) { + return eval_ast(ast, env, depth) + } + else { + if (ast.atoms.isEmpty()) { + return ast + } + else { + val next = ast.head() + val rest = ast.tail() + if(next is MalSymbol) { + when(next.sym) { + "def!" -> { + val v = EVAL(rest[1], env, n) + val name = (rest[0] as MalSymbol) + if (v is MalUserFunc) { + v.name = name.sym + } + return env.set(name, v) + } + "defmacro!" -> { + val v = EVAL(rest[1], env, n) + if (v !is MalUserFunc) { + throw MalCoreEx("Can't defmacro! with a: "+pr_str(v)) + } + else { + v.isMacro = true + } + val name = (rest[0] as MalSymbol) + return env.set(name, v) + + } + "macroexpand" -> { + return macroexpand(rest[0], env) + } + "let*" -> { + // Set env (i.e. the local variable passed in as second parameter of EVAL) to the new let environment. + env = make_env((rest[0] as MalSeq), env, depth) + // Set ast (i.e. the local variable passed in as first parameter of EVAL) to be the second ast argument. + ast = ast[2] + continue@eval_loop // TCO + } + "do" -> { + // change the eval_ast call to evaluate all the parameters except for the last (2nd list element up to but not including last). + eval_ast(rest.butlast(), env, depth) + // Set ast to the last element of ast. Continue at the beginning of the loop (env stays unchanged). + ast = rest.last() + continue@eval_loop // TCO + } + "if" -> { + // the condition continues to be evaluated, however, rather than evaluating the true or false branch, ast is set to the unevaluated value of the chosen branch. + ast = if(is_true(EVAL(rest[0], env, n))) rest[1] else + if(rest.atoms.count() == 3) rest[2] else ast + continue@eval_loop // TCO + } + "fn*" -> { + // The return value from the fn* special form will now become an object/structure with attributes that allow the default invoke case of EVAL to do TCO on mal functions. Those attributes are: + val binds = rest[0] as MalSeq + val body = rest[1] +// val func = malFun("funccall") + return MalUserFunc(body, binds, env) { + EVAL(body, Env(env, binds, it), n) + } + } + "quote" -> { + return rest.head() + } + // This is called from `EVAL` with the first `ast` argument (second list element) and then `ast` is set to the result and execution continues at the top of the loop (TCO). + "quasiquote" -> { + // TODO TCO + // XXX Support >1 args? + ast = quasiquote(rest[0]) + continue@eval_loop // TCO + } + "try*" -> { + val body = rest[0] + return try { + EVAL(body, env, n) + } + catch(e: MalUserEx) { + if(rest.size > 1) { + val catchSeq = rest[1] as MalSeq + val catchSym = catchSeq[0] + if (catchSym != malSym("catch*")) + throw MalCoreEx("Expected 'catch*', got '${pr_str(catchSym)}'") + val exBind = catchSeq[1] as MalSymbol + val catchBody = catchSeq[2] + val catchEnv = Env(env, malListOf(exBind), malListOf(e.src)) + EVAL(catchBody, catchEnv, n) + } + else { + // Throw it up if nothing can catch it + throw e + } + } + } + } + } + + val op = eval_ast(ast, env, depth) as MalList + val func = op.head() + val args = op.tail() + + if(func is MalUserFunc) { + ast = func.ast + env = Env(func.env, func.params, args) + continue@eval_loop // TCO + } + else if(func is MalFunc) { + return func(args) + } + else { + return func +// throw MalCoreEx("Don't know what to do with " + func) + } + } + } + } +} + +fun PRINT(v: MalType) = pr_str(v) + +fun rep(s: String) = PRINT(EVAL(READ(s), repl_env, 0)) + +val repl_env = Env().apply { + core.ns.forEach { (k,v) -> set(k, v) } +} + +fun main(args: Array) { + repl_env.set(malSym("eval"), malFun("eval") { + val res = eval_ast(it, repl_env, 0) + when(res) { + is MalList -> res.last() + else -> res + } + }) + + rep("""(def! load-file (fn* [f] (eval (read-string (str "(do " (slurp f) ")")))))""") + rep("""(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw "odd number of forms to cond")) (cons 'cond (rest (rest xs)))))))""") + rep("""(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) `(let* (or_FIXME ~(first xs)) (if or_FIXME or_FIXME (or ~@(rest xs))))))))""") + rep("(def! not (fn* [v] (if v false true)))") + + if(args.size > 0) { + repl_env.set(malSym("*ARGV*"), malListOf(args.drop(1).map(::MalString))) + rep("""(load-file "${args[0]}")""") + } + else { + repl_env.set(malSym("*ARGV*"), emptyMalList()) + repl@ while(true) { + print("user> ") + + try { + val line = readLine() ?: continue@repl + if (setOf("quit","exit").contains(line.trim())) { + println("Bye!") + break@repl + } + println(rep(line)) + } + catch(e: Throwable) { + println( + when(e) { + is MalUserEx -> "Exception raised: " + pr_str(e.src) + is MalCoreEx -> "Error encountered: " + e.msg + else -> "Non mal exception: " + e.message + } + ) + e.printStackTrace() + } + finally { + eval_count = 0 + } + } + } +} diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index dca2766b..ef31e46b 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -24,7 +24,7 @@ data class MalKeyword(val kw: String) : MalString(kw) // Would use MalAtom but that's already a thing :/ data class MalCljAtom(var value : MalType) : MalType -class MalNil() : MalAtom +class MalNil : MalAtom interface MalSeq : MalType { val atoms : List @@ -40,8 +40,8 @@ interface MalSeq : MalType { // TODO Maybe implement complementN too? } -class MalList(override val atoms: List) : MalSeq -class MalVector(override val atoms: List) : MalSeq +data class MalList(override val atoms: List) : MalSeq +data class MalVector(override val atoms: List) : MalSeq class MalMap(val pairs: Map) : MalType { operator fun get(k: MalString): MalType = pairs[k] ?: MalNil() @@ -67,6 +67,9 @@ class MalUserFunc( func: MalFn ) : MalCallable(func, name) +data class MalUserEx(val src: MalType) : Exception("Exception raised"), MalType +data class MalCoreEx(val msg: String) : Exception(msg) + // Helper functions. fun emptyMalList() = MalList(listOf()) fun malListOf(vararg elems: MalType) = MalList(elems.asList()) From 0f9d219ff4ec2bd5af9bac48ce5de1cade6ea4c0 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Fri, 27 Mar 2020 23:21:00 +0000 Subject: [PATCH 28/48] Implement step 9 apply & map If Kotlin had currying map could be even cleaner. --- kotlin/src/mal/core.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 2d0e4f61..0b411965 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -180,6 +180,22 @@ object core { 0 -> MalUserEx(MalString("error raised anon")) else -> MalUserEx(it[0]) } + }, + to_fun("apply") { + // The first argument is a function and the last argument + // is list (or vector). The arguments between the function + // and the last argument (if there are any) are + // concatenated with the final argument to create the + // arguments that are used to call the function. + val fn = it[0] as MalCallable + val argSeq = it.last() as MalSeq + val args = it.atoms.slice(1 .. (if(it.size > 2) it.size - 2 else 0)) + fn(malListOf(argSeq.atoms + args)) + }, + to_fun("map") { + val fn = it[0] as MalCallable + val args = it[1] as MalSeq + malListOf(args.atoms.map { fn(malListOf(it)) }) } ) } From c99e1ddc9f5e989e8c507b2661f6340cf3735f8b Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Fri, 27 Mar 2020 23:26:38 +0000 Subject: [PATCH 29/48] Implement step 9 predicates --- kotlin/src/mal/core.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 0b411965..64d01a76 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -175,12 +175,14 @@ object core { throw MalCoreEx("Can't fall 'rest' on " + pr_str(v)) } }, + to_fun("throw") { throw when(it.size) { 0 -> MalUserEx(MalString("error raised anon")) else -> MalUserEx(it[0]) } }, + to_fun("apply") { // The first argument is a function and the last argument // is list (or vector). The arguments between the function @@ -196,6 +198,19 @@ object core { val fn = it[0] as MalCallable val args = it[1] as MalSeq malListOf(args.atoms.map { fn(malListOf(it)) }) + }, + + to_fun("nil?") { + MalBoolean(it[0] is MalNil) + }, + to_fun("true?") { + MalBoolean(it[0] == MalBoolean(true)) + }, + to_fun("false?") { + MalBoolean(it[0] == MalBoolean(false)) + }, + to_fun("symbol?") { + MalBoolean(it[0] is MalSymbol) } ) } From 00b3cad257eef3c8644b70be75e7cdffdef00102 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Fri, 27 Mar 2020 23:59:31 +0000 Subject: [PATCH 30/48] Implement step 9 deferrable functions Luckily Kotlin's world view lines up closely with mal. --- kotlin/src/mal/core.kt | 50 ++++++++++++++++++++++++++++++++++++++++ kotlin/src/mal/reader.kt | 6 +++++ 2 files changed, 56 insertions(+) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 64d01a76..50fa505b 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -211,6 +211,56 @@ object core { }, to_fun("symbol?") { MalBoolean(it[0] is MalSymbol) + }, + + to_fun("symbol") { + MalSymbol((it[0] as MalString).str) + }, + to_fun("keyword") { + val kw = it[0] + if (kw is MalKeyword) kw else MalKeyword((kw as MalString).str) + }, + to_fun("keyword?") { + MalBoolean(it[0] is MalKeyword) + }, + to_fun("vector") { + MalVector(it.atoms) + }, + to_fun("vector?") { + MalBoolean(it[0] is MalVector) + }, + to_fun("hash-map") { + seq_to_map(it) + }, + to_fun("map?") { + MalBoolean(it[0] is MalMap) + }, + to_fun("assoc") { + val m = it[0] as MalMap + MalMap(m.pairs + seq_to_map(it.tail()).pairs) + }, + to_fun("dissoc") { + val m = it[0] as MalMap + MalMap(m.pairs - it.tail().atoms.map { it as MalString }) + }, + to_fun("get") { + val m = it[0] as MalMap + m.pairs.getOrDefault(it[1] as MalString, MalNil()) + }, + to_fun("contains?") { + val m = it[0] as MalMap + MalBoolean(m.pairs.contains(it[1])) + }, + to_fun("keys") { + val m = it[0] as MalMap + malListOf(m.pairs.keys.toList()) + }, + to_fun("vals") { + val m = it[0] as MalMap + malListOf(m.pairs.values.toList()) + }, + to_fun("sequential?") { + MalBoolean(it[0] is MalSeq) } ) } diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index faad8cce..e5c009f1 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -123,6 +123,12 @@ fun read_map(pairs: List) : MalMap { return MalMap(map) } +fun seq_to_map(pairs: MalSeq) = + if(pairs.size % 2 == 0) + read_map(pairs.atoms) + else + throw MalUserEx(MalString("maps requires an even number of items, got ${pairs.size} items")) + var readLimit = 0 // Safety limit to prevent the REPL never coming back. From 4a1a1e4df0efc72a4884a5b5c2d65ea2db69835e Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sat, 28 Mar 2020 10:58:09 +0000 Subject: [PATCH 31/48] Implement step A meta functions This seems the appropriate point to implement meta. --- kotlin/src/mal/core.kt | 17 +++ kotlin/src/mal/reader.kt | 5 + kotlin/src/mal/stepA_mal.kt | 266 ++++++++++++++++++++++++++++++++++++ kotlin/src/mal/types.kt | 24 ++-- 4 files changed, 304 insertions(+), 8 deletions(-) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 50fa505b..87f0830d 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -44,6 +44,8 @@ private fun pr_str_core(seq: MalSeq) = private fun str_core(seq: MalSeq) = seq.atoms.map { as_str(it, readable=false) }.joinToString("") +private val eof = "" + object core { val ns : Map = mutableMapOf( // Basic number ops. @@ -261,6 +263,21 @@ object core { }, to_fun("sequential?") { MalBoolean(it[0] is MalSeq) + }, + + to_fun("readline") { + print((it[0] as MalString).str) + val line = readLine() + if (line == null) MalNil() else MalString(line.trim()) + }, + + to_fun("meta") { + val f = it[0] as MalCallable + f.meta + }, + to_fun("with-meta") { + val f = it[0] as MalCallable + f.withMeta(it[1]) } ) } diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index e5c009f1..ea89aa3e 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -93,6 +93,11 @@ fun read_atom(r: Reader, n: Int) : MalType { else if (t[0] == ':') { MalKeyword(t.substring(1 .. t.length - 1)) } + else if (t[0] == '^') { + val meta = read_form(r, n) + val func = read_form(r, n) + malListOf(malSym("with-meta"), func, meta) + } else if (t == "@") { make_atom(r.next()) } diff --git a/kotlin/src/mal/stepA_mal.kt b/kotlin/src/mal/stepA_mal.kt index 8b137891..5f9fd43b 100644 --- a/kotlin/src/mal/stepA_mal.kt +++ b/kotlin/src/mal/stepA_mal.kt @@ -1 +1,267 @@ +fun READ(s: String) = read_str(s) + +fun eval_ast(ast: MalType, env: Env, depth: Int) : MalType { + return when(ast) { + is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalVector -> MalVector(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalMap -> malMapOf(ast.pairs.map { (k,v) -> k to EVAL(v, env, depth + 1) }) + is MalSymbol -> env.get(ast) + else -> ast + } +} + +fun make_env(pairs: MalSeq, outer_env: Env, depth: Int) : Env { + val new_env = Env(outer_env) + for (idx in pairs.atoms.indices step 2) { + val k = pairs.atoms[idx] as MalSymbol + val v = pairs.atoms[idx + 1] + new_env.set(k, EVAL(v, new_env, depth + 1)) + } + return new_env +} + +fun is_true(cond: MalType) = + when (cond) { + is MalNil -> false + is MalBoolean -> cond.bool + else -> true + } + +// Not implemented as safe-casing is gooder. +// fun is_pair(p: MalType) = p is MalList && p.size > 0 + +fun quasiquote(ast: MalType) : MalType { + return if (ast is MalList && ast.size > 0) { + val fst = ast.head() + val rest = ast.tail() + if (fst == malSym("unquote")) + rest.head() + else if (fst is MalList && fst.head() == malSym("splice-unquote")) + malListOf(malSym("concat"), fst.tail().head(), quasiquote(rest)) + else + malListOf(malSym("cons"), quasiquote(fst), quasiquote(rest)) + } + else { + malListOf(malSym("quote"), ast) + } +} + +fun is_macro_call(ast: MalType, env: Env) : Boolean { + if(ast is MalList && ast.size > 0 && ast[0] is MalSymbol) { + val sym = ast[0] as MalSymbol + val e = env.find(sym) ?: return false + val v = e.get(sym) + return v is MalCallable && v.isMacro + } + else { + return false + } +} + +fun macroexpand(orig_ast: MalType, env: Env) : MalType { + var ast = orig_ast + while(is_macro_call(ast, env)) { + val l = ast as MalList + // Inside the loop, the first element of the ast list (a symbol), is looked up in the environment to get the macro function + val f = env.get(l.head() as MalSymbol) as MalUserFunc + // This macro function is then called/applied with the rest of the ast elements (2nd through the last) as arguments. + // The return value of the macro call becomes the new value of ast. + ast = f(l.tail()) + } + return ast +} + +var eval_count = 0 +fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { +// val say = { m: String -> println(" ".repeat(depth) + m) } + // Only use n when recursing into EVAL + val n = depth + 1 + // Allow modification of where in the ast is being evaluated. + var ast = cur_ast + // Allow modification of which env is pointed at while evaluating. + var env = cur_env + eval_loop@ while (true) { + eval_count++ + // The depth check is low enough so as not to hit a terminal StackOverflow error. + // But eval_count is just an arbitrary limit. + if (depth > 1012 || eval_count > 654321) { + throw MalCoreEx("Recursion/eval limit hit: depth ${depth} / evalled ${eval_count}!") + } + + ast = macroexpand(ast, env) + + if (ast !is MalList) { + return eval_ast(ast, env, depth) + } + else { + if (ast.atoms.isEmpty()) { + return ast + } + else { + val next = ast.head() + val rest = ast.tail() + if(next is MalSymbol) { + when(next.sym) { + "def!" -> { + val v = EVAL(rest[1], env, n) + val name = (rest[0] as MalSymbol) + if (v is MalUserFunc) { + v.name = name.sym + } + return env.set(name, v) + } + "defmacro!" -> { + val v = EVAL(rest[1], env, n) + if (v !is MalUserFunc) { + throw MalCoreEx("Can't defmacro! with a: "+pr_str(v)) + } + else { + v.isMacro = true + } + val name = (rest[0] as MalSymbol) + return env.set(name, v) + + } + "macroexpand" -> { + return macroexpand(rest[0], env) + } + "let*" -> { + // Set env (i.e. the local variable passed in as second parameter of EVAL) to the new let environment. + env = make_env((rest[0] as MalSeq), env, depth) + // Set ast (i.e. the local variable passed in as first parameter of EVAL) to be the second ast argument. + ast = ast[2] + continue@eval_loop // TCO + } + "do" -> { + // change the eval_ast call to evaluate all the parameters except for the last (2nd list element up to but not including last). + eval_ast(rest.butlast(), env, depth) + // Set ast to the last element of ast. Continue at the beginning of the loop (env stays unchanged). + ast = rest.last() + continue@eval_loop // TCO + } + "if" -> { + // the condition continues to be evaluated, however, rather than evaluating the true or false branch, ast is set to the unevaluated value of the chosen branch. + ast = if(is_true(EVAL(rest[0], env, n))) rest[1] else + if(rest.atoms.count() == 3) rest[2] else ast + continue@eval_loop // TCO + } + "fn*" -> { + // The return value from the fn* special form will now become an object/structure with attributes that allow the default invoke case of EVAL to do TCO on mal functions. Those attributes are: + val binds = rest[0] as MalSeq + val body = rest[1] + return MalUserFunc(body, binds, env, "anon", MalNil()) { + EVAL(body, Env(env, binds, it), n) + } + } + "quote" -> { + return rest.head() + } + // This is called from `EVAL` with the first `ast` argument (second list element) and then `ast` is set to the result and execution continues at the top of the loop (TCO). + "quasiquote" -> { + // TODO TCO + // XXX Support >1 args? + ast = quasiquote(rest[0]) + continue@eval_loop // TCO + } + "try*" -> { + val body = rest[0] + return try { + EVAL(body, env, n) + } + catch(e: MalUserEx) { + if(rest.size > 1) { + val catchSeq = rest[1] as MalSeq + val catchSym = catchSeq[0] + if (catchSym != malSym("catch*")) + throw MalCoreEx("Expected 'catch*', got '${pr_str(catchSym)}'") + val exBind = catchSeq[1] as MalSymbol + val catchBody = catchSeq[2] + val catchEnv = Env(env, malListOf(exBind), malListOf(e.src)) + EVAL(catchBody, catchEnv, n) + } + else { + // Throw it up if nothing can catch it + throw e + } + } + } + } + } + + val op = eval_ast(ast, env, depth) as MalList + val func = op.head() + val args = op.tail() + + if(func is MalUserFunc) { + ast = func.ast + env = Env(func.env, func.params, args) + continue@eval_loop // TCO + } + else if(func is MalFunc) { + return func(args) + } + else { + return func +// throw MalCoreEx("Don't know what to do with " + func) + } + } + } + } +} + +fun PRINT(v: MalType) = pr_str(v) + +fun rep(s: String) = PRINT(EVAL(READ(s), repl_env, 0)) + +val repl_env = Env().apply { + core.ns.forEach { (k,v) -> set(k, v) } +} + +fun main(args: Array) { + repl_env.set(malSym("eval"), malFun("eval") { + val res = eval_ast(it, repl_env, 0) + when(res) { + is MalList -> res.last() + else -> res + } + }) + + rep("""(def! load-file (fn* [f] (eval (read-string (str "(do " (slurp f) ")")))))""") + rep("""(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw "odd number of forms to cond")) (cons 'cond (rest (rest xs)))))))""") + rep("""(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) `(let* (or_FIXME ~(first xs)) (if or_FIXME or_FIXME (or ~@(rest xs))))))))""") + rep("(def! not (fn* [v] (if v false true)))") + + if(args.size > 0) { + repl_env.set(malSym("*ARGV*"), malListOf(args.drop(1).map(::MalString))) + rep("""(load-file "${args[0]}")""") + } + else { + repl_env.set(malSym("*ARGV*"), emptyMalList()) + repl@ while(true) { + print("user> ") + + try { + val line = readLine() ?: continue@repl + if (setOf("quit", "exit").contains(line.trim())) { + println("Bye!") + break@repl + } + println(rep(line)) + } + catch(e: Throwable) { + println( + when(e) { + is MalUserEx -> "Exception raised: " + pr_str(e.src) + is MalCoreEx -> "Error encountered: " + e.msg + else -> "Non mal exception: " + e.message + } + ) + e.printStackTrace() + } + finally { + eval_count = 0 + } + } + } +} diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index ef31e46b..b06f806d 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -49,23 +49,31 @@ class MalMap(val pairs: Map) : MalType { typealias MalFn = (MalSeq) -> MalType -open class MalCallable(val func: MalFn, var name: String) : MalType { +abstract class MalCallable(val func: MalFn, var name: String, val meta: MalType) : MalType { var isMacro = false - operator fun invoke(args: MalSeq) : MalType { - return func(args) - } + + operator fun invoke(args: MalSeq) = func(args) + + abstract fun withMeta(m: MalType): MalCallable } // Allow name to be set after the fact so functions in Env are named. -class MalFunc(func: MalFn, name: String = "anon") : MalCallable(func, name) +class MalFunc(func: MalFn, name: String = "anon", meta: MalType = MalNil()) : MalCallable(func, name, meta) { + override fun withMeta(m: MalType) = + MalFunc(func, name, m) +} class MalUserFunc( val ast: MalType, val params: MalSeq, val env: Env, - name: String = "anon", - func: MalFn -) : MalCallable(func, name) + name: String, + meta: MalType, + func: MalFn +) : MalCallable(func, name, meta) { + override fun withMeta(m: MalType) = + MalUserFunc(ast, params, env, name, m, func) +} data class MalUserEx(val src: MalType) : Exception("Exception raised"), MalType data class MalCoreEx(val msg: String) : Exception(msg) From 75424b31eb91a63deb50230d9e03a51883082171 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sat, 28 Mar 2020 11:09:32 +0000 Subject: [PATCH 32/48] Use when over if in read_atom It makes the code much more pleasing to look at. --- kotlin/src/mal/reader.kt | 64 +++++++++++++++------------------------- 1 file changed, 23 insertions(+), 41 deletions(-) diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index ea89aa3e..c5269d7b 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -58,7 +58,7 @@ class Reader(val tokens: List) { fun is_number(s: String) = Regex("\\d+").matches(s) -fun make_atom(token: String) = MalList(listOf("deref", token).map(::malSym)) +fun make_atom_deref(token: String) = MalList(listOf("deref", token).map(::malSym)) // Reflect any changes in printer.kt val readEscapeMap = mapOf( @@ -73,48 +73,30 @@ val readEscapes = Regex(listOf("\\\\\"", "\\\\n", "\\\\\\\\").joinToString("|", fun make_string(s: String) = MalString(s.replace(readEscapes) { readEscapeMap.get(it.value) ?: it.value }) -// This function will look at the contents of the token and return the -// appropriate scalar (simple/single) data type value. +fun make_with_meta(r: Reader, n: Int): MalType { + val meta = read_form(r, n) + val func = read_form(r, n) + return malListOf(malSym("with-meta"), func, meta) +} + +private fun is_bool(s: String) = setOf("true", "false").contains(s) + fun read_atom(r: Reader, n: Int) : MalType { -// println("Reading atom: " + r) + // println("Reading atom: " + r) val t = r.next() - return if (is_number(t)) { - MalNumber(t.toInt()) - } - else if (t[0] == '"') { - make_string(t.substring(1 .. t.length - 2)) - } - else if (t == "true" || t == "false") { - MalBoolean(t == "true") - } - else if (t == "nil") { - MalNil() - } - else if (t[0] == ':') { - MalKeyword(t.substring(1 .. t.length - 1)) - } - else if (t[0] == '^') { - val meta = read_form(r, n) - val func = read_form(r, n) - malListOf(malSym("with-meta"), func, meta) - } - else if (t == "@") { - make_atom(r.next()) - } - else if (t == "'") { - malListOf(malSym("quote"), read_form(r, n)) - } - else if (t == "`") { - malListOf(malSym("quasiquote"), read_form(r, n)) - } - else if (t == "~") { - malListOf(malSym("unquote"), read_form(r, n)) - } - else if (t == "~@") { - malListOf(malSym("splice-unquote"), read_form(r, n)) - } - else { - MalSymbol(t) + return when { + t[0] == '"' -> make_string(t.substring(1 .. t.length - 2)) + t[0] == ':' -> MalKeyword(t.substring(1 .. t.length - 1)) + t[0] == '^' -> make_with_meta(r, n) + is_number(t) -> MalNumber(t.toInt()) + is_bool(t) -> MalBoolean(t == "true") + t == "nil" -> MalNil() + t == "@" -> make_atom_deref(r.next()) + t == "'" -> malListOf(malSym("quote"), read_form(r, n)) + t == "`" -> malListOf(malSym("quasiquote"), read_form(r, n)) + t == "~" -> malListOf(malSym("unquote"), read_form(r, n)) + t == "~@" -> malListOf(malSym("splice-unquote"), read_form(r, n)) + else -> MalSymbol(t) } } From 4b43b1838e90cf993b89df4947976b0e8911b8b7 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sat, 28 Mar 2020 11:26:22 +0000 Subject: [PATCH 33/48] Apply private in more places Misc tweaks as I dawdle before finishing. --- kotlin/src/mal/core.kt | 4 ++-- kotlin/src/mal/reader.kt | 47 +++++++++++++++++----------------------- kotlin/src/mal/types.kt | 2 +- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 87f0830d..a459537d 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -232,14 +232,14 @@ object core { MalBoolean(it[0] is MalVector) }, to_fun("hash-map") { - seq_to_map(it) + make_map(it) }, to_fun("map?") { MalBoolean(it[0] is MalMap) }, to_fun("assoc") { val m = it[0] as MalMap - MalMap(m.pairs + seq_to_map(it.tail()).pairs) + MalMap(m.pairs + make_map(it.tail()).pairs) }, to_fun("dissoc") { val m = it[0] as MalMap diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index c5269d7b..72c25dc6 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -2,7 +2,7 @@ // of the regular expression a new token will be created. // TODO Support escaped double quotes e.g "foo \"bar\" baz" -> """foo "bar" baz""" -var tokenizer = Regex(""" +private var tokenizer = Regex(""" # Matches any number of whitespaces or commas [\s,]* (?: @@ -43,45 +43,39 @@ fun tokenize(s: String) : List { class Reader(val tokens: List) { var pos = 0 // returns the token at the current position and increments the position - fun next() : String { - val s = tokens[pos] - pos++ - return s - } - + fun next() = tokens[pos++] // Check whether we're at the end. fun isLast() = pos == (tokens.size - 1) // just returns the token at the current position. fun peek() = tokens[pos] } +private fun is_number(s: String) = Regex("\\d+").matches(s) -fun is_number(s: String) = Regex("\\d+").matches(s) +private fun is_bool(s: String) = setOf("true", "false").contains(s) -fun make_atom_deref(token: String) = MalList(listOf("deref", token).map(::malSym)) +private fun make_atom_deref(token: String) = MalList(listOf("deref", token).map(::malSym)) // Reflect any changes in printer.kt -val readEscapeMap = mapOf( +private val readEscapeMap = mapOf( "\\\"" to "\"", "\\n" to "\n", "\\\\" to "\\" ) // Bleurgh, the escapes need escapes as they become interpolated into Regex ;_; // So we need to manage three levels escaping different >_< -val readEscapes = Regex(listOf("\\\\\"", "\\\\n", "\\\\\\\\").joinToString("|", "(", ")")) +private val readEscapes = Regex(listOf("\\\\\"", "\\\\n", "\\\\\\\\").joinToString("|", "(", ")")) -fun make_string(s: String) = +private fun make_string(s: String) = MalString(s.replace(readEscapes) { readEscapeMap.get(it.value) ?: it.value }) -fun make_with_meta(r: Reader, n: Int): MalType { +private fun make_with_meta(r: Reader, n: Int): MalType { val meta = read_form(r, n) val func = read_form(r, n) return malListOf(malSym("with-meta"), func, meta) } -private fun is_bool(s: String) = setOf("true", "false").contains(s) - -fun read_atom(r: Reader, n: Int) : MalType { +private fun read_atom(r: Reader, n: Int) : MalType { // println("Reading atom: " + r) val t = r.next() return when { @@ -100,7 +94,10 @@ fun read_atom(r: Reader, n: Int) : MalType { } } -fun read_map(pairs: List) : MalMap { +private fun make_map(pairs: List) : MalMap { + if(pairs.size % 2 != 0) + throw MalUserEx(MalString("maps requires an even number of items, got ${pairs.size} items")) + val map : MutableMap = mutableMapOf() for (idx in pairs.indices step 2) { val k = pairs[idx] as MalString @@ -110,16 +107,12 @@ fun read_map(pairs: List) : MalMap { return MalMap(map) } -fun seq_to_map(pairs: MalSeq) = - if(pairs.size % 2 == 0) - read_map(pairs.atoms) - else - throw MalUserEx(MalString("maps requires an even number of items, got ${pairs.size} items")) +fun make_map(pairs: MalSeq) = make_map(pairs.atoms) -var readLimit = 0 +private var readLimit = 0 // Safety limit to prevent the REPL never coming back. -fun check_limit() { +private fun check_limit() { readLimit++ if (readLimit > 1024) { throw MalCoreEx("Parser found no end :/") @@ -137,7 +130,7 @@ fun read_form(r: Reader, n: Int) : MalType { return when(r.peek()) { "(" -> MalList(read_seq(")", r, n + 1)) "[" -> MalVector(read_seq("]", r, n + 1)) - "{" -> read_map(read_seq("}", r, n + 1)) + "{" -> make_map(read_seq("}", r, n + 1)) else -> read_atom(r, n + 1) } } @@ -146,7 +139,7 @@ fun read_form(r: Reader, n: Int) : MalType { } } -fun read_seq(endTok: String, r: Reader, n: Int) : List { +private fun read_seq(endTok: String, r: Reader, n: Int) : List { r.next() // Move past the opening paren. // val say = { m: String -> println("v1> " + " ".repeat(n) + m) } val list : MutableList = mutableListOf() @@ -160,7 +153,7 @@ fun read_seq(endTok: String, r: Reader, n: Int) : List { return list } -fun read_form_safely(r: Reader) : MalType { +private fun read_form_safely(r: Reader) : MalType { try { return if(r.tokens.isEmpty()) { emptyMalList() diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index b06f806d..319bcb00 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -80,7 +80,7 @@ data class MalCoreEx(val msg: String) : Exception(msg) // Helper functions. fun emptyMalList() = MalList(listOf()) -fun malListOf(vararg elems: MalType) = MalList(elems.asList()) +fun malListOf(vararg elems: MalType) = malListOf(elems.asList()) fun malListOf(elems: List) = MalList(elems) fun malMapOf(elems: List>) = MalMap(mapOf(*elems.toTypedArray())) fun malSym(sym: String) = MalSymbol(sym) From 0551222ac73c7c9c8a94cf32fecd8922bedbd879 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sat, 28 Mar 2020 17:11:44 +0000 Subject: [PATCH 34/48] Detangle MalKeyword from MalString Not sure why I went with the initial rubbish solution, probably because late night coding does not a good result make. --- kotlin/src/mal/core.kt | 5 +++-- kotlin/src/mal/printer.kt | 2 +- kotlin/src/mal/reader.kt | 4 ++-- kotlin/src/mal/types.kt | 12 ++++++------ 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index a459537d..cdedc662 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -243,11 +243,12 @@ object core { }, to_fun("dissoc") { val m = it[0] as MalMap - MalMap(m.pairs - it.tail().atoms.map { it as MalString }) + MalMap(m.pairs - it.tail().atoms.map { it as MalKey }) }, to_fun("get") { val m = it[0] as MalMap - m.pairs.getOrDefault(it[1] as MalString, MalNil()) + val k = it[1] as MalKey + m[k] }, to_fun("contains?") { val m = it[0] as MalMap diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index b5a927f1..af45cea8 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -47,5 +47,5 @@ fun as_str(v: MalType, readable: Boolean) : String { // str end up with multiply quoted strings. fun pr_str(v: MalType, readable: Boolean = true) : String { val s = as_str(v, readable) - return if(v is MalString && v !is MalKeyword) "\"$s\"" else s + return if(v is MalString) "\"$s\"" else s } diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index 72c25dc6..3f932885 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -98,9 +98,9 @@ private fun make_map(pairs: List) : MalMap { if(pairs.size % 2 != 0) throw MalUserEx(MalString("maps requires an even number of items, got ${pairs.size} items")) - val map : MutableMap = mutableMapOf() + val map : MutableMap = mutableMapOf() for (idx in pairs.indices step 2) { - val k = pairs[idx] as MalString + val k = pairs[idx] as MalKey val v = pairs[idx + 1] map[k] = v } diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index 319bcb00..2b505b08 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -16,10 +16,10 @@ data class MalSymbol(val sym: String) : MalAtom data class MalBoolean(val bool: Boolean) : MalAtom -open class MalString(val str: String) : MalAtom +interface MalKey : MalType -// XXX Inheriting from MalString is a bit shonky. -data class MalKeyword(val kw: String) : MalString(kw) +data class MalString(val str: String) : MalKey +data class MalKeyword(val kw: String) : MalKey // Would use MalAtom but that's already a thing :/ data class MalCljAtom(var value : MalType) : MalType @@ -43,8 +43,8 @@ interface MalSeq : MalType { data class MalList(override val atoms: List) : MalSeq data class MalVector(override val atoms: List) : MalSeq -class MalMap(val pairs: Map) : MalType { - operator fun get(k: MalString): MalType = pairs[k] ?: MalNil() +class MalMap(val pairs: Map) : MalType { + operator fun get(k: MalKey): MalType = pairs[k] ?: MalNil() } typealias MalFn = (MalSeq) -> MalType @@ -82,6 +82,6 @@ data class MalCoreEx(val msg: String) : Exception(msg) fun emptyMalList() = MalList(listOf()) fun malListOf(vararg elems: MalType) = malListOf(elems.asList()) fun malListOf(elems: List) = MalList(elems) -fun malMapOf(elems: List>) = MalMap(mapOf(*elems.toTypedArray())) +fun malMapOf(elems: List>) = MalMap(mapOf(*elems.toTypedArray())) fun malSym(sym: String) = MalSymbol(sym) fun malFun(name: String, f: MalFn) = MalFunc(f, name) From a64ba630e21e281da695d0d6c9567590a4d27001 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sat, 28 Mar 2020 17:12:46 +0000 Subject: [PATCH 35/48] Add `*host-language*` to the env --- kotlin/src/mal/stepA_mal.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kotlin/src/mal/stepA_mal.kt b/kotlin/src/mal/stepA_mal.kt index 5f9fd43b..2761be9a 100644 --- a/kotlin/src/mal/stepA_mal.kt +++ b/kotlin/src/mal/stepA_mal.kt @@ -225,6 +225,7 @@ fun main(args: Array) { else -> res } }) + repl_env.set(malSym("*host-language*"), MalString("Kotlin")) rep("""(def! load-file (fn* [f] (eval (read-string (str "(do " (slurp f) ")")))))""") rep("""(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw "odd number of forms to cond")) (cons 'cond (rest (rest xs)))))))""") @@ -237,6 +238,7 @@ fun main(args: Array) { } else { repl_env.set(malSym("*ARGV*"), emptyMalList()) + rep("""(println (str "Mal [" *host-language* "]"))""") repl@ while(true) { print("user> ") From b661b95419b6f02a65fda1415264ee076ccb1534 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sat, 28 Mar 2020 23:59:03 +0000 Subject: [PATCH 36/48] Add gensym And format mal code for sanity. --- kotlin/src/mal/stepA_mal.kt | 40 +++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/kotlin/src/mal/stepA_mal.kt b/kotlin/src/mal/stepA_mal.kt index 2761be9a..1f469279 100644 --- a/kotlin/src/mal/stepA_mal.kt +++ b/kotlin/src/mal/stepA_mal.kt @@ -227,10 +227,42 @@ fun main(args: Array) { }) repl_env.set(malSym("*host-language*"), MalString("Kotlin")) - rep("""(def! load-file (fn* [f] (eval (read-string (str "(do " (slurp f) ")")))))""") - rep("""(defmacro! cond (fn* (& xs) (if (> (count xs) 0) (list 'if (first xs) (if (> (count xs) 1) (nth xs 1) (throw "odd number of forms to cond")) (cons 'cond (rest (rest xs)))))))""") - rep("""(defmacro! or (fn* (& xs) (if (empty? xs) nil (if (= 1 (count xs)) (first xs) `(let* (or_FIXME ~(first xs)) (if or_FIXME or_FIXME (or ~@(rest xs))))))))""") - rep("(def! not (fn* [v] (if v false true)))") + rep("""(do +(def! *gensym-counter* (atom 0)) + +(def! gensym + (fn* [] + (symbol (str "G__" (swap! *gensym-counter* (fn* [x] (+ 1 x))))))) + +(defmacro! or + (fn* (& xs) + (if (empty? xs) + nil + (if (= 1 (count xs)) + (first xs) + (let* (condvar (gensym)) + `(let* (~condvar ~(first xs)) + (if ~condvar ~condvar (or ~@(rest xs))))))))) + +(def! load-file + (fn* [f] + (eval (read-string (str "(do " (slurp f) ")"))))) + +(defmacro! cond + (fn* (& xs) + (if (> (count xs) 0) + (list + 'if (first xs) + (if (> (count xs) 1) + (nth xs 1) + (throw "odd number of forms to cond")) + (cons 'cond (rest (rest xs))))))) + +(def! not + (fn* [v] + (if v false true))) +)""") + rep("") if(args.size > 0) { repl_env.set(malSym("*ARGV*"), malListOf(args.drop(1).map(::MalString))) From f9e8c992b5acec2849c9961c064ee1a5656b491e Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sun, 29 Mar 2020 19:00:34 +0100 Subject: [PATCH 37/48] Fix else-less if bug Something I thought was a macro bug turned out to bug in else-less if which I'd never tried in anger. --- kotlin/src/mal/stepA_mal.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlin/src/mal/stepA_mal.kt b/kotlin/src/mal/stepA_mal.kt index 1f469279..590b9bea 100644 --- a/kotlin/src/mal/stepA_mal.kt +++ b/kotlin/src/mal/stepA_mal.kt @@ -142,7 +142,7 @@ fun EVAL(cur_ast: MalType, cur_env: Env, depth: Int) : MalType { "if" -> { // the condition continues to be evaluated, however, rather than evaluating the true or false branch, ast is set to the unevaluated value of the chosen branch. ast = if(is_true(EVAL(rest[0], env, n))) rest[1] else - if(rest.atoms.count() == 3) rest[2] else ast + if(rest.atoms.count() == 3) rest[2] else MalNil() continue@eval_loop // TCO } "fn*" -> { From cb7dd6aed956e3e84b65e8ae20055a3ee682877f Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sun, 29 Mar 2020 19:08:14 +0100 Subject: [PATCH 38/48] End the REPL on ^D Not sure why I didn't do this before. --- kotlin/src/mal/stepA_mal.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlin/src/mal/stepA_mal.kt b/kotlin/src/mal/stepA_mal.kt index 590b9bea..ee39a838 100644 --- a/kotlin/src/mal/stepA_mal.kt +++ b/kotlin/src/mal/stepA_mal.kt @@ -275,7 +275,7 @@ fun main(args: Array) { print("user> ") try { - val line = readLine() ?: continue@repl + val line = readLine() ?: break@repl if (setOf("quit", "exit").contains(line.trim())) { println("Bye!") break@repl From f53e4d906499a41ed63bcc039d7f93c819168dbd Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sun, 29 Mar 2020 19:37:16 +0100 Subject: [PATCH 39/48] Extend meta info to aggregate types They're getting a little unwieldy now but it'll do. --- kotlin/src/mal/core.kt | 4 ++-- kotlin/src/mal/types.kt | 31 +++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index cdedc662..1560742f 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -273,11 +273,11 @@ object core { }, to_fun("meta") { - val f = it[0] as MalCallable + val f = it[0] as MalMeta f.meta }, to_fun("with-meta") { - val f = it[0] as MalCallable + val f = it[0] as MalMeta f.withMeta(it[1]) } ) diff --git a/kotlin/src/mal/types.kt b/kotlin/src/mal/types.kt index 2b505b08..f9b98616 100644 --- a/kotlin/src/mal/types.kt +++ b/kotlin/src/mal/types.kt @@ -26,8 +26,13 @@ data class MalCljAtom(var value : MalType) : MalType class MalNil : MalAtom -interface MalSeq : MalType { - val atoms : List +interface MalMeta { + val meta: MalType + fun withMeta(m: MalType): MalType +} + +interface MalSeq : MalMeta, MalType { + val atoms: List val size: Int get() = atoms.size fun head() = atoms[0] @@ -40,21 +45,31 @@ interface MalSeq : MalType { // TODO Maybe implement complementN too? } -data class MalList(override val atoms: List) : MalSeq -data class MalVector(override val atoms: List) : MalSeq +data class MalList(override val atoms: List, override val meta: MalType = MalNil()) : MalSeq { + // XXX Not technically a copy ... + override fun withMeta(m: MalType) = + MalList(atoms, m) +} +data class MalVector(override val atoms: List, override val meta: MalType = MalNil()) : MalSeq { + // XXX Not technically a copy ... + override fun withMeta(m: MalType) = + MalVector(atoms, m) +} -class MalMap(val pairs: Map) : MalType { +class MalMap(val pairs: Map, override val meta: MalType = MalNil()) : MalMeta, MalType { operator fun get(k: MalKey): MalType = pairs[k] ?: MalNil() + + // XXX Not technically a copy ... + override fun withMeta(m: MalType) = + MalMap(pairs, m) } typealias MalFn = (MalSeq) -> MalType -abstract class MalCallable(val func: MalFn, var name: String, val meta: MalType) : MalType { +abstract class MalCallable(val func: MalFn, var name: String, override val meta: MalType) : MalMeta, MalType { var isMacro = false operator fun invoke(args: MalSeq) = func(args) - - abstract fun withMeta(m: MalType): MalCallable } // Allow name to be set after the fact so functions in Env are named. From 17fdc27e2e7030e3bb0e8911d8931f4e945a6b2f Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sun, 29 Mar 2020 20:27:27 +0100 Subject: [PATCH 40/48] Implement deferrable step A functions It seems self-hosting from step 4 requires number? so I implemented the rest while I was there. First time (maybe?) that _all_ the tests are passing for a step! --- kotlin/src/mal/core.kt | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 1560742f..02bf9c95 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -150,6 +150,26 @@ object core { to_fun("concat") { malListOf(it.atoms.flatMap { (it as MalSeq).atoms }) }, + to_fun("conj") { + val seq = it[0] as MalSeq + val rest = it.tail().atoms + when(seq) { + is MalList -> malListOf(rest.reversed() + seq.atoms) + else -> MalVector(seq.atoms + rest) + } + }, + // takes a list, vector, string, or nil. If an empty list, empty vector, or empty string ("") is passed in then nil is returned. Otherwise, a list is returned unchanged, a vector is converted into a list, and a string is converted to a list that containing the original string split into single character strings. + to_fun("seq") { + val s = it.head() + when(s) { + is MalList -> if(s.size > 0) s else MalNil() + is MalVector -> if(s.size > 0) malListOf(s.atoms) else MalNil() + is MalString -> if(s.str.count() > 0) + malListOf(s.str.chunked(1).map(::MalString)) + else MalNil() + else -> MalNil() + } + }, to_fun("nth") { val seq = it[0] as MalSeq @@ -279,6 +299,25 @@ object core { to_fun("with-meta") { val f = it[0] as MalMeta f.withMeta(it[1]) + }, + + to_fun("time-ms") { + MalNumber(java.time.Instant.now().toEpochMilli().toInt()) + }, + + to_fun("string?") { + MalBoolean(it[0] is MalString) + }, + to_fun("number?") { + MalBoolean(it[0] is MalNumber) + }, + to_fun("fn?") { + val f = it[0] + MalBoolean(f is MalCallable && !f.isMacro) + }, + to_fun("macro?") { + val f = it[0] + MalBoolean(f is MalCallable && f.isMacro) } ) } From 5ce394ac35e7a4ee080dc01aaaefd09a86683a92 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Sun, 29 Mar 2020 20:28:33 +0100 Subject: [PATCH 41/48] Support negative numbers Could've done this a while back just didn't think to! --- kotlin/src/mal/reader.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index 3f932885..557b45a7 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -50,7 +50,7 @@ class Reader(val tokens: List) { fun peek() = tokens[pos] } -private fun is_number(s: String) = Regex("\\d+").matches(s) +private fun is_number(s: String) = Regex("-?\\d+").matches(s) private fun is_bool(s: String) = setOf("true", "false").contains(s) From 499877fec11ae018a85ca5e0548ea5851ac7d355 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Mon, 30 Mar 2020 00:28:28 +0100 Subject: [PATCH 42/48] Sort out fiddly pr_str code What is expected and what is documented is a little hard to follow and so the code became. Now it's clearer what's needed so is the code. --- kotlin/src/mal/core.kt | 4 ++-- kotlin/src/mal/printer.kt | 47 ++++++++++++++++----------------------- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 02bf9c95..66cf992e 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -39,10 +39,10 @@ private fun compare_lists(a: MalSeq, b: MalSeq): Boolean { private fun compare_maps(a: MalMap, b: MalMap) = a == b private fun pr_str_core(seq: MalSeq) = - seq.atoms.map { as_str(it, readable=true) }.joinToString(" ") + seq.atoms.map { pr_str(it, print_readably=true) }.joinToString(" ") private fun str_core(seq: MalSeq) = - seq.atoms.map { as_str(it, readable=false) }.joinToString("") + seq.atoms.map { pr_str(it, print_readably=false) }.joinToString("") private val eof = "" diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index af45cea8..a685756c 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -4,48 +4,39 @@ private val bs = "\u005C" // '\' U+005C \ REVERSE SOLIDUS (Other_Punctuati // Reflect any changes in reader.kt private val printEscapeMap = mapOf( - "$q" to "$bs$q", - "\n" to "${bs}n", - "$bs" to "$bs$bs" + "$q" to "$bs$q", // " to \" + "\n" to "${bs}n", // ␤ to \n + "$bs" to "$bs$bs" // \ to \\ ) // Bleurgh, the escapes need escapes as they become interpolated into Regex ;_; // So we need to manage three levels escaping different >_< -private val printEscapes = Regex(listOf(q, "${bs}n", "$bs$bs(?![n$q])").joinToString("|", "(", ")")) +private val printEscapes = + Regex(listOf(q, "${bs}n", "$bs$bs(?![n$q])").joinToString("|", "(", ")")) private fun malstr_as_string(s: String) = - s.replace(printEscapes) { printEscapeMap.get(it.value) ?: it.value } + q + s.replace(printEscapes) { printEscapeMap.get(it.value) ?: it.value } + q // … does the opposite of read_str: take a mal data structure and // return a string representation of it. -fun as_str(v: MalType, readable: Boolean) : String { +fun pr_str(v: MalType, print_readably: Boolean = true) : String { + // Close over print_readably to simplify code below. + val pr = { w: MalType -> pr_str(w, print_readably) } + val pr_map = { p: Map.Entry -> pr(p.key) + " " + pr(p.value) } + + // Inner function to save repeating 'print_readably' in every call. return when(v) { - is MalList -> { - "(" + v.atoms.map { pr_str(it) }.joinToString(" ") + ")" - } - is MalVector -> { - "[" + v.atoms.map { pr_str(it) }.joinToString(" ") + "]" - } - is MalMap -> { - "{" + v.pairs.map { (k,v) -> pr_str(k) + " " + pr_str(v) }.joinToString(" ") + "}" - } + is MalList -> v.atoms.map(pr).joinToString(" ", "(", ")") + is MalVector -> v.atoms.map(pr).joinToString(" ", "[", "]") + is MalMap -> v.pairs.map(pr_map).joinToString(" ", "{", "}") is MalNumber -> v.num.toString() is MalKeyword -> ":${v.kw}" - is MalString -> if(readable) malstr_as_string(v.str) else v.str + is MalString -> if(print_readably) malstr_as_string(v.str) else v.str is MalSymbol -> v.sym is MalBoolean -> v.bool.toString() - // Use this specific format to make tests pass :/ - is MalCljAtom -> "(atom ${pr_str(v.value)})" + is MalCljAtom -> "(atom ${pr(v.value)})" is MalNil -> "nil" - is MalUserFunc -> "#<${v.name}>" - is MalFunc -> "#<${v.name}>" - is MalUserEx -> "Exception raised: "+as_str(v, readable) + is MalCallable -> "#<${v.name}>" + is MalUserEx -> "Exception raised: ${pr(v.src)}" else -> "Can't stringify a "+v::class } } - -// Only add quotes to strings at the point of output, otherwise functions like -// str end up with multiply quoted strings. -fun pr_str(v: MalType, readable: Boolean = true) : String { - val s = as_str(v, readable) - return if(v is MalString) "\"$s\"" else s -} From 284a41ef7e768c1c34535acf48869c023f999595 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Mon, 30 Mar 2020 01:07:44 +0100 Subject: [PATCH 43/48] Handle quoted strings better Now passes step 1 tests. --- kotlin/src/mal/reader.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index 557b45a7..b4d6798b 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -14,7 +14,7 @@ private var tokenizer = Regex(""" # it was preceded by a backslash in which case it includes it until the next # double-quote (tokenized). It will also match unbalanced strings (no ending # double-quote) which should be reported as an error. - "(?:\\"|[^"])*?" | + "(?:\\"|[^"])*"? | # Captures any sequence of characters starting with ; (tokenized). ;.* | # Captures a sequence of zero or more non special characters (e.g. symbols, @@ -26,6 +26,8 @@ private var tokenizer = Regex(""" setOf(RegexOption.COMMENTS, RegexOption.MULTILINE) ) +private val commas = Regex("""^[\s,]*|[,\s]*$""", RegexOption.MULTILINE) + // This function will take a single string and return an array/list of all the tokens (strings) in it. fun tokenize(s: String) : List { // For some reason this fails where findAll doesn't. @@ -33,7 +35,7 @@ fun tokenize(s: String) : List { // throw MalCoreEx("Failed tokenizing") // } return tokenizer.findAll(s) - .map { it.value.trim() } + .map { it.value.replace(commas, "") } .filter { it.length > 0 } .filter { !it.startsWith(";") } .toList() @@ -67,7 +69,10 @@ private val readEscapeMap = mapOf( private val readEscapes = Regex(listOf("\\\\\"", "\\\\n", "\\\\\\\\").joinToString("|", "(", ")")) private fun make_string(s: String) = - MalString(s.replace(readEscapes) { readEscapeMap.get(it.value) ?: it.value }) + if (s.last() == '"') + MalString(s.dropLast(1).replace(readEscapes) { readEscapeMap.get(it.value) ?: it.value }) + else + throw MalCoreEx("Unexpected end of input, unbalanced quote?") private fun make_with_meta(r: Reader, n: Int): MalType { val meta = read_form(r, n) @@ -79,8 +84,8 @@ private fun read_atom(r: Reader, n: Int) : MalType { // println("Reading atom: " + r) val t = r.next() return when { - t[0] == '"' -> make_string(t.substring(1 .. t.length - 2)) - t[0] == ':' -> MalKeyword(t.substring(1 .. t.length - 1)) + t[0] == '"' -> make_string(t.substring(1 .. t.lastIndex)) + t[0] == ':' -> MalKeyword(t.substring(1 .. t.lastIndex)) t[0] == '^' -> make_with_meta(r, n) is_number(t) -> MalNumber(t.toInt()) is_bool(t) -> MalBoolean(t == "true") @@ -135,7 +140,7 @@ fun read_form(r: Reader, n: Int) : MalType { } } catch(e: IndexOutOfBoundsException) { - throw MalCoreEx("Ran out of tokens, missing right paren?") + throw MalCoreEx("Unexpected end of input, unbalanced paren/brace/bracket?") } } From 93325332ce7d8d7f2ea804e8ab74367dcde75359 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Wed, 1 Apr 2020 21:51:41 +0100 Subject: [PATCH 44/48] Update step2 code to get tests passing Trying to get all the tests passing now. Fortunately this is just a matter of updating interfaces and evalling structures proper. --- kotlin/src/mal/step2_eval.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/kotlin/src/mal/step2_eval.kt b/kotlin/src/mal/step2_eval.kt index 5fddd96e..7eb04441 100644 --- a/kotlin/src/mal/step2_eval.kt +++ b/kotlin/src/mal/step2_eval.kt @@ -1,10 +1,10 @@ fun READ(s: String) = read_str(s) val repl_env : Map = mapOf( - "+" to MalFunc({ a, b -> MalNumber((a as MalNumber).num + (b as MalNumber).num) }), - "-" to MalFunc({ a, b -> MalNumber((a as MalNumber).num - (b as MalNumber).num) }), - "*" to MalFunc({ a, b -> MalNumber((a as MalNumber).num * (b as MalNumber).num) }), - "/" to MalFunc({ a, b -> MalNumber((a as MalNumber).num / (b as MalNumber).num) }) + "+" to malFun("+") { MalNumber((it[0] as MalNumber).num + (it[1] as MalNumber).num) }, + "-" to malFun("-") { MalNumber((it[0] as MalNumber).num - (it[1] as MalNumber).num) }, + "*" to malFun("*") { MalNumber((it[0] as MalNumber).num * (it[1] as MalNumber).num) }, + "/" to malFun("/") { MalNumber((it[0] as MalNumber).num / (it[1] as MalNumber).num) } ) // Create a new function eval_ast which takes ast (mal data type) and @@ -20,6 +20,8 @@ fun eval_ast(ast: MalType, env: Map, depth: Int) : MalType { // println(PRINT(ast)) return when(ast) { is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth) }.toList()) + is MalVector -> MalVector(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalMap -> malMapOf(ast.pairs.map { (k,v) -> k to EVAL(v, env, depth + 1) }) is MalSymbol -> env[ast.sym] ?: throw Exception("Unknown symbol '${ast.sym}'") else -> ast } From 5c5717d4fd016e8499acc471e3fb06ebce8ad236 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Wed, 1 Apr 2020 21:53:10 +0100 Subject: [PATCH 45/48] Fix step 3 to get all tests passing Similar to step 2 fixes but also tweaked fail message to make tests pass. --- kotlin/src/mal/env.kt | 2 +- kotlin/src/mal/step3_env.kt | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/kotlin/src/mal/env.kt b/kotlin/src/mal/env.kt index 2907a59c..627bb191 100644 --- a/kotlin/src/mal/env.kt +++ b/kotlin/src/mal/env.kt @@ -34,7 +34,7 @@ class Env(val outer: Env? = null, // get: takes a symbol key and uses the find method to locate the environment with the key, then returns the matching value. If no key is found up the outer chain, then throws/raises a "not found" error. fun get(key: MalSymbol): MalType { - val env = find(key) ?: throw MalCoreEx("Could not find '${key.sym}' in env") + val env = find(key) ?: throw MalCoreEx("The symbol '${key.sym}' not found in env") return env.data.getValue(key) } } diff --git a/kotlin/src/mal/step3_env.kt b/kotlin/src/mal/step3_env.kt index 7eaf6106..b4f59967 100644 --- a/kotlin/src/mal/step3_env.kt +++ b/kotlin/src/mal/step3_env.kt @@ -13,6 +13,8 @@ fun eval_ast(ast: MalType, env: Env, depth: Int) : MalType { // println(PRINT(ast)) return when(ast) { is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth) }.toList()) + is MalVector -> MalVector(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalMap -> malMapOf(ast.pairs.map { (k,v) -> k to EVAL(v, env, depth + 1) }) is MalSymbol -> env.get(ast) else -> ast } @@ -52,7 +54,7 @@ completion). */ -fun make_env(pairs: MalList, outer_env: Env, depth: Int) : Env { +fun make_env(pairs: MalSeq, outer_env: Env, depth: Int) : Env { val new_env = Env(outer_env) for (idx in pairs.atoms.indices step 2) { val k = pairs.atoms[idx] as MalSymbol @@ -88,7 +90,7 @@ fun EVAL(ast: MalType, env: Env, depth: Int) : MalType { val v = EVAL(args[1], env, depth + 1) return env.set((args[0] as MalSymbol), v) } - "let*" -> return EVAL(args[1], make_env((args[0] as MalList), env, depth), depth + 1) + "let*" -> return EVAL(args[1], make_env((args[0] as MalSeq), env, depth), depth + 1) } } val l = eval_ast(ast, env, depth + 1) @@ -101,10 +103,10 @@ fun EVAL(ast: MalType, env: Env, depth: Int) : MalType { fun PRINT(v: MalType) = pr_str(v) val repl_env = Env().apply { - set(MalSymbol("+"), MalFunc({ a, b -> MalNumber((a as MalNumber).num + (b as MalNumber).num) })) - set(MalSymbol("-"), MalFunc({ a, b -> MalNumber((a as MalNumber).num - (b as MalNumber).num) })) - set(MalSymbol("*"), MalFunc({ a, b -> MalNumber((a as MalNumber).num * (b as MalNumber).num) })) - set(MalSymbol("/"), MalFunc({ a, b -> MalNumber((a as MalNumber).num / (b as MalNumber).num) })) + set(MalSymbol("+"), malFun("+") { MalNumber((it[0] as MalNumber).num + (it[1] as MalNumber).num) }) + set(MalSymbol("-"), malFun("-") { MalNumber((it[0] as MalNumber).num - (it[1] as MalNumber).num) }) + set(MalSymbol("*"), malFun("*") { MalNumber((it[0] as MalNumber).num * (it[1] as MalNumber).num) }) + set(MalSymbol("/"), malFun("/") { MalNumber((it[0] as MalNumber).num / (it[1] as MalNumber).num) }) } fun rep(s: String) { From d9da325a7e98ecedaef0ced89a7e822aafc77481 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Wed, 1 Apr 2020 21:55:18 +0100 Subject: [PATCH 46/48] Fix step 4 to get most tests passing Also hadn't quite implemented a couple of things properly so had to fix those for most tests to pass. --- kotlin/src/mal/core.kt | 17 ++++++++++++----- kotlin/src/mal/step4_if_fn_do.kt | 11 +++++++---- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/kotlin/src/mal/core.kt b/kotlin/src/mal/core.kt index 66cf992e..d6796bce 100644 --- a/kotlin/src/mal/core.kt +++ b/kotlin/src/mal/core.kt @@ -24,6 +24,10 @@ private fun is_equal(a: MalType, b: MalType) = else -> throw MalCoreEx("Unknown type $a in is_equal (aka =)") } } + // Handle class when comparing lists & vectors. + else if (a is MalSeq && b is MalSeq) { + compare_lists(a, b) + } else { false } @@ -41,8 +45,8 @@ private fun compare_maps(a: MalMap, b: MalMap) = a == b private fun pr_str_core(seq: MalSeq) = seq.atoms.map { pr_str(it, print_readably=true) }.joinToString(" ") -private fun str_core(seq: MalSeq) = - seq.atoms.map { pr_str(it, print_readably=false) }.joinToString("") +private fun str_core(seq: MalSeq, joiner: String) = + seq.atoms.map { pr_str(it, print_readably=false) }.joinToString(joiner) private val eof = "" @@ -60,7 +64,7 @@ object core { }, // `str`: calls `pr_str` on each argument with `print_readably` set to false, concatenates the results together ("" separator), and returns the new string. to_fun("str") { - MalString(str_core(it)) + MalString(str_core(it, joiner = "")) }, // prn: calls `pr_str` on each argument with `print_readably` set to true, joins the results with " ", prints the string to the screen and then returns `nil`. to_fun("prn") { @@ -69,7 +73,7 @@ object core { }, // `println`: calls `pr_str` on each argument with `print_readably` set to false, joins the results with " ", prints the string to the screen and then returns `nil`. to_fun("println") { - println(str_core(it)) + println(str_core(it, joiner = " ")) MalNil() }, @@ -80,7 +84,10 @@ object core { // empty?: treat the first parameter as a list and return true if the list is empty and false if it contains any elements. to_fun("empty?") { MalBoolean(it[0] is MalSeq && (it[0] as MalSeq).atoms.isEmpty()) }, // count: treat the first parameter as a list and return the number of elements that it contains. - to_fun("count") { MalNumber((it[0] as MalSeq).atoms.count()) }, + to_fun("count") { + val seq = it[0] + MalNumber(if (seq is MalSeq) seq.atoms.count() else 0) + }, // =: see is_equal malSym("=") to malFun("equals?") { diff --git a/kotlin/src/mal/step4_if_fn_do.kt b/kotlin/src/mal/step4_if_fn_do.kt index 4b2986e4..42018bcf 100644 --- a/kotlin/src/mal/step4_if_fn_do.kt +++ b/kotlin/src/mal/step4_if_fn_do.kt @@ -13,6 +13,8 @@ fun eval_ast(ast: MalType, env: Env, depth: Int) : MalType { // println(PRINT(ast)) return when(ast) { is MalList -> MalList(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalVector -> MalVector(ast.atoms.map { EVAL(it, env, depth + 1) }.toList()) + is MalMap -> malMapOf(ast.pairs.map { (k,v) -> k to EVAL(v, env, depth + 1) }) is MalSymbol -> env.get(ast) else -> ast } @@ -28,7 +30,7 @@ fun eval_ast(ast: MalType, env: Env, depth: Int) : MalType { */ -fun make_env(pairs: MalList, outer_env: Env, depth: Int) : Env { +fun make_env(pairs: MalSeq, outer_env: Env, depth: Int) : Env { val new_env = Env(outer_env) for (idx in pairs.atoms.indices step 2) { val k = pairs.atoms[idx] as MalSymbol @@ -73,14 +75,15 @@ fun EVAL(ast: MalType, env: Env, depth: Int) : MalType { val v = EVAL(args[1], env, n) return env.set((args[0] as MalSymbol), v) } - "let*" -> return EVAL(args[1], make_env((args[0] as MalList), env, depth), n) + "let*" -> return EVAL(args[1], make_env((args[0] as MalSeq), env, depth), n) "do" -> return (eval_ast(args, env, depth) as MalList).last() "if" -> { - val body = if(is_true(EVAL(args[0], env, n))) args[1] else args[2] + val body = if(is_true(EVAL(args[0], env, n))) args[1] else + if(args.atoms.count() == 3) args[2] else MalNil() return EVAL(body, env, n) } "fn*" -> { - val binds = args[0] as MalList + val binds = args[0] as MalSeq val body = args[1] return malFun("funccall") { EVAL(body, Env(env, binds, it), n) } } From 9847ea161d43020860f3936cce3e260f936d8522 Mon Sep 17 00:00:00 2001 From: Dan Brook Date: Wed, 1 Apr 2020 21:55:56 +0100 Subject: [PATCH 47/48] Hopefully final fix for string escaping With this fix step 4 finally passes. Having the host language share effectively the same escaping mechanism got confusing for a while there. --- kotlin/src/mal/printer.kt | 2 +- kotlin/src/mal/reader.kt | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/kotlin/src/mal/printer.kt b/kotlin/src/mal/printer.kt index a685756c..4cab7931 100644 --- a/kotlin/src/mal/printer.kt +++ b/kotlin/src/mal/printer.kt @@ -11,7 +11,7 @@ private val printEscapeMap = mapOf( // Bleurgh, the escapes need escapes as they become interpolated into Regex ;_; // So we need to manage three levels escaping different >_< private val printEscapes = - Regex(listOf(q, "${bs}n", "$bs$bs(?![n$q])").joinToString("|", "(", ")")) + Regex(listOf(q, "${bs}n", "$bs$bs").joinToString("|", "(", ")")) private fun malstr_as_string(s: String) = q + s.replace(printEscapes) { printEscapeMap.get(it.value) ?: it.value } + q diff --git a/kotlin/src/mal/reader.kt b/kotlin/src/mal/reader.kt index b4d6798b..5905cd21 100644 --- a/kotlin/src/mal/reader.kt +++ b/kotlin/src/mal/reader.kt @@ -58,15 +58,18 @@ private fun is_bool(s: String) = setOf("true", "false").contains(s) private fun make_atom_deref(token: String) = MalList(listOf("deref", token).map(::malSym)) +// Use named variables otherwise we have to escape escaping x_x +private val q = "\u0022" // '"' U+0022 " QUOTATION MARK (Other_Punctuation) +private val bs = "\u005C" // '\' U+005C \ REVERSE SOLIDUS (Other_Punctuation) + // Reflect any changes in printer.kt -private val readEscapeMap = mapOf( - "\\\"" to "\"", - "\\n" to "\n", - "\\\\" to "\\" +private var readEscapeMap = mapOf( + "$bs$q" to q, // \" to " + "${bs}n" to "\n", // \n to ␤ + "$bs$bs" to bs // \\ to \ ) // Bleurgh, the escapes need escapes as they become interpolated into Regex ;_; -// So we need to manage three levels escaping different >_< -private val readEscapes = Regex(listOf("\\\\\"", "\\\\n", "\\\\\\\\").joinToString("|", "(", ")")) +private var readEscapes = Regex(listOf("$bs$bs$bs$bs", "(? Date: Wed, 1 Apr 2020 20:58:13 +0000 Subject: [PATCH 48/48] Bump esm from 3.0.84 to 3.2.25 in /es6 Bumps [esm](https://github.com/standard-things/esm) from 3.0.84 to 3.2.25. - [Release notes](https://github.com/standard-things/esm/releases) - [Commits](https://github.com/standard-things/esm/compare/3.0.84...3.2.25) Signed-off-by: dependabot[bot] --- es6/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es6/package.json b/es6/package.json index 850dff09..b7b160bc 100644 --- a/es6/package.json +++ b/es6/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Make a Lisp (mal) language implemented in ES6 (ECMAScript 6 / ECMAScript 2015)", "dependencies": { - "esm": "3.0.x", + "esm": "3.2.x", "ffi-napi": "2.4.x" }, "esm": {