Logos. Runtime
(Logos v0.2.0)
Copy Markdown
Logos.Runtime -- the per-embedding-instance, :public ETS-backed
namespace registry. Never a global singleton: every embedder of
Logos calls Logos.Runtime.new/1 (or the Logos.new_runtime/0
convenience) to get its own isolated table, so multiple independent
Logos instances can coexist in the same VM without seeing each other's
namespaces or vars. An earlier version of this codebase instead kept a
single bare, module-level, :named_table ETS table shared by the whole
VM; this module replaced that global-singleton design.
Storage shape
One ETS table (:set, :public, not :named_table -- named tables
would be exactly the kind of accidental global-singleton trap this
module exists to avoid) holds every piece of registry state, keyed by
tagged tuples so unrelated concerns never collide:
{:ns, name}->%{meta:, aliases:, refers:, auto_refer_core?:, var_names:}-- one row per namespace.var_namesis aMapSetof the bare names interned directly in this namespace (not refers/aliases), kept alongside the namespace row sorequire :refer/usecan enumerate "every public var this namespace defines" without a table scan.{:var, ns, name}->%{value:, meta:}-- one row perLogos.Var.Logos.Varitself (lib/logos/var.ex) is just the%{ns, name}identity pair; itsvalue/metagenuinely live here, in the:publictable, so a process spawned viaspawn/spawn-link/spawn-monitorcan read/write a Var directly without round-tripping through an owning process -- this is the concrete mechanism:publicwas chosen for.:current_ns-> the default current-namespace name (String.t(), always"user"fromnew/1), consulted only as the fallback for a BEAM process that has never calledset_current_ns!/2itself.{:current_ns, pid}-> a per-process override: real Clojure's*ns*is thread-local, so the BEAM-native equivalent is process-local.current_ns/1checks{:current_ns, self()}first and only falls back to the shared:current_nsdefault row if this process has never set its own -- so two processes(in-ns ...)-ing concurrently against the sameRuntime(e.g. after one spawns the other) no longer stomp on each other. A freshlyspawned process simply inherits the shared default ("user", or whatever the last unscopedset_current_ns!/2caller -- realistically only ever the one process that ran before anyspawnhappened -- last wrote) until it callsin-nsitself, matching a fresh Clojure thread's*ns*starting from a sane default rather than literally inheriting its parent's dynamic binding (real Clojure threads don't inherit*ns*bindings either without explicitbound-fn/bindingplumbing). Known accepted tradeoff:{:current_ns, pid}rows are never cleaned up when a process exits -- a long-running embedder thatspawns enormous numbers of short-lived processes which each callin-nswill accumulate ETS rows forever. Not fixed here (would need a monitor-and-delete per spawned process, adding real overhead to everyspawnfor a leak that's bounded by "total processes ever spawned across the Runtime's lifetime", not unbounded within a single process) -- flagged here as a potential future fix if it ever matters in practice.:loading-> aMapSet.t(String.t())of namespace names currently mid-require, for circular-require detection. Seestart_loading!/2/finish_loading!/2.:gensym_counter-> an integer counter forgensym.:load_paths-> a fixed (set once atnew/1, never mutated) list of directory stringsrequire/usesearch on disk for a namespace not already in-memory. Seeload_paths/1andLogos.Primitives'load_ns!/2.:data_readers-> a%{tag_text => {module, function}}map backing#tag valuetagged literals (priv/grammar/logos.aether'stagged_literalrule, resolved at read time byLogos.Reader.Actions'shandle_rule(:tagged_literal, ...)). Seeded atnew/1with"inst"/"uuid"(Logos.DataReaders);register_data_reader!/3(theregister-data-reader!primitive's backing call) adds more, always resolved throughLogos.Interop.Allowlistfirst -- never an arbitrary Logos closure, the same sandboxing chokepointimportuses.
Granular per-var/per-ns rows (rather than one giant nested map for the
whole registry) are deliberate: two concurrent defs into two
different Vars never contend on the same ETS row. Two concurrent
writes to the same Var (or the same namespace's aliases/refers/
var_names) are still a read-modify-write race in this implementation
(ordinary :ets.lookup + :ets.insert, not :ets.update_element with
a match spec). This is a known, currently-unaddressed race now that
spawn/spawn-link/spawn-monitor make concurrent writers real: two
processes racing to def the same Var, or to alias!/refer! into
the same namespace, can lose one write. In practice the stdlib's atom
(see concurrency.logos) sidesteps this entirely by funneling all mutation
of a given piece of state through one owning process rather than
relying on this table's own atomicity.
Summary
Functions
The current (system-argv) value -- see set_argv!/2.
The name of the current namespace for the calling process -- see moduledoc's :current_ns per-process fix.
The {module, function} registered for tag_text (e.g. "inst", "my/tag"), or :error if nothing is registered under that tag -- see moduledoc's :data_readers row.
Clears name's loading mark (call in an after/finally-style cleanup, mirroring require's bracket).
This Runtime's configured require/use search path -- see new/1's
:load_paths opt and Logos.Primitives' load_ns!/2. Fixed at new/1
time; there is no setter, matching every other embedder-configured
Runtime option.
Whether name is currently mid-require (circular-require detection).
Creates a fresh, isolated Runtime: a new :public/:set ETS table,
seeded with an empty logos.core namespace, every Layer-1 primitive
(Logos.Primitives.install!/1) interned into it, and a user
namespace (auto-referring logos.core) as the starting namespace --
every fresh Runtime starts with "user" as its current namespace. Does
not load priv/stdlib/*.logos -- that is Logos.Stdlib.load!/1 (or the
Logos.new_runtime/0 convenience, which does both).
A fresh gensym name, e.g. "G__1", "G__2", ... -- unique within this Runtime.
Registers {module, function} under tag_text for #tag_text value tagged literals -- the register-data-reader! primitive's backing call. Overwrites any existing registration for the same tag.
The (system-argv) primitive's backing store -- Mix.Tasks.Logos.Run
calls set_argv!/2 once, right after building a fresh Runtime and
before evaluating the script, with whatever command-line args followed
the script path (mirroring System.argv()). Defaults to [] (set at
new/1 time) for every Runtime that never calls this -- e.g.
logos.repl.
Sets the current namespace for the calling process only (per moduledoc).
Does not create it -- callers (e.g. the in-ns primitive) should
Namespace.ensure!/2 first.
Marks name as loading. Returns {:error, {:circular_require, name}}
if it is already loading, so a require cycle fails with a clear error
instead of hanging or stack-overflowing. Returns an error tuple rather
than raising directly, leaving the choice of whether to raise to the
caller; Logos.Primitives' require primitive raises.
Types
@type t() :: %Logos.Runtime{table: :ets.tid()}
Functions
The current (system-argv) value -- see set_argv!/2.
The name of the current namespace for the calling process -- see moduledoc's :current_ns per-process fix.
The {module, function} registered for tag_text (e.g. "inst", "my/tag"), or :error if nothing is registered under that tag -- see moduledoc's :data_readers row.
Clears name's loading mark (call in an after/finally-style cleanup, mirroring require's bracket).
This Runtime's configured require/use search path -- see new/1's
:load_paths opt and Logos.Primitives' load_ns!/2. Fixed at new/1
time; there is no setter, matching every other embedder-configured
Runtime option.
Whether name is currently mid-require (circular-require detection).
Creates a fresh, isolated Runtime: a new :public/:set ETS table,
seeded with an empty logos.core namespace, every Layer-1 primitive
(Logos.Primitives.install!/1) interned into it, and a user
namespace (auto-referring logos.core) as the starting namespace --
every fresh Runtime starts with "user" as its current namespace. Does
not load priv/stdlib/*.logos -- that is Logos.Stdlib.load!/1 (or the
Logos.new_runtime/0 convenience, which does both).
opts[:load_paths] -- a list of directory strings require/use
search (in order, first match wins) when asked for a namespace not
already in-memory; see load_paths/1 and Logos.Primitives'
load_ns!/2. Defaults to [], meaning require/use only ever
succeed against a namespace already loaded in-memory (this Runtime's
behavior before file-based loading existed).
A fresh gensym name, e.g. "G__1", "G__2", ... -- unique within this Runtime.
Registers {module, function} under tag_text for #tag_text value tagged literals -- the register-data-reader! primitive's backing call. Overwrites any existing registration for the same tag.
The (system-argv) primitive's backing store -- Mix.Tasks.Logos.Run
calls set_argv!/2 once, right after building a fresh Runtime and
before evaluating the script, with whatever command-line args followed
the script path (mirroring System.argv()). Defaults to [] (set at
new/1 time) for every Runtime that never calls this -- e.g.
logos.repl.
Sets the current namespace for the calling process only (per moduledoc).
Does not create it -- callers (e.g. the in-ns primitive) should
Namespace.ensure!/2 first.
Marks name as loading. Returns {:error, {:circular_require, name}}
if it is already loading, so a require cycle fails with a clear error
instead of hanging or stack-overflowing. Returns an error tuple rather
than raising directly, leaving the choice of whether to raise to the
caller; Logos.Primitives' require primitive raises.
Brackets both routes load_ns!/2 can take for a namespace not already
in-memory: real file-based loading off disk (following the
my-app.core -> my_app/core.logos dot-to-slash/dash-to-underscore
naming convention, searched across load_paths/1) recursing into a
require of its own inside the loaded file, and the (now purely
historical) already-in-memory short-circuit that never actually reaches
this function at all. See Logos.Primitives' load_ns!/2 for the full
resolution.