Skip to content

Commit b06cea4

Browse files
authored
Update metadata and dynamic flag on Var redef (#642)
1 parent 4b55fc6 commit b06cea4

File tree

9 files changed

+126
-63
lines changed

9 files changed

+126
-63
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
* Fixed a bug that allowed dynamic Vars to be `set!` even if they weren't thread-bound (#638)
2222
* Fixed a bug where it was impossible to specify negative CLI options for the compiler flags (#638)
2323
* Fixed a bug where it was impossible to use more than a single body expression in a `try` special form (#640)
24+
* Fixed a bug where re-`def`ing a Var (regardless of `^:redef` metadata) would not update metadata or dynamic flag (#642)
2425

2526
### Removed
2627
* Removed Click as a dependency in favor of builtin `argparse` (#622, #624, #636)

src/basilisp/core.lpy

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4193,7 +4193,9 @@
41934193
"Set the binding of the Var. Must be thread-local."
41944194
[v val]
41954195
(if (thread-bound? v)
4196-
(set! (.-value v) val)
4196+
(do
4197+
(.set-value v val)
4198+
val)
41974199
(throw
41984200
(ex-info "Cannot set non-thread-bound Var binding" {:var v}))))
41994201

@@ -4239,7 +4241,7 @@
42394241
([ns name val]
42404242
(let [ns (the-ns ns)
42414243
v (basilisp.lang.runtime/Var ns name)]
4242-
(set! (.-root v) val)
4244+
(.set-root v val)
42434245
(.intern ns v))))
42444246

42454247
(defn create-ns

src/basilisp/lang/compiler/generator.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,7 @@ def _var_ns_as_python_sym(name: str) -> str:
640640

641641
_NS_VAR_VALUE = f"{_NS_VAR}.value"
642642

643+
_NS_VAR_VALUE_SETTER_FN_NAME = _load_attr(f"{_NS_VAR}.set_value")
643644
_NS_VAR_NAME = _load_attr(f"{_NS_VAR_VALUE}.name")
644645
_NEW_DECIMAL_FN_NAME = _load_attr(f"{_UTIL_ALIAS}.decimal_from_str")
645646
_NEW_FRACTION_FN_NAME = _load_attr(f"{_UTIL_ALIAS}.fraction")
@@ -2602,9 +2603,12 @@ def _require_to_py_ast(_: GeneratorContext, node: Require) -> GeneratedPyAST:
26022603
finalbody=[
26032604
# Restore the original namespace after each require to ensure that the
26042605
# following require starts with a clean slate
2605-
ast.Assign(
2606-
targets=[_load_attr(_NS_VAR_VALUE, ctx=ast.Store())],
2607-
value=ast.Name(id=requiring_ns_name, ctx=ast.Load()),
2606+
statementize(
2607+
ast.Call(
2608+
func=_NS_VAR_VALUE_SETTER_FN_NAME,
2609+
args=[ast.Name(id=requiring_ns_name, ctx=ast.Load())],
2610+
keywords=[],
2611+
)
26082612
),
26092613
],
26102614
)
@@ -2630,16 +2634,18 @@ def _set_bang_to_py_ast(ctx: GeneratorContext, node: SetBang) -> GeneratedPyAST:
26302634
target, (HostField, Local, VarRef)
26312635
), f"invalid set! target type {type(target)}"
26322636

2637+
assign_ast: List[ast.AST]
26332638
if isinstance(target, HostField):
26342639
target_ast = _interop_prop_to_py_ast(ctx, target, is_assigning=True)
2640+
assign_ast = [ast.Assign(targets=[target_ast.node], value=val_ast.node)]
26352641
elif isinstance(target, VarRef):
26362642
# This is a bit of a hack to force the generator to generate code for accessing
26372643
# a Var directly so we can store a temp reference to that Var rather than
26382644
# performing a global Var find twice for the same single expression.
26392645
temp_var_name = genname("var")
26402646
var_ast = _var_sym_to_py_ast(ctx, target.assoc(return_var=True))
26412647
target_ast = GeneratedPyAST(
2642-
node=_load_attr(f"{temp_var_name}.value", ctx=ast.Store()),
2648+
node=_load_attr(f"{temp_var_name}.set_value"),
26432649
dependencies=list(
26442650
chain(
26452651
var_ast.dependencies,
@@ -2676,8 +2682,10 @@ def _set_bang_to_py_ast(ctx: GeneratorContext, node: SetBang) -> GeneratedPyAST:
26762682
)
26772683
),
26782684
)
2685+
assign_ast = [ast.Call(func=target_ast.node, args=[val_ast.node], keywords=[])]
26792686
elif isinstance(target, Local):
26802687
target_ast = _local_sym_to_py_ast(ctx, target, is_assigning=True)
2688+
assign_ast = [ast.Assign(targets=[target_ast.node], value=val_ast.node)]
26812689
else: # pragma: no cover
26822690
raise GeneratorException(
26832691
f"invalid set! target type {type(target)}", lisp_ast=target
@@ -2697,7 +2705,7 @@ def _set_bang_to_py_ast(ctx: GeneratorContext, node: SetBang) -> GeneratedPyAST:
26972705
)
26982706
],
26992707
target_ast.dependencies,
2700-
[ast.Assign(targets=[target_ast.node], value=val_ast.node)],
2708+
assign_ast,
27012709
)
27022710
),
27032711
)
@@ -2708,7 +2716,7 @@ def _set_bang_to_py_ast(ctx: GeneratorContext, node: SetBang) -> GeneratedPyAST:
27082716
chain(
27092717
val_ast.dependencies,
27102718
target_ast.dependencies,
2711-
[ast.Assign(targets=[target_ast.node], value=val_ast.node)],
2719+
assign_ast,
27122720
)
27132721
),
27142722
)

