Skip to content

Commit e041fc3

Browse files
authored
Re-working import logic and symbol table (#68)
* Re-working import logic and symbol table [WIP] * Use namespace aliases, mappings, and imports for canonical reference * Fix all typechecking errors * Remove unused symbol table contexts * Remove commented code. Leave some nice comments. 🎉 * Allow individual vars to be specified as dynamic * Minor changes after rebase
1 parent 86e3cc7 commit e041fc3

File tree

9 files changed

+315
-172
lines changed

9 files changed

+315
-172
lines changed

basilisp/compiler.py

Lines changed: 170 additions & 133 deletions
Large diffs are not rendered by default.

basilisp/importer.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,43 @@ def get_filename(self, fullname: str) -> str:
5858
return spec.loader_state.filename
5959

6060
def create_module(self, spec: importlib.machinery.ModuleSpec):
61-
mod = super().create_module(spec)
62-
self._cache[spec.name] = {"module": mod, "spec": spec}
61+
mod = types.ModuleType(spec.name)
62+
mod.__loader__ = spec.loader
63+
mod.__package__ = spec.parent
64+
mod.__spec__ = spec
65+
self._cache[spec.name] = {"spec": spec}
6366
return mod
6467

65-
def get_code(self, fullname: str) -> types.CodeType:
66-
runtime.set_current_ns(fullname)
68+
def exec_module(self, module):
69+
"""Compile the Basilisp module into Python code.
70+
71+
Basilisp is fundamentally a form-at-a-time compilation, meaning that
72+
each form in a module may require code compiled from an earlier form, so
73+
we incrementally compile a Python module by evaluating a single top-level
74+
form at a time and inserting the resulting AST nodes into the Pyton module."""
75+
fullname = module.__name__
6776
cached = self._cache[fullname]
77+
cached["module"] = module
6878
spec = cached["spec"]
6979
filename = spec.loader_state["filename"]
80+
81+
# During the bootstrapping process, the 'basilisp.core namespace is created with
82+
# a blank module. If we do not replace the module here with the module we are
83+
# generating, then we will not be able to use advanced compilation features such
84+
# as direct Python variable access to functions and other def'ed values.
85+
ns: runtime.Namespace = runtime.set_current_ns(fullname).value
86+
ns.module = module
87+
7088
forms = reader.read_file(filename)
71-
bytecode = compiler.compile_module_bytecode(forms, compiler.CompilerContext(), filename)
89+
compiler.compile_module(forms, compiler.CompilerContext(), module, filename)
90+
91+
# Because we want to (by default) add 'basilisp.core into every namespace by default,
92+
# we want to make sure we don't try to add 'basilisp.core into itself, causing a
93+
# circular import error.
94+
#
95+
# Later on, we can probably remove this and just use the 'ns macro to auto-refer
96+
# all 'basilisp.core values into the current namespace.
7297
runtime.Namespace.add_default_import(fullname)
73-
return bytecode
7498

7599

76100
def hook_imports():

basilisp/lang/runtime.py

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import itertools
22
import threading
3+
import types
34
from typing import Optional
45

56
from functional import seq
@@ -19,6 +20,22 @@
1920
_PRINT_GENERATED_PY_VAR_NAME = '*print-generated-python*'
2021

2122

23+
def _new_module(name: str, doc=None) -> types.ModuleType:
24+
"""Create a new empty Basilisp Python module.
25+
26+
Modules are created for each Namespace when it is created."""
27+
mod = types.ModuleType(name, doc=doc)
28+
mod.__loader__ = None
29+
mod.__package__ = None
30+
mod.__spec__ = None
31+
mod.__basilisp_bootstrapped__ = False # type: ignore
32+
return mod
33+
34+
35+
class RuntimeException(Exception):
36+
pass
37+
38+
2239
class Var:
2340
__slots__ = ('_name', '_ns', '_root', '_dynamic', '_is_bound', '_tl', '_meta')
2441

@@ -118,14 +135,14 @@ def intern_unbound(ns: sym.Symbol,
118135
return var
119136

120137
@staticmethod
121-
def find_in_ns(ns_sym: sym.Symbol, name_sym: sym.Symbol) -> "Var":
138+
def find_in_ns(ns_sym: sym.Symbol, name_sym: sym.Symbol) -> "Optional[Var]":
122139
"""Return the value current bound to the name `name_sym` in the namespace
123140
specified by `ns_sym`."""
124141
ns = Namespace.get_or_create(ns_sym)
125142
return ns.find(name_sym)
126143

127144
@staticmethod
128-
def find(ns_qualified_sym: sym.Symbol) -> "Var":
145+
def find(ns_qualified_sym: sym.Symbol) -> "Optional[Var]":
129146
"""Return the value currently bound to the name in the namespace specified
130147
by `ns_qualified_sym`."""
131148
ns = Maybe(ns_qualified_sym.ns).or_else_raise(
@@ -175,8 +192,9 @@ class Namespace:
175192

176193
__slots__ = ('_name', '_module', '_mappings', '_refers', '_aliases', '_imports')
177194

178-
def __init__(self, name: sym.Symbol) -> None:
195+
def __init__(self, name: sym.Symbol, module: types.ModuleType = None) -> None:
179196
self._name = name
197+
self._module = Maybe(module).or_else(lambda: _new_module(name._as_python_sym))
180198
self._mappings: atom.Atom = atom.Atom(pmap())
181199
self._aliases: atom.Atom = atom.Atom(pmap())
182200
self._imports: atom.Atom = atom.Atom(pset(Namespace.DEFAULT_IMPORTS.deref()))
@@ -194,6 +212,18 @@ def add_default_import(cls, module: str):
194212
def name(self) -> str:
195213
return self._name.name
196214

215+
@property
216+
def module(self):
217+
return self._module
218+
219+
@module.setter
220+
def module(self, m: types.ModuleType):
221+
"""Override the Python module for this Namespace.
222+
223+
This should only be done by basilisp.importer code to make sure the
224+
correct module is generated for `basilisp.core`."""
225+
self._module = m
226+
197227
@property
198228
def aliases(self) -> PMap:
199229
return self._aliases.deref()
@@ -239,7 +269,7 @@ def _intern(m: PMap, sym: sym.Symbol, new_var: Var,
239269
return m.set(sym, new_var)
240270
return m
241271

242-
def find(self, sym: sym.Symbol) -> Var:
272+
def find(self, sym: sym.Symbol) -> Optional[Var]:
243273
"""Find Vars mapped by the given Symbol input or None if no Vars are
244274
mapped by that Symbol."""
245275
return self.mappings.get(sym, None)
@@ -273,25 +303,26 @@ def __import_core_mappings(ns_cache: PMap,
273303
@staticmethod
274304
def __get_or_create(ns_cache: PMap,
275305
name: sym.Symbol,
306+
module: types.ModuleType = None,
276307
core_ns_name=_CORE_NS) -> PMap:
277308
"""Private swap function used by `get_or_create` to atomically swap
278309
the new namespace map into the global cache."""
279310
ns = ns_cache.get(name, None)
280311
if ns is not None:
281312
return ns_cache
282-
new_ns = Namespace(name)
313+
new_ns = Namespace(name, module=module)
283314
if name.name != core_ns_name:
284315
Namespace.__import_core_mappings(
285316
ns_cache, new_ns, core_ns_name=core_ns_name)
286317
return ns_cache.set(name, new_ns)
287318

288319
@classmethod
289-
def get_or_create(cls, name: sym.Symbol) -> "Namespace":
320+
def get_or_create(cls, name: sym.Symbol, module: types.ModuleType = None) -> "Namespace":
290321
"""Get the namespace bound to the symbol `name` in the global namespace
291322
cache, creating it if it does not exist.
292323
293324
Return the namespace."""
294-
return cls._NAMESPACES.swap(Namespace.__get_or_create, name)[name]
325+
return cls._NAMESPACES.swap(Namespace.__get_or_create, name, module=module)[name]
295326

296327
@classmethod
297328
def remove(cls, name: sym.Symbol) -> Optional["Namespace"]:
@@ -412,12 +443,14 @@ def init_ns_var(which_ns: str = _CORE_NS,
412443

413444

414445
def set_current_ns(ns_name: str,
446+
module: types.ModuleType = None,
415447
ns_var_name: str = _NS_VAR_NAME,
416448
ns_var_ns: str = _NS_VAR_NS) -> Var:
417449
"""Set the value of the dynamic variable `*ns*` in the current thread."""
418450
symbol = sym.Symbol(ns_name)
419-
ns = Namespace.get_or_create(symbol)
420-
ns_var = Var.find(sym.Symbol(ns_var_name, ns=ns_var_ns))
451+
ns = Namespace.get_or_create(symbol, module=module)
452+
ns_var = Maybe(Var.find(sym.Symbol(ns_var_name, ns=ns_var_ns))) \
453+
.or_else_raise(lambda: RuntimeException(f"Dynamic Var {sym.Symbol(ns_var_name, ns=ns_var_ns)} not bound!"))
421454
ns_var.push_bindings(ns)
422455
return ns_var
423456

@@ -426,7 +459,9 @@ def get_current_ns(ns_var_name: str = _NS_VAR_NAME,
426459
ns_var_ns: str = _NS_VAR_NS) -> Namespace:
427460
"""Get the value of the dynamic variable `*ns*` in the current thread."""
428461
ns_sym = sym.Symbol(ns_var_name, ns=ns_var_ns)
429-
ns: Namespace = Var.find(ns_sym).value
462+
ns: Namespace = Maybe(Var.find(ns_sym)) \
463+
.map(lambda v: v.value) \
464+
.or_else_raise(lambda: RuntimeException(f"Dynamic Var {ns_sym} not bound!"))
430465
return ns
431466

432467

@@ -451,7 +486,9 @@ def print_generated_python(var_name: str = _PRINT_GENERATED_PY_VAR_NAME,
451486
core_ns_name: str = _CORE_NS) -> bool:
452487
"""Return the value of the `*print-generated-python*` dynamic variable."""
453488
ns_sym = sym.Symbol(var_name, ns=core_ns_name)
454-
return Var.find(ns_sym).value
489+
return Maybe(Var.find(ns_sym)) \
490+
.map(lambda v: v.value) \
491+
.or_else_raise(lambda: RuntimeException(f"Dynamic Var {ns_sym} not bound!"))
455492

456493

457494
def bootstrap(ns_var_name: str = _NS_VAR_NAME,
@@ -460,13 +497,15 @@ def bootstrap(ns_var_name: str = _NS_VAR_NAME,
460497
express with the very minimal lisp environment."""
461498
core_ns_sym = sym.symbol(core_ns_name)
462499
ns_var_sym = sym.symbol(ns_var_name, ns=core_ns_name)
463-
__NS = Var.find(ns_var_sym)
500+
__NS = Maybe(Var.find(ns_var_sym)) \
501+
.or_else_raise(lambda: RuntimeException(f"Dynamic Var {ns_var_sym} not bound!"))
464502

465503
def set_BANG_(var_sym: sym.Symbol, expr):
466504
ns = Maybe(var_sym.ns).or_else(lambda: __NS.value.name)
467505
name = var_sym.name
468506

469-
v = Var.find(sym.symbol(name, ns=ns))
507+
v = Maybe(Var.find(sym.symbol(name, ns=ns))) \
508+
.or_else_raise(lambda: RuntimeException(f"Var {ns_var_sym} not bound!"))
470509
v.value = expr
471510
return expr
472511

basilisp/lang/symbol.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Optional
22

33
from basilisp.lang.meta import Meta
4+
from basilisp.lang.util import munge
45

56

67
class Symbol(Meta):
@@ -27,6 +28,12 @@ def with_meta(self, meta) -> "Symbol":
2728
new_meta = meta if self._meta is None else self._meta.update(meta)
2829
return Symbol(self._name, self._ns, meta=new_meta)
2930

31+
@property
32+
def _as_python_sym(self) -> str:
33+
if self.ns is not None:
34+
return f"{munge(self.ns)}.{munge(self.name)}"
35+
return f"{munge(self.name)}"
36+
3037
def __str__(self):
3138
if self._ns is not None:
3239
return "{ns}/{name}".format(ns=self._ns, name=self._name)

basilisp/lang/util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def lrepr(f) -> str:
4747
}
4848

4949

50-
def munge(s: str, allow_builtins: bool = True) -> str:
50+
def munge(s: str, allow_builtins: bool = False) -> str:
5151
"""Replace characters which are not valid in Python symbols
5252
with valid replacement strings."""
5353
new_str = []

basilisp/main.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,37 @@
22
# noinspection PyUnresolvedReferences
33
import readline
44
import traceback
5+
import types
56

67
import basilisp.compiler as compiler
78
import basilisp.importer as importer
89
import basilisp.lang.runtime as runtime
910
import basilisp.reader as reader
1011

1112

12-
def eval_file(filename: str, ctx: compiler.CompilerContext):
13+
def eval_file(filename: str, ctx: compiler.CompilerContext, module: types.ModuleType):
1314
"""Evaluate a file with the given name into a Python module AST node."""
1415
last = None
1516
for form in reader.read_file(filename, resolver=runtime.resolve_alias):
16-
last = compiler.compile_and_exec_form(form, ctx=ctx)
17+
last = compiler.compile_and_exec_form(form, ctx, module, filename)
1718
return last
1819

1920

20-
def eval_str(s: str, ctx: compiler.CompilerContext):
21+
def eval_str(s: str, ctx: compiler.CompilerContext, module: types.ModuleType):
2122
"""Evaluate the forms in a string into a Python module AST node."""
2223
last = None
2324
for form in reader.read_str(s, resolver=runtime.resolve_alias):
24-
last = compiler.compile_and_exec_form(form, ctx=ctx)
25+
last = compiler.compile_and_exec_form(form, ctx, module, source_filename='REPL Input')
2526
return last
2627

2728

2829
def repl(default_ns=runtime._REPL_DEFAULT_NS):
2930
ctx = compiler.CompilerContext()
3031
ns_var = runtime.set_current_ns(default_ns)
3132
while True:
33+
ns: runtime.Namespace = ns_var.value
3234
try:
33-
lsrc = input(f'{ns_var.value.name}=> ')
35+
lsrc = input(f'{ns.name}=> ')
3436
except EOFError:
3537
break
3638
except KeyboardInterrupt:
@@ -41,7 +43,7 @@ def repl(default_ns=runtime._REPL_DEFAULT_NS):
4143
continue
4244

4345
try:
44-
print(compiler.lrepr(eval_str(lsrc, ctx)))
46+
print(compiler.lrepr(eval_str(lsrc, ctx, ns.module)))
4547
except reader.SyntaxError as e:
4648
traceback.print_exception(reader.SyntaxError, e, e.__traceback__)
4749
continue

0 commit comments

Comments
 (0)