Skip to content

Commit 15055c7

Browse files
authored
Add Namespace and Var utility functions (#636)
* Add Namespace and Var utility functions * Fix set! test * Check Var set!s at runtime * Remove unused generator changes * Allow multiple body expressions in try body * with-bindings* et al * Change the log * l o a d e d l i b s * Use bind_root method rather than setter for updating Var root * Use a true method for setting Var value * Formatting I guess * Apply new meta and dynamic flag to Var when redefed * Fix with to return the with body * Load functions * var-set returns set val * Test re-interning Vars works as expected * Test it out * Move it on out * This is why we test it out * Ok I am a fool
1 parent b06cea4 commit 15055c7

File tree

4 files changed

+289
-15
lines changed

4 files changed

+289
-15
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 support for watchers and validator functions on Atoms and Vars (#627)
1111
* Added support for Taps (#631)
1212
* Added support for hierarchies (#633)
13+
* Added support for several more utility Namespace and Var utility functions (#636)
1314

1415
### Changed
1516
* PyTest is now an optional extra dependency, rather than a required dependency (#622)

src/basilisp/core.lpy

Lines changed: 215 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -953,14 +953,18 @@
953953

954954
(atexit/register (.-shutdown *executor-pool*))
955955

956+
;; Declare `bound-fn*` now for use in `future-call`, though it is defined much later
957+
;; with the rest of the Var binding functions and macros.
958+
(def ^:redef bound-fn*)
959+
956960
(defn future-call
957961
"Call the no args function f in another thread. Returns a Future object.
958962
The value returned by f can be fetched using `deref` or `@`, though
959963
doing so may block unless the `deref` with a timeout argument is used."
960964
([f]
961965
(future-call f *executor-pool*))
962966
([f pool]
963-
(.submit pool f)))
967+
(.submit pool (bound-fn* f))))
964968

965969
(defmacro future
966970
"Execute the expressions of body in another thread. Returns a Future object.
@@ -3647,6 +3651,11 @@
36473651
:macro true}
36483652
with-open with)
36493653

3654+
(defn get-thread-bindings
3655+
"Return the current thread-local bindings as a map of Var/value pairs."
3656+
[]
3657+
(basilisp.lang.runtime/get-thread-bindings))
3658+
36503659
(defn push-thread-bindings
36513660
"Takes a map of Var/value pairs and applies the given value to the Var in the
36523661
current thread.
@@ -3688,6 +3697,126 @@
36883697
(finally
36893698
(pop-thread-bindings))))))
36903699