src/basilisp/lang/interfaces.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,15 @@ class IReference(IMeta):
116116
__slots__ = ()
117117

118118
@abstractmethod
119-
def alter_meta(self, f: Callable[..., "IPersistentMap"], *args) -> "IPersistentMap":
119+
def alter_meta(
120+
self, f: Callable[..., Optional["IPersistentMap"]], *args
121+
) -> Optional["IPersistentMap"]:
120122
raise NotImplementedError()
121123

122124
@abstractmethod
123-
def reset_meta(self, meta: "IPersistentMap") -> "IPersistentMap":
125+
def reset_meta(
126+
self, meta: Optional["IPersistentMap"]
127+
) -> Optional["IPersistentMap"]:
124128
raise NotImplementedError()
125129

126130

src/basilisp/lang/reference.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
try:
1919
from typing import Protocol
2020
except ImportError:
21-
AlterMeta = Callable[..., IPersistentMap]
21+
AlterMeta = Callable[..., Optional[IPersistentMap]]
2222
else:
2323

2424
class AlterMeta(Protocol): # type: ignore [no-redef]
@@ -45,12 +45,12 @@ def meta(self) -> Optional[IPersistentMap]:
4545
with self._rlock:
4646
return self._meta
4747

48-
def alter_meta(self, f: AlterMeta, *args) -> IPersistentMap:
48+
def alter_meta(self, f: AlterMeta, *args) -> Optional[IPersistentMap]:
4949
with self._wlock:
5050
self._meta = f(self._meta, *args)
5151
return self._meta
5252

53-
def reset_meta(self, meta: IPersistentMap) -> IPersistentMap:
53+
def reset_meta(self, meta: Optional[IPersistentMap]) -> Optional[IPersistentMap]:
5454
with self._wlock:
5555
self._meta = meta
5656
return meta

src/basilisp/lang/runtime.py

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,14 @@ def name(self) -> sym.Symbol:
277277
def dynamic(self) -> bool:
278278
return self._dynamic
279279

280+
def set_dynamic(self, dynamic: bool) -> None:
281+
with self._wlock:
282+
if dynamic == self._dynamic:
283+
return
284+
285+
self._dynamic = dynamic
286+
self._tl = _VarBindings() if dynamic else None
287+
280288
@property
281289
def is_private(self) -> Optional[bool]:
282290
if self._meta is not None:
@@ -301,23 +309,25 @@ def root(self):
301309
with self._rlock:
302310
return self._root
303311

