mirror of
https://github.com/kanaka/mal.git
synced 2024-11-11 00:52:44 +03:00
180 lines
4.7 KiB
Plaintext
180 lines
4.7 KiB
Plaintext
readline = require 'readline'
|
|
{id, map, pairs-to-obj} = require 'prelude-ls'
|
|
{list-to-pairs} = require './utils'
|
|
|
|
export class OnlyComment
|
|
|
|
parse-error = (msg) -> throw new Error msg
|
|
|
|
class Reader
|
|
(tokens) ->
|
|
@tokens = tokens
|
|
@pos = 0
|
|
|
|
# returns the token at the current position
|
|
# and increments position.
|
|
next: ->
|
|
result = @peek!
|
|
if result? then @pos += 1
|
|
result
|
|
|
|
# just returns the token at the current position.
|
|
peek: ->
|
|
if @pos < @tokens.length
|
|
@tokens[@pos]
|
|
|
|
|
|
eof-or-comment = (reader) ->
|
|
token = reader.peek!
|
|
if token? and not token.startsWith ';'
|
|
then parse-error "expected EOF, got '#{token}'"
|
|
|
|
|
|
export read_str = (str) ->
|
|
str
|
|
|> tokenizer
|
|
|> (tokens) -> new Reader tokens
|
|
|> (reader) ->
|
|
result = read_form reader
|
|
if token? then parse-error "expected EOF, got '#{token}'"
|
|
result
|
|
|
|
|
|
# This function will take a single string and return an array/list
|
|
# of all the tokens (strings) in it.
|
|
tokenizer = (str) ->
|
|
re = //
|
|
[\s,]* # whitespace or commas
|
|
( ~@ # special two-char ~@
|
|
| [\[\]{}()'`~^@] # special single char one of []{}'`~^@
|
|
| "(?:\\.| [^\\"])*" # double-quoted string
|
|
| ;.* # any seq of chars starting ;
|
|
| [^\s\[\]{}('"`,;)]+ # seq of non-special chars: symbols, numbers,
|
|
) # "true", "false" and "nil".
|
|
//y
|
|
|
|
tokens = []
|
|
while re.lastIndex < str.length
|
|
idx = re.lastIndex
|
|
m = re.exec str
|
|
if not m
|
|
# Allow whitespace or commas at the end of the input.
|
|
break if /[\s,]+/.exec str.substring idx
|
|
parse-error "parse error at character #{idx}"
|
|
|
|
tok = m[1]
|
|
# Ignore comments.
|
|
if tok[0] != ';' then tokens.push m[1]
|
|
|
|
tokens
|
|
|
|
read_form = (reader) ->
|
|
switch reader.peek!
|
|
| '(' => read_list reader, ')'
|
|
| '[' => read_list reader, ']'
|
|
| '{' => read_list reader, '}'
|
|
| '\'' => read-macro 'quote', reader
|
|
| '\`' => read-macro 'quasiquote', reader
|
|
| '~' => read-macro 'unquote', reader
|
|
| '~@' => read-macro 'splice-unquote', reader
|
|
| '@' => read-macro 'deref', reader # todo only symbol?
|
|
| '^' => read-with-meta reader
|
|
| otherwise =>
|
|
if that? then read_atom reader
|
|
else parse-error 'expected a form, got EOF'
|
|
|
|
|
|
read_list = (reader, end) ->
|
|
list = []
|
|
reader.next! # accept '(', '[' or '{'
|
|
loop
|
|
token = reader.peek!
|
|
if not token?
|
|
parse-error "expected '#{end}', got EOF"
|
|
else if token == end
|
|
reader.next!
|
|
break
|
|
|
|
list.push read_form reader
|
|
|
|
switch end
|
|
| ')' => {type: \list, value: list}
|
|
| ']' => {type: \vector, value: list}
|
|
| '}' => list-to-map list
|
|
|
|
|
|
special_chars = '[]{}\'`~^@'
|
|
constants = [\true \false \nil]
|
|
|
|
|
|
read_atom = (reader) ->
|
|
token = reader.peek!
|
|
if token in constants
|
|
{type: \const, value: reader.next!}
|
|
else if token[0] == '"'
|
|
{type: \string, value: decode-string reader.next!}
|
|
else if token.match /^-?\d+$/
|
|
{type: \int, value: parseInt reader.next!}
|
|
else if token != '~@' and token not in special_chars
|
|
if token.startsWith ':'
|
|
{type: \keyword, value: reader.next!}
|
|
else
|
|
{type: \symbol, value: reader.next!}
|
|
else
|
|
parse-error "expected an atom, got #{token}"
|
|
|
|
|
|
decode-string = (str) ->
|
|
str |> (.slice 1, -1)
|
|
|> (.replace /\\[\"\\n]/g,
|
|
(esc) -> switch esc
|
|
| '\\n' => '\n'
|
|
| '\\"' => '"'
|
|
| '\\\\' => '\\')
|
|
|
|
|
|
export keyword-prefix = '\u029e'
|
|
|
|
export map-keyword = (key) ->
|
|
switch key.type
|
|
| \string => key.value
|
|
| \keyword => keyword-prefix + key.value
|
|
| otherwise =>
|
|
parse-error "#{key.type} can't be a map key"
|
|
|
|
export list-to-map = (list) ->
|
|
if list.length % 2 != 0
|
|
parse-error "map should have an even number
|
|
of elements, got #{list.length}"
|
|
|
|
list-to-pairs list
|
|
|> map ([key, value]) -> [(map-keyword key), value]
|
|
|> pairs-to-obj
|
|
|> (obj) -> {type: \map, value: obj}
|
|
|
|
|
|
read-macro = (symbol, reader) ->
|
|
reader.next! # accept macro start token
|
|
|
|
do
|
|
type: \list
|
|
value:
|
|
* {type: \symbol, value: symbol}
|
|
* read_form reader
|
|
|
|
|
|
read-with-meta = (reader) ->
|
|
reader.next! # accept ^
|
|
if reader.peek! != '{'
|
|
parse-error "expected a map after with-meta reader macro '^'"
|
|
|
|
meta = read_list reader, '}'
|
|
form = read_form reader
|
|
|
|
do
|
|
type: \list
|
|
value:
|
|
* {type: \symbol, value: 'with-meta'}
|
|
* form
|
|
* meta
|