Direct-link + whole-program by default for program runs; open for the REPL

Running a program is a closed world — every namespace is required, then it runs
to completion — so make it direct-link by default (inlining, record shapes, the
inference's specialization), and for a -m/-M entry auto-enable the whole-program
cross-namespace inference pass. A decomposed multi-namespace program was ~3.7x
slower than the same code in one namespace purely because per-namespace
inference can't see a caller in a not-yet-loaded namespace; this closes that for
the common case with no flags and no hints.

Interactive modes (repl, -e, nrepl-server) stay indirect/open — they have to let
you redefine vars, which direct-linking seals against. Opt-outs:
JOLT_NO_DIRECT_LINK forces the open path even for a program run (hot-reload,
runtime redefinition); JOLT_NO_WHOLE_PROGRAM keeps direct-linking but per-ns;
JOLT_DIRECT_LINK / JOLT_WHOLE_PROGRAM still force-on. Namespaces required inside
-main (after the batch pass) fall back to per-ns inference.

The success checker (RFC 0006) rides on the inference for free, but a casual
program run shouldn't spam type warnings just because it now direct-links, so its
default-on is suppressed when direct-linking was auto-enabled (:direct-link-auto?);
an explicit JOLT_DIRECT_LINK or JOLT_TYPE_CHECK still turns it on. whole-program-
test and devirt-test opt their per-ns baseline out of the new auto-default.

Docs: RFC 0005 gains 'Compilation modes and defaults' + 'Cross-namespace
inference'; RFC 0004 documents cross-ns/param hints; self-hosting-compiler and
--help updated. Full gate green.
This commit is contained in:
Yogthos 2026-06-14 15:44:01 -04:00
parent 230a0c2160
commit 6abfd660ce
8 changed files with 159 additions and 26 deletions

View file

@ -81,6 +81,33 @@ The same machinery covers both `(:k m)` and `(get m :k [default])` when the key
is a constant keyword. A `get` with a variable, numeric, or string key falls
through to `core-get` unchanged.
## Record hints across namespaces, and as inference seeds
A `^RecordType` hint does two things beyond dropping the lookup guard.
**It carries the specific type, not just "a struct".** The guard-skip only needs
to know the value is raw-get-safe (`:struct`), but the structural inference (RFC
0005) wants the actual record type so a field read gets the field's type —
`(:origin ray)` on a `^Ray ray` is a `Vec3`, not `:any`. A record hint on a
parameter is resolved to the record's constructor key and used to **seed the
inference's parameter type**. That is what keeps a record parameter's reads typed
across a namespace boundary *without* whole-program inference (RFC 0005,
"Cross-namespace inference") — the open-world counterpart to the whole-program
pass. Hinting only the public entry point is not enough; the hint has to be on
the function where the hot reads actually happen.
**It resolves across namespaces.** A hint may name a record defined in another
namespace, in either spelling — `^Vec3` where the type is `:refer`-ed, or
`^v/Vec3` where the namespace is `:as`-aliased. Resolution (`record-ctor-key` in
`src/jolt/host_iface.janet`, backed by `record-hint-ctor-key` in
`src/jolt/evaluator.janet`) runs against the *compile* namespace and maps the
type to its home constructor key through a constructor-value index — keyed by the
constructor value, not a var's namespace, so a `:refer`-interned var (whose
namespace is the referring one) still resolves home. The reader keeps a tag's
namespace qualifier (`^v/Vec3``"v/Vec3"`, not `"Vec3"`) so the aliased
spelling has something to resolve. Both `defrecord` field hints and function
parameter hints use this resolution.
## Soundness and the checked mode
An accurate hint is correctness-preserving by construction: for a struct or