VibeVM
Contents
On this page
en
Publisher
org.vibevm.core
Version
1.0.0latest
Audiences
author, agent
Reading time
27 min
Rendered
Read aloud
never

PROP-035: The spec compiler — directive preprocessor, package formats, and the two-mode boot linker

01Status: IMPLEMENTED (designed 2026-07-14 at the owner's request — the flagship "static-compiler vision"; verified against the tree 2026-07-25 by the spec-actualization campaign). §17 records the compiler shipping three times over: §5–§13 as the vibe-spec crate wired into bootgen (2026-07-15), the link-type rename (2026-07-16), and normal + static compiled end to end with the link × format question resolved as eager AOT (2026-07-20). What remains: the structural / JIT loader of §13 (normal + dynamic) and the §10 link tables, both still marked (provisional).

02Extends: PROP-009 (the loading model — inclusion types, the two-tree model, the STATIC.md / INDEX.md artifacts). This PROP turns PROP-009's "ordered list of contributions" into a real preprocessor + linker.

03Supersedes / folds in: PROP-034 (transitive links + the static boot-link graph). PROP-034's linker becomes the emission layer of this system (§12); PROP-034 is retained as the narrower, already-drafted contract for that layer until this PROP is ratified.

04Related: PROP-028 (families — the aggregator role), PROP-029 (spec:// addressing, the / group↔name joiner), PROP-008 (pkgref grammar kind:group/name@version), the addressable-specs flow (anchor / section grammar), vibevm/vibespecs/design/loading-and-boot-model.xml (the static/dynamic-linking metaphor this PROP completes), PROP-014 specmap (the uri↔file index the router extends).

1. Motivation — boot loading must become a toolchain

  • 05PROP-009 established the frame: installing a dependency must never modify authored spec — "the C++ rule that you do not paste a header's text into your #include" (PROP-009 §1) — and gave each direct edge an inclusion type (static / dynamic, PROP-009 §2.4).
  • PROP-034 added transitive links and pinned the boot as a static-linked, deduplicated, topologically-ordered graph. Both are correct and both are package-granular and directive-free.

06Three things they do not yet give:

  1. 07A preprocessor. There is no way for one spec to say "splice this exact section here" (#embed), "I depend on this — pull it, in order, before me" (#use), or "this contract is implemented over there" (#source). Cross-spec composition is done today by prose citation, which the loader does not act on.
  2. A resolver. Every directive takes a spec:// address, and the static compiler must turn it into a concrete file/section algorithmically, without an LLM. The codebase has no spec:// → path function today — resolution is the inverse (a filesystem scan mints path → URI into specmap.json). See §6.
  3. Section granularity. Real economy needs the unit of loading to be a section of a document, not a whole package or file. PROP-034's graph is over packages; the cascade this PROP needs (a pulls one file of a big package, which pulls the next) requires a graph over document sections (§5).
  • 08The stake is the context budget. Loading vibevm itself already spends ~10% of a 1M window; an operating-system-scale project authored in Spec-Driven Development does not fit at all if every dependency loads whole.
  • This PROP makes boot a two-mode compiler: an algorithmic static compiler that statically links a whole closure into one file, and a structural loader that reads only what is actually used, lazily, in dependency order — the same economy delegation-first buys for work, bought here for loading.

2. Two build modes — static vs structural (AOT vs JIT)

09The system is one directive semantics with two executors, exactly the GraalVM / Project Leyden split the owner names:

  • 10Static build — packages are concatenated into one (or few) files (STATIC.md). Directives are resolved statically, by code, without an LLM. This is the AOT / devirtualized path: no runtime indirection, the agent reads a finished file.
  • Structural build — the agent reads specs on demand, following directives as it meets them. This is the JIT / late-binding path; it subsumes PROP-009's current static and dynamic modes.
  • 11The equivalence invariant. Both executors MUST produce the same effective spec — as AOT and JIT must run the same program. The static compiler is the reference semantics; the structural loader (a prompt today, a hard algorithmic agent later, §13) is checked against it.
  • Differential testing of the two is real, empirical, and expensive; it is deferred and planned separately (§16), not part of the first build. Until then, the structural side is best-effort and the static side is authoritative.

12Two executors, one contract:

  • 13Static compiler — code, fully algorithmic. Buildable now (§8).
  • Structural loader — a set of first-loaded instructions (§13) that make the agent honour the directives, pending the future algorithmic agent.

3. Package formats — simple and normal

14A new vibe.toml [package] field format, alongside version:

  • 15format = "simple"the default (absent format, a package is simple). Legacy / adapted prompts, carried whole, with no VibeVM-specific structure — for importing existing corpora without rewriting them, and the fail-safe posture. Rules: inclusion in [requires.packages] means (a) structural — the agent reads the file; (b) static — its text is compiled into the target. If [boot_snippet].source names a file, only that file is read/spliced; absent even that, every file in the package is read/spliced by a recursive walk — the over-load is the author's problem, the deliberate cost of not adopting normal.
  • format = "normal" — the VibeVM-native form, opt-in: the contract / source split (§4), directives (§7), and the compiler (§8). A normal package is not read just because it is present — it participates only when something actually #uses it (§7.2). This is tree-shaking; the optimized posture for authors who understand the machinery, at the price of structuring the package correctly.
  • 16Why simple is the default (owner decision, 2026-07-15). A forgotten format must fail safe, not silent.
  • With normal as the default, a naive or mis-built package that nobody #uses loads nothing — a silent no-op, the worst failure. With simple as the default it loads everything — noisy and unoptimized, but visibly working; the author opts into normal and its discipline deliberately.
  • Migration (§15) is thereby a non-event: the existing corpus keeps working as simple, and packages convert to normal one at a time as their authors optimize them.

4. Normal packages — the contract / source split

17Inspired by C/C++ .h / .cpp:

  • 18contract/ — small, simple, boot-snippet-like. The surface a package exposes outward; short files, cheap to load. The analogue of a header.
  • source/ — large, heavy. The full implementation; pulled only when actually needed. The analogue of a translation unit.
  • 19Because the structural executor lacks a C++ compiler's global view of the source tree, we use a deliberate hack: the contract author declares what implements it, via #source (§7.3). The author hand-draws edges a globally-aware compiler would infer.
  • That global view does exist in static mode and can be pre-compiled into link tables (§10), which is how we give the same knowledge back to the structural executor cheaply and make the hand-drawn #source edges verifiable.

20The contract surface is what other packages include; source is reached only through a resolved contract edge or an explicit #use/#embed into it.

5. The document IR — one hierarchical tree, two frontends

21Everything downstream operates on a single document IR: a DOM-like tree. Markdown and (future) XML are two frontends parsed into the same tree, so algorithms written against the IR scale to deeply nested XML for free.

  • 22Node = { id (anchor / tag), depth, kind, body-span, children[] }.
  • Markdown frontend. Headings form the tree by level (######). A node's id is its explicit {#anchor}. A node's body span runs from its anchored heading to the next heading of the same or higher level (the owner-fixed rule); its children are the nested headings inside that span.
  • Fact leaves (fact amendment, owner-ratified 2026-07-24). A ##<ID> first token of a paragraph or list item (the fact unit of PROP-014 §2.1 / PROP-043 §3.8) is a leaf node of the IR: kind = fact, id = <ID>, body-span = the carrying paragraph or item with its continuation lines; its parent is the enclosing section node. Fact ids share the document's one anchor namespace. The resolver (§6) resolves a fact address like any node; #embed of a fact splices exactly its unit (§7.1's arbitrary granularity, unchanged); #use of a fact address pulls the top-level anchored ancestor of its enclosing section (the existing rule, unchanged).
  • XML frontend (future). Elements already are the tree; tag/id is the address. Held for later, but the IR is designed for it now.
  • Addressing depth. A spec://…#a.b.c fragment is a path down the tree (abc), which already matches addressable-specs' dotted anchors (#verification.timeout). Sections at any depth are addressable.

23Granularity rules (owner-set), stated over the IR:

  • 24#use pulls the whole top-level anchored ancestor of the addressed node — reference a subsection, load its enclosing top-level section as one connected block. It does not pull that ancestor's siblings; siblings are read only when themselves needed.
  • #embed has arbitrary granularity — it splices exactly the addressed node, no more.
  • "Top-level anchored ancestor" is a parameter of the resolver, not a hardcoded heading level, so the XML frontend can define it structurally.

6. spec:// addressing and the resolver ("router")

  • 25Format choice. spec:// stays. Alternatives are strictly worse: path-based is fragile across packages/vibedeps/ materialization and drops versions; content-addressed (by hash) is unreadable and cannot "name a section"; query-based (by tag) is non-deterministic (may match N nodes) where a preprocessor needs exactly one. spec:// is already symbolic, human-readable, and carries group/name/path/anchor. The gap is not the format — it is the missing resolver.

26Unified grammar (reconciled with the pkgref grammar of PROP-008):

27spec://<group>/<name>[@<version>]/<doc-path>#<anchor>[.<sub>…][~r<N>]
  • 28groupname joiner is /, never . (PROP-029).
  • @<version> is optional — a feature, never an obligation (owner-ruled 2026-08-04, B-028: «указание версий — опциональная фича; если версия не указана — используется самая свежая»); absent, the address resolves against the freshest installed version of the package (semver-newest among the materialised vibedeps/ slots — the one deterministic offline reading of «самая свежая»). Several coexisting slots are therefore not an ambiguity but an ordered set with a defined maximum; an explicit @<version> still picks its exact slot, including a non-newest one.
  • #<anchor>.<sub>… is a tree path into the document IR (§5).
  • ~r<N> pins a spec-unit revision (PROP-014), not a package version.

29The router is the new component: a deterministic function spec:// → IR node, evaluated over the resolved, materialized tree. It is the prerequisite everything else stands on. It must handle, without an LLM:

  • 30The self coordinate (B-031, owner-approved 2026-08-04) — the root project's own <group>/<name> (declared in [project], e.g. org.vibevm.core/vibevm) resolves to the workspace's authored spec/ tree, matched before any vibedeps/ slot lookup and never versioned; an undotted authority (the retired host token, illustrative fixtures) parses but never resolves — a hard error carrying the rename hint.
  • Doc-id truncationPROP-NNN / FEAT-NNN in a URI resolve to PROP-NNN-<slug>.md; other docs use the full stem. (This is canonical_doc_path in the specmap engine, reused, not reinvented.)
  • packages/ (source) vs vibedeps/ (materialized slot) — the compiler resolves against the materialized vibedeps/ tree (the installed reality), consistent with the specmap engine, which never scans packages/.
  • Version selection when @version is absent — the freshest installed (semver-newest slot; owner-ruled 2026-08-04, B-028, superseding the earlier lockfile wording). A lockfile-informed selection layer above the router remains possible machinery, but the router's own default is the newest materialised slot — deterministic over the installed set.
  • 31Determinism is a hard requirement: the static compiler must resolve every address to exactly one node or fail loudly.
  • The router extends the specmap.json index (already a uri↔file table) rather than duplicating it.

7. The three directives

  • 32Common shape: #<directive> [options] <specpath>, where specpath is a spec:// address (§6) — a section, or a whole file.
  • Every file referenced by any directive MUST be declared in the package's vibe.toml (the directive graph and the manifest cannot disagree).
  • The directive instructions are among the first things loaded, in every project, package, and library (§13).

7.1 #embed — the macro (materialization-time)

33The simplest preprocessor directive: splice one section (or whole file) into another — a C-macro substitution over spec:// addresses.

  • 34Fires at materialization (vibe install into vibedeps) and must be fully expanded during STATIC.md compilation. No unexpanded #embed may survive in a compiled STATIC.md.
  • Mode-independent. #embed is purely a materialization concern — it produces the same text in both build modes. Therefore vibedeps stores embeds already expanded, while #use/#source (mode-dependent) remain as directives. This split removes a large part of the ordering problem: embeds are fixed early and identically for both executors.
  • Contract-section rule. An #embed targeting a contract-section of a normal package inherits the full #source compilation rules (§7.3) for that paragraph — so the merged (contract+source) text is what gets spliced.
  • Unrestricted otherwise. #embed may splice any paragraph of any spec, or a whole spec (including format = "simple"), with no structural checks.
  • Arbitrary granularity (§5). Algorithmic; the tool of building highly-optimized statically-assembled package hierarchies — its main purpose.

7.2 #use — the dependency edge (tree-shaking)

35The harder directive: pull only the packages actually needed.

  • 36Problem it solves. Specs refer to entities defined elsewhere, often without an explicit link, and linking constantly is tedious. Without #use, the smallest script would have to pull the whole standard library.
  • Tree-shaking default. A format = "normal" package that nobody #uses does not participate — not read, not used, anywhere. The moment any text does #use spec://…, that package enters the build and MUST be linked before its user in topological order.
  • Structural mode. #use is an instruction to read the target's content when it is needed — not eagerly, but definitely before using anything inside it. Reads cascade: a #uses b, b #uses c, so using a loads b and c transitively. The cascade is what lets a big package be entered through one file and expand from there, rather than loaded whole.
  • Inline mode. The same, statically: the #used library's text is fully copied higher up in STATIC.md so it is available before the user.
  • Granularity. Pulls the whole top-level anchored ancestor of the addressed node (§5).
  • Contract-section rule. A #use into a contract-section of a normal package inherits the #source rules (§7.3). Otherwise #use may pull any paragraph or whole spec (even simple) with no structural checks.
  • The as clause (B-011, owner-approved 2026-08-04). #use [options] <specpath> as <Alias> additionally binds <Alias> — an identifier under the anchor-segment grammar (§6) — to the directive's address. The alias is file-scoped (visible throughout the declaring file regardless of declaration position, never exported, never inherited by #use-ers); two declarations of one alias name in one file are a compile error. The alias binds to the address, never to any compiled text — so it survives splicing and any later cleaning of the compiled lane. A trailing as clause is ignored by pre-B-011 scanners (the token tail after the address was never parsed), so the clause is backward-compatible by construction.

7.3 #source — contract↔implementation virtual linking

37Like a C++ interface, but with section-level merging. contract sections are the exposed surface; #source names the file(s) that implement them. Sections are treated as the analogue of class methods, needing a merge (in the static build) or virtual-lookup (in structural mode) mechanism.

38Merge algorithm, per section (by matching {#tag}):

  1. 39In contract, absent in source — structural: a full part of the spec, readable at will; static: compiled into the build whole.
  2. In source, absent in contract — always counted; structural: readable at will; static: compiled in whole. (Calling a section that exists only in the implementation is poor taste, but permitted — we deliberately impose no private/public access control.)
  3. In both, same {#tag} — merged by the tag's mode:
  4. :replace# name {#tag} :replace — the contract text is ignored; the source text is canonical (read by the agent / put in the static build; an already-read contract text is explicitly superseded).
  5. :add# name {#tag} :add — the result is the sum: contract text first, then source text. Static: compile the concatenation. Structural: the agent reads both and weights them equally.
  6. Default is :add (absent a :-suffix) — so the interface text need not be duplicated to appear in the result.

40Fact inheritance (fact amendment, owner-ratified 2026-07-24 — closes F-022):

  1. 41Section fate by default. Facts ride their section: :add carries both sides' facts into the sum; :replace supersedes the contract's facts — text and fact anchors together.
  2. Per-fact override. Within a merged :add section, a source fact redeclaring a contract fact's ##<ID> overrides it: the contract fact's span is dropped from the merged output and the source's is canonical (last-wins in contract→source order). One id, one unit — redeclaration IS the override gesture, so refining a single statement never requires :replace-ing the whole section.
  3. The merged view holds uniqueness. After merging, the compiler re-runs the anchor-uniqueness check — fact and heading ids, one namespace — over the merged document; a surviving duplicate (a non-override collision across sections, or fact-vs-heading) is a build error, never a warning. Per-file cleanliness of the inputs does not exempt the merged output. (Precision, from the implementation review 2026-07-24: the gate flags a repeat only when at least one occurrence is a fact leaf — a pure heading-vs-heading repeat is the :add concatenation's own artifact (both section versions legitimately carry the same {#tag}), not a collision.)

42Several sources, and the plugin form (B-056, owner-ruled 2026-08-04, built 2026-08-05):

  1. 43A contract may declare more than one #source, and every one is honoured, in declaration order. The algorithm above reads "the source" as that sequence: for an anchor a, let S(a) be the sub-sequence of sources carrying a section at a, in the order the directives were written (a glob contributing its members sorted). Before this the compiler took the FIRST #source and dropped the rest without a word — the silence B-055 (closed by bc88e530) recorded.
  2. :replace from ANY source discards the contract text only; the sources still sum among themselves, in order. Over a sequence :replace stops being "whose text is canonical" and becomes a flag, after which two sources both carrying it is not a conflict to adjudicate. With one source the result is byte-identical to ##MERGE-REPLACE, so the generalisation is backward compatible.
  3. Per-fact override widens to the union over S(a). A contract fact is dropped when ANY member redeclares its id — one id, one unit, and whoever redeclares it takes it. Two sources redeclaring one id is not a fold question: both survive into the merged text and @fact:FACT-MERGED-UNIQUENESS fails on the duplicate, loudly.
  4. A source-only section declared by two sources is an error, and the asymmetry is deliberate. A source section matching a contract anchor is an addition to something already declared, so summing is right; a source-only section is a new declaration, and two of them are two definitions of one name — declaration is idempotent, definition is not (the ODR parallel §9 draws). Where it is caught matters: @fact:FACT-MERGED-UNIQUENESS sees a document whose provenance is already gone and deliberately tolerates a repeated heading (that shape is the legitimate :add artefact), so the collision is judged in the fold, which still knows which source brought what.
  5. The fold follows #source recursively under §9's cycle law, and it carries an include guard. A source that itself declares #source folds before it merges into its parent; a cycle among contracts is legal and its not-yet-folded member contributes nothing (the forward declaration); a cycle touching an implementation is a build error naming the path. This is @fact:NO-DEADLOCK-INVARIANT reaching the fold — the same walker, one more edge set, never a second traversal. The guard is not bookkeeping: visiting a shared node once does not make its TEXT appear once, because the fold is textual inclusion — without a guard a diamond carries the shared body in along both paths, and a shared source declaring a fact then fails the uniqueness check, turning an ordinary composition into an un-buildable error. A node's text therefore enters the composed document exactly once, by the first path in fold order.
  6. The plugin form. A #source address may carry a * in the package-NAME half; it names every installed package whose name matches AND that carries the addressed document, expanded in sorted order — so the composed document is a pure function of (tree, lockfile). A glob matching nothing is a legal empty set, never a missing source; a pointed address still fails loudly when its target is absent. Both the fold and the cycle guard read a document's #source edges through one function, so the graph the guard judges is the graph the fold walks.

7.4 In-place use — the @spec:// sigil

44Implicit dependency without an explicit #use, made precise by a sigil:

  • 45@spec://… (an @ before spec) is an in-place use: the agent MUST read it (mandatory), exactly as if a #use had been declared at the top of the file. Amendment (2026-08-24, the normal-flip ruling): the obligation is the AGENT's, not the AOT compiler's — a @spec target is never spliced into a compiled lane; the address itself is the compiled artefact, and the §2 equivalence invariant is carried by the agent's read obligation. (Splicing every @spec realised ##OPEN-CLOSURE-EXPLOSION on the first normal-format host build: the lane grew 250 KB → 2.5 MB off prose pointers.) Only an explicit #use is a compiler splice edge.
  • Bare spec://… (no @) is at the agent's discretion — read it if useful, skip it otherwise. (A future algorithmic agent narrows this further.)
  • Read once. An @spec target is read only on first encounter, never re-read blindly — see the read-set (§below).
  • Resolution order. Collect all explicit #use into a map, enrich it with the @spec in-place uses, then act on the combined map.
  • @!<Alias> (B-011, owner-approved 2026-08-04) is the aliased twin of @spec://…: a mandatory in-place use of the address the file's as-declared alias binds to (§7.2), with identical read-once / read-set semantics. @!X where X is not declared in the file is a compile error naming the file's known aliases. In the compiled static lane every @!X is rewritten to the full @spec://… address it denotes — the compiled output is self-describing without the alias table and resolvable after any cleaning; structural mode reads the declaration and the sigil directly.
  • The short-reference lookup rule (B-011). An unqualified label reference (an intra-document (#x) link) resolves against exactly two scopes, in order: (1) the anchor namespace of the containing document; (2) the file's declared aliases. Found in neither, or in both: a compile error listing the candidates — the resolver never widens the search and never picks silently.
  • 46The read-set (surviving compaction). To honour "read once" across an agent's context compaction, a persistent, file-based read-set (.vibe/session/read-set.json or equivalent) records { specpath, content_hash } on each read; the content_hash is reused from specmap, so a changed section is re-read.
  • A first-loaded instruction (§13) tells the agent to consult the read-set before reading and append after — this survives compaction because the boot instructions are re-read.
  • Crucially the read-set records what exists and where, not what is currently in context: compaction evicts the text but not the fact, and reads are cheap (files sit in vibedeps).
  • So the rule is: read an @spec target if (a) it is not in the read-set, or (b) it is, but its content is no longer in context.
  • A clean solution without a harness compaction signal is out of scope; the file-based read-set plus a boot instruction is the pragmatic floor, and the future algorithmic agent keeps the read-set rigorously. Mental model: a linker symbol table, but for what has been read.

8. The compilation pipeline — the standard order

47The single ordering standard both executors follow (the "procedure of macro-substitution" the owner asked to pin down):

  1. 48Parse. Build the IR tree (§5) of every participating file; collect all directives with their positions.
  2. Build the use-graph and topologically sort. Nodes = document sections (or packages, at the coarser tier); edges = explicit #use declarations only — an @spec in-place use is the agent's read edge, never a compiler splice (§7.4 amendment 2026-08-24). Topological order = every dependency before its dependent. Absorbed nodes (READ-ONCE over overlapping spans, 2026-08-24): two closure nodes of one doc may nest (a whole-doc node beside a section inside it); a node whose text is wholly contained in a same-doc sibling's is absorbed — emitted once with its ancestor, never twice.
  3. Source-merge. For every contract section, resolve #source (§7.3) into its effective (merged) body. This runs before embed because an embed may target a merged contract section (§7.1).
  4. Embed-expand. Apply #embed (§7.1) as textual substitution, top-down within a file, in topological order over the embed-graph across files (a package is fully compiled — its source-merge and its own embeds done — before it is embedded), recursively to a fixed point, with cycle guards (§9).
  5. Qualify (B-011, owner-approved 2026-08-04; per-node refinement — the B-006 rider, owner-approved 2026-08-04). For the static build only: rewrite label definitions — heading {#x} and fact ##X, one namespace (§7.3) — to the qualified form <origin-slug>--<original> (the slug is the lowercased <group>/<name> with dots and the joiner mapped to -/--; the original tail keeps its case), per node, each node under its own authoring origin: a compiled closure that splices cross-origin nodes qualifies every node under the origin that authored it, never the carrying entry's — provenance is never re-attributed by splicing. An intra-closure (#x) link rewrites to the qualified name of the defining node (so a cross-origin link stays resolvable under the two-scope lookup); a simple contribution is one node, where this reduces to B-011's original whole-contribution rewrite. Full spec:// addresses, @spec uses, directive lines, and fenced content are never touched. The qualified name is a pure function of (origin, original label) — independent of splice order and composition, which is what makes late lane additions append-only. @!X rewrites to its full address here (§7.4). Multi-doc rider (2026-08-24): a closure node whose doc-path lies OUTSIDE the package's contract homes (boot/, contract/) qualifies with its doc in the slug — <origin-slug>--<doc-seg-doc-seg…>--<original>, a pure function of (origin, doc, label) — so two docs of one package cannot collide on the near-universal anchors (root, summary); contract-home nodes keep the plain origin slug, so every existing boot-snippet unit is byte-stable. Definition positions (2026-08-24): the qualify pass recognises every position the fact frontends do — the lead of a paragraph or list item, a table CELL opening (a table bar followed by the @fact sigil and :ID …, the K6.5 cell grain), and a blockquote lead (> followed by the @fact sigil and :ID …, the <quote> projection) — one namespace, all renamed.
  6. Emit. Concatenate in topological order with open/close markers (§11), prefixed by the resolution preamble and the tombstone table (§11). For the static build: STATIC.md. For structural: the loader consults the same order lazily.

49Determinism: independent nodes are tie-broken by a stable key (category → boot-snippet slot → fully-qualified name), as PROP-034 §2.3 already specifies for the emission layer.

9. Cycles and guards

50The owner's C++ intuition made precise. In C++ two distinct mechanisms are at work: an #include cycle is broken by include guards (idempotent re-inclusion becomes a no-op), and mutual recursion of types is resolved by forward declaration — a declaration needs no definition. "Including only interfaces never deadlocks" precisely because declarations can close a cycle without bodies. Mapped onto us:

  • 51#embed cycle → hard error. #embed is #include without a guard, so a cycle is an infinite substitution. We forbid it: a guard keyed on the specpath currently on the expansion stack detects it, aborts compilation, and emits debug info naming the full cycle path (A → B → A). (Owner-required: the guard's firing is reported, not silent.)
  • #use cycle between contracts → allowed (the forward-declaration case). Because a contract is small and self-contained, static mode breaks the cycle by emitting the contracts before any source bodies. Structural mode is "read when needed", so a contract-level cycle simply resolves lazily.
  • #use cycle that needs a source body to compile itself → error (the "incomplete type where a complete type is required" case).

52Invariant. The contract layer is where cycles are legal; the source layer is where topological order is mandatory. This is the theoretical no-deadlock guarantee: as long as the contract hierarchy is acyclic-under-#embed and no source body participates in a #use cycle, the build always terminates.

11. Markers in compiled output

  • 57When a file's text is placed into a static file (e.g. STATIC.md), a path comment is added both before and after it (today only before — the after-comment is the closing tag). Around a package body (which contains several files) the same: a package-open comment, then many file open/close comments inside, then a package-close comment.
  • This makes STATIC.md reversible — a compiled artifact can be decomposed back to its constituent files and packages, giving the same bidirectional traceability specmap already provides for code.
  • Compiled labels are origin-qualified (B-011). A compiled block's heading anchors and fact ids carry the §8 qualify phase's <origin-slug>-- prefix, so the compiled document's label namespace is collision-free by construction; reversibility survives because the block's own marker key names the origin, and stripping that block's prefix restores the source labels.
  • The header carries the resolution preamble, first (B-011, owner addition 2026-08-04). The compiled lane opens with a short generated preamble — the qualified-label convention, the alias semantics, the two-scope lookup rule, «unresolved short name → the tombstone table below», and «full addresses resolve against package sources, never against this file» — placed as the first lines of the first file a session reads, because boot files are re-read every session and after compaction, which is what makes the rules un-forgettable.
  • The tombstone table sits directly under the header. Every short name the qualify phase renamed is listed with its qualified heirs and their origins — a retired name never vanishes silently (the addressable-specs tombstone law applied to renames). A resolver that misses a short splice anchor answers with these candidates, never with emptiness.
  • The compiled lane is not a citation target (B-011, §6.1 layer 1 of the design). A spec:// address whose document path names a generated STATIC.md is an illegal target for authored text — the lane is a cache; source-of-truth is the package source. The directive compiler rejects such an address; the gate lints the tree for them.

12. Transitive static — folding in PROP-034

  • 58static-transitive may be set at the top of a package hierarchy; then every element below it in the dependency graph is pulled static, regardless of what it declared before. Since the visibility layer landed (2026-08-23, PROP-050 §3), «below it» means below it within the effective set E(R): the fold walks visible edges only and never widens visibility (PROP-050 ##FORCING-NEVER-WIDENS).
  • This is safe precisely because the static build is algorithmic, not LLM-driven, and loses nothing. It is the path to large highly-optimized builds.
  • 59This is PROP-034 §2.1/§2.3 (transitive links + dedup + topological order + cycle rejection), which becomes the emission layer of this compiler: after §8's pipeline resolves directives, PROP-034's linker deduplicates and dependency-orders the node list into STATIC.md / INDEX.md.
  • transitive-static / transitive-dynamic remain reserved (no use case yet), but the graph analyzer is built to operate at that level.

13. The structural loader — the "first instructions"

  • 60Until hard algorithmic agents exist (§14), structural mode is executed by an LLM following instructions.
  • Those instructions — how to honour #use, #embed, #source, @spec, and the read-set — MUST load first, everywhere: in every project, package, and library vibevm manages.
  • A project or package without them is considered broken; the project- and package-creation tools MUST check for and inject them. This is one of the most critical loading mechanisms — nothing works without it.
  • The resolution rules are part of the first instructions (B-011, owner addition 2026-08-04). The first-loaded text includes the B-011 resolution rules: qualified-label convention and short↔qualified derivation, alias (as / @!) semantics, the two-scope lookup with fail-with-candidates, the tombstone table's role, and «full addresses resolve against package sources under vibedeps/, never against a compiled lane». B-011 wires this contract into a live boot for the first time — until it lands, the loader instructions remain the structural-loader.xml hold.
  • Inline compilation, by contrast, needs no LLM and can build the whole thing algorithmically today; its tooling is what remains to be built (the current STATIC.md machinery is naive by comparison).

14. Future algorithmic agents

  • 61We are preparing for purpose-built algorithmic agents that run alongside Claude Code (and in specific cases instead of it) and honour every directive (#use, #embed, #source) rigidly and unconditionally.
  • The design must not assume only an LLM executor: the directive semantics (§7), the pipeline (§8), and the link tables (§10) are all specified so a deterministic agent can execute them. Remember this executor is coming.

15. Migration

62Incremental, safety-first (owner-set):

  • 63Build and test on a demo fixture corpus first — throwaway packages exercising simple/normal, contract/source, #embed/#use/#source, cycles, and @spec. These are not real packages, and experimenting on them must never break vibevm itself.
  • Migrate real packages gradually, improving them onto the new format one at a time.
  • Convert vibevm itself last: first the whole of org.vibevm.world, and only then (if at all) the core feature specs. With simple as the default (§3) there is no blast radius — an un-migrated package keeps loading as simple (whole), so conversion to normal is per-package and opt-in, never a flag-day.

16. Open questions

  1. 64Equivalence testing (§2). Differential testing of the static vs structural executors — real, empirical, experiment-heavy; planned separately, meaningless before a working base exists. The static compiler is the reference semantics in the meantime.
  2. @spec read-set across compaction (§7.4). No clean solution without a harness compaction signal; the file-based read-set + boot instruction is the floor. Revisit if the harness exposes a compaction event.
  3. XML frontend (§5). Timing and the exact IR mapping; the data structures are designed for it now, the frontend is built later.
  4. Implicit-reference closure explosion (§7.4) — REALISED AND RESOLVED (2026-08-24). The first normal-format host build spliced every @spec target and grew the lane tenfold (250 KB → 2.5 MB). Resolution: @spec is the agent's read edge, never a compiler splice (the §7.4 amendment); only explicit #use splices. The compiled lane returned to snippet size (+3% for the cell-fact tombstones).
  5. Link tables (§10). Whether they land at install-time, static-compile-time, or a separate phase — folded into the implementation task.
  6. Deferred deliberately, by owner ruling 2026-07-29, and this records the reason so nobody re-derives it. Built today: the graph and a deterministic dump (crates/vibe-spec/src/link_table.rs). Not built: the persisted on-disk format and the structural consumer. The tables are the vtable of the §13 structural executor — a prebuilt index so a late-bound reader dispatches instead of searching — and this project does not run that mode, so they are an optimisation of navigation cost, never a precondition of correctness. Meanwhile #embed spec://… resolves and splices at compile time (render_staticexpand_embeds), and an @spec:// pointer that costs a lookup is strictly better than the confidently wrong relative path it replaced. Building the layer mid-refactor would create code the refactor then has to refactor.
  7. What promotes it, so the deferral cannot quietly become permanent: either @spec:// pointers in the boot lane are MEASURED to cost a reader more than the lane saves, or the §13 structural loader is opened — whichever comes first. Either makes the searching real rather than hypothetical; until one fires, the cost being optimised away is a cost nobody has paid.
  8. No access control (§7.3). We deliberately omit private/public. Confirm this holds once real packages exercise cross-contract calls.
  9. dynamic-transitive (§12). Inherited from PROP-034 §5; still reserved.

17. Version history

  • 652026-07-14 — drafted (owner-requested), provisional. Captures the "static-compiler vision" design dialogue: two build modes as AOT/JIT with the equivalence invariant (§2); simple/normal package formats (§3); the contract/source split (§4); the hierarchical document IR with MD and future-XML frontends (§5); the unified spec:// grammar and the deterministic router (§6); the three directives #embed / #use / #source plus the @spec in-place-use sigil and the read-set (§7); the five-phase compilation pipeline and the embed-ordering standard (§8); the C++-derived cycle rules and the contract-layer no-deadlock invariant (§9); link tables as the vtable analogue (§10, provisional); reversible open/close markers (§11); transitive-inline folding in PROP-034 as the emission layer (§12); the first-loaded structural loader (§13); the future algorithmic executor (§14); and the demo-corpus-first, vibevm-last migration (§15). Implementation begins with the router (§6) under this contract.
  • 2026-07-15 — implemented, and the default flipped to simple. §5–§13 shipped as the vibe-spec crate and wired into bootgen (the payoff: render_inline runs expand_embeds, guarded); transitive-inline (§12) landed on LinkType. §3's default changed from normal to simple (owner decision): a forgotten format must fail safe (over-load, visibly working) rather than silent (a normal no-op), which also removes the §15 migration blast radius. Still open and under review: the link × format interaction (does a normal + static edge read eagerly or lazily?).
  • 2026-07-16 — link-type rename (owner decision), the link set shrinks to two. LinkType::Inline → Static (the verbatim STATIC.md lane — "the static compiler"), Static → Dynamic (the default, a by-reference INDEX.md read with an optional when), and the old Dynamic removed — a conditional load is now just a dynamic entry carrying a when. inline-transitive → static-transitive; INLINE.md → STATIC.md; render_inline → render_static; compile_inline → compile_static. Pure terminology, aligning vibevm with the CS static/dynamic-linking standard so "the static compiler" reads naturally; shipped across vibe-core, vibe-workspace, vibe-spec, the package manifests, and these specs.
  • 2026-08-04 — deterministic loading: qualified splice, aliases, the lookup rule (B-011, owner-approved). The owner's highest-priority build lands its contract: the §8 pipeline gains the qualify phase (labels become <origin-slug>--<original>, a pure function of origin — splice-order-independent, append-only under late dynamic lane additions); §7.2 gains the as <Alias> clause and §7.4 the @!Alias mandatory in-place use plus the two-scope short-reference lookup (fail with candidates, never a silent pick); §11 gains the qualified-label, resolution-preamble, tombstone-table, and lane-is-not-a-citation-target facts; §13's first instructions gain the resolution rules as named content (the owner's priority-placement addition). Design rationale and the fork record: vibevm/vibespecs/design/deterministic-loading-aliasing.xml; the commissioning entry: BACKLOG.md B-011. PROP-009 §2.3's «verbatim concatenation» becomes «anchor-qualified concatenation» in the same landing.
  • 2026-07-20 — normal + static compiled end to end; the link × format question resolved. The [package].format field now parses (PackageFormat in vibe-core, default simple, deny_unknown_fields-clean), threads through the boot model (DependencyBoot / UnitInputBootEntry, both emission paths), and drives the static renderer: a normal-format contribution pulled static is compiled to its #use / #source-resolved, tree-shaken, dependency-ordered closure (vibe-workspace::boot_artifacts::normal::compile_normal_entry, seeded at the contract's whole-document address and delegating to the shipped vibe_spec::compile_static), where a simple one stays a verbatim concatenation. This resolves the open link × format question raised in the 2026-07-15 entry ("does a normal + static edge read eagerly or lazily?"): it is eager — the tree-shaken, #source-merged closure is AOT-compiled into STATIC.md, which is the equivalence-invariant reading (§2), never the whole file. The default stays simple, so an un-migrated tree's static lane is byte-identical (§3/§15). normal + dynamic — the structural / JIT loader (§13) — is still pending; only the static (AOT) executor honours the format so far.

For an agent

This page has a machine mirror. The citation carries the version rather than latest, so what an agent quotes does not move under it.

spec://org.vibevm.core/vibevm@1.0.0/modules/vibe-workspace/PROP-035-spec-compiler

.md.xmlllms.txt