The shared Elixir derivation layer behind use Vendor.Element, tag: …
(EXTENSION-CONTRACT.md §5). Runtime-agnostic on purpose: Tier 1 (compiled to
Lit) uses it now, and Tier 2 (Hologram-in-element, brief 3f) reuses the same
merge + dispatcher generation — the spec is the single interface between
definition and runtime (§9).
Responsibilities:
derive!/3— the expansion of the base module's generated__using__/1macro: option validation (tag:/:inherit/priority:/extension_api:), the double-useand cycle guards, and the AST that re-emits the inherited Hologram props and internallyusesNoizu.Tessera.Componentin derivation mode;merge_spec!/3— deep-copy the base spec's declarations and layer the derived module's deltas: additive new names append in declaration order, override-by-name replaces wholesale, removal is forbidden,required:may relax not tighten, and the collision/duplicate/round-trip/reserved checks re-run over the MERGED spec (the caller runsvalidate_all!);check_pin!/3— the §8 extension-api semver pin (major mismatch raises, minor-range mismatch warns);- chain construction (§5.9, brief 2e) —
takeover?/1,chain_depth/1,chain_order/1(dependency depth →priority:→ module name, innermost first — the mirror of the JS registry comparator),record_takeover_layer!/2(the app-wide sibling registry + the §5.7 chain-conflict warning), andartifact_stem/1so sibling takeover layers never clobber each other's base-tag-keyed artifacts (§5.8); - dispatcher generation —
base_init/2,3(init hooks) and thebase_action/3scaffolding Tier 2 consumes. Hologram's IR has nosuper, so the Elixir down-call is an injected dispatcher, never language-levelsuper. Tier-1 fn-actions never compile to the BEAM; their down-call is real ESsuperin the lowered JS (seeNoizu.Tessera.Component.Expr), so no BEAMbase_action/3is generated for them.
Inherited entries in a merged spec carry inherited: true; the derived
module's own (new or overriding) entries do not. The Lit lowering uses that
marker to emit only new/overridden members and inherit the rest through the
ES class chain.
Summary
Functions
The canonical Tier-2 element macro, invoked by a vendor module as
use Noizu.Tessera.Component.Extension, … (D4: the vendor module IS the
macro — the __using__ its extenders consume is generated here, not
hand-rolled). Options (compile-time literals)
Tier-2 base_action/3 dispatcher scaffolding (brief 3f): one clause per
inherited action, dispatching to the base module's __tessera_action__/3
entry point. Generated ONLY when the base actually exports that entry point
(a Hologram-runtime component). Tier-1 fn-actions never compile to the BEAM
and have no __tessera_action__/3, so this returns [] for them — their
down-call is real ES super in the lowered JS, not a BEAM dispatcher.
The artifact filename STEM for a spec. Fresh and fresh-tag-derived specs
keep the plain tag (byte-stable goldens). A takeover layer registers under
the BASE tag (§5.8), and sibling layers would otherwise clobber each other's
base-tag-keyed element/contract files — so takeover artifacts carry the
declaring module: <base-tag>__<module_snake>.
Layers above the vendor base along the derived_from spine (§5.9 dependency
order): 0 for a fresh component, 1 for the first :inherit layer, and so
on. A cycle cannot reach here — guard_cycle!/2 rejects it at expansion.
Sorts takeover-chain layer descriptors INNERMOST → OUTERMOST per §5.9:
dependency depth first, then priority: (higher sits further out), then
module name ascending (alphabetically-first sits closer to the base). The
outermost layer runs first and wins conflicts. Entries are maps with
:depth, :priority and :module (string). This is the compile-time
mirror of priv/js/src/extend.mjs's comparator (which adds a
first-registered-outermost fallback for unlabelled hand-written mixins).
Checks a derived extension_api: pin against the base's declared version.
Unsatisfied MAJOR raises CompileError; an unsatisfied minor range warns
(host policy decides whether to fail). nil requirement is a no-op. A base
that declares no version is treated as the first stable 1.0.0.
Builds the AST spliced into a deriving module by the base module's generated
__using__/1. base_spec is the base module's compiled spec; opts are the
raw use Base, … options (AST); caller is the deriving module's env.
The expansion of a Tier-2 vendor's generated __using__/1
(use Vendor, tag: … — §5.3, D4). Runs the SAME shared layer Tier 1 uses —
option guards, the §5.9 chain machinery, merge_spec!/3 (hence the §5.5
merge rules + the T1-4/T2-7 reference-graph type-change guard) and the §8
pin check — then splices the Tier-2 lowering: a Hologram component whose
inherited surface is reachable through the injected base_action/3
dispatcher + defoverridable (Hologram's IR has no super; D4 forbids
promising Elixir-level super).
Injected init-hook dispatchers for a deriving module: base_init/2 and
base_init/3 reach the base module's (possibly overridden)
__tessera_init__/2,3. The derived module's own __tessera_init__ defaults
dispatch through these, so an unoverridden hook still runs the base's init
(§5.3). Names mirror base_action/3 (T1-2 resolved).
Deep-copies the base spec's declarations and layers the derived module's
deltas (a map of raw author entries keyed by kind). Returns a merged
declaration map the caller feeds through validate_all! + the element
translator. Inherited entries carry inherited: true.
Records a compiled tag: :inherit layer in the app-wide sibling registry
and emits the §5.7 CHAIN-CONFLICT warning for every same-depth sibling that
overrides an action body or a template region this layer also overrides.
Called from Component.__before_compile__/1 once the merged spec exists:
whichever sibling compiles SECOND sees the first and warns (compile order
never hides the conflict). The chain itself needs no re-ordering here —
depth/priority ride each spec + the JS registry entries, and the runtime
composition IS chain_order/1 at define time.
snake form of artifact_stem/1 (contract filenames).
True when spec is a tag: :inherit tag-takeover: it derives from another
module AND registers under the BASE tag. Fresh tags MUST differ from the
base tag (resolve_tag!/3), so derived_from + tag equality is exactly the
takeover condition — no extra spec field, and goldens/manifests for
fresh-tag specs are untouched.
Functions
The canonical Tier-2 element macro, invoked by a vendor module as
use Noizu.Tessera.Component.Extension, … (D4: the vendor module IS the
macro — the __using__ its extenders consume is generated here, not
hand-rolled). Options (compile-time literals):
tag:(required) — the vendor's element tag;actions:— snake atoms in declaration order, the §6.1 OPEN surface (the runtime bodies are the vendor's owndef action/3clauses — Hologram-transpilable Elixir, never fn-AST DSL bodies);props:—name: [type: :, default: :]triples mirroring the vendor's Hologramprop/2,3declarations (re-emitted into extenders);regions:— atoms naming the arity-1 template helper functions the markup calls (e.g.[:row_label]) — extenders get delegatingdef name(arg), do: Vendor.name(arg)+defoverridable;template:— the markup string (the same markup the vendor's owndef templaterenders) — ridesspec.template.rawso extenders re-emit it and the T1-4/T2-7 reference-graph guard can walk it;extension_api:— the §8 semver declaration (defaults to 1.0.0).
The expansion splices the CONTRACT scaffold onto the vendor:
__tessera_spec__/0 + __tessera_manifest__/0 (the minimal Tier-2 spec —
the interface extenders merge against), the per-action literal
__tessera_action__/3 entry clauses (static calls into the vendor's own
action/3; unknown names raise the boxed FunctionClauseError the §6.3
unknown-action classification expects), and the generated __using__/1
that drives derive_tier2!/3 for every extender.
Tier-2 base_action/3 dispatcher scaffolding (brief 3f): one clause per
inherited action, dispatching to the base module's __tessera_action__/3
entry point. Generated ONLY when the base actually exports that entry point
(a Hologram-runtime component). Tier-1 fn-actions never compile to the BEAM
and have no __tessera_action__/3, so this returns [] for them — their
down-call is real ES super in the lowered JS, not a BEAM dispatcher.
@spec artifact_stem(Noizu.Tessera.Spec.t()) :: String.t()
The artifact filename STEM for a spec. Fresh and fresh-tag-derived specs
keep the plain tag (byte-stable goldens). A takeover layer registers under
the BASE tag (§5.8), and sibling layers would otherwise clobber each other's
base-tag-keyed element/contract files — so takeover artifacts carry the
declaring module: <base-tag>__<module_snake>.
@spec chain_depth(Noizu.Tessera.Spec.t()) :: non_neg_integer()
Layers above the vendor base along the derived_from spine (§5.9 dependency
order): 0 for a fresh component, 1 for the first :inherit layer, and so
on. A cycle cannot reach here — guard_cycle!/2 rejects it at expansion.
Sorts takeover-chain layer descriptors INNERMOST → OUTERMOST per §5.9:
dependency depth first, then priority: (higher sits further out), then
module name ascending (alphabetically-first sits closer to the base). The
outermost layer runs first and wins conflicts. Entries are maps with
:depth, :priority and :module (string). This is the compile-time
mirror of priv/js/src/extend.mjs's comparator (which adds a
first-registered-outermost fallback for unlabelled hand-written mixins).
@spec check_pin!(String.t() | nil, String.t() | nil, Macro.Env.t()) :: :ok
Checks a derived extension_api: pin against the base's declared version.
Unsatisfied MAJOR raises CompileError; an unsatisfied minor range warns
(host policy decides whether to fail). nil requirement is a no-op. A base
that declares no version is treated as the first stable 1.0.0.
@spec derive!(Noizu.Tessera.Spec.t(), keyword(), Macro.Env.t()) :: Macro.t()
Builds the AST spliced into a deriving module by the base module's generated
__using__/1. base_spec is the base module's compiled spec; opts are the
raw use Base, … options (AST); caller is the deriving module's env.
Validates at expansion time (pointed CompileErrors, §5.7) and returns code
that flags the module as derived, internally uses
Noizu.Tessera.Component, and re-emits the inherited Hologram props so the
adapter's @__props__ accumulator is complete before Hologram's
__before_compile__ runs.
@spec derive_tier2!(module(), keyword(), Macro.Env.t()) :: Macro.t()
The expansion of a Tier-2 vendor's generated __using__/1
(use Vendor, tag: … — §5.3, D4). Runs the SAME shared layer Tier 1 uses —
option guards, the §5.9 chain machinery, merge_spec!/3 (hence the §5.5
merge rules + the T1-4/T2-7 reference-graph type-change guard) and the §8
pin check — then splices the Tier-2 lowering: a Hologram component whose
inherited surface is reachable through the injected base_action/3
dispatcher + defoverridable (Hologram's IR has no super; D4 forbids
promising Elixir-level super).
Multi-level chaining (T2-9 gap fix): the scaffold records the PARENT
(the module the extender used) and links through it — per-action
base_action/3 clauses dispatch to the parent's __tessera_action__/3
entry, and the dynamic-name fallback dispatches the parent's action/3
(whose clauses/catch-all continue down-chain), so A→B→C never re-includes
the whole stack.
Injected init-hook dispatchers for a deriving module: base_init/2 and
base_init/3 reach the base module's (possibly overridden)
__tessera_init__/2,3. The derived module's own __tessera_init__ defaults
dispatch through these, so an unoverridden hook still runs the base's init
(§5.3). Names mirror base_action/3 (T1-2 resolved).
@spec merge_spec!(Noizu.Tessera.Spec.t(), map(), Macro.Env.t()) :: map()
Deep-copies the base spec's declarations and layers the derived module's
deltas (a map of raw author entries keyed by kind). Returns a merged
declaration map the caller feeds through validate_all! + the element
translator. Inherited entries carry inherited: true.
Rules (§5.5): additive new names append in declaration order; override-by-name
replaces wholesale (events merge detail, keeping inherited keys); removal is
forbidden; required: may relax not tighten; retyping an inherited
attr/prop/state key the base READS is rejected (the type-change
reference-graph guard). Duplicate AUTHOR declarations raise here (the merge
would otherwise silently last-wins them).
Event renames (§5.6, brief 2d): event "new", renames: "inherited" replaces
the inherited name in place (detail inherited keys kept, renamed_from
marker). The rename is then propagated over the merged emission points —
inherited action/lifecycle JS that still emits the OLD event name is
rewritten to the new one and marked re_emit: true so the derived class
re-emits the method and shadows the base's (which would otherwise emit the
dead name).
@spec record_takeover_layer!(Noizu.Tessera.Spec.t(), Macro.Env.t()) :: :ok
Records a compiled tag: :inherit layer in the app-wide sibling registry
and emits the §5.7 CHAIN-CONFLICT warning for every same-depth sibling that
overrides an action body or a template region this layer also overrides.
Called from Component.__before_compile__/1 once the merged spec exists:
whichever sibling compiles SECOND sees the first and warns (compile order
never hides the conflict). The chain itself needs no re-ordering here —
depth/priority ride each spec + the JS registry entries, and the runtime
composition IS chain_order/1 at define time.
The registry lives in :persistent_term keyed by the BASE TAG: it must
outlive any single module's compile (siblings expand in separate rounds)
and one re-compiling module never warns against itself.
@spec snake_stem(Noizu.Tessera.Spec.t()) :: String.t()
snake form of artifact_stem/1 (contract filenames).
@spec takeover?(Noizu.Tessera.Spec.t()) :: boolean()
True when spec is a tag: :inherit tag-takeover: it derives from another
module AND registers under the BASE tag. Fresh tags MUST differ from the
base tag (resolve_tag!/3), so derived_from + tag equality is exactly the
takeover condition — no extra spec field, and goldens/manifests for
fresh-tag specs are untouched.