-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsym_lis.py
487 lines (396 loc) · 14.8 KB
/
sym_lis.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
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# Symbolic Lisp
# (c) Alex Toldaiev, 2019
# https://github.com/xealits/symbolic-lisp
from __future__ import division
import math
import operator as op
from collections import OrderedDict, UserString
import importlib
import re
from sys import stdout, exit
import logging
import pdb
import traceback
################ Types
class List(list):
"""A Lisp List is implemented as a Python list"""
pass
re_repetitions = re.compile(r"(/)\1{1,}", re.DOTALL)
class Symbol(UserString):
def no_repeated_slashes(self):
self.data = re_repetitions.sub(r"\1", self.data)
def split(self, char):
# just convert the output to Symbols again
return [Symbol(s) for s in self.data.split(char)]
class String(UserString):
pass
class Int(int):
pass
#Number = (int, float) # A Lisp Number is implemented as a Python int or float
################ Parsing: parse, tokenize, and read_from_tokens
def parse(program):
"Read a Scheme expression from a string."
return read_all_from_tokens(tokenize(program))
# the reg for the "-quoted strings
string_pattern = re.compile(r'\"([^"]+?)\"')
def tokenize(s):
'''Convert a string into a list of tokens.
an example:
>>> tokenize('foo (a b "bar (baz d)" z "seven (7 two)" ab (foo jaz ))')
['foo' '(' 'a' 'b' "bar (baz d)" 'z' "seven (7 two)" 'ab' '(' 'foo' 'jaz' ')' ')']
'''
# tokenize the text between the strings
tokens = []
prev_char = 0
for m in string_pattern.finditer(s):
mstart, mend = m.span()
prev_toks = s[prev_char: mstart].replace('(',' ( ').replace(')',' ) ').split()
tokens.extend(prev_toks + [String(m.group()[1:-1])]) # tokens and String
prev_char = mend
# the last chunk of the input string
tokens += s[prev_char:].replace('(',' ( ').replace(')',' ) ').split()
return tokens
def lispstr(exp):
"Convert a Python object back into a Lisp-readable string."
if isinstance(exp, List):
return '(' + ' '.join(map(lispstr, exp)) + ')'
else:
return str(exp)
def read_all_from_tokens(tokens):
'''Read all ()-closed expressions from tokens.'''
all_expr = []
while tokens:
an_expr = read_from_tokens(tokens)
# read_from_tokens pops the expression from tokens
all_expr.append(an_expr)
return all_expr
def read_from_tokens(tokens, nesting=0):
"""Read one expression from a sequence of tokens.
An expression is either a List or an atom."""
token = tokens.pop(0)
# parse one nesting
if '(' == token:
L_one_expr = List()
while tokens[0] != ')':
L_one_expr.append(read_from_tokens(tokens, nesting+1))
tokens.pop(0) # pop off ')'
# TODO the rest of tokens could be used for literate documentation
# for now would nice to just run them in sequence
# but it breaks
#if nesting==0 and tokens:
# #
# raise SyntaxError('unexpected continuation %s' % lispstr(tokens))
return L_one_expr
elif ')' == token:
raise SyntaxError('unexpected )')
else:
return atom(token)
def atom(token):
"Numbers become numbers; every other token is a symbol."
if isinstance(token, String):
return token
try: return Int(token)
except ValueError:
try: return float(token)
except ValueError:
return Symbol(token)
################ Environments
def _note(note, obj):
obj.note = note
return obj
def standard_env():
"""An environment with some standard functional procedures.
These procedures do not have the access to the dynamical environment of
eval."""
env = Env()
#env.update(vars(math)) # sin, cos, sqrt, pi, ...
env.update({
'+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv,
'>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq,
'abs': abs,
'append': op.add,
#'apply': apply, # no apply in python3
'begin': lambda *x: x[-1],
'car': lambda x: x[0],
'cdr': lambda x: x[1:],
'cons': lambda x,y: [x] + y,
'eq?': op.is_,
'equal?': op.eq,
'length': len,
'list': lambda *x: list(x),
'list?': lambda x: isinstance(x,list),
# list-ing the map objects in Python3
'map': lambda *x: list(map(*x)),
'max': max,
'min': min,
'not': op.not_,
'null?': lambda x: x == [],
'int?': lambda x: isinstance(x, int),
'float?': lambda x: isinstance(x, float),
'number?': lambda x: isinstance(x, (int, float)),
'env?' : lambda x: isinstance(x, Env),
'procedure?': callable,
'round': round,
'symbol?': lambda x: isinstance(x, Symbol),
'exit': exit,
'None': lambda : None,
'in?': lambda nsp, y: y in nsp,
'get': lambda nsp, x: nsp(x),
'get_default': lambda nsp, x, d: nsp(x, default=d),
'stdout': lambda *x: print(*x),
'_note': _note,
'obj': lambda x: x.note if 'note' in dir(x) else None,
'par_l': '(',
'par_r': ')',
'str_empty': '',
'join': lambda separator, l: separator.join(str(i) for i in l),
})
return env
def standard_name_path_list(name_path):
# a variable name
assert isinstance(name_path, Symbol)
name_path.no_repeated_slashes()
if name_path[-1] == '/':
name_path = name_path[:-1]
return name_path.split('/')
def standard_missing_name_handler(varname):
#raise NameError("name '%s' is not defined" %varname)
#raise TypeError("name '%s' is not defined" %varname)
return None
class Env(dict):
"An environment: a dict of {'var':val} pairs, with an outer Env."
def __init__(self, parms=(), args=(), outer=None):
self.update(zip(parms, args))
self.outer = outer
# all global environments (anonymous are the same)
# have the handler for missing names
if self.outer is None:
self['_missing_handler'] = standard_missing_name_handler
def find_global_env(self):
if self.outer is None:
return self
else:
return self.outer.find_global_env()
def bubble_find(self, var):
"Find the outermost Env where var appears."
if var in self:
return self
elif self.outer:
return self.outer.bubble_find(var)
else:
# the case when there is no such variable
# return self again
# request of the missing key will call the programmable handler
return self
def __missing__(self, varname):
return self['_missing_handler'](varname)
def find(self, name_path):
"""Find the outermost Env where name_path appears and return the variable from the name_path.
Return the global Env if not found."""
#name_path = standard_name_path_list(name_path)
# if the name path is empty
if not name_path: return self
# bubble the root part of the name up to the outermost
start_name = name_path[0]
if start_name == '':
start_env = self.find_global_env()
# and remove the root name
name_path = name_path[1:]
elif start_name == '.':
start_env = self
elif start_name == '..':
start_env = self # self.outer
else:
#return self if (var in self) else self.outer.bubble_find(var)
start_env = self.bubble_find(start_name) #[start_name] # it must be another env
# the path is not found
if start_env is None: return None
assert isinstance(start_env, Env)
# nest down the name path
for name in name_path[:-1]:
if name == '.':
continue
elif name == '..':
start_env = start_env.outer
else:
start_env = start_env[name]
# the final environment
#return start_env if name_path[-1] in start_env else None # return None if no such name is defined
#return start_env[name_path[-1]] # or crash
return start_env
def __call__(self, key, default=None):
#return self[key] # TODO: now it is only the current namespace, expand?
if not isinstance(key, Symbol):
return self.get(key, default)
name_path = standard_name_path_list(key)
var_name = name_path[-1]
path = name_path
located_var = self.find(path)
# not found variable
if var_name not in located_var:
return default
return located_var[var_name]
################ Procedures
class Procedure(object):
"A user-defined Scheme procedure."
def __init__(self, parms, body, env):
self.parms, self.body, self.outer = parms, body, env
def __call__(self, *args):
return lisp_eval(self.body, Env(self.parms, args, outer=self.outer))
################ eval
def lisp_eval(x, env=None):
"""Evaluate an expression in an environment.
If no environment is given, evaluate in a blank environment.
"""
if env is None:
env = standard_env()
if isinstance(x, Symbol): # variable reference
# quote symbols
if len(x) > 1 and x[0] == "'":
return x[1:]
# else it is a name path
name_path = standard_name_path_list(x)
var_name = name_path[-1]
path = name_path
return env.find(path)[var_name]
elif not isinstance(x, List): # constant literal, like None
return x
# in the rest x is List
# first cases when x[0] is a keyword
# they cannot be dynamically generated TODO: change it?
# it seems this is the spot of the dynamic name scope
# defined functions operate in their lexical scope
# (they are defined into the lexical scope)
# the keywords have access to the current namespace
# same as look-up but with set at the end
elif x[0] == 'set!': # (set! var exp)
(_, var, exp) = x
name_path = standard_name_path_list(var)
var_name = name_path[-1]
path = name_path
env.find(path)[var_name] = lisp_eval(exp, env)
elif x[0] == 'env':
nsp = Env(outer=env)
args = [lisp_eval(exp, env) for exp in x[1:]]
nsp.update(args)
return nsp
elif x[0] == 'env_anonymous':
nsp = Env()
args = [lisp_eval(exp, env) for exp in x[1:]]
nsp.update(args)
return nsp
elif x[0] == 'quote': # (quote exp)
(_, exp) = x
return exp
elif x[0] == 'if': # (if test conseq alt)
(_, test, conseq, alt) = x
exp = (conseq if lisp_eval(test, env) else alt)
return lisp_eval(exp, env)
elif x[0] == 'define': # (define var exp)
(_, name_path, exp) = x
# dynamic names in define
if isinstance(name_path, List):
name_path = lisp_eval(name_path, env)
name_path = standard_name_path_list(name_path)
var_name = name_path[-1]
path = name_path[:-1]
# nest down the path creating env-s
nested_env = env
for name in path:
if name == '':
nested_env = nested_env.find_global_env()
elif name in nested_env:
assert isinstance(nested_env[name], Env)
nested_env = nested_env[name]
else:
new_env = Env(outer=nested_env)
nested_env[name] = new_env
nested_env = new_env
# but eval expr in the original Env of define!
var = lisp_eval(exp, env)
#var = lisp_eval(exp, nested_env)
# the result is attached in the nested chain of env-s
nested_env[var_name] = var
# if the result is an env and it is not anonymous
# set the outer to the nested chain of env-s
attach_scope = isinstance(var, Env) and var.outer is not None
attach_scope |= isinstance(var, Procedure)
if attach_scope:
var.outer = nested_env
return var
elif x[0] == 'source':
filenames = [lisp_eval(exp, env) for exp in x[1:]]
results = []
for fname in filenames:
with open(fname.data) as f:
file_program = f.read()
res = lisp_eval_str(file_program, env)
results.append(res)
return results
elif x[0] == 'lambda': # (lambda (var...) body)
(_, parms, body) = x
return Procedure(parms, body, env)
else: # (proc arg...)
proc = lisp_eval(x[0], env)
if isinstance(proc, int):
# convenience
_, a_list = x
args = lisp_eval(a_list, env)
#pdb.set_trace()
return args[proc]
else:
# proc must be a callable
args = [lisp_eval(exp, env) for exp in x[1:]]
return proc(*args)
def lisp_eval_str(string, env=None):
res = None
for expr in parse(string):
res = lisp_eval(expr) if env is None else lisp_eval(expr, env)
return res
class PythonPlugins(Env):
'''Import external modules and call their contents.
'''
def __init__(self):
'''Creates an empty global Env with 2 procedures: import and get.'''
# make an empty Env
super().__init__()
self['import'] = self.imp
self['get'] = self.get
def imp(self, modname):
assert isinstance(modname, Symbol)
self[modname] = importlib.import_module(modname.data)
def get(self, objpath):
# DANGER: is there is a safer way to do it?
assert isinstance(objpath, Symbol)
return eval(objpath.data, globals(), self)
class GlobalEnv(Env):
'''The behavior of a global environment.
The important part is the global state.
That is what needs to be encapsulated,
lisp_eval is simply added to it with no reciprocal dependency.
Hence, define the logic of the global state:
the Env and the lisp eval methods attached.
'''
def __init__(self, env=None):
'''Initialize an independent global environment.
env = None or Env class
the initial state of the global environment
'''
# make an empty Env
super().__init__()
# populate it with defaults
if env is None:
self.update(standard_env())
elif isinstance(env, Env):
self.update(env)
else:
raise TypeError("wrong content for GlobalEnv: %s" % repr(env))
# also add the python plugins
self['_Python'] = PythonPlugins()
def eval(self, x):
return lisp_eval(x, self)
def eval_str(self, string):
res = None
for expr in parse(string):
res = self.eval(expr)
return res