304-
@root.setter
305-
def root(self, val):
312+
def bind_root(self, val) -> None:
313+
"""Atomically update the root binding of this Var to val."""
306314
with self._wlock:
307315
self._set_root(val)
308316

309-
def alter_root(self, f, *args):
317+
def alter_root(self, f, *args) -> None:
318+
"""Atomically alter the root binding of this Var to the result of calling
319+
f with the existing root value and any additional arguments."""
310320
with self._wlock:
311321
self._set_root(f(self._root, *args))
312322

313323
def push_bindings(self, val):
314-
if self._tl is None:
324+
if not self._dynamic or self._tl is None:
315325
raise RuntimeException("Can only push bindings to dynamic Vars")
316326
self._validate(val)
317327
self._tl.bindings.append(val)
318328

319329
def pop_bindings(self):
320-
if self._tl is None:
330+
if not self._dynamic or self._tl is None:
321331
raise RuntimeException("Can only pop bindings from dynamic Vars")
322332
return self._tl.bindings.pop()
323333

@@ -330,15 +340,22 @@ def is_thread_bound(self):
330340

331341
@property
332342
def value(self):
343+
"""Return the current value of the Var visible in the current thread.
344+
345+
For non-dynamic Vars, this will just be the root. For dynamic Vars, this will
346+
be any thread-local binding if one is defined. Otherwise, the root value."""
333347
with self._rlock:
334348
if self._dynamic:
335349
assert self._tl is not None
336350
if len(self._tl.bindings) > 0:
337351
return self._tl.bindings[-1]
338352
return self._root
339353

340-
@value.setter
341-
def value(self, v):
354+
def set_value(self, v) -> None:
355+
"""Set the current value of the Var.
356+
357+
If the Var is not dynamic, this is equivalent to binding the root value. If the
358+
Var is dynamic, this will set the thread-local bindings for the Var."""
342359
with self._wlock:
343360
if self._dynamic:
344361
assert self._tl is not None
@@ -350,32 +367,46 @@ def value(self, v):
350367
return
351368
self._set_root(v)
352369

