Commit | Line | Data |
---|---|---|
18ed1641 | 1 | readline = require 'readline' |
2ff2d84b JB |
2 | {id, map, pairs-to-obj} = require 'prelude-ls' |
3 | {list-to-pairs} = require './utils' | |
4 | ||
5 | export class OnlyComment | |
6 | ||
7 | parse-error = (msg) -> throw new Error msg | |
18ed1641 JB |
8 | |
9 | class Reader | |
10 | (tokens) -> | |
11 | @tokens = tokens | |
12 | @pos = 0 | |
13 | ||
14 | # returns the token at the current position | |
15 | # and increments position. | |
16 | next: -> | |
17 | result = @peek! | |
18 | if result? then @pos += 1 | |
19 | result | |
20 | ||
21 | # just returns the token at the current position. | |
22 | peek: -> | |
23 | if @pos < @tokens.length | |
24 | @tokens[@pos] | |
25 | ||
26 | ||
2ff2d84b JB |
27 | eof-or-comment = (reader) -> |
28 | token = reader.peek! | |
29 | if token? and not token.startsWith ';' | |
30 | then parse-error "expected EOF, got '#{token}'" | |
31 | ||
32 | ||
18ed1641 JB |
33 | export read_str = (str) -> |
34 | str | |
35 | |> tokenizer | |
36 | |> (tokens) -> new Reader tokens | |
cb86911f JB |
37 | |> (reader) -> |
38 | result = read_form reader | |
25bb14c9 | 39 | if token? then parse-error "expected EOF, got '#{token}'" |
cb86911f JB |
40 | result |
41 | ||
18ed1641 JB |
42 | |
43 | # This function will take a single string and return an array/list | |
44 | # of all the tokens (strings) in it. | |
45 | tokenizer = (str) -> | |
46 | re = // | |
47 | [\s,]* # whitespace or commas | |
48 | ( ~@ # special two-char ~@ | |
49 | | [\[\]{}()'`~^@] # special single char one of []{}'`~^@ | |
50 | | "(?:\\.| [^\\"])*" # double-quoted string | |
51 | | ;.* # any seq of chars starting ; | |
52 | | [^\s\[\]{}('"`,;)]+ # seq of non-special chars: symbols, numbers, | |
53 | ) # "true", "false" and "nil". | |
54 | //y | |
55 | ||
56 | tokens = [] | |
57 | while re.lastIndex < str.length | |
58 | idx = re.lastIndex | |
59 | m = re.exec str | |
18ed1641 JB |
60 | if not m |
61 | # Allow whitespace or commas at the end of the input. | |
62 | break if /[\s,]+/.exec str.substring idx | |
2ff2d84b | 63 | parse-error "parse error at character #{idx}" |
18ed1641 | 64 | |
25bb14c9 JB |
65 | tok = m[1] |
66 | # Ignore comments. | |
67 | if tok[0] != ';' then tokens.push m[1] | |
18ed1641 JB |
68 | |
69 | tokens | |
70 | ||
71 | read_form = (reader) -> | |
2ff2d84b JB |
72 | switch reader.peek! |
73 | | '(' => read_list reader, ')' | |
74 | | '[' => read_list reader, ']' | |
75 | | '{' => read_list reader, '}' | |
76 | | '\'' => read-macro 'quote', reader | |
77 | | '\`' => read-macro 'quasiquote', reader | |
78 | | '~' => read-macro 'unquote', reader | |
79 | | '~@' => read-macro 'splice-unquote', reader | |
80 | | '@' => read-macro 'deref', reader # todo only symbol? | |
81 | | '^' => read-with-meta reader | |
82 | | otherwise => | |
83 | if that? then read_atom reader | |
84 | else parse-error 'expected a form, got EOF' | |
85 | ||
18ed1641 | 86 | |
cb86911f | 87 | read_list = (reader, end) -> |
18ed1641 | 88 | list = [] |
2ff2d84b | 89 | reader.next! # accept '(', '[' or '{' |
18ed1641 JB |
90 | loop |
91 | token = reader.peek! | |
92 | if not token? | |
2ff2d84b | 93 | parse-error "expected '#{end}', got EOF" |
cb86911f | 94 | else if token == end |
18ed1641 JB |
95 | reader.next! |
96 | break | |
97 | ||
98 | list.push read_form reader | |
2ff2d84b JB |
99 | |
100 | switch end | |
101 | | ')' => {type: \list, value: list} | |
102 | | ']' => {type: \vector, value: list} | |
103 | | '}' => list-to-map list | |
104 | ||
18ed1641 JB |
105 | |
106 | special_chars = '[]{}\'`~^@' | |
3181c695 | 107 | constants = [\true \false \nil] |
18ed1641 | 108 | |
2ff2d84b | 109 | |
18ed1641 JB |
110 | read_atom = (reader) -> |
111 | token = reader.peek! | |
3181c695 JB |
112 | if token in constants |
113 | {type: \const, value: reader.next!} | |
114 | else if token[0] == '"' | |
2ff2d84b | 115 | {type: \string, value: decode-string reader.next!} |
18ed1641 | 116 | else if token.match /^-?\d+$/ |
cb86911f | 117 | {type: \int, value: parseInt reader.next!} |
18ed1641 | 118 | else if token != '~@' and token not in special_chars |
2ff2d84b JB |
119 | if token.startsWith ':' |
120 | {type: \keyword, value: reader.next!} | |
121 | else | |
122 | {type: \symbol, value: reader.next!} | |
18ed1641 | 123 | else |
2ff2d84b JB |
124 | parse-error "expected an atom, got #{token}" |
125 | ||
126 | ||
127 | decode-string = (str) -> | |
128 | str |> (.slice 1, -1) | |
129 | |> (.replace /\\[\"\\n]/g, | |
130 | (esc) -> switch esc | |
131 | | '\\n' => '\n' | |
132 | | '\\"' => '"' | |
133 | | '\\\\' => '\\') | |
134 | ||
135 | ||
136 | export keyword-prefix = '\u029e' | |
137 | ||
894f5ce8 JB |
138 | export map-keyword = (key) -> |
139 | switch key.type | |
140 | | \string => key.value | |
141 | | \keyword => keyword-prefix + key.value | |
142 | | otherwise => | |
143 | parse-error "#{key.type} can't be a map key" | |
2ff2d84b | 144 | |
894f5ce8 | 145 | export list-to-map = (list) -> |
2ff2d84b JB |
146 | if list.length % 2 != 0 |
147 | parse-error "map should have an even number | |
148 | of elements, got #{list.length}" | |
149 | ||
150 | list-to-pairs list | |
894f5ce8 | 151 | |> map ([key, value]) -> [(map-keyword key), value] |
2ff2d84b JB |
152 | |> pairs-to-obj |
153 | |> (obj) -> {type: \map, value: obj} | |
154 | ||
155 | ||
156 | read-macro = (symbol, reader) -> | |
157 | reader.next! # accept macro start token | |
158 | ||
159 | do | |
160 | type: \list | |
161 | value: | |
162 | * {type: \symbol, value: symbol} | |
163 | * read_form reader | |
164 | ||
165 | ||
166 | read-with-meta = (reader) -> | |
167 | reader.next! # accept ^ | |
168 | if reader.peek! != '{' | |
169 | parse-error "expected a map after with-meta reader macro '^'" | |
170 | ||
171 | meta = read_list reader, '}' | |
172 | form = read_form reader | |
173 | ||
174 | do | |
175 | type: \list | |
176 | value: | |
177 | * {type: \symbol, value: 'with-meta'} | |
178 | * form | |
179 | * meta |