SporeLanguage referenceAbout the solverHow it solves
Configurator demo

SSC language reference

SSC (Spore System Configuration) describes what combinations of characteristics a device can legally have. A knowledge base is a directory of these declarations, compiled by kb-emit into a binary the solver in engine.c loads and evaluates - unmodified on a PC, in a browser, or on a microcontroller.

OverviewCharacteristicsClassesObjectsConstraintsPreconditionsProceduresVariant tables

Overview

Every characteristic value is either committed (a user pick, a policy default, or a sensor reading) or derived (a domain narrowed by constraints, recomputed fresh on every solve). The engine never mixes the two: a derived value is recomputed from scratch each time, so it can never drift out of sync with the constraints that produced it.

A characteristic is one of three types:

  • SINGLE - one value from a declared list (a mode, a size, an on/off code)
  • MULTI - a set of values from a declared list (optional features, active alarms)
  • RANGE - a plain integer in a declared interval (a setpoint, a sensor reading)

Characteristics

A characteristic block declares one named, typed slot. SINGLE and MULTI list their legal values with values; RANGE declares its legal interval with intervals. A MULTI is the same as SINGLE with multiValue added.

characteristic MODE {
  names EN "Mode"
  textLength 8
  values
    'ECO' names EN "Eco",
    'COMFORT' names EN "Comfort",
    'BOOST' names EN "Boost"
}

characteristic FEATURES {
  names EN "Optional Features"
  textLength 10
  multiValue
  values
    'AUTOTUNE' names EN "Auto-tune on startup",
    'DEADBAND' names EN "Output deadband"
}

characteristic SETPOINT {
  names EN "Setpoint"
  numericLength 4 decimalPlaces 1 unit "C"
  intervals
    0 - 400
}

decimalPlaces/unit on a RANGE are display-only - a UI divides the wire value by 10**decimalPlaces and labels it; the engine itself always solves over the plain integer.


Classes

A class groups characteristics into the shape one kind of object has. Each usage inside characteristics can carry modifiers:

  • required - the config isn't complete until this has a value
  • defaultValues [...] - a policy default, committed but replaceable by a later user pick
  • noinput - solver output only; never accepted as a user commit (drive it from a procedure instead)
  • observed - a sensor reading; written with a distinct API (set_cstic_observed, not set_cstic) that skips the normal admission gate
class Thermostat {
  characteristics
    TEMP observed,
    MODE defaultValues ['ECO'],
    FAN noinput
}

Objects

An object is a persistent, named instance of a class - the thing constraints, procedures and preconditions actually attach to.

object thermo is_a Thermostat;

Constraints

A constraint binds one or more ?alias to a class in objects:, optionally gates on a condition:, and narrows what's legal in restrictions: - evaluated fresh on every solve, for every matching combination of objects.

constraint fan_high_CS {
  objects:
    ?t is_a Thermostat
  condition:
    ?t.MODE = 'BOOST'
  restrictions:
    ?t.FAN in ['HIGH']
}

objects: can bind more than one ?alias - the constraint is then evaluated for every matching combination across both classes. specified/not specified tests whether a characteristic (typically an observed sensor) has reported a value at all yet:

constraint hot_hvac_CS {
  objects:
    ?h is_a House,
    ?r is_a Room
  condition:
    ?r.TEMP specified and ?r.TEMP > 28 and ?h.HOUSE_MODE = 'AWAY'
  restrictions:
    ?r.HVAC in ['FROST']
}

A restriction target can also be another characteristic, not just a literal list - useful for keeping two RANGE fields consistent:

constraint output_bounds_CS {
  objects:
    ?p is_a PidLoop
  restrictions:
    ?p.OUTPUT_MIN <= ?p.OUTPUT_MAX
}

in [...] restricts a SINGLE/MULTI's own domain (what's still allowed) - a plain = is not valid here. includes, only meaningful on a MULTI, tests what's actually chosen right now instead of the domain.


Preconditions

A precondition is a single-object, single-target visibility gate: true makes the target characteristic available, false makes it invisible. Unlike a constraint it has no explicit ?alias - $SELF refers to whichever object the precondition was expanded for.

precondition ki_visible_PRE for PidLoop on KI {
  condition: $SELF.CONTROLLER_TYPE = 'PI' or $SELF.CONTROLLER_TYPE = 'PID'
}

A precondition only ever gates one target on $SELF. For a multi-object, if-guardable version - one alias setting another alias's characteristic invisible or read-only - use a property restriction directly inside a constraint's restrictions: instead:

constraint away_lights_CS {
  objects:
    ?h is_a House,
    ?r is_a Room
  restrictions:
    ?r.no_input LIGHTS if ?h.HOUSE_MODE = 'AWAY'
}

.invisible, .required, and .no_input are all supported this way; each is re-evaluated fresh on every solve, so a property can appear and disappear as the configuration changes, not just accumulate.


Procedures

A procedure runs imperative, single-object logic against $SELF - plain arithmetic assignment, or $set/$set_default/$del_default calls. It re-runs, and its target recomputes, on every solve that touches anything it reads.

procedure error_PROC for PidLoop {
  $SELF.ERROR = $SELF.SETPOINT - $SELF.PROCESS_VALUE
}

procedure output_PROC for PidLoop {
  $SELF.OUTPUT = ($SELF.KP * $SELF.ERROR) / 100
}

Supported arithmetic today: + - * / only, evaluated over plain integers. The grammar also parses ** and a set of math functions (abs, round, sqrt, sin/cos/tan and friends) for forward compatibility, but the compiler doesn't emit an opcode for either yet - either one in an expression is skipped with a warning, not silently wrong. A characteristic that's still unset reads as 0 in an expression, same as a plain assignment.


Variant tables

A table maps one or more primary (key) columns to the rest, either inline or from a companion tab-separated .vtable file. Referencing it from a constraint's restrictions: narrows every mapped column both ways - picking a dependent column value narrows the primary column back too, not just primary → dependent.

table POWER_TABLE {
  characteristics
    MODE primary,
    FAN  primary,
    POWER_W
  file 'power_table.vtable'
}

constraint power_table_CS {
  objects:
    ?t is_a Thermostat
  restrictions:
    table POWER_TABLE(MODE = ?t.MODE, FAN = ?t.FAN, POWER_W = ?t.POWER_W)
}