Sugar

Overview

Sugar specs are convenience names for a nominal type plus a set of structural typing constraints. The term borrows from syntax sugar in programming languages: syntax that makes code sweeter to write but adds nothing that could not be expressed without it.

They are declared with the sugar marker meta:

DischargeFanRunCmd: DuctFanRunCmd <sugar> { discharge }

This defines DischargeFanRunCmd as a name for "a DuctFanRunCmd with the discharge marker". Any instance that matches DuctFanRunCmd and has the discharge tag matches DischargeFanRunCmd - whether or not the instance references DischargeFanRunCmd by name.

Sugar specs are eliminable vocabulary: they add names, not semantics. Every sugar spec can be mechanically replaced by its nominal anchor plus its constraints with no change in meaning. Sugar specs are never required for semantic interoperability; the nominal specs and tags alone carry the semantics.

Motivation

Ontology concepts frequently vary along orthogonal dimensions. A fan run command is one concept along the function axis, but the fan may sit in the discharge, return, or exhaust duct section; it may serve the cold or hot deck; it may be stage one or stage two of a fan array. These dimensions multiply. Modeling every combination as its own nominal spec explodes the type system: 100 fan and damper points across nine duct sections and three decks is thousands of specs, hand-authored and always incomplete.

Sugar specs factor the model instead:

  • The nominal hierarchy carries one axis: function. DuctFanRunCmd is a nominal spec, and its ductSection choice slot declares the dimension.
  • The dimension values are ordinary tags on the instance: discharge, coldDeck, stage. Choices ensure coherent selection.
  • Combinations that need names get them as sugar one-liners. Combinations that don't remain fully queryable as filters.

No combination is ever enumerated up front, yet every combination can be queried on demand with filters and validated as anonymous sugar in query constraints - named sugar specs are minted only where a handle is useful, never to make a combination usable.

Syntax

A sugar spec is any spec with the sugar marker in its meta. The marker is inherited, so every subtype of a sugar spec is itself sugar:

DischargeFanRunCmd: DuctFanRunCmd <sugar> { discharge }

// sugar is inherited from DischargeFanRunCmd
ColdDeckDischargeFanRunCmd: DischargeFanRunCmd { coldDeck }

// invariant scalar constraint
Stage2DischargeFanRunCmd: DischargeFanRunCmd { stage: Int <invariant> 2 }

Because the marker is inherited, the nominal/sugar boundary is crossed at most once down any inheritance chain and only in one direction: a nominal spec may not extend a sugar spec.

Body Rules

Every slot in a sugar spec's body must resolve to a global slot thru the inheritance chain of the spec's type. Each slot is exactly one of:

  • Marker constraints: discharge - a required marker; the instance must have the tag
  • Invariant constraints: stage: Int <invariant> 2 - a scalar the instance must equal; scalars are matched by equality only
  • Defaults: unit: "cfm", bacnetCurAddr: { addr: "AI3" } - scalar and nested values used by instantiation
  • Queries: query slots that constrain the graph, such as required points on an equip; see Queries below

Only markers and invariants are constraints. The distinction is syntactic: a bare marker or a slot with the invariant meta constrains membership; any other value is a default and never participates in matching or filters:

NaturalGasFlowSp: FluidVolumetricFlowSp <sugar> {
  naturalGas          // constraint
  unit: "cfm"         // default
}

Any fluid volumetric flow setpoint with the naturalGas tag matches NaturalGasFlowSp whatever its unit; instantiating it defaults unit to cfm.

A sugar spec may not declare new structural slots. Declaring structure is nominal-spec work; a sugar spec only names, constrains, and defaults. The following are compile errors:

Bad1: DuctFanRunCmd <sugar> { surgeMargin: Number }  // not a global tag
Bad2: DuctFanRunCmd <sugar> { dischrage }            // not a global tag
Bad3: DuctFanRunCmd <sugar> { stage: Int }           // neither constraint nor default
Bad4: DuctFanRunCmd <sugar> { discharge: Marker? }   // optional marker constrains nothing
Bad5: DuctFanRunCmd <sugar> { *newTag: Marker }      // declares a global

