Skip to content

Add declare, defn-, and defonce macros #480

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Added multiline REPL support using `prompt-toolkit` (#467)
* Added node syntactic location (statement or expression) to Basilisp AST nodes emitted by the analyzer (#463)
* Added `letfn` special form (#473)
* Added `defn-`, `declare`, and `defonce` macros (#480)

### Changed
* Change the default user namespace to `basilisp.user` (#466)
Expand Down
49 changes: 28 additions & 21 deletions src/basilisp/core.lpy
Original file line number Diff line number Diff line change
Expand Up @@ -467,28 +467,18 @@
~@body))))

(defmacro defasync
"Define a new asynchronous function with an optional docstring."
"Define a new asynchronous function as by `defn`.

Asynchronous functions are compiled as Python `async def`s."
[name & body]
`(defn ~(vary-meta name assoc :async true)
~@body))

(defmacro defn-
"Define a new private function as by `defn`."
[name & body]
(let [body (concat body)
doc (if (string? (first body))
(first body)
nil)
body (if doc
(rest body)
body)
fmeta (if (map? (first body))
(assoc (first body))
nil)
body (if fmeta
(rest body)
body)
fmeta (apply assoc fmeta (concat [:async true]
(if doc
[:doc doc]
nil)))]
`(defn ~name
~fmeta
~@body)))
`(defn ~(vary-meta name assoc :private true)
~@body))

(defn macroexpand-1
"Macroexpand form one time. Returns the macroexpanded form. The return
Expand Down Expand Up @@ -2528,6 +2518,23 @@
{:test test-expr}))))
test-expr))))

(defmacro declare
"Declare the given names as Vars with no bindings, as a forward declaration."
[& names]
`(do
~@(map (fn [nm]
`(def ~(vary-meta nm assoc :redef true)))
names)))

(defmacro defonce
"Define the Var named by `name` with root binding set to `expr` if and only if
a `name` is not already defined as a Var in this namespace. `expr` will not be
evaluated if the Var already exists."
[name expr]
`(let [v (def ~name)]
(when-not (.-is-bound v)
(def ~name ~expr))))

(defmacro for
"Produce a list comprehension from 1 or more input sequences, subject to
optional modifiers.
Expand Down
47 changes: 38 additions & 9 deletions src/basilisp/lang/compiler/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ def _noop_node() -> ast.AST:
_NEW_UUID_FN_NAME = _load_attr(f"{_UTIL_ALIAS}.uuid_from_str")
_NEW_VEC_FN_NAME = _load_attr(f"{_VEC_ALIAS}.vector")
_INTERN_VAR_FN_NAME = _load_attr(f"{_VAR_ALIAS}.intern")
_INTERN_UNBOUND_VAR_FN_NAME = _load_attr(f"{_VAR_ALIAS}.intern_unbound")
_FIND_VAR_FN_NAME = _load_attr(f"{_VAR_ALIAS}.find_safe")
_ATTR_CLASS_DECORATOR_NAME = _load_attr(f"attr.s")
_ATTRIB_FIELD_FN_NAME = _load_attr(f"attr.ib")
Expand Down Expand Up @@ -610,20 +611,16 @@ def __should_warn_on_redef(
return False

current_ns = ctx.current_ns
if safe_name in current_ns.module.__dict__:
return True
elif defsym in current_ns.interns:
if defsym in current_ns.interns:
var = current_ns.find(defsym)
assert var is not None, f"Var {defsym} cannot be none here"

if var.meta is not None and var.meta.val_at(SYM_REDEF_META_KEY):
return False
elif var.is_bound:
return True
else:
return False
return bool(var.is_bound)
else:
return False
return safe_name in current_ns.module.__dict__


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

defsym = node.name
is_defn = False
is_var_bound = node.var.is_bound
is_noop_redef_of_bound_var = is_var_bound and node.init is None

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

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

# Warn if this symbol is potentially being redefined
if __should_warn_on_redef(ctx, defsym, safe_name, def_meta):
# Warn if this symbol is potentially being redefined (if the Var was
# previously bound)
if is_var_bound and __should_warn_on_redef(ctx, defsym, safe_name, def_meta):
logger.warning(
f"redefining local Python name '{safe_name}' in module "
f"'{ctx.current_ns.module.__name__}' ({node.env.ns}:{node.env.line})"
Expand All @@ -699,6 +701,16 @@ def _def_to_py_ast( # pylint: disable=too-many-branches
[] if meta_ast is None else meta_ast.dependencies,
)
)
elif is_noop_redef_of_bound_var:
# Re-def-ing previously bound Vars without providing a value is
# essentially a no-op, which should not modify the Var root.
assert def_ast is None, "def_ast is not defined at this point"
def_dependencies = list(
chain(
[] if node.top_level else [ast.Global(names=[safe_name])],
[] if meta_ast is None else meta_ast.dependencies,
)
)
else:
def_dependencies = list(
chain(
Expand All @@ -714,6 +726,23 @@ def _def_to_py_ast( # pylint: disable=too-many-branches
)
)

if is_noop_redef_of_bound_var:
return GeneratedPyAST(
node=ast.Call(
func=_INTERN_UNBOUND_VAR_FN_NAME,
args=[ns_name, def_name],
keywords=list(
chain(
dynamic_kwarg,
[]
if meta_ast is None
else [ast.keyword(arg="meta", value=meta_ast.node)],
)
),
),
dependencies=def_dependencies,
)

return GeneratedPyAST(
node=ast.Call(
func=_INTERN_VAR_FN_NAME,
Expand Down
19 changes: 19 additions & 0 deletions tests/basilisp/compiler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,25 @@ def test_def_fn_with_meta(self, ns: runtime.Namespace):
assert lmap.map({kw.keyword("meta-kw"): True}) == v.value.meta
assert kw.keyword("fn-with-meta-node") == v.value()

def test_redef_unbound_var(self, ns: runtime.Namespace):
v1: Var = lcompile("(def unbound-var)")
assert None is v1.root

v2: Var = lcompile("(def unbound-var :a)")
assert kw.keyword("a") == v2.root
assert v2.is_bound

def test_def_unbound_does_not_clear_var_root(self, ns: runtime.Namespace):
v1: Var = lcompile("(def bound-var :a)")
assert kw.keyword("a") == v1.root
assert v1.is_bound

v2: Var = lcompile("(def bound-var)")
assert kw.keyword("a") == v2.root
assert v2.is_bound

assert v1 == v2


class TestDefType:
@pytest.mark.parametrize("code", ["(deftype*)", "(deftype* Point)"])
Expand Down
85 changes: 85 additions & 0 deletions tests/basilisp/core_macros_test.lpy
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,91 @@
(is (= "0.1" (:added vmeta)))
(is (= "another multi-arity docstring" (:doc vmeta)))))))

(deftest defn-private-test
(testing "single arity defn-"
(testing "simple"
(let [fvar (defn- pf1 [] :kw)
vmeta (meta fvar)]
(is (= 'pf1 (:name vmeta)))
(is (= '([]) (:arglists vmeta)))
(is (= true (:private vmeta)))
(is (not (contains? vmeta :doc)))))

(testing "with docstring"
(let [fvar (defn- pf2 "a docstring" [] :kw)
vmeta (meta fvar)]
(is (= 'pf2 (:name vmeta)))
(is (= '([]) (:arglists vmeta)))
(is (= true (:private vmeta)))
(is (= "a docstring" (:doc vmeta)))))

(testing "with attr-map"
(let [fvar (defn- pf3 {:added "0.1"} [] :kw)
vmeta (meta fvar)]
(is (= 'pf3 (:name vmeta)))
(is (= '([]) (:arglists vmeta)))
(is (= true (:private vmeta)))
(is (= "0.1" (:added vmeta)))
(is (not (contains? vmeta :doc)))))

(testing "attr-map and docstring"
(let [fvar (defn- pf4
"another docstring"
{:added "0.1"}
[]
:kw)
vmeta (meta fvar)]
(is (= 'pf4 (:name vmeta)))
(is (= '([]) (:arglists vmeta)))
(is (= true (:private vmeta)))
(is (= "0.1" (:added vmeta)))
(is (= "another docstring" (:doc vmeta))))))

(testing "multi arity defn"
(testing "simple"
(let [fvar (defn- pf5 ([] :kw) ([a] a))
vmeta (meta fvar)]
(is (= 'pf5 (:name vmeta)))
(is (= '([] [a]) (:arglists vmeta)))
(is (= true (:private vmeta)))
(is (not (contains? vmeta :doc)))))

(testing "with docstring"
(let [fvar (defn- pf6
"multi-arity docstring"
([] :kw)
([a] a))
vmeta (meta fvar)]
(is (= 'pf6 (:name vmeta)))
(is (= '([] [a]) (:arglists vmeta)))
(is (= true (:private vmeta)))
(is (= "multi-arity docstring" (:doc vmeta)))))

(testing "with attr-map"
(let [fvar (defn- pf7
{:added "0.1"}
([] :kw)
([a] a))
vmeta (meta fvar)]
(is (= 'pf7 (:name vmeta)))
(is (= '([] [a]) (:arglists vmeta)))
(is (= true (:private vmeta)))
(is (= "0.1" (:added vmeta)))
(is (not (contains? vmeta :doc)))))

(testing "attr-map and docstring"
(let [fvar (defn- pf8
"another multi-arity docstring"
{:added "0.1"}
([] :kw)
([a] a))
vmeta (meta fvar)]
(is (= 'pf8 (:name vmeta)))
(is (= '([] [a]) (:arglists vmeta)))
(is (= true (:private vmeta)))
(is (= "0.1" (:added vmeta)))
(is (= "another multi-arity docstring" (:doc vmeta)))))))

(deftest fn-meta-test
(testing "fn has no meta to start"
(is (nil? (meta (fn* []))))
Expand Down