Skip to content

Commit d357c18

Browse files
authored
defonce no longer throws a SyntaxError (#525)
* `defonce` no longer throws a SyntaxError * Change the log * Clean it up * Refactor `def` generator * Support unbound Vars
1 parent 7009020 commit d357c18

File tree

10 files changed

+207
-113
lines changed

10 files changed

+207
-113
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010

1111
### Fixed
1212
* Fixed a bug where the Basilisp AST nodes for return values of `deftype` members could be marked as _statements_ rather than _expressions_, resulting in an incorrect `nil` return (#523)
13+
* Fixed a bug where `defonce` would throw a Python SyntaxError due to a superfluous `global` statement in the generated Python (#525)
1314

1415
## [v0.1.dev13] - 2020-03-16
1516
### Added

src/basilisp/core.lpy

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2826,8 +2826,8 @@
28262826
a `name` is not already defined as a Var in this namespace. `expr` will not be
28272827
evaluated if the Var already exists."
28282828
[name expr]
2829-
`(let [v (def ~name)]
2830-
(when-not (.-is-bound v)
2829+
`(let [v# (def ~name)]
2830+
(when-not (.-is-bound v#)
28312831
(def ~name ~expr))))
28322832

28332833
(defmacro for
@@ -3698,7 +3698,7 @@
36983698

36993699
(import importlib)
37003700

3701-
(defn- require-libspec
3701+
(defn ^:private require-libspec
37023702
"Convert a user-specified require libspec into a map with well-defined keys.
37033703

37043704
Required keys:
@@ -3720,7 +3720,7 @@
37203720
(ex-info "Invalid libspec for require"
37213721
{:value req}))))
37223722

3723-
(defn- require-lib
3723+
(defn ^:private require-lib
37243724
"Require the library described by `libspec` into the Namespace `requiring-ns`."
37253725
[requiring-ns libspec]
37263726
(let [required-ns-sym (:namespace libspec)]
@@ -3737,7 +3737,7 @@
37373737
;; during the require process
37383738
(set! *ns* requiring-ns)))
37393739

3740-
(defn- refer-filtered-interns
3740+
(defn ^:private refer-filtered-interns
37413741
"Return a map of symbols to interned Vars in the Namespace `referred-ns` subject
37423742
to the filters described in `libspec`."
37433743
[referred-ns libspec]
@@ -3759,7 +3759,7 @@
37593759
m))
37603760
{}))))
37613761

3762-
(defn- refer-lib
3762+
(defn ^:private refer-lib
37633763
"Refer Vars into `requiring-ns` as described by `libspec`.
37643764

37653765
This function assumes the referred-to Namespace has already been loaded by
@@ -3985,6 +3985,9 @@
39853985
[name & body]
39863986
(let [doc (when (string? (first body))
39873987
(first body))
3988+
name (if doc
3989+
(vary-meta name assoc :doc doc)
3990+
name)
39883991
body (if doc
39893992
(rest body)
39903993
body)
@@ -4414,9 +4417,9 @@
44144417
([o & {:keys [keywordize-keys] :or {keywordize-keys true}}]
44154418
(basilisp.lang.runtime/to-lisp o keywordize-keys)))
44164419

4417-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
4418-
;; Data Types & Protocols ;;
4419-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;
4420+
;;;;;;;;;;;;;;;;
4421+
;; Interfaces ;;
4422+
;;;;;;;;;;;;;;;;
44204423

44214424
(import* abc)
44224425

@@ -4492,6 +4495,10 @@
44924495
`(def ~interface-name
44934496
(gen-interface ~name-str [~@method-sigs]))))
44944497

4498+
;;;;;;;;;;;;;;;;
4499+
;; Data Types ;;
4500+
;;;;;;;;;;;;;;;;
4501+
44954502
(defn ^:private collect-methods
44964503
"Collect method and interface declarations for `deftype` and `defrecord`
44974504
into a map containing `:interfaces` and `:methods` keys."
@@ -4561,6 +4568,10 @@
45614568
(def ~ctor-name ~type-name)
45624569
~type-name)))
45634570

4571+
;;;;;;;;;;;;;
4572+
;; Records ;;
4573+
;;;;;;;;;;;;;
4574+
45644575
(import* attr)
45654576

45664577
(defn record?

src/basilisp/lang/compiler/analyzer.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,30 +394,57 @@ def should_allow_unresolved_symbols(self) -> bool:
394394

395395
@property
396396
def should_macroexpand(self) -> bool:
397+
"""Return True if macros should be expanded."""
397398
return self._should_macroexpand
398399

399400
@property
400401
def is_async_ctx(self) -> bool:
402+
"""If True, the current node appears inside of an async function definition.
403+
It is possible that the current function is defined inside other functions,
404+
so this does not imply anything about the nesting level of the current node."""
401405
try:
402406
return self._func_ctx[-1] is True
403407
except IndexError:
404408
return False
405409

410+
@property
411+
def in_func_ctx(self) -> bool:
412+
"""If True, the current node appears inside of a function definition.
413+
It is possible that the current function is defined inside other functions,
414+
so this does not imply anything about the nesting level of the current node."""
415+
try:
416+
self._func_ctx[-1]
417+
except IndexError:
418+
return False
419+
else:
420+
return True
421+
406422
@contextlib.contextmanager
407423
def new_func_ctx(self, is_async: bool = False):
424+
"""Context manager which can be used to set a function context for child
425+
nodes to examine. A new function context is pushed onto the stack each time
426+
the Analyzer finds a new function definition, so there may be many nested
427+
function contexts."""
408428
self._func_ctx.append(is_async)
409429
yield
410430
self._func_ctx.pop()
411431

412432
@property
413433
def recur_point(self) -> Optional[RecurPoint]:
434+
"""Return the current recur point which applies to the current node, if there
435+
is one."""
414436
try:
415437
return self._recur_points[-1]
416438
except IndexError:
417439
return None
418440

419441
@contextlib.contextmanager
420442
def new_recur_point(self, loop_id: str, args: Collection[Any] = ()):
443+
"""Context manager which can be used to set a recur point for child nodes.
444+
A new recur point is pushed onto the stack each time the Analyzer finds a
445+
form which supports recursion (such as `fn*` or `loop*`), so there may be
446+
many recur points, though only one may be active at any given time for a
447+
node."""
421448
self._recur_points.append(RecurPoint(loop_id, args=args))
422449
yield
423450
self._recur_points.pop()
@@ -801,6 +828,7 @@ def _def_ast( # pylint: disable=too-many-branches,too-many-locals
801828
var=var,
802829
init=init,
803830
doc=doc,
831+
in_func_ctx=ctx.in_func_ctx,
804832
children=children,
805833
env=def_node_env,
806834
)

src/basilisp/lang/compiler/generator.py

Lines changed: 64 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -657,37 +657,6 @@ def _def_to_py_ast( # pylint: disable=too-many-branches
657657
assert node.op == NodeOp.DEF
658658

659659
defsym = node.name
660-
is_defn = False
661-
is_var_bound = node.var.is_bound
662-
is_noop_redef_of_bound_var = is_var_bound and node.init is None
663-
664-
if node.init is not None:
665-
# Since Python function definitions always take the form `def name(...):`,
666-
# it is redundant to assign them to the their final name after they have
667-
# been defined under a private alias. This codepath generates `defn`
668-
# declarations by directly generating the Python `def` with the correct
669-
# function name and short-circuiting the default double-declaration.
670-
if node.init.op == NodeOp.FN:
671-
assert isinstance(node.init, Fn)
672-
def_ast: Optional[GeneratedPyAST] = _fn_to_py_ast( # type: ignore[call-arg]
673-
ctx, node.init, def_name=defsym.name
674-
)
675-
is_defn = True
676-
elif (
677-
node.init.op == NodeOp.WITH_META
678-
and isinstance(node.init, WithMeta)
679-
and node.init.expr.op == NodeOp.FN
680-
):
681-
assert isinstance(node.init, WithMeta)
682-
def_ast = _with_meta_to_py_ast(ctx, node.init, def_name=defsym.name)
683-
is_defn = True
684-
else:
685-
def_ast = gen_py_ast(ctx, node.init)
686-
elif is_noop_redef_of_bound_var:
687-
def_ast = None
688-
else:
689-
def_ast = GeneratedPyAST(node=ast.Constant(None))
690-
691660
ns_name = _load_attr(_NS_VAR_VALUE)
692661
def_name = ast.Call(
693662
func=_NEW_SYM_FN_NAME, args=[ast.Constant(defsym.name)], keywords=[]
@@ -709,74 +678,79 @@ def _def_to_py_ast( # pylint: disable=too-many-branches
709678

710679
# Warn if this symbol is potentially being redefined (if the Var was
711680
# previously bound)
712-
if is_var_bound and __should_warn_on_redef(ctx, defsym, safe_name, def_meta):
681+
if node.var.is_bound and __should_warn_on_redef(ctx, defsym, safe_name, def_meta):
713682
logger.warning(
714683
f"redefining local Python name '{safe_name}' in module "
715684
f"'{ctx.current_ns.module.__name__}' ({node.env.ns}:{node.env.line})"
716685
)
717686

718-
meta_ast = gen_py_ast(ctx, node.meta)
719-
720-
# For defn style def generation, we specifically need to generate the
721-
# global declaration prior to emitting the Python `def` otherwise the
722-
# Python compiler will throw an exception during compilation
723-
# complaining that we assign the value prior to global declaration.
724-
if is_defn:
725-
assert def_ast is not None, "def_ast must be defined at this point"
726-
def_dependencies = list(
727-
chain(
728-
[] if node.top_level else [ast.Global(names=[safe_name])],
729-
def_ast.dependencies,
730-
[] if meta_ast is None else meta_ast.dependencies,
687+
if node.init is not None:
688+
# Since Python function definitions always take the form `def name(...):`,
689+
# it is redundant to assign them to the their final name after they have
690+
# been defined under a private alias. This codepath generates `defn`
691+
# declarations by directly generating the Python `def` with the correct
692+
# function name and short-circuiting the default double-declaration.
693+
assert node.init is not None # silence MyPy
694+
if node.init.op == NodeOp.FN:
695+
assert isinstance(node.init, Fn)
696+
def_ast = _fn_to_py_ast( # type: ignore[call-arg]
697+
ctx, node.init, def_name=defsym.name
731698
)
732-
)
733-
elif is_noop_redef_of_bound_var:
734-
# Re-def-ing previously bound Vars without providing a value is
735-
# essentially a no-op, which should not modify the Var root.
736-
assert def_ast is None, "def_ast is not defined at this point"
737-
def_dependencies = list(
738-
chain(
739-
[] if node.top_level else [ast.Global(names=[safe_name])],
740-
[] if meta_ast is None else meta_ast.dependencies,
699+
is_defn = True
700+
elif (
701+
node.init.op == NodeOp.WITH_META
702+
and isinstance(node.init, WithMeta)
703+
and node.init.expr.op == NodeOp.FN
704+
):
705+
assert isinstance(node.init, WithMeta)
706+
def_ast = _with_meta_to_py_ast(ctx, node.init, def_name=defsym.name)
707+
is_defn = True
708+
else:
709+
def_ast = gen_py_ast(ctx, node.init)
710+
is_defn = False
711+
712+
func = _INTERN_VAR_FN_NAME
713+
extra_args = [ast.Name(id=safe_name, ctx=ast.Load())]
714+
if is_defn:
715+
# For defn style def generation, we specifically need to generate
716+
# the global declaration prior to emitting the Python `def` otherwise
717+
# the Python compiler will throw an exception during compilation
718+
# complaining that we assign the value prior to global declaration.
719+
def_dependencies = list(
720+
chain(
721+
[ast.Global(names=[safe_name])] if node.in_func_ctx else [],
722+
def_ast.dependencies,
723+
)
741724
)
742-
)
743-
else:
744-
assert def_ast is not None, "def_ast must be defined at this point"
745-
def_dependencies = list(
746-
chain(
747-
def_ast.dependencies,
748-
[] if node.top_level else [ast.Global(names=[safe_name])],
749-
[
750-
ast.Assign(
751-
targets=[ast.Name(id=safe_name, ctx=ast.Store())],
752-
value=def_ast.node,
753-
)
754-
],
755-
[] if meta_ast is None else meta_ast.dependencies,
725+
else:
726+
def_dependencies = list(
727+
chain(
728+
def_ast.dependencies,
729+
[ast.Global(names=[safe_name])] if node.in_func_ctx else [],
730+
[
731+
ast.Assign(
732+
targets=[ast.Name(id=safe_name, ctx=ast.Store())],
733+
value=def_ast.node,
734+
)
735+
],
736+
)
756737
)
757-
)
738+
else:
739+
# Callers can either `(def v)` to declare an unbound Var or they
740+
# can redef a bound Var with no init value to fetch a reference
741+
# to the var. Re-def-ing previously bound Vars without providing
742+
# a value is essentially a no-op, which should not modify the Var
743+
# root.
744+
func = _INTERN_UNBOUND_VAR_FN_NAME
745+
extra_args = []
746+
def_dependencies = [ast.Global(names=[safe_name])] if node.in_func_ctx else []
758747

759-
if is_noop_redef_of_bound_var:
760-
return GeneratedPyAST(
761-
node=ast.Call(
762-
func=_INTERN_UNBOUND_VAR_FN_NAME,
763-
args=[ns_name, def_name],
764-
keywords=list(
765-
chain(
766-
dynamic_kwarg,
767-
[]
768-
if meta_ast is None
769-
else [ast.keyword(arg="meta", value=meta_ast.node)],
770-
)
771-
),
772-
),
773-
dependencies=def_dependencies,
774-
)
748+
meta_ast = gen_py_ast(ctx, node.meta)
775749

776750
return GeneratedPyAST(
777751
node=ast.Call(
778-
func=_INTERN_VAR_FN_NAME,
779-
args=[ns_name, def_name, ast.Name(id=safe_name, ctx=ast.Load())],
752+
func=func,
753+
args=list(chain([ns_name, def_name], extra_args)),
780754
keywords=list(
781755
chain(
782756
dynamic_kwarg,
@@ -786,7 +760,9 @@ def _def_to_py_ast( # pylint: disable=too-many-branches
786760
)
787761
),
788762
),
789-
dependencies=def_dependencies,
763+
dependencies=list(
764+
chain(def_dependencies, [] if meta_ast is None else meta_ast.dependencies,)
765+
),
790766
)
791767

792768

src/basilisp/lang/compiler/nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ class Def(Node[SpecialForm]):
351351
var: Var
352352
init: Optional[Node]
353353
doc: Optional[str]
354+
in_func_ctx: bool
354355
env: NodeEnv
355356
meta: NodeMeta = None
356357
children: Sequence[kw.Keyword] = vec.Vector.empty()

0 commit comments

Comments
 (0)