Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ generated in the following situations:

### 2.0.5-dev

* [TRLC] Add VCG support for field access on record/union references
(two-phase check analysis, #156 step 2/2).

* [TRLC, API] Fix various typos in the documentation.

* [TRLC, LRM] Allow union types as tuple field types:
Expand Down
42 changes: 39 additions & 3 deletions documentation/TUTORIAL-CHECKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,43 @@ checks Requirement {
}
```

## Current limitations and notes
## Following references

Right now it is not possible to "follow references" in the check
language. We are considering to add this in a future release.
It is possible to access fields of a referenced record directly in
check expressions using the `.` operator. For example, given:

```
type Node {
value Integer
next optional Node
}

checks Node {
next != null implies next.value > value, fatal "must be in order"
}
```

You can also use field access inside quantifiers. For example, if you
have an array of record references you can write:

```
type Item {
value Integer
}

type Container {
items Item [0 .. *]
limit Integer
}

checks Container {
(forall e in items => e.value < limit), fatal "all items must be below limit"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quantifier example (forall e in items => e.value < limit) currently produces a VCG warning "functional evaluation of field access not yet supported" — so the VCG does not analyse it. The docs should mention this limitation, otherwise users will expect VCG coverage they are not getting.

}
```

Note that accessing a field on an optional reference without a null
guard will produce a runtime error if the reference is null. The
static analyser (VCG) will check for potential null dereferences and
warn you accordingly. This extends to quantifier bodies: if `value`
were declared `optional`, the VCG would warn about the dereference
inside the `forall` expression.
56 changes: 56 additions & 0 deletions documentation/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,62 @@ that `fatal_error_1` could not have occurred. But we cannot make the
same assumption about `normal_error_1`, any checks under
`fatal_error_1` do *not* get the truth of `normal_error_1` asserted.

#### Two-phase check analysis
Copy link
Copy Markdown

@SurajBDeore SurajBDeore May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check.uses_field_access is a @Property (no parentheses) but on Expression subclasses it is a regular method (with ()). Could we add a note about this or make them consistent? It is easy to miss when reading the code.


Checks within a type are split into two phases before building the VCG graph:

* **Phase A — "at declaration"**: Checks whose expressions do *not*
dereference any record or union reference (i.e., the expression
tree contains no `Field_Access_Expression` with a `Record_Type` or
`Union_Type` prefix). These checks operate only on the type's own
components and are analyzed first.

* **Phase B — "after references"**: Checks whose expressions *do*
dereference a record or union reference via a `Field_Access_Expression`.
These are analyzed after Phase A, so they benefit from any
knowledge accumulated by Phase A `fatal` checks.

The classification is computed lazily and cached on each `Check` node
via `Check.uses_field_access`. Note that on `Check` this is a
`@property` (no parentheses), whereas the same-named method on
`Expression` subclasses is a regular method (called with `()`).

The asymmetry is intentional. `Check` is a stable, long-lived AST
node; its expression tree never changes, so caching the result in
`_uses_field_access` is safe and future-proofs against callers that
may read the property more than once. `Expression` subclasses recurse
into their children and are only ever called from within that `Check`
property, so they execute at most once already — adding a backing
field and lazy-init boilerplate to every one of the ~15 concrete
expression classes would cost more than it saves.

The split is implemented in `checks_on_composite_type` as two passes
over `iter_checks()`, with Phase B having access to the
`fatal`-accumulated state from Phase A.

This ordering cannot be observed at runtime (all checks are evaluated
in declaration order), but the VCG is more precise when
non-reference checks have already contributed their `fatal` knowledge
before reference-based checks are proved.

The reason precision improves is rooted in how referenced records are
modelled. A record reference (e.g. `parent optional Requirement`) is
encoded as a bare integer (the record's unique ID). To access a field
on it — say `parent.priority` — the VCG creates an *uninterpreted
function* `access.Package.Requirement : Int → RecordSort` that maps
the ID to a synthetic SMT record sort. An uninterpreted function is
axiom-free: the solver can assign any field value it pleases unless we
have added explicit assertions. Phase A `fatal` checks establish
concrete facts about the type's *own* components (integers, booleans,
etc.) without involving any UF. When those facts are in the SMT
context before Phase B runs, they constrain what the solver can try
when reasoning about UF results — for example, knowing `severity <= 3`
(fatal, Phase A) means the solver cannot use `severity = 99` when
trying to defeat a later Phase B check on `parent.priority >
severity`. Without the split the Phase A knowledge would arrive too
late and the solver would treat the component as unbounded, producing
a false positive.

#### Types

We model types as follows
Expand Down
33 changes: 0 additions & 33 deletions tests-system/field-access-1/output

This file was deleted.

3 changes: 0 additions & 3 deletions tests-system/field-access-1/output.brief

This file was deleted.

5 changes: 0 additions & 5 deletions tests-system/field-access-1/output.json

This file was deleted.

33 changes: 0 additions & 33 deletions tests-system/field-access-1/output.smtlib

This file was deleted.

12 changes: 0 additions & 12 deletions tests-system/field-access-1/test_1.rsl

This file was deleted.

14 changes: 0 additions & 14 deletions tests-system/field-access-1/test_2.rsl

This file was deleted.

10 changes: 0 additions & 10 deletions tests-system/field-access-1/test_3.rsl

This file was deleted.

11 changes: 0 additions & 11 deletions tests-system/field-access-1/test_4.rsl

This file was deleted.

24 changes: 0 additions & 24 deletions tests-system/field-access-1/test_5.rsl

This file was deleted.

17 changes: 0 additions & 17 deletions tests-system/field-access-1/test_5.trlc

This file was deleted.

18 changes: 18 additions & 0 deletions tests-system/vcg-field-access-basic/output
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
c, "potato"
^ vcg-field-access-basic/test1.rsl:11: issue: expression could be null [vcg-evaluation-of-null]
| example record_type triggering error:
| T bad_potato {
| x = 0
| /* ref is null */
| /* c is null */
| }
10 / len(ref.x) > 0, "potato"
^ vcg-field-access-basic/test2.rsl:9: issue: divisor could be 0 [vcg-div-by-zero]
| example record_type triggering error:
| T bad_potato {
| x = [-1, -1]
| ref = T_instance_0
| }
T bar {
^^^ vcg-field-access-basic/test3.trlc:9: check error: potato
Processed 3 models and 1 requirement file and found 2 warnings and 1 error
2 changes: 2 additions & 0 deletions tests-system/vcg-field-access-basic/output.brief
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
vcg-field-access-basic/test3.trlc:9:3: trlc check error: potato
Processed 3 models and 1 requirement file and found 1 error
3 changes: 3 additions & 0 deletions tests-system/vcg-field-access-basic/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
T bar {
^^^ vcg-field-access-basic/test3.trlc:9: check error: potato
Processed 3 models and 1 requirement file and found 1 error
18 changes: 18 additions & 0 deletions tests-system/vcg-field-access-basic/output.smtlib
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
c, "potato"
^ vcg-field-access-basic/test1.rsl:11: issue: expression could be null [vcg-evaluation-of-null]
| example record_type triggering error:
| T bad_potato {
| x = 0
| /* ref is null */
| /* c is null */
| }
10 / len(ref.x) > 0, "potato"
^ vcg-field-access-basic/test2.rsl:9: issue: divisor could be 0 [vcg-div-by-zero]
| example record_type triggering error:
| T bad_potato {
| x = [-1, -1]
| ref = T_instance_0
| }
T bar {
^^^ vcg-field-access-basic/test3.trlc:9: check error: potato
Processed 3 models and 1 requirement file and found 2 warnings and 1 error
12 changes: 12 additions & 0 deletions tests-system/vcg-field-access-basic/test1.rsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package Test1

type T {
x Integer
ref optional T
c optional Boolean
}

checks T {
ref != null implies ref.x < x, fatal "potato"
c, "potato"
}
10 changes: 10 additions & 0 deletions tests-system/vcg-field-access-basic/test2.rsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package Test2

type T {
x Integer [2 .. *]
ref T
}

checks T {
10 / len(ref.x) > 0, "potato"
}
11 changes: 11 additions & 0 deletions tests-system/vcg-field-access-basic/test3.rsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package Test3

type T {
x Integer
ref T
extra Integer
}

checks T {
ref.x + extra > 0, fatal "potato"
}
13 changes: 13 additions & 0 deletions tests-system/vcg-field-access-basic/test3.trlc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package Test3

T foo {
x = 5
ref = foo
extra = 3
}

T bar {
x = -10
ref = bar
extra = 3
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
LRM.File_Parsing_References
LRM.Order_Of_Evaluation_Unordered
LRM.Evaluation_Truly_Unordered
LRM.Check_Evaluation_Order
LRM.Check_Severity
LRM.Dereference
Loading
Loading