Skip to content

Commit 69365c2

Browse files
authored
Add declare, defn-, and defonce macros (#480)
* Add `declare`, `defn-`, and `defonce` macros * Tests for no-op def
1 parent e79db6b commit 69365c2

File tree

5 files changed

+171
-30
lines changed

5 files changed

+171
-30
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
* Added multiline REPL support using `prompt-toolkit` (#467)
1111
* Added node syntactic location (statement or expression) to Basilisp AST nodes emitted by the analyzer (#463)
1212
* Added `letfn` special form (#473)
13+
* Added `defn-`, `declare`, and `defonce` macros (#480)
1314

1415
### Changed
1516
* Change the default user namespace to `basilisp.user` (#466)

src/basilisp/core.lpy

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -467,28 +467,18 @@
467467
~@body))))
468468

469469
(defmacro defasync
470-
"Define a new asynchronous function with an optional docstring."
470+
"Define a new asynchronous function as by `defn`.
471+
472+
Asynchronous functions are compiled as Python `async def`s."
473+
[name & body]
474+
`(defn ~(vary-meta name assoc :async true)
475+
~@body))
476+
477+
(defmacro defn-
478+
"Define a new private function as by `defn`."
471479
[name & body]
472-
(let [body (concat body)
473-
doc (if (string? (first body))
474-
(first body)
475-
nil)
476-
body (if doc
477-
(rest body)
478-
body)
479-
fmeta (if (map? (first body))
480-
(assoc (first body))
481-
nil)
482-
body (if fmeta
483-
(rest body)
484-
body)
485-
fmeta (apply assoc fmeta (concat [:async true]
486-
(if doc
487-
[:doc doc]
488-
nil)))]
489-
`(defn ~name
490-
~fmeta
491-
~@body)))
480+
`(defn ~(vary-meta name assoc :private true)
481+
~@body))
492482

493483
(defn macroexpand-1
494484
"Macroexpand form one time. Returns the macroexpanded form. The return
@@ -2528,6 +2518,23 @@
25282518
{:test test-expr}))))
25292519
test-expr))))
25302520

2521+
(defmacro declare
2522+
"Declare the given names as Vars with no bindings, as a forward declaration."
2523+
[& names]
2524+
`(do
2525+
~@(map (fn [nm]
2526+
`(def ~(vary-meta nm assoc :redef true)))
2527+
names)))
2528+
2529+
(defmacro defonce
2530+
"Define the Var named by `name` with root binding set to `expr` if and only if
2531+
a `name` is not already defined as a Var in this namespace. `expr` will not be
2532+
evaluated if the Var already exists."
2533+
[name expr]
2534+
`(let [v (def ~name)]
2535+
(when-not (.-is-bound v)
2536+
(def ~name ~expr))))
2537+
25312538
(defmacro for
25322539
"Produce a list comprehension from 1 or more input sequences, subject to
25332540
optional modifiers.

src/basilisp/lang/compiler/generator.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ def _noop_node() -> ast.AST:
496496
_NEW_UUID_FN_NAME = _load_attr(f"{_UTIL_ALIAS}.uuid_from_str")
497497
_NEW_VEC_FN_NAME = _load_attr(f"{_VEC_ALIAS}.vector")
498498
_INTERN_VAR_FN_NAME = _load_attr(f"{_VAR_ALIAS}.intern")
499+
_INTERN_UNBOUND_VAR_FN_NAME = _load_attr(f"{_VAR_ALIAS}.intern_unbound")
499500
_FIND_VAR_FN_NAME = _load_attr(f"{_VAR_ALIAS}.find_safe")
500501
_ATTR_CLASS_DECORATOR_NAME = _load_attr(f"attr.s")
501502
_ATTRIB_FIELD_FN_NAME = _load_attr(f"attr.ib")
@@ -610,20 +611,16 @@ def __should_warn_on_redef(
610611
return False
611612

612613
current_ns = ctx.current_ns
613-
if safe_name in current_ns.module.__dict__:
614-
return True
615-
elif defsym in current_ns.interns:
614+
if defsym in current_ns.interns:
616615
var = current_ns.find(defsym)
617616
assert var is not None, f"Var {defsym} cannot be none here"
618617

619618
if var.meta is not None and var.meta.val_at(SYM_REDEF_META_KEY):
620619
return False
621-
elif var.is_bound:
622-
return True
623620
else:
624-
return False
621+
return bool(var.is_bound)
625622
else:
626-
return False
623+
return safe_name in current_ns.module.__dict__
627624

628625

629626
@_with_ast_loc
@@ -635,6 +632,8 @@ def _def_to_py_ast( # pylint: disable=too-many-branches
635632

636633
defsym = node.name
637634
is_defn = False
635+
is_var_bound = node.var.is_bound
636+
is_noop_redef_of_bound_var = is_var_bound and node.init is None
638637

639638
if node.init is not None:
640639
# Since Python function definitions always take the form `def name(...):`,
@@ -656,6 +655,8 @@ def _def_to_py_ast( # pylint: disable=too-many-branches
656655
is_defn = True
657656
else:
658657
def_ast = gen_py_ast(ctx, node.init)
658+
elif is_noop_redef_of_bound_var:
659+
def_ast = None
659660
else:
660661
def_ast = GeneratedPyAST(node=ast.Constant(None))
661662

@@ -678,8 +679,9 @@ def _def_to_py_ast( # pylint: disable=too-many-branches
678679
else []
679680
)
680681

681-
# Warn if this symbol is potentially being redefined
682-
if __should_warn_on_redef(ctx, defsym, safe_name, def_meta):
682+
# Warn if this symbol is potentially being redefined (if the Var was
683+
# previously bound)
684+
if is_var_bound and __should_warn_on_redef(ctx, defsym, safe_name, def_meta):
683685
logger.warning(
684686
f"redefining local Python name '{safe_name}' in module "
685687
f"'{ctx.current_ns.module.__name__}' ({node.env.ns}:{node.env.line})"
@@ -699,6 +701,16 @@ def _def_to_py_ast( # pylint: disable=too-many-branches
699701
[] if meta_ast is None else meta_ast.dependencies,
700702
)
701703
)
704+
elif is_noop_redef_of_bound_var:
705+
# Re-def-ing previously bound Vars without providing a value is
706+
# essentially a no-op, which should not modify the Var root.
707+
assert def_ast is None, "def_ast is not defined at this point"
708+
def_dependencies = list(
709+
chain(
710+
[] if node.top_level else [ast.Global(names=[safe_name])],
711+
[] if meta_ast is None else meta_ast.dependencies,
712+
)
713+
)
702714
else:
703715
def_dependencies = list(
704716
chain(
@@ -714,6 +726,23 @@ def _def_to_py_ast( # pylint: disable=too-many-branches
714726
)
715727
)
716728

729+
if is_noop_redef_of_bound_var:
730+
return GeneratedPyAST(
731+
node=ast.Call(
732+
func=_INTERN_UNBOUND_VAR_FN_NAME,
733+
args=[ns_name, def_name],
734+
keywords=list(
735+
chain(
736+
dynamic_kwarg,
737+
[]
738+
if meta_ast is None
739+
else [ast.keyword(arg="meta", value=meta_ast.node)],
740+
)
741+
),
742+
),
743+
dependencies=def_dependencies,
744+
)
745+
717746
return GeneratedPyAST(
718747
node=ast.Call(
719748
func=_INTERN_VAR_FN_NAME,

tests/basilisp/compiler_test.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,25 @@ def test_def_fn_with_meta(self, ns: runtime.Namespace):
377377
assert lmap.map({kw.keyword("meta-kw"): True}) == v.value.meta
378378
assert kw.keyword("fn-with-meta-node") == v.value()
379379

380+
def test_redef_unbound_var(self, ns: runtime.Namespace):
381+
v1: Var = lcompile("(def unbound-var)")
382+
assert None is v1.root
383+
384+
v2: Var = lcompile("(def unbound-var :a)")
385+
assert kw.keyword("a") == v2.root
386+
assert v2.is_bound
387+
388+
def test_def_unbound_does_not_clear_var_root(self, ns: runtime.Namespace):
389+
v1: Var = lcompile("(def bound-var :a)")
390+
assert kw.keyword("a") == v1.root
391+
assert v1.is_bound
392+
393+
v2: Var = lcompile("(def bound-var)")
394+
assert kw.keyword("a") == v2.root
395+
assert v2.is_bound
396+
397+
assert v1 == v2
398+
380399

381400
class TestDefType:
382401
@pytest.mark.parametrize("code", ["(deftype*)", "(deftype* Point)"])

tests/basilisp/core_macros_test.lpy

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,91 @@
173173
(is (= "0.1" (:added vmeta)))
174174
(is (= "another multi-arity docstring" (:doc vmeta)))))))
175175

176+
(deftest defn-private-test
177+
(testing "single arity defn-"
178+
(testing "simple"
179+
(let [fvar (defn- pf1 [] :kw)
180+
vmeta (meta fvar)]
181+
(is (= 'pf1 (:name vmeta)))
182+
(is (= '([]) (:arglists vmeta)))
183+
(is (= true (:private vmeta)))
184+
(is (not (contains? vmeta :doc)))))
185+
186+
(testing "with docstring"
187+
(let [fvar (defn- pf2 "a docstring" [] :kw)
188+
vmeta (meta fvar)]
189+
(is (= 'pf2 (:name vmeta)))
190+
(is (= '([]) (:arglists vmeta)))
191+
(is (= true (:private vmeta)))
192+
(is (= "a docstring" (:doc vmeta)))))
193+
194+
(testing "with attr-map"
195+
(let [fvar (defn- pf3 {:added "0.1"} [] :kw)
196+
vmeta (meta fvar)]
197+
(is (= 'pf3 (:name vmeta)))
198+
(is (= '([]) (:arglists vmeta)))
199+
(is (= true (:private vmeta)))
200+
(is (= "0.1" (:added vmeta)))
201+
(is (not (contains? vmeta :doc)))))
202+
203+
(testing "attr-map and docstring"
204+
(let [fvar (defn- pf4
205+
"another docstring"
206+
{:added "0.1"}
207+
[]
208+
:kw)
209+
vmeta (meta fvar)]
210+
(is (= 'pf4 (:name vmeta)))
211+
(is (= '([]) (:arglists vmeta)))
212+
(is (= true (:private vmeta)))
213+
(is (= "0.1" (:added vmeta)))
214+
(is (= "another docstring" (:doc vmeta))))))
215+
216+
(testing "multi arity defn"
217+
(testing "simple"
218+
(let [fvar (defn- pf5 ([] :kw) ([a] a))
219+
vmeta (meta fvar)]
220+
(is (= 'pf5 (:name vmeta)))
221+
(is (= '([] [a]) (:arglists vmeta)))
222+
(is (= true (:private vmeta)))
223+
(is (not (contains? vmeta :doc)))))
224+
225+
(testing "with docstring"
226+
(let [fvar (defn- pf6
227+
"multi-arity docstring"
228+
([] :kw)
229+
([a] a))
230+
vmeta (meta fvar)]
231+
(is (= 'pf6 (:name vmeta)))
232+
(is (= '([] [a]) (:arglists vmeta)))
233+
(is (= true (:private vmeta)))
234+
(is (= "multi-arity docstring" (:doc vmeta)))))
235+
236+
(testing "with attr-map"
237+
(let [fvar (defn- pf7
238+
{:added "0.1"}
239+
([] :kw)
240+
([a] a))
241+
vmeta (meta fvar)]
242+
(is (= 'pf7 (:name vmeta)))
243+
(is (= '([] [a]) (:arglists vmeta)))
244+
(is (= true (:private vmeta)))
245+
(is (= "0.1" (:added vmeta)))
246+
(is (not (contains? vmeta :doc)))))
247+
248+
(testing "attr-map and docstring"
249+
(let [fvar (defn- pf8
250+
"another multi-arity docstring"
251+
{:added "0.1"}
252+
([] :kw)
253+
([a] a))
254+
vmeta (meta fvar)]
255+
(is (= 'pf8 (:name vmeta)))
256+
(is (= '([] [a]) (:arglists vmeta)))
257+
(is (= true (:private vmeta)))
258+
(is (= "0.1" (:added vmeta)))
259+
(is (= "another multi-arity docstring" (:doc vmeta)))))))
260+
176261
(deftest fn-meta-test
177262
(testing "fn has no meta to start"
178263
(is (nil? (meta (fn* []))))

0 commit comments

Comments
 (0)