3700+
(defn with-bindings*
3701+
"Execute the function `f` with the given arguments `args` (as by `apply`) with the
3702+
thread-local Var bindings specified in the Var/value map `bindings-map` installed.
3703+
3704+
The thread-local Var bindings will be popped after executing `f` in all cases.
3705+
3706+
Returns the return value of `f`."
3707+
[bindings-map f & args]
3708+
(push-thread-bindings bindings-map)
3709+
(try
3710+
(apply f args)
3711+
(finally
3712+
(pop-thread-bindings))))
3713+
3714+
(defmacro with-bindings
3715+
"Execute the expressions given in the `body` with the thread-local Var bindings
3716+
specified in the Var/value map `bindings-map` installed.
3717+
3718+
The thread-local Var bindings will be popped after executing the body in all cases.
3719+
3720+
Returns the value of `body`."
3721+
[bindings-map & body]
3722+
`(with-bindings* ~bindings-map (fn [] ~@body)))
3723+
3724+
(defn bound-fn*
3725+
"Return a function which executes `f` with the same thread-local Var bindings as
3726+
were in effect when `bound-fn*` was called.
3727+
3728+
`f` will be called with the same arguments as the returned function.
3729+
3730+
Returns the return value of `f`.
3731+
3732+
This is primarily useful for creating functions which might need to run on a different
3733+
thread, but which should have the same Var bindings in place as when it was defined."
3734+
[f]
3735+
(let [current-bindings (get-thread-bindings)]
3736+
(fn [& args]
3737+
(apply with-bindings* current-bindings f args))))
3738+
3739+
(defmacro bound-fn
3740+
"Return a function with the same function tail (everything after `fn` when defining
3741+
a function) which will be executed with the same thread-local Var bindings as were
3742+
in effect when `bound-fn` was called.
3743+
3744+
Returns the return value of the defined function.
3745+
3746+
This is primarily useful for creating functions which might need to run on a different
3747+
thread, but which should have the same Var bindings in place as when it was defined."
3748+
[& fn-tail]
3749+
`(bound-fn* (fn ~@fn-tail)))
3750+
3751+
(defn with-redefs-fn
3752+
"Temporarily re-bind the given Var roots to the given values while executing
3753+
the function `f`, binding back to the original value afterwards.
3754+
3755+
Changes to the Var roots will be visible in all threads.
3756+
3757+
Note that Basilisp directly links Var references in compiled code by default
3758+
for performance reasons. Direct linking can be disabled for all Vars during
3759+
compilation by setting the `--use-var-indirection` compiler flag at startup.
3760+
Direct linking can be disabled for individual Vars by setting the `^:redef`
3761+
meta flag where the Var is `def`'ed.
3762+
3763+
`with-redefs` will throw an Exception if directly linked Vars are given in
3764+
the bindings."
3765+
[bindings-map f]
3766+
(when-not (or (:use-var-indirection *compiler-options*)
3767+
(every? #(:redef (meta %)) (keys bindings-map)))
3768+
(throw
3769+
(ex-info (str "Cannot redef selected Vars; either apply ^:redef meta to Vars or "
3770+
"restart Basilisp with '--use-var-indirection false' compiler flag")
3771+
{:unredefable-vars (remove #(:redef (meta %)) (keys bindings-map))})))
3772+
(let [redef-vars (fn [m]
3773+
(doseq [v m
3774+
:let [vvar (first v)
3775+
vval (second v)]]
3776+
(.bind-root vvar vval)))
3777+
originals (reduce (fn [m elem]
3778+
(let [vvar (first elem)]
3779+
(assoc m vvar (.-root vvar))))
3780+
{}
3781+
bindings-map)]
3782+
(redef-vars bindings-map)
3783+
(try
3784+
(f)
3785+
(finally
3786+
(redef-vars originals)))))
3787+
3788+
(defmacro with-redefs
3789+
"Temporarily re-bind the given Var roots to the given values while executing
3790+
the body, binding back to the original value afterwards.
3791+
3792+
Changes to the Var roots will be visible in all threads.
3793+
3794+
Note that Basilisp directly links Var references in compiled code by default
3795+
for performance reasons. Direct linking can be disabled for all Vars during
3796+
compilation by setting the `--use-var-indirection` compiler flag at startup.
3797+
Direct linking can be disabled for individual Vars by setting the `^:redef`
3798+
meta flag where the Var is `def`'ed.
3799+
3800+
`with-redefs` will throw an Exception if directly linked Vars are given in
3801+
the bindings."
3802+
[bindings & body]
3803+
(when-not (and (vector? bindings)
3804+
(even? (count bindings))
3805+
(pos? (count bindings)))
3806+
(throw
3807+
(ex-info "Expected an even number of bindings"
3808+
{:bindings bindings})))
3809+
(let [var-bindings (reduce (fn [v pair]
3810+
(let [vvar (first pair)
3811+
vval (second pair)]
3812+
(conj v `(var ~vvar) vval)))
3813+
[]
3814+
(partition 2 bindings))]
3815+
`(with-redefs-fn
3816+
~(apply hash-map var-bindings)
3817+
(fn []
3818+
~@body))))
3819+
36913820
(defn ^:private perf-counter
36923821
[]
36933822
(py-time/perf-counter))
@@ -4089,6 +4218,67 @@
40894218
ctx
40904219
namespace))))
40914220

4221+
(defn load-reader
4222+
"Read and evaluate the set of forms in the `io.TextIOBase` instance `reader`.
4223+
4224+
Most often this is useful if you want to split your namespace across multiple source
4225+
files. `require` will try to force you into a namespace-per-file paradigm (which is
4226+
generally preferred, but not right in every scenario). `load-reader` will load the
4227+
contents of the named file directly into the current namespace.
4228+
4229+
Note that unlike `require`, files loaded by `load-reader` will not be cached and will
4230+
thus incur compilation time on subsequent loads."
4231+
[reader]
4232+
(let [src (some-> (python/getattr reader "name" nil)
4233+
(pathlib/Path)
4234+
(.resolve)
4235+
(python/str))
4236+
ctx (basilisp.lang.compiler.CompilerContext. (or src "<Load Input>"))]
4237+
(doseq [form (seq (basilisp.lang.reader/read reader
4238+
*resolver*
4239+
*data-readers*))]
4240+
(basilisp.lang.compiler/compile-and-exec-form form
4241+
ctx
4242+
*ns*))))
4243+
4244+
(defn load-file
4245+
"Read and evaluate the set of forms contained in the file located at `path`.
4246+
4247+
Most often this is useful if you want to split your namespace across multiple source
4248+
files. `require` will try to force you into a namespace-per-file paradigm (which is
4249+
generally preferred, but not right in every scenario). `load-file` will load the
4250+
contents of the named file directly into the current namespace.
4251+
4252+
Note that unlike `require`, files loaded by `load-file` will not be cached and will
4253+
thus incur compilation time on subsequent loads."
4254+
[path]
4255+
(with-open [f (python/open path ** :mode "r")]
4256+
(load-reader f)))
4257+
4258+
(defn load
4259+
"Read and evaluate the set of forms contained in the files identified by `paths`.
4260+
4261+
The provided paths should not include a file suffix.
4262+
4263+
Most often this is useful if you want to split your namespace across multiple source
4264+
files. `require` will try to force you into a namespace-per-file paradigm (which is
4265+
generally preferred, but not right in every scenario). `load` will load the contents
4266+
of the named file directly into the current namespace.
4267+
4268+
Note that unlike `require`, files loaded by `load` will not be cached and will thus
4269+
incur compilation time on subsequent loads.
4270+
4271+
This function is provided for compatibility with Clojure. Users should prefer
4272+
`load-file` (or perhaps `load-reader` or `load-string`) to this function."
4273+
[& paths]
4274+
(doseq [path (seq paths)]
4275+
(load-file (str path ".lpy"))))
4276+
4277+
(defn load-string
4278+
"Read and evaluate the set of forms contained in the string `s`."
4279+
[s]
4280+
(load-reader (io/StringIO s)))
4281+
40924282
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
40934283
;; Reference (Atom/Namespace/Var) Utilities ;;
40944284
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -4206,7 +4396,7 @@
42064396
(defn all-ns
42074397
"Return a sequence of all namespaces."
42084398
[]
4209-
(vals @basilisp.lang.runtime.Namespace/-NAMESPACES))
4399+
(vals (basilisp.lang.runtime.Namespace/ns-cache)))
42104400

42114401
(defn find-ns
42124402
"Return the namespace named by sym if it exists, or nil otherwise."
@@ -4519,6 +4709,16 @@
45194709
(refer-lib current-ns libspec))
45204710
nil))
45214711

4712+
(defonce ^:dynamic ^:private *loaded-libs*
4713+
(atom #{}))
4714+
4715+
(defn loaded-libs
4716+
"Return a set of all loaded Basilisp namespace names as symbols.
4717+
4718+
Namespace names are only added to this list if they appear in an `ns` macro."
4719+
[]
4720+
@*loaded-libs*)
4721+
45224722
(defmacro ns
45234723
"Use this namespace pre-amble at the top of every namespace to declare
45244724
the namespace name and import necessary Python modules and require
@@ -4566,13 +4766,26 @@
45664766
(:import opts)))]
45674767
`(do
45684768
(in-ns (quote ~name))
4769+
(swap! *loaded-libs* conj (quote ~name))
45694770
~(when doc
45704771
`(alter-meta! (the-ns (quote ~name)) assoc :doc ~doc))
45714772
(refer-basilisp ~@refer-filters)
45724773
~requires
45734774
~uses
45744775
~@imports)))
45754776

4777+
(defn requiring-resolve
4778+
"Resolve the namespaced symbol `sym` as by `resolve`. If resolution fails, attempts
4779+
to require `sym`'s namespace (as by `require`) before resolving again."
4780+
[sym]
4781+
(if (qualified-symbol? sym)
4782+
(or (resolve sym)
4783+
(do (require (symbol (namespace sym)))
4784+
(resolve sym)))
4785+
(throw
4786+
(ex-info "Cannot resolve an unqualified symbol"
4787+
{:sym sym}))))
4788+
45764789
;;;;;;;;;;;;;;;;;;;;;
45774790
;; Regex Functions ;;
45784791
;;;;;;;;;;;;;;;;;;;;;

src/basilisp/lang/runtime.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
IPersistentList,
5252
IPersistentMap,
5353
IPersistentSet,
54+
IPersistentStack,
5455
IPersistentVector,
5556
ISeq,
5657
ITransientSet,
@@ -197,8 +198,6 @@ class RuntimeException(Exception):
197198

198199

199200
class _VarBindings(threading.local):
200-
__slots__ = ("bindings",)
201-
202201
def __init__(self):
203202
self.bindings: List = []
204203

@@ -449,21 +448,25 @@ def find_safe(cls, ns_qualified_sym: sym.Symbol) -> "Var":
449448
return v
450449

451450

452-
Frame = FrozenSet[Var]
453-
FrameStack = List[Frame]
451+
Frame = IPersistentSet[Var]
452+
FrameStack = IPersistentStack[Frame]
454453

455454

456455
class _ThreadBindings(threading.local):
457-
__slots__ = ("_bindings",)
458-
459456
def __init__(self):
460-
self._bindings: FrameStack = []
457+
self._bindings: FrameStack = vec.PersistentVector.empty()
461458

462-
def push_bindings(self, frame: Frame):
463-
self._bindings.append(frame)
459+
def get_bindings(self) -> FrameStack:
460+
return self._bindings
464461

465-
def pop_bindings(self):
466-
return self._bindings.pop()
462+
def push_bindings(self, frame: Frame) -> None:
463+
self._bindings = self._bindings.cons(frame)
464+
465+
def pop_bindings(self) -> Frame:
466+
frame = self._bindings.peek()
467+
self._bindings = self._bindings.pop()
468+
assert frame is not None
469+
return frame
467470

468471

469472
_THREAD_BINDINGS = _ThreadBindings()
@@ -908,6 +911,14 @@ def complete(self, text: str) -> Iterable[str]:
908911
return results
909912

910913

914+
def get_thread_bindings() -> IPersistentMap[Var, Any]:
915+
"""Return the current thread-local bindings."""
916+
bindings = {}
917+
for frame in _THREAD_BINDINGS.get_bindings():
918+
bindings.update({var: var.value for var in frame})
919+
return lmap.map(bindings)
920+
921+
911922
def push_thread_bindings(m: IPersistentMap[Var, Any]) -> None:
912923
"""Push thread local bindings for the Var keys in m using the values."""
913924
bindings = set()
@@ -920,7 +931,7 @@ def push_thread_bindings(m: IPersistentMap[Var, Any]) -> None:
920931
var.push_bindings(val)
921932
bindings.add(var)
922933

923-
_THREAD_BINDINGS.push_bindings(frozenset(bindings))
934+
_THREAD_BINDINGS.push_bindings(lset.set(bindings))
924935

925936

926937
def pop_thread_bindings() -> None:

0 commit comments

Comments
 (0)