1
1
mirror of https://github.com/kanaka/mal.git synced 2024-09-17 16:47:22 +03:00
mal/impls/skew/reader.sk
Joel Martin 8a19f60386 Move implementations into impls/ dir
- Reorder README to have implementation list after "learning tool"
  bullet.

- This also moves tests/ and libs/ into impls. It would be preferrable
  to have these directories at the top level.  However, this causes
  difficulties with the wasm implementations which need pre-open
  directories and have trouble with paths starting with "../../". So
  in lieu of that, symlink those directories to the top-level.

- Move the run_argv_test.sh script into the tests directory for
  general hygiene.
2020-02-10 23:50:16 -06:00

141 lines
3.6 KiB
Plaintext

class Reader {
const tokens List<string>
var position = 0
def peek string {
if position >= tokens.count {
return null
}
return tokens[position]
}
def next string {
const token = peek
position++
return token
}
}
def tokenize(str string) List<string> {
var re = RegExp.new("[\\s,]*(~@|[\\[\\]{}()'`~^@]|\"(?:\\\\.|[^\\\\\"])*\"?|;.*|[^\\s\\[\\]{}('\"`,;)]*)", "g")
var tokens List<string> = []
var match string
while (match = re.exec(str)[1]) != "" {
if match[0] == ';' {
continue
}
tokens.append(match)
}
return tokens
}
def unescape(s string) string {
return s.replaceAll("\\\\", "\x01").replaceAll("\\\"", "\"").replaceAll("\\n", "\n").replaceAll("\x01", "\\")
}
def read_atom(rdr Reader) MalVal {
var sre = RegExp.new("^\"(?:\\\\.|[^\\\\\"])*\"$")
const token = rdr.peek
if token == "nil" {
rdr.next
return gNil
}
if token == "true" {
rdr.next
return gTrue
}
if token == "false" {
rdr.next
return gFalse
}
switch token[0] {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' { return MalNumber.new(stringToInt(rdr.next)) }
case '-' {
if token.count <= 1 { return MalSymbol.new(rdr.next) }
switch token[1] {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' { return MalNumber.new(stringToInt(rdr.next)) }
default { return MalSymbol.new(rdr.next) }
}
}
case '"' {
const s = rdr.next
if sre.exec(s) {
return MalString.new(unescape(s.slice(1, s.count - 1)))
} else {
throw MalError.new("expected '\"', got EOF")
}
}
case ':' { return MalKeyword.new(rdr.next.slice(1)) }
default { return MalSymbol.new(rdr.next) }
}
}
def read_sequence(rdr Reader, open string, close string) List<MalVal> {
if rdr.next != open {
throw MalError.new("expected '" + open + "'")
}
var token string
var items List<MalVal> = []
while (token = rdr.peek) != close {
if token == null {
throw MalError.new("expected '" + close + "', got EOF")
}
items.append(read_form(rdr))
}
rdr.next # consume the close paren/bracket/brace
return items
}
def read_list(rdr Reader) MalList {
return MalList.new(read_sequence(rdr, "(", ")"))
}
def read_vector(rdr Reader) MalVector {
return MalVector.new(read_sequence(rdr, "[", "]"))
}
def read_hash_map(rdr Reader) MalHashMap {
return MalHashMap.fromList(read_sequence(rdr, "{", "}"))
}
def reader_macro(rdr Reader, symbol_name string) MalVal {
rdr.next
return MalList.new([MalSymbol.new(symbol_name), read_form(rdr)])
}
def read_form(rdr Reader) MalVal {
switch rdr.peek[0] {
case '\'' { return reader_macro(rdr, "quote") }
case '`' { return reader_macro(rdr, "quasiquote") }
case '~' {
if rdr.peek == "~" { return reader_macro(rdr, "unquote") }
else if rdr.peek == "~@" { return reader_macro(rdr, "splice-unquote") }
else { return read_atom(rdr) }
}
case '^' {
rdr.next
const meta = read_form(rdr)
return MalList.new([MalSymbol.new("with-meta"), read_form(rdr), meta])
}
case '@' { return reader_macro(rdr, "deref") }
case ')' { throw MalError.new("unexpected ')'") }
case '(' { return read_list(rdr) }
case ']' { throw MalError.new("unexpected ']'") }
case '[' { return read_vector(rdr) }
case '}' { throw MalError.new("unexpected '}'") }
case '{' { return read_hash_map(rdr) }
default { return read_atom(rdr) }
}
}
def read_str(str string) MalVal {
const tokens = tokenize(str)
if tokens.isEmpty { return null }
var rdr = Reader.new(tokens)
return read_form(rdr)
}
@import {
const RegExp dynamic
}