obix-compiler-dop
v0.5.0
Published
The DOP lowering of the .obix compiler: the Vue frontend artifacts (canonical SFC, script setup, template) lowered into the canonical, frontend-neutral OBIX DOP IR of obix-compiler-ir — with the source provenance kept in a separate document and the origin
Maintainers
Readme
obix-compiler-dop
Previous name:
@obinexusltd/obix-compiler-dop— OBIX packages are named without an npm scope since decision D-102 (2026-09-29); the package, its version and its exports are unchanged.
The DOP lowering of the .obix compiler — the Vue frontend's artifacts (canonical SFC, <script setup>, template) lowered into the canonical, framework-neutral OBIX DOP IR of obix-compiler-ir.
npm install obix-compiler-dop Vue frontend Vue frontend ───┐
SFC · script setup · template ──► lowerVueToDop ──► IR ├──► canonical OBIX DOP IR ──► later: native / hybrid
(frontend artifacts) (this package) (obix-compiler-ir) React frontend ┘ (React: a later phase, lowering independently)The IR is the semantic authority of OBIX (D-44); this package is one of its writers. It reads what the Vue frontend produced — the template's sourceAst, which is what the author wrote before Vue's transforms, and the Babel AST of the script setup — and writes an IR that is plain, frozen, valid and free of Vue: no Vue AST node, no directive name (v-if, v-for, v-model, v-slot), no React or JSX node, no runtime object. Where the source said each thing is a separate provenance document. The frontend's own artifacts are kept beside the result, never inside it.
import { lowerVueToDop } from "obix-compiler-dop";
// the stages of the family run first; the lowering is given what they returned
const result = lowerVueToDop({ sfc, script, template }); // script: the result of the stage that owns the file's script
result.status; // "compiled" | "deferred" | "failed" | "absent"
result.artifact.ir; // the canonical IR — serializeIr(ir), irEqual(a, b) from obix-compiler-ir
result.artifact.provenance; // node id → range in the file; the frontend's name and version; every import as written
result.artifact.frontend; // { sfc, script, template } — the frontend's own artifacts, untouchedThe result is the family's stage result (obix-compiler-diagnostics), and like every stage the lowering is total — it throws only for an input that is not the stages' output (a TypeError, a programming error) — and speaks only for itself:
| status | when |
|---|---|
| compiled | the IR, its provenance and the frontend artifacts; no diagnostics |
| deferred | the source uses a construct that is valid for the frontend and not lowered yet: one diagnostic per construct (OBIX_DOP_DEFERRED_*), with its place — and no IR at all, never a partial one, because an IR that is missing a piece means something else |
| failed | an earlier stage's verdict leaves nothing to lower (OBIX_DOP_UPSTREAM_FAILED: the SFC parse or a stage reported errors), or the lowering built something the IR checker refuses (OBIX_DOP_INVALID_IR, a defect of the lowering — never returned as IR) |
| absent | the file has no template and no script: nothing to lower |
A stage that defers the file makes the lowering defer it (OBIX_DOP_UPSTREAM_DEFERRED). What is built is checked with checkIr before it is returned, and leaves as parseIr(serializeIr(ir)) — the canonical document, deep-frozen, sharing no object with the frontend's.
What each thing becomes
| The Vue frontend read | The IR has | Notes |
|---|---|---|
| defineProps({...}), defineProps<T>(), withDefaults(...), a type literal / interface / alias of the same file | props | type in the IR's own words (string number boolean array object function unknown); required; a constant default (a value, or a function that returns one, as Vue calls it). Boolean props mean what HTML boolean attributes mean |
| const x = ref(<constant>) | state | changes only by an assign step |
| const y = computed(...), and a constant const | derived | a pure expression over props, state, derived values |
| a function / arrow of the setup | action | count.value++, n.value += 2, a.value = b.value + 1, other() → assign / invoke steps, in order |
| watch(source, (now, before) => {...}) | effect | watches a ref or a getter that reads the same twice; runs after a change, before its component renders, with the new and the previous value — effects that touch nothing of each other's (below) |
| :title="x" | a property binding | title — with its expression |
| title="x" | an attribute | fixed |
| role, tabindex, alt, for, aria-* (fixed or bound) | accessibility of the element | kept apart from the other attributes, given exactly as written — nothing is inferred |
| @click="…", v-on:click, a method name, a function, several statements | an event binding | event: "click" and steps; $event (or the function's parameter) is the event payload |
| {{ expr }} and text | text with static and display parts | adjacent text and interpolations run together |
| v-if / v-else-if / v-else, and <template v-if> | a conditional | branches with conditions; otherwise; a <template> wrapper is transparent |
| v-for="(item, i) in items", :key | an iteration | source, item, index, key, body; over arrays |
| <Child :a="x" b="y">…</Child> | an invocation + a dependency | the dependency is addressed by module specifier and export, exactly as imported — never resolved here |
| default slot content, <template #name="{ item }"> | projection contents | the default content is the slot named default; parameters bind arguments |
| <slot name="x" :arg="…">fallback</slot> | a projection | with arguments and a fallback |
| <style lang scoped> | styles metadata | language, scope (component | global), the text — nothing is compiled |
Expressions are lowered by one function for the script and the template: literals, arrays, objects, references (prop state derived local), member and index access, ! - +, + - * / %, === !== < <= > >=, && || ??, and the choice a ? b : c. TypeScript's own syntax (as, !, satisfies, type arguments) is erased: it has no meaning at run time.
Not in the IR, on purpose
- Vue's names.
v-if,v-for,v-model,v-slot,v-bind,v-on,ref,computed,defineProps,<slot>— the frontend keeps them.tests/vuets/dop-equivalence.test.mjsscans every IR the corpus lowers for a word of Vue's and for anything that is not plain data. - Vue's objects. No
RootNode,ElementNode,DirectiveNode,SimpleExpressionNode, no Babel node, no.valueof a ref. - Locations. Ranges are in the provenance document, keyed by id:
component,props.<name>,state.<name>,view.<i>.children.<j>.events.<k>… (the id scheme is specified intest/support.mjs, independently of the lowering). - Frontend artifacts. They travel in
artifact.frontend, next to the IR, for diagnostics and provenance.
Deferred — with a diagnostic each, never a guess
Vue constructs the kernel of the IR does not model yet: v-model, v-show, v-html, v-text, v-once, v-memo, v-cloak, custom directives, object v-bind / v-on (v-pre lowers: Vue's parser leaves raw text and plain attributes); dynamic class / style, dynamic names, modifiers, key / ref outside their place; built-in components (Transition, KeepAlive, Teleport, Suspense, component), dynamic and recursive components, events on components, class / style on a component (Vue merges them into the invoked root); calls, function values, optional chaining, undefined, spread, and an empty {{ }} in expressions (-0 lowers: it is the negation of 0); if, loops, return, declarations, await in actions; watchEffect, lifecycle hooks, watch options; reactive, provide / inject, defineEmits, defineExpose, defineModel…; a normal <script>; lang="tsx" / "jsx"; <style module>, v-bind() in CSS, <style src>.
Deferred because the IR would mean something else than Vue does — found by running generated programs on both, each with its place:
- a name the IR cannot carry —
café,größe, a kebab-case prop such as"a-b"(isIrIdentifier: ASCII letters, digits,_and$, not a leading digit); - a name that is two things — a prop and a state, a derived value or an action of the same name (the IR keeps them in one namespace) — and derived values that read one another in a circle;
- a name whose declaration nothing says — a
constwhose initializer reads the name it declares, orconsts that read each other, however far round: each name on the circle is a deferral at its declaration, with the way round (checkWaiting). Aconstthat reads a name whose deferral is said elsewhere says nothing more; when the names wait on each other nothing would, and the name used to be left out without a word; - effects that Vue orders and the IR does not (
src/effects.ts): Vue runs effects in the order their values changed, the IR in the order they are declared, so an effect that changes what another watches, reads or changes is deferred, and so is one that changes what it watches; and Vue compares a derived value by the identity of a cached result, so an effect that watches a new array or a new object at every read — a literal, a derived value made of one, a prop that is not a string, a number or a boolean — is deferred.
Also deferred, by decision, in this phase: the "obix" authoring alias — import { ref } from "obix" is recorded in the provenance as written and is not resolved (a later phase resolves it, never by string replacement; obix is an alias, never a package) — a filesystem host (a defineProps<T> whose T lives in another file is stopped by the script stage, before this one), JSX / TSX, source-map composition, runtime reactivity, emit, and the React, native and hybrid stages.
Dependency role
obix-compiler-diagnostics (the stage-result contract), obix-compiler-ir (the target), the frontend stages whose artifacts it reads (-sfc, -script, -template) and @vue/compiler-core — for the numeric NodeTypes / ElementTypes by which it reads the template's sourceAst; it imports no Vue runtime and no other Vue-family package (obix-vue.json lists it, with the reason). The IR it produces reaches none of them. Graph rule R5 keeps the compiler family out of every browser graph.
Tests
npm test -w obix-compiler-dop — 196 tests: 95 written before the code (view · script · events · structure · components · effects · statuses); 70 that mutation testing asked for (exact · exact-template — the refusal of a wrong input with its reason, names, the order of the diagnostics, TypeScript syntax erased everywhere, what cannot be written or invoked and why, what the analysis of effects is given when a step could not be made, a slot header that cannot be lowered, and what the lowering does with a tree the parser never makes); 31 that generated sources and programs asked for (effect-order · collisions — the effect analysis, name collisions, derived circles, the names that nothing said and the names a destructuring binds, each with its exact message and place; generated — the eight laws over generated sources, with a meta-test that a lowering that breaks each law is caught): the IR expected is written by hand from the schema, and the id scheme and provenance are specified independently of the lowering.
The equivalence proof is one level up, because it needs the real Vue runtime: tests/vuets/dop-equivalence.test.mjs runs each fixture of tests/corpus/dop (every component a byte-identical .vue / .obix pair) on Vue's own runtime — official compiler, createRenderer — and on the reference evaluator of the IR the lowering produced, in both syntaxes, and compares the normalized trees at every step; tests/vuets/dop-differential.test.mjs does the same for generated programs. Both generators are seeded, so a failure is reproduced from its seed: OBIX_GENERATED=<first>:<count> node --test test/generated.test.mjs (here) and OBIX_DIFFERENTIAL=<first>:<count> node --test tests/vuets/dop-differential.test.mjs (from the repository root).
Installation
npm install obix-compiler-dopAPI surface
obix-compiler-dop— 2 value exports:OBIX_DOP_CODES,lowerVueToDop- Type declarations:
./dist/index.d.ts(and a declaration next to every JS entry point).
Architecture role
obix-compiler-dop is part of the OBIX compiler (build-time tooling): it never runs in an application's browser graph.
The architecture of OBIX — the package families and which packages are public API — is indexed in the umbrella: docs/architecture.md.
Package relationships
- Depends on (OBIX):
obix-compiler-diagnostics,obix-compiler-ir,obix-compiler-script,obix-compiler-sfc,obix-compiler-template. - Used by (OBIX): no other OBIX package.
- Third-party:
@vue/compiler-core.
Testing
- 17 test files ship in the npm package (
test/): the evidence of the package's contract, published so that its verification can be inspected — not runtime code (no entry point reaches them). - Standalone: 1 of 17 — it reads nothing outside the package.
- Need the OBIX development / test harness: 16 — they read the OBIX monorepo's shared harness, oracles or fixtures, so they do not run from an npm install or from this package's repository alone; they are shipped for inspection and provenance:
test/call.test.mjs— imports test/support.mjs, which is not shippedtest/collisions.test.mjs— imports test/support.mjs, which is not shippedtest/components.test.mjs— imports test/support.mjs, which is not shippedtest/effect-order.test.mjs— imports test/support.mjs, which is not shippedtest/effects.test.mjs— imports test/support.mjs, which is not shippedtest/events.test.mjs— imports test/support.mjs, which is not shippedtest/exact-template.test.mjs— reads ../../../tests/vuets/compose.mjs, outside the packagetest/exact.test.mjs— reads ../../../tests/vuets/compose.mjs, outside the packagetest/generated.test.mjs— imports test/laws.mjs, which is not shippedtest/laws.mjs— reads ../../../tests/vuets/compose.mjs, outside the packagetest/outputs.test.mjs— imports test/support.mjs, which is not shippedtest/script.test.mjs— reads ../../../tests/vuets/compose.mjs, outside the packagetest/statuses.test.mjs— reads ../../../tests/vuets/compose.mjs, outside the packagetest/structure.test.mjs— imports test/support.mjs, which is not shippedtest/support.mjs— reads ../../../tests/vuets/compose.mjs, outside the packagetest/view.test.mjs— imports test/support.mjs, which is not shipped
- Run them with
npm test(node --test "test/*.test.mjs") in the OBIX monorepo, which provides the test tooling (Node's test runner, TypeScript) and the harness.
Documentation
- CHANGELOG.md
- The OBIX architecture index: obix/docs/architecture.md
Repository
- https://github.com/obinexus/obix-compiler-dop —
[email protected]:obinexus/obix-compiler-dop.git - Issues: https://github.com/obinexus/obix-compiler-dop/issues
- The repository is a clean export of the package from the OBIX monorepo. Its lineage — the sources it was recovered from and its earlier names — is
PROVENANCE.json, shipped in this package; the repository's copy also records the monorepo commit it was exported from.
License
MIT — see LICENSE.