353-
@staticmethod
354-
def intern(
370+
__UNBOUND_SENTINEL = object()
371+
372+
@classmethod
373+
def intern( # pylint: disable=too-many-arguments
374+
cls,
355375
ns: Union["Namespace", sym.Symbol],
356376
name: sym.Symbol,
357377
val,
358378
dynamic: bool = False,
359-
meta=None,
379+
meta: Optional[IPersistentMap] = None,
360380
) -> "Var":
361-
"""Intern the value bound to the symbol `name` in namespace `ns`."""
381+
"""Intern the value bound to the symbol `name` in namespace `ns`.
382+
383+
If the Var already exists, it will have its root value updated to `val`,
384+
its meta will be reset to match the provided `meta`, and the dynamic flag
385+
will be updated to match the provided `dynamic` argument."""
362386
if isinstance(ns, sym.Symbol):
363387
ns = Namespace.get_or_create(ns)
364-
var = ns.intern(name, Var(ns, name, dynamic=dynamic, meta=meta))
365-
var.root = val
388+
new_var = cls(ns, name, dynamic=dynamic, meta=meta)
389+
var = ns.intern(name, new_var)
390+
if val is not cls.__UNBOUND_SENTINEL:
391+
var.bind_root(val)
392+
# Namespace.intern will return an existing interned Var for the same name
393+
# if one exists. We only want to set the meta and/or dynamic flag if the
394+
# Var is not new.
395+
if var is not new_var:
396+
var.reset_meta(meta)
397+
var.set_dynamic(dynamic)
366398
return var
367399

368-
@staticmethod
400+
@classmethod
369401
def intern_unbound(
402+
cls,
370403
ns: Union["Namespace", sym.Symbol],
371404
name: sym.Symbol,
372405
dynamic: bool = False,
373-
meta=None,
406+
meta: Optional[IPersistentMap] = None,
374407
) -> "Var":
375408
"""Create a new unbound `Var` instance to the symbol `name` in namespace `ns`."""
376-
if isinstance(ns, sym.Symbol):
377-
ns = Namespace.get_or_create(ns)
378-
return ns.intern(name, Var(ns, name, dynamic=dynamic, meta=meta))
409+
return cls.intern(ns, name, cls.__UNBOUND_SENTINEL, dynamic=dynamic, meta=meta)
379410

380411
@staticmethod
381412
def find_in_ns(
@@ -390,8 +421,8 @@ def find_in_ns(
390421
return ns.find(name_sym)
391422
return None
392423

393-
@staticmethod
394-
def find(ns_qualified_sym: sym.Symbol) -> "Optional[Var]":
424+
@classmethod
425+
def find(cls, ns_qualified_sym: sym.Symbol) -> "Optional[Var]":
395426
"""Return the value currently bound to the name in the namespace specified
396427
by `ns_qualified_sym`."""
397428
ns = Maybe(ns_qualified_sym.ns).or_else_raise(
@@ -401,16 +432,16 @@ def find(ns_qualified_sym: sym.Symbol) -> "Optional[Var]":
401432
)
402433
ns_sym = sym.symbol(ns)
403434
name_sym = sym.symbol(ns_qualified_sym.name)
404-
return Var.find_in_ns(ns_sym, name_sym)
435+
return cls.find_in_ns(ns_sym, name_sym)
405436

406-
@staticmethod
407-
def find_safe(ns_qualified_sym: sym.Symbol) -> "Var":
437+
@classmethod
438+
def find_safe(cls, ns_qualified_sym: sym.Symbol) -> "Var":
408439
"""Return the Var currently bound to the name in the namespace specified
409440
by `ns_qualified_sym`. If no Var is bound to that name, raise an exception.
410441
411442
This is a utility method to return useful debugging information when code
412443
refers to an invalid symbol at runtime."""
413-
v = Var.find(ns_qualified_sym)
444+
v = cls.find(ns_qualified_sym)
414445
if v is None:
415446
raise RuntimeException(
416447
f"Unable to resolve symbol {ns_qualified_sym} in this context"
@@ -1888,7 +1919,7 @@ def bootstrap_core(compiler_opts: CompilerOpts) -> None:
18881919

18891920
def in_ns(s: sym.Symbol):
18901921
ns = Namespace.get_or_create(s)
1891-
_NS.value = ns
1922+
_NS.set_value(ns)
18921923
return ns
18931924

18941925
# Vars used in bootstrapping the runtime

tests/basilisp/compiler_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5360,7 +5360,7 @@ def test_private_aliased_var_does_not_resolve(
53605360
private_var = Var(
53615361
other_ns, private_var_sym, meta=lmap.map({SYM_PRIVATE_META_KEY: True})
53625362
)
5363-
private_var.value = kw.keyword("private-var")
5363+
private_var.set_value(kw.keyword("private-var"))
53645364
other_ns.intern(private_var_sym, private_var)
53655365

53665366
with pytest.raises(compiler.CompilerException):
@@ -5401,14 +5401,14 @@ def test_fully_namespaced_sym_resolves(self, lcompile: CompileFn):
54015401

54025402
# Intern a public symbol in `other.ns`
54035403
public_var = Var(other_ns, public_var_sym)
5404-
public_var.value = kw.keyword("public-var")
5404+
public_var.set_value(kw.keyword("public-var"))
54055405
other_ns.intern(public_var_sym, public_var)
54065406

54075407
# Intern a private symbol in `other.ns`
54085408
private_var = Var(
54095409
other_ns, private_var_sym, meta=lmap.map({SYM_PRIVATE_META_KEY: True})
54105410
)
5411-
private_var.value = kw.keyword("private-var")
5411+
private_var.set_value(kw.keyword("private-var"))
54125412
other_ns.intern(private_var_sym, private_var)
54135413

54145414
with runtime.ns_bindings(third_ns_name.name):

0 commit comments

Comments
 (0)