Status: Draft
Version: 0.3
Date: June 12, 2026
Author: Egor Merkushev
Licence: MIT
This document defines how a Hypercode structure (.hc) and a Hypercode Cascade
Sheet (.hcs) resolve, under an execution context, into a resolved graph —
the same tree of commands, each carrying the properties chosen by the cascade,
every value tagged with its provenance.
It is the semantic companion to the syntax specification
(which defines well-formed .hc) and refines §4.2 of the
RFC.
The rules below are executable, not merely prose: in the reference Swift
implementation each is a SpecificationCore Specification / DecisionSpec
object under Sources/Hypercode/HCS/. This document narrates those specs
so other implementations can reproduce them.
- Node — a command from the
.hctree:type, optionalclass, optionalid, and orderedchildren. A node is always evaluated together with its ancestor path (root → parent). - Rule — from
.hcs: aselector, a map ofkey → valueproperties, an optional context guard@dimension[value], and a 0-based sourceorder. - Context — a map of
dimension → valuebindings (e.g.env → production,client → acme) supplied at resolution time.
A selector matches a node in context (the child combinator needs the parent):
match(type T, n) ⇔ n.type = T
match(.class C, n) ⇔ n.class = C
match(#id I, n) ⇔ n.id = I
match(A > B, n) ⇔ match(B, n) ∧ match(A, parent(n)) -- direct child only
(TypeSelectorSpec, ClassSelectorSpec, IdSelectorSpec, ChildSelectorSpec.)
As in CSS, a selector's specificity is the triple (ids, classes, types),
compared lexicographically; child selectors sum their parts:
spec(type) = (0, 0, 1)
spec(.class) = (0, 1, 0)
spec(#id) = (1, 0, 0)
spec(A > B) = spec(A) + spec(B) -- componentwise
So #id ≻ .class ≻ type.
active(rule, ctx) ⇔ rule.guard = ∅ ∨ ctx[rule.guard.dimension] = rule.guard.value
A guardless (global) rule is always active. White-label / environment switching
is exactly choosing a different ctx — the .hc never changes.
For a node n and property key k, gather the contributions of every active,
matching rule that sets k, and take the one with the greatest precedence:
precedence(rule) = (spec(rule.selector), rule.order) -- lexicographic
D(n, k) = { (rule.properties[k], precedence(rule), provenance(rule))
| active(rule, ctx) ∧ match(rule.selector, n) ∧ k ∈ rule.properties }
value(n, k) = the value of max D(n, k)
provenance(n, k) = (winning selector, winning source line)
Because order is unique per rule, max is unambiguous: higher specificity
wins, and equal specificity is broken by later source order. (PropertyCascade
is the DecisionSpec that performs this choice.)
The rule sequence the cascade operates on may be composed from several sheets:
rules(sheet) = expand(imports(sheet)) ++ ownRules(sheet)
- An imported sheet expands depth-first at the position of its directive;
orderis assigned over the fully expanded sequence. Imports must precede all rules, so the importing sheet's own rules always come later in source order and win specificity ties — the importer overrides what it imports. - Each sheet expands at most once per resolution (a diamond keeps the first occurrence); a cyclic import is an error.
- Contracts compose by the same expansion and then accumulate as in §7 — a contract declared in an imported sheet still gates values set by the importer.
provenance(rule)keeps the file the rule was defined in, not the entry sheet.
ctx ⊢ n ⇓ ⟨ n.type, n.class, n.id,
properties: { k ↦ (value(n,k), provenance(n,k)) | k ∈ keys(D(n, ·)) },
children: [ ctx ⊢ c ⇓ … for c in children(n) ] ⟩
The whole document resolves by applying this to each top-level node.
A @contract: block (syntax: Hypercode_Syntax.md §6.3)
attaches property constraints to selectors. Values cascade (last sufficiently
specific writer wins); contracts accumulate — every applicable contract
governs the node simultaneously.
For a node n and property key k, the effective contract is the
intersection of all contracts whose selector matches n and that
constrain k:
applicable(n, k) = { c ∈ contracts | match(c.selector, n) ∧ k ∈ c.properties }
effective(n, k).type = the common type of all applicable (must agree)
effective(n, k).min = max over declared lower bounds
effective(n, k).max = min over declared upper bounds
effective(n, k).required = true if any applicable contract requires k
An omitted bound is not a statement — it inherits through the
intersection. A more specific contract that re-declares k without min
keeps the inherited lower bound; it does not lift it.
Specificity relates two contracts only when they can govern the same node —
exactly as in the CSS cascade. The validator therefore checks a pair of
contracts only if at least one node in the document matches both
selectors. For such a pair where spec(A) < spec(B):
| Violation | Code | Description |
|---|---|---|
| Type changed | HC2101 | B declares a different type than A for the same key |
| Interval widened | HC2102 | B lowers a declared min or raises a declared max of A |
| Required → optional | HC2103 | B marks [?] a key that A requires |
At equal specificity both contracts apply with equal force; a type conflict makes the intersection unsatisfiable and is reported as HC2101. Bounds at equal specificity simply intersect and are not a conflict.
All three are error-severity diagnostics; hypercode validate exits
non-zero.
Resolved values are checked against every applicable contract
(ContractValueValidator): type conformance (an int value satisfies a
float contract, ℤ ⊂ ℝ; everything else must match exactly), declared
bounds, and required presence. Because resolution is context-dependent,
this check runs on the resolved graph — the same sheet can be clean under
one --ctx and violating under another, so hypercode validate accepts
--ctx key=value. Violations point at the winning rule (where the value
was written); a missing required property points at the contract that
demands it.
IR v2 echoes the applicable contracts per property, sorted by ascending specificity (declaration order as tie-breaker), so a consumer can re-derive the effective contract without re-parsing the sheet.
The reference fixtures are Examples/service.hc
and service.hcs, resolved in
Tests/HypercodeTests/CascadeResolverTests.swift for both the development
context ({}) and env=production, including specificity override
(#main-db ≻ Database), source-order override, non-overridden inheritance,
and provenance. Any conforming resolver must reproduce those results, e.g.:
hypercode resolve Examples/service.hc --hcs Examples/service.hcs --ctx env=production- Origin / importance. RFC §4.2.3 lists origin/importance above specificity
in the precedence order. There is no syntax for it yet (no
!important, no override-file origin), so the current precedence key is(specificity, source-order). When syntax is introduced, it becomes the most-significant component ofprecedence. (Typed scalars, previously deferred, landed with IR v2: bare scalars are type-inferred at parse time, with the source lexeme preserved for v1 round-tripping.)