Assertions and Attributes¶
Assertions are post-evaluation checks that verify invariants and expected values. Attributes are metadata annotations on declarations. Together, they enable in-language testing and engineering assumption tracking.
Assert Declarations¶
An assert declaration checks a boolean condition after the entire computation
graph has been evaluated:
Key Properties¶
- Assert names conventionally use
lower_snake_case, likeparamandnode. - Assert bodies can reference any
@paramor@node, plus constants. - Assertions are leaf nodes -- no declaration can reference an assert with
@. Attempting@my_assertis a compile error (A003). - Assertions are evaluated after the full graph, in declaration order.
- A failed assertion produces a non-zero exit code.
Boolean Assertions¶
The simplest form evaluates an expression that must produce Bool:
param pressure: Pressure = 8.5 MPa;
param max_pressure: Pressure = 10.0 MPa;
assert pressure_safe = @pressure < @max_pressure;
If the body evaluates to false, the assertion fails with
"assertion evaluated to false".
Indexed Boolean Assertions¶
When the body evaluates to Bool[SomeIndex], each variant is checked
individually. Failing variants are listed in the diagnostic:
index Stage = { First, Second, Third };
param thrust: Force[Stage] = ...;
param min_thrust: Force = 100.0 kN;
assert all_stages_ok = @thrust > @min_thrust;
// If First and Third fail:
// FAIL (failed at Stage: First, Third)
Tolerance Assertions¶
For approximate equality checks, use the ~= and +/- syntax:
This is semantically equivalent to abs(@fuel_mass - 2847.0 kg) <= 5.0 kg but
produces richer failure diagnostics showing the actual value, expected value,
tolerance, and delta.
Absolute Tolerance¶
All three operands are arbitrary expressions. They can reference @param,
@node, constants, call functions, and use arithmetic -- anything valid in a
node expression. The dimension rules are:
actualandexpectedmust have the same dimension.tolerancemust also have the same dimension asactualandexpected.
The check passes when abs(actual - expected) <= tolerance.
Examples:
// All three operands can be graph references
assert mass_check = @computed_mass ~= @expected_mass +/- @mass_tolerance;
// Or mix literals, constants, and references
assert velocity_ok = @v_final ~= 3000.0 m/s +/- 10.0 m/s;
Relative Tolerance¶
Append % after the tolerance value to use relative (percentage) tolerance:
The dimension rules change for relative tolerance:
actualandexpectedmust have the same dimension.tolerancemust be dimensionless (or an integer literal).
The check passes when abs(actual - expected) <= abs(expected) * tolerance / 100.
The % is not a unit. The % in tolerance assertions is special syntax,
not a built-in unit. It is only valid immediately after the tolerance expression
in a ~= ... +/- context. You cannot write 5 % as a unit literal elsewhere
in the language.
This means there is no ambiguity in expressions like:
This always means "within 10% of 0.20" (i.e., the range [0.18, 0.22]),
because % is parsed as part of the tolerance syntax, not as a unit on the
number 10.
Examples:
assert efficiency = @eta ~= 0.85 +/- 5 %;
// Passes if eta is within [0.8075, 0.8925]
assert velocity_approx = @velocity ~= 49.5 m/s +/- 5 %;
// Passes if velocity is within [47.025, 51.975] m/s
Attributes¶
Attributes are metadata annotations written before a declaration using the
#[name] or #[name(args)] syntax:
Syntax¶
#[name] // no arguments
#[name(arg1)] // one argument
#[name(arg1, arg2, arg3)] // multiple arguments
#[name(Index.Variant)] // qualified path argument
#[name((Idx.A, Idx.B), (Idx.C, Idx.D))] // tuple key arguments
Multiple attributes can be stacked:
Unknown attribute names are compile errors (A007).
#[assumes(...)]¶
The #[assumes(...)] attribute documents that a declaration's value is valid
only if the named assertion(s) hold. It does not create a graph dependency.
assert pressure_safe = @pressure < 10.0 MPa;
#[assumes(pressure_safe)]
node safety_factor: Dimensionless = 1.5;
When pressure_safe fails, the diagnostic mentions that safety_factor may be
invalid:
Rules¶
- Arguments must reference
assertdeclarations. Referencing aparam,node,const node, or nonexistent name is a compile error (A005). - Valid on
nodeandparamdeclarations. Using#[assumes]onconst nodeis an error (A006) because constants do not depend on runtime values. - Multiple assertions can be listed:
#[assumes(a, b, c)]. - Cross-file: you can import an assert via
importand reference it in#[assumes].
#[expected_fail]¶
The #[expected_fail] attribute marks an assertion that is expected to fail.
This is useful for documenting known failures in engineering calculations
without causing the overall evaluation to fail.
A failing assertion marked #[expected_fail] is treated as a pass.
A passing assertion marked #[expected_fail] is treated as a failure
("unexpected pass"), since the known issue may have been resolved and the
attribute should be removed.
// This assertion fails (10 > 20 is false), but it's a known issue
#[expected_fail]
assert x_greater = @x > @y;
Constraints¶
- Valid only on
assertdeclarations. Using#[expected_fail]onparam,node,const node, etc. is a compile error (A008). - Evaluation errors (e.g., division by zero) are never inverted -- they remain
errors regardless of
#[expected_fail].
Blanket Form¶
When used without arguments, the entire assertion is expected to fail:
Per-Variant Form (Single Index)¶
For indexed assertions, specific index variants can be marked as expected failures while other variants must still pass:
index Mode = { Normal, Eco, Boost };
#[expected_fail(Mode.Boost)]
assert power_ok = for m: Mode { @power_use[m] < @power_gen[m] };
Here, Mode.Boost is expected to fail (and is treated as a pass if it does),
while Mode.Normal and Mode.Eco must still pass normally.
Per-Tuple-Key Form (Multi Index)¶
For multi-indexed assertions, tuple keys identify specific index combinations:
index Mode = { Normal, Eco, Boost };
index Phase = { Launch, Cruise };
#[expected_fail((Mode.Normal, Phase.Cruise), (Mode.Boost, Phase.Launch))]
assert within_limits = for m: Mode, p: Phase { @actual[m, p] < @threshold[m, p] };
#[lazy]¶
Recognized by the parser but not yet implemented. Will mark a node for lazy evaluation (computed only when requested, not eagerly during graph evaluation).
Assertions in Multi-File Projects¶
When a file is imported (either selectively or as a module), all assertions in that file are automatically evaluated. You do not need to explicitly import assertions for them to be checked -- they run as part of the imported file's evaluation.
// main.gcl
import checks.{limit};
// limit_positive is automatically evaluated and reported,
// even though it was not listed in the import braces.
This applies transitively: if a.gcl imports b.gcl, which imports c.gcl,
then assertions in all three files are evaluated and reported.
In diamond imports (where two files import the same dependency), the shared dependency is evaluated once and its assertions are reported once.
Using #[assumes] with imported assertions¶
To reference an imported assertion in #[assumes(...)], you must explicitly
import it by name:
// main.gcl
import checks.{limit, limit_positive};
#[assumes(limit_positive)]
node ratio: Dimensionless = @limit / 2.0;
Error Codes¶
| Code | Description |
|---|---|
| A001 | Assertion failure (LSP diagnostic) |
| A002 | Assumed assertion failed (CLI, lists affected nodes) |
| A003 | Cannot reference assert with @ sigil |
| A004 | Assert body must evaluate to Bool |
| A005 | Unknown assert in #[assumes(...)] |
| A006 | #[assumes] on invalid declaration kind (e.g., const node) |
| A007 | Unknown attribute name |
| A008 | #[expected_fail] on invalid declaration kind (not assert) |
| A009 | Invalid argument in #[expected_fail(...)] |
| A010 | #[expected_fail(...)] with variant args on non-indexed assertion |