1
1
mirror of https://github.com/kanaka/mal.git synced 2024-11-11 00:52:44 +03:00
mal/livescript/reader.ls
2017-05-14 22:05:49 +02:00

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