Because every name must resolve, misspelled tags are compile errors rather than silently new slots. Invariants follow the normal rule that a subtype cannot redeclare an invariant value, so conflicting scalar constraints down a chain are compile errors. Two markers which select different options of one exclusive choice, such as {discharge, return}, are reported by choice validation.

Nominal Anchor

Every sugar spec must bottom out at exactly one nominal spec, called its nominal anchor. The spec's type may be a nominal spec, another sugar spec, or an And of specs - but after flattening, the nominal ancestors must reduce to a single most specific anchor:

// legal: both operands anchor on DuctFanRunCmd
X: DischargeFanRunCmd & Stage2DischargeFanRunCmd <sugar>

// compile error: two unrelated nominal anchors
Y: Foo & Bar <sugar>

If two concepts genuinely cross-cut, the cross-cutting dimension belongs in a tag, not a second anchor. Or and maybe types are not permitted in sugar specs; a sugar spec is always a pure conjunction of one anchor plus constraints.

At compile time every sugar spec is flattened to its nominal anchor and its effective constraints (the union of constraints down the chain, canonically sorted). For example ColdDeckDischargeFanRunCmd above flattens to anchor DuctFanRunCmd with constraints {discharge, coldDeck}. Note the flattening stops at the nominal anchor: DuctFanRunCmd's own body tags such as fan and air do not become matching constraints. For a nominal spec, body tags are necessary conditions checked by validation, never sufficient conditions for membership.

Matching

An instance matches a sugar spec if either:

  1. its spec tag nominally asserts the sugar spec (or a subtype), or
  2. it matches the nominal anchor and satisfies every effective constraint

Membership is the union of what the instance asserts and what its tags prove. This is the same rule as nominal specs with the second clause added; for a nominal spec the second clause contributes nothing.

// all of these match DischargeFanRunCmd
{spec: @ph.points::DuctFanRunCmd, discharge}   // by anchor + tags
{spec: @acme::DischargeFanRunCmd, discharge}   // by assertion (and tags)
{spec: @acme::DischargeFanRunCmd}              // by assertion (invalid - see below)

Matching tests only the anchor and constraints. It never checks the anchor's slot conformance and never evaluates queries - those are validation concerns. An instance missing a required slot or a required point is an invalid member, not a non-member; it still matches queries for its type, which is essential for finding and fixing malformed data. See Constraints for validation.

Subtyping between sugar specs is computed: A is a subtype of B iff A's anchor is a subtype of B's anchor and A's constraints are a superset of B's.

Filters

A sugar spec name in a filter denotes its computed extension, using the same membership rule as matching:

DischargeFanRunCmd

is equivalent to:

DuctFanRunCmd and discharge

(plus the nominal assertion clause described above). Evaluation needs no structural evaluator: it is an is-a test against the anchor plus tag tests on the record.

Combinations without a sugar name are queried the same way, as the anchor and its tags:

DuctFanRunCmd and discharge and coldDeck

Instances

The canonical instance form asserts the most specific nominal spec and carries the dimensions as tags:

{spec: @ph.points::DuctFanRunCmd, discharge, stage: 2}

Instances may also assert a sugar spec directly in their spec tag. Instantiation stamps the spec's effective constraints and defaults onto the instance, and validation requires the constraints to be present even when the name is asserted - the name never substitutes for the tags. An instance that asserts a sugar spec but is missing or contradicting its constraint tags is invalid.

Instances and exports always carry the spec they declare, sugar or not. As with any spec, understanding an asserted sugar name requires its lib.

Queries

Sugar spec bodies may declare query slots. Queries constrain the graph neighborhood of an instance - for example the points required on an equip:

StandardVav: Vav <sugar> {
  singleDuct
  points: {
    DischargeAirTempSensor
    DischargeFanRunCmd
  }
}

Queries participate only in validation, never in matching or filters. An instance matches StandardVav based on anchor and tags alone; whether it actually has the required points is checked by validation. This keeps filter evaluation free of graph traversal and preserves the rule that broken instances remain findable as what they are.

Queries make sugar specs suitable as named validation profiles: a shareable, versioned spec describing the required shape of an installation that any party can publish over the core ontology.

