-
Notifications
You must be signed in to change notification settings - Fork 0
/
lang.py
405 lines (314 loc) · 12.5 KB
/
lang.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
import inspect
import itertools
import argspec
import errors
import tokens
import util
class Atom:
'''The base class all custom language constructs inherit from.'''
def __init__(self, value):
self.value = value
def __str__(self):
return unicode(self.value)
def __repr__(self):
v = self.value if hasattr(self, 'value') else None
return self.__class__.__name__ + '(' + repr(self.value) + ')'
def __eq__(self, other):
return isinstance(other, self.__class__) and self.value == other.value
@staticmethod
def to_atom(token):
'''
Takes the given string token and returns the class representing that
string in its most natural form (int, str, Symbol, etc.).
'''
# number types: complex are invalid floats, and floats are invalid ints
try:
# automatically returns 'long' if necessary
return int(token)
except:
try:
return float(token)
except:
try:
return complex(token)
except:
pass
# boolean
if token.lower() == tokens.TRUE:
return True
elif token.lower() == tokens.FALSE:
return False
# string (strips wrapping string tokens)
elif token.startswith(tokens.STRING) and token.endswith(tokens.STRING):
return unicode(token[len(tokens.STRING):-len(tokens.STRING)])
# the base case for all tokens is a symbol
return Symbol(token)
class Cons:
'''Represents a pair of elements.'''
def __init__(self, car, cdr):
self.car = car
self.cdr = cdr
@staticmethod
def build(*items):
'''Build a Cons sequence recursively from a nested list structure.'''
result = NIL
for item in reversed(items):
if isinstance(item, (list, tuple)):
result = Cons(Cons.build(*item), result)
else:
result = Cons(item, result)
return result
@staticmethod
def build_list(items):
'''Same as build, but takes a single list as its arguments instead.'''
return Cons.build(*items)
@staticmethod
def is_list(e):
'''Returns whether an element is a list or not (nil is a list).'''
# nil is a list
if e is NIL:
return True
# non-cons can't be lists
if not isinstance(e, Cons):
return False
# only cons that len() works on are lists (throws an exception otherwise)
try:
return bool(len(e)) or True
except errors.WrongArgumentTypeError:
return False
def __str_helper(self, item):
# nil has no contents
if item is NIL:
return u''
if item.cdr is NIL:
return util.to_string(item.car)
if not isinstance(item.cdr, Cons):
return util.to_string(item.car) + u' . ' + util.to_string(item.cdr)
return util.to_string(item.car) + u' ' + self.__str_helper(item.cdr)
def __str__(self):
return u'(' + self.__str_helper(self) + ')'
def __repr__(self):
return (self.__class__.__name__ +
u'(' + repr(self.car) + ', ' + repr(self.cdr) + ')')
def __len__(self):
# nil is the empty list
if self is NIL:
return 0
return sum(1 for i in self)
def __eq__(self, other):
'''Compare recursively to another iterable.'''
for si, oi in itertools.izip(self, other):
if si != oi:
return False
return True
def __iter__(self):
'''
Yield all the items in this cons sequence in turn. If the final item is
NIL, it isn't yielded. If the final item is non-NIL, raises an error.
'''
item = self
while 1:
if item is NIL:
# stop if the item is NIL
return
elif isinstance(item.cdr, Cons):
yield item.car
item = item.cdr
else:
raise errors.WrongArgumentTypeError('not a proper list: ' +
unicode(self))
def __getitem__(self, index):
'''Allow indexing into the list.'''
if isinstance(index, slice):
if index.stop < len(self):
raise IndexError(self.__class__.__name__.lower() +
' does not support end indexes')
s = self
for i in xrange(index.start):
if s is NIL:
break
s = s.cdr
return s
else:
index += len(self) if index < 0 else 0
for i, item in enumerate(self):
if i == index:
return item
raise IndexError(self.__class__.__name__.lower() +
' index out of range')
# the singleton 'nil' value, an empty Cons: we define it here so Cons can use it
NIL = Cons(None, None)
class Symbol(Atom):
'''Symbols store other values, and evaluate to their stored values.'''
def __init__(self, value):
'''Symbols are stored and looked up by their string names.'''
Atom.__init__(self, unicode(value))
def __hash__(self):
return hash(self.value)
def __eq__(self, other):
return isinstance(other, Symbol) and self.value == other.value
class Callable(Atom):
'''A base class for object in our language that can be 'called'.'''
def __init__(self, name=None, docstring=None):
'''A name can later be set for display purposes.'''
Atom.__init__(self, name)
self.docstring = docstring
@staticmethod
def build_string(kind, name, spec):
'''
Build a string to display a typical callable in the interpreter. kind is
the object type to use, name is the (optional) name to give this
specific instance of the object, and spec is the ArgSpec object to
use to build the arguments list.
'''
s = u'<' + unicode(kind)
# set the function name if possible
if name is not None:
s += ' ' + unicode(name)
s += ' ('
# compose a list of all arg symbols
a = []
for arg_type, arg in spec:
if arg_type == argspec.ArgSpec.REQUIRED:
a.append(unicode(arg))
elif arg_type == argspec.ArgSpec.OPTIONAL:
arg, default = arg
a.append('(' + unicode(arg) + ' ' + util.to_string(default) + ')')
elif arg_type == argspec.ArgSpec.VARIADIC:
a.append(unicode(arg))
a.append(tokens.VARIADIC_ARG)
else:
raise ValueError('Unhandled arg type: ' + arg_type)
s += ' '.join(a)
s += ')>'
return s
def name(self, name):
# set the name if it hasn't been stored yet
if self.value is None:
self.value = name
def __eq__(self, other):
'''Callables are only equal if the other callable is this callable.'''
return isinstance(other, self.__class__) and other is self
class Function(Callable):
'''
Represents a function in our language. Functions take some number of
arguments, have a body, and are evaluated in some context.
'''
def __init__(self, evaluate, parent, args, body, name=None):
'''
Creates a function given a list of its arguments, its body, and
its parent environment, i.e. the environment it was created and will be
evaluated in (its closure). If the last argument is the variadic
argument symbol, the preceding symbol is treated as a variadic arg.
Optional arguments follow required arguments but precede any variadic
arg, and consist of a two item list of symbol and expression, of which
the expression is evaluated immediately.
'''
# NOTE: arguments MUST come in the order: required, optional, variadic
assert isinstance(parent, Environment)
self.spec = argspec.ArgSpec.build(evaluate, parent, args)
self.body = body
self.parent = parent
Callable.__init__(self, name)
def __str__(self):
return Callable.build_string('function', self.value, self.spec)
def __call__(self, evaluate, *arg_values):
'''
Evaluate this function given a list of values to use for its arguments
and return the result.
'''
# create a new environment with the parent set as our parent environment
env = Environment(self.parent)
# fill it with the arg spec's values (throws an error if impossible)
env.update(self.spec.fill(arg_values, Cons.build_list))
# evaluate our body using the new environment and return the result
return evaluate(self.body, env)
class PrimitiveFunction(Function):
'''
Represents a base-level function that can't be broken down into an AST. One
of the constructs that enables the language to function.
'''
def __init__(self, method, name=None):
'''
Create a primitive function that works much like a normal function,
except that the method is a Python function that does work using the
arguments given to __call__.
'''
self.method = method
# parse the arg spec (no support for keyword args)
args, vararg, _, defaults = inspect.getargspec(method)
defaults = () if defaults is None else defaults
self.spec = argspec.ArgSpec()
# get the counts for our respective arg types
num_required = len(args) - len(defaults)
num_optional = len(args) - num_required
# add required arguments
for arg in args[:num_required]:
self.spec.required(arg)
# add optional arguments, which come after required ones
for arg, default in itertools.izip(args[num_required:], defaults):
self.spec.optional(arg, default)
# add the variadic arg if it exists
if vararg is not None:
self.spec.variadic(vararg)
Callable.__init__(self, name)
def __str__(self):
return Callable.build_string('primitive-function', self.value,
self.spec)
def __call__(self, evaluate, *arg_values):
'''
Calls our internal method on the given arguments, ensuring that the
correct number of values was passed in. The evaluate argument is present
only for method-parity with the Function class.
'''
self.spec.validate(arg_values)
return self.method(*arg_values)
class Macro(Callable):
'''
A code-rewriting construct. A macro takes code and returns (expands) code
dynamically at runtime. Arguments aren't evaluated before being inserted
into the macro body.
'''
def __init__(self, evaluate, env, args, body, name=None):
self.spec = argspec.ArgSpec.build(evaluate, env, args)
self.body = body
Callable.__init__(self, name)
def __str__(self):
return Callable.build_string('macro', self.value, self.spec)
def __call__(self, evaluate, env, *arg_sexps):
'''
Expand the macro's body in some environment using the given argument
expressions.
'''
# map symbols to their replacement expressions in a new environment
expand_env = Environment(env)
expand_env.update(self.spec.fill(arg_sexps, Cons.build_list))
# evaluate our body in the created environment
return evaluate(self.body, expand_env)
class Environment(dict):
'''
A scope that holds variables mapped to their values. Allows us to easily
package execution state.
'''
def __init__(self, parent):
'''
Create an environment with the given parent and any number of predefined
variables.
'''
# None means no parent, otherwise must be an environment
assert parent is None or isinstance(parent, Environment)
# the environment that contains this environment
self.parent = parent
dict.__init__(self)
def __getitem__(self, symbol):
'''
Attempts to locate a given symbol by name in the current environment,
then in every environment up the parent chain if the symbol could not be
found. If the symbol is not bound within the parent chain, raises an
error.
'''
if symbol in self:
return self.get(symbol)
elif self.parent is not None:
return self.parent[symbol]
raise errors.SymbolNotFoundError.build(symbol)