Each item in a query body is itself an anonymous sugar spec: its declared type is the anchor and its body follows the same body rules. Named and anonymous items are equivalent, and an item may carry defaults for instantiation and tooling:

points: {
  DischargeFanRunCmd                // named sugar
  DuctFanRunCmd { discharge }       // same constraints, anonymous
  zoneTemp: ZoneAirTempSensor {     // no constraints beyond the type
    dis: "Zone Temp"                // default
    bacnetCurAddr: { addr: "AI1" }  // default
  }
}

Validation matches each item against the query's extent by anchor and constraints only; defaults such as dis and protocol addresses are never matched.

Mixins

Mixins may target sugar specs, subject to the same body rules: a mixin on a sugar spec may add constraints, defaults, and queries, but not structural slots, so that the extended spec remains a valid sugar spec:

// legal: adds a constraint and a query requirement
+StandardVav {
  hotWaterHeating
  points: {
    HotWaterValveModulatingCmd
  }
}

// compile error: structural slot on a sugar spec
+StandardVav {
  installNotes: Str
}

Mixin constraints apply when matching against the extended spec, such as the one returned by specx, which narrows the sugar spec's extension within the namespaces that include the mixin's lib. As with all mixins, the extended spec is namespace dependent. Matching the plain spec, as filters do by name, uses only its declared constraints.

Sugar Libs

By convention, sugar specs for a core ontology library are distributed in a companion library with the sugar suffix. For example the sugar vocabulary over ph.points is published as ph.points.x.

The convention carries a contract:

  • A sugar lib contains only sugar specs; the core lib contains only nominal specs
  • A sugar lib is a conservative extension: eliminating it changes no semantics, only names
  • Core libs never depend on sugar libs; shapes in core libs use anonymous sugar syntax instead of sugar names

Sugar names are minted on demand - when documentation, common queries, or validation profiles need a handle - never speculatively to enumerate a product space. Unnamed combinations cost nothing: they remain fully expressible as filters and as anonymous sugar in query constraints.

Design Rationale

The split between ph.points and ph.points.x illustrates these rules. The principles used to draw that line generalize to any core/sugar lib pair.

Structure lives on nominal specs. A dimension enters the model as a choice slot, and sugar bodies cannot declare new slots - so every constrained dimension needs a nominal home. DuctAirTempSensor declares ductSection: DuctSection and ductDeck: DuctDeck?; PipeWaterTempSensor declares pipeSection: PipeSection; the fluid point bases narrow pointSubject to a required Fluid. Sugar then names points along the dimension - DischargeAirTempSensor, HotWaterValveModulatingCmd - and its constraint markers simultaneously satisfy matching and the choice validation.

A required choice replaces abstract. The choice-gated bases are deliberately concrete. A DuctAirTempSensor cannot validly exist without selecting a duct section, which makes it effectively abstract - but it must remain assertable, because the canonical instance form is the base plus tags: {spec: @ph.points::DuctAirTempSensor, discharge}. Marking it abstract would leave sugar names as the only route into the extension, breaking the rule that sugar is eliminable.

Which concepts get nominal names. A spec must be nominal when other nominal specs extend it, since a nominal spec may not extend sugar: AirTempSensor and WaterFlowSensor are nominal because the Duct* and Pipe* layers anchor on them. A subject marker with no choice machinery and no combinatorics is a first-class concept rather than sugar: ZoneAirTempSensor, ParkingBayOccupiedSensor, WindowOpenSensor are nominal. Sugar earns its keep only where a choice-validated dimension or a combinatorial space needs names - naturalGas over the fluid choice, discharge over the duct section choice. Beyond these tests, a core ontology may deliberately grant first-class nominal treatment to important vocabulary, as ph.points does for the air, water, steam, and refrigerant point families.

Versioning

A sugar spec's extension is determined by its definition at a pinned lib version. Adding a constraint to a published sugar spec narrows its extension and is a breaking change, exactly as adding a required slot to a nominal spec is. The non-breaking way to tighten is to publish a new, more specific sugar spec alongside the old one. Adding or changing a default leaves the extension unchanged and is not a breaking change.

Haxall 4.0.7 ∙ 27-Oct-2026 09:10 EDT