Skip to content

Commit dea6bcc

Browse files
committed
feat(laurel): chained field access and paren-free field increment
Chained field access (`a#b#c`) and paren-free field increment (`c#n++`) were both blocked by the `fieldAccess` grammar op. Combine the fixes: * Grammar: `fieldAccess` becomes `@[prec(95), leftassoc]`. prec(95) binds tighter than the postfix incr/decr ops (prec 90) so `c#n++` parses as `(c#n)++` without parens; `leftassoc` lets `a#b#c` left-recurse. * Resolution: `targetTypeName` is generalized to resolve a chained `.Var (.Field tgt f)` target by recursing on `tgt` and looking the field up in the inner type's scope. The recursion is kept total via fuel (cf. `underlyingBaseType`), not `partial`. The shared type-scope field lookup is factored into `fieldTypeInScope`, which `incrDecrTargetType` now reuses instead of duplicating. * Heap: the field-read arm recurses into its target so a nested `FieldSelect` is eliminated. This single line unblocks *both* chained reads and chained writes: the write arm already calls `recurseOne` on its target, but for a nested target that recursion bottoms out in the read arm, so neither path worked before. Without the fix, an un-eliminated `FieldSelect` hits a downstream guard and aborts with a `strata-bug` (confirmed by ablation). Tests: * T9_ChainedFieldAccess: chained read/write, read-after-inner-assign, must-alias, depth-3 (`a#mid#inner#count`), chained read on both sides of a comparison, paren-free chained incr/decr, plus three negative tests (unconstrained read, two-object may-alias, write isolation) pinning the encoding as non-vacuous and frame-sound. * T23b_IncrDecrField: paren-free field `++`/`--`; header comment updated (parens are no longer required). Implements #1371.
1 parent 0abd9ef commit dea6bcc

5 files changed

Lines changed: 228 additions & 23 deletions

File tree

Strata/Languages/Laurel/Grammar/LaurelGrammar.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module
1212
-- Laurel dialect definition, loaded from LaurelGrammar.st
1313
-- NOTE: Changes to LaurelGrammar.st are not automatically tracked by the build system.
1414
-- Update this file (e.g. this comment) to trigger a recompile after modifying LaurelGrammar.st.
15-
-- Last grammar change: renamed strConcat token to `^`; added preIncr/preDecr/postIncr/postDecr; `return` value is now Option StmtExpr (supports a valueless return).
15+
-- Last grammar change: fieldAccess prec raised 90 -> 95 (paren-free `c#n++`); shares prec(95) with `call`.
1616
public import StrataDDM.AST
1717
import StrataDDM.BuiltinDialects.Init
1818
import StrataDDM.Integration.Lean.HashCommands

Strata/Languages/Laurel/Grammar/LaurelGrammar.st

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ op call(callee: StmtExpr, args: CommaSepBy StmtExpr): StmtExpr =>
4040
// Instantiation
4141
op new(name: Ident): StmtExpr => "new " name;
4242

43-
// Field access. `leftassoc` so chained access (`a#b#c`) parses as `(a#b)#c`.
44-
op fieldAccess (obj: StmtExpr, field: Ident): StmtExpr => @[prec(90), leftassoc] obj "#" field;
43+
// Field access. prec coupled to `call` (also 95, via its `callee:89`): keep them
44+
// equal or `a#b(x)` parsing shifts. prec 95 > postfix `++`/`--` (90) is what lets
45+
// `c#n++` parse paren-free as `(c#n)++`.
46+
op fieldAccess (obj: StmtExpr, field: Ident): StmtExpr => @[prec(95), leftassoc] obj "#" field;
4547

4648
// Identifiers/Variables - must come after fieldAccess so c.value parses as fieldAccess not identifier
4749
op identifier (name: Ident): StmtExpr => name;

Strata/Languages/Laurel/Resolution.lean

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,22 @@ def resolveRef (name : Identifier) (source : Option FileRange := none)
236236
private def containerScopedName (containerName memberName : Identifier) : Identifier :=
237237
mkId s!"{containerName.text}${memberName.text}"
238238

239-
/-- Extract the UserDefined type name from a resolved target expression by looking up its scope entry. -/
239+
/-- Declared type of `fieldName` in the scope of composite type `typeName`; `none` if
240+
unknown. Shared by `targetTypeName` and `incrDecrTargetType`. (`resolveFieldInTypeScope`
241+
below is the same walk returning the field's *id* instead of its type.) -/
242+
private def fieldTypeInScope (typeName : String) (fieldName : Identifier) : ResolveM (Option HighType) := do
243+
let s ← get
244+
match s.typeScopes.get? typeName with
245+
| some typeScope =>
246+
match typeScope.get? fieldName.text with
247+
| some (_, node) => pure (some node.getType.val)
248+
| none => pure none
249+
| none => pure none
250+
251+
/-- UserDefined type name of a resolved target: a local directly, or a chained field
252+
access (`a#b#c`) by recursing on the inner target then `fieldTypeInScope`.
253+
Self-recursive on `.Var (.Field inner _)`; the `decreasing_by` proof below holds
254+
only because `inner` is a strict subterm of `target`, so recurse only on subterms. -/
240255
private def targetTypeName (target : StmtExprMd) : ResolveM (Option String) := do
241256
let s ← get
242257
match _h : target.val with
@@ -251,15 +266,9 @@ private def targetTypeName (target : StmtExprMd) : ResolveM (Option String) := d
251266
match (← targetTypeName inner) with
252267
| none => pure none
253268
| some innerTy =>
254-
match s.typeScopes.get? innerTy with
255-
| none => pure none
256-
| some typeScope =>
257-
match typeScope.get? fieldName.text with
258-
| some (_, node) =>
259-
match node.getType.val with
260-
| .UserDefined typRef => pure (some typRef.text)
261-
| _ => pure none
262-
| none => pure none
269+
match (← fieldTypeInScope innerTy fieldName) with
270+
| some (.UserDefined typRef) => pure (some typRef.text)
271+
| _ => pure none
263272
| _ => pure none
264273
termination_by sizeOf target
265274
decreasing_by
@@ -516,13 +525,7 @@ private def incrDecrTargetType (target : VariableMd) : ResolveM (Option HighType
516525
| none => pure none
517526
| .Field tgt fieldName =>
518527
match (← targetTypeName tgt) with
519-
| some typeName =>
520-
match s.typeScopes.get? typeName with
521-
| some typeScope =>
522-
match typeScope.get? fieldName.text with
523-
| some (_, node) => pure (some node.getType.val)
524-
| none => pure none
525-
| none => pure none
528+
| some typeName => fieldTypeInScope typeName fieldName
526529
| none => pure none
527530
| .Declare param => pure (some param.type.val)
528531

StrataTest/Languages/Laurel/Examples/Fundamentals/T23b_IncrDecrField.lean

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ local (`$tmp_i` or `$heap`), so its snapshot mechanism — which is keyed on
3131
`liftAssignExpr` (which falls through `| _ => pure ()`) is defensive but
3232
never reached in this pipeline order.
3333
34-
The parentheses around `(c#n)` are needed in the surface syntax because
35-
`#field` and `++` are both trailing operators with the same precedence
36-
(90); `c#n++` parses ambiguously without them.
34+
`c#n++` now parses paren-free: `fieldAccess` is at `prec(95)`, above the
35+
postfix incr/decr ops (`prec(90)`), so `#` binds tighter than `++` and
36+
`c#n++` reads as `(c#n)++`. The parenthesised spelling `(c#n)++` remains
37+
valid; `parenFreeFieldIncrDecr` below covers the paren-free form.
3738
-/
3839

3940
#eval testLaurel <|
@@ -123,4 +124,21 @@ procedure postDecrFieldInExpression()
123124
assert c#n == 4;
124125
assert y == 5
125126
};
127+
128+
procedure parenFreeFieldIncrDecr()
129+
opaque
130+
{
131+
// Paren-free field incr/decr: `c#n++` parses as `(c#n)++` because `fieldAccess`
132+
// (prec 95) binds tighter than postfix `++`/`--` (prec 90).
133+
var c: IncrDecrCounter := new IncrDecrCounter;
134+
c#n := 10;
135+
c#n++;
136+
assert c#n == 11;
137+
c#n--;
138+
assert c#n == 10;
139+
// Postfix in expression position yields the OLD value (10); c#n becomes 11.
140+
var y: int := c#n++;
141+
assert c#n == 11;
142+
assert y == 10
143+
};
126144
#end
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/-
2+
Copyright Strata Contributors
3+
4+
SPDX-License-Identifier: Apache-2.0 OR MIT
5+
-/
6+
7+
import StrataTest.Util.TestLaurel
8+
9+
open StrataTest.Util
10+
open Strata
11+
12+
/-!
13+
Chained field access on nested composites (`o#inner#count`), both read and
14+
write. Single-level field access (`o#inner`, `i#count`) already worked; this
15+
exercises the chaining enabled by three changes:
16+
17+
* Parse — `fieldAccess` is now `leftassoc`, so `a#b#c` left-recurses.
18+
* Resolution — `targetTypeName` resolves a `.Var (.Field ..)` target by
19+
recursing on the inner target and looking the field up in that type's scope.
20+
* Heap — the field-read arm recurses into its target, so a nested
21+
`FieldSelect` is eliminated rather than leaking raw into `readField`.
22+
-/
23+
24+
#eval testLaurel <|
25+
#strata
26+
program Laurel;
27+
composite Inner {
28+
var count: int
29+
}
30+
31+
composite Outer {
32+
var inner: Inner
33+
}
34+
35+
// Three nested composite levels, for the depth-3 chain `a#mid#inner#count`.
36+
composite Innermost {
37+
var count: int
38+
}
39+
40+
composite Middle {
41+
var inner: Innermost
42+
}
43+
44+
composite Outermost {
45+
var mid: Middle
46+
}
47+
48+
procedure chainedReadWrite()
49+
opaque
50+
{
51+
var o: Outer := new Outer;
52+
var i: Inner := new Inner;
53+
o#inner := i;
54+
55+
o#inner#count := 7;
56+
assert o#inner#count == 7;
57+
58+
// Write through the chain again and read it back.
59+
o#inner#count := o#inner#count + 1;
60+
assert o#inner#count == 8;
61+
62+
// The chained read agrees with the single-level read of the same object.
63+
assert i#count == 8
64+
};
65+
66+
procedure chainedReadAfterInnerAssign()
67+
opaque
68+
{
69+
var o: Outer := new Outer;
70+
var i: Inner := new Inner;
71+
i#count := 3;
72+
o#inner := i;
73+
// Reading through the chain sees the value written on `i` directly.
74+
assert o#inner#count == 3
75+
};
76+
77+
procedure chainedAliasing()
78+
opaque
79+
{
80+
var o: Outer := new Outer;
81+
var i: Inner := new Inner;
82+
o#inner := i;
83+
o#inner#count := 1;
84+
85+
// `i` and `o#inner` alias the same Inner, so a write through one is seen
86+
// through the other.
87+
i#count := 42;
88+
assert o#inner#count == 42
89+
};
90+
91+
procedure depth3ReadWrite()
92+
opaque
93+
{
94+
var a: Outermost := new Outermost;
95+
var b: Middle := new Middle;
96+
var c: Innermost := new Innermost;
97+
a#mid := b;
98+
b#inner := c;
99+
100+
a#mid#inner#count := 9;
101+
assert a#mid#inner#count == 9;
102+
// The depth-3 chain agrees with the single-level read of the same object.
103+
assert c#count == 9
104+
};
105+
106+
procedure chainedReadOnBothSides()
107+
opaque
108+
{
109+
var o: Outer := new Outer;
110+
var p: Outer := new Outer;
111+
var i: Inner := new Inner;
112+
var j: Inner := new Inner;
113+
o#inner := i;
114+
p#inner := j;
115+
i#count := 4;
116+
j#count := 4;
117+
// Chained read on both operands of a comparison.
118+
assert o#inner#count == p#inner#count
119+
};
120+
121+
// Paren-free chained incr/decr: `o#inner#count++` parses as `(o#inner#count)++`
122+
// because `fieldAccess` (prec 95) binds tighter than postfix `++`/`--` (prec 90),
123+
// and `leftassoc` lets the chain `o#inner#count` left-recurse.
124+
procedure chainedParenFreeIncrDecr()
125+
opaque
126+
{
127+
var o: Outer := new Outer;
128+
var i: Inner := new Inner;
129+
o#inner := i;
130+
o#inner#count := 10;
131+
o#inner#count++;
132+
assert o#inner#count == 11;
133+
o#inner#count--;
134+
assert o#inner#count == 10;
135+
// Postfix in expression position yields the OLD value (10); the cell becomes 11.
136+
var y: int := o#inner#count++;
137+
assert o#inner#count == 11;
138+
assert y == 10
139+
};
140+
141+
// Negative: an unconstrained chained field has no known value, so asserting a
142+
// concrete value must NOT be provable. Pins the chained read as non-vacuous.
143+
procedure chainedReadUnconstrainedFails()
144+
opaque
145+
{
146+
var o: Outer := new Outer;
147+
var i: Inner := new Inner;
148+
o#inner := i;
149+
assert o#inner#count == 7
150+
//^^^^^^^^^^^^^^^^^^^^^^^^^ error: assertion could not be proved
151+
};
152+
153+
// Negative (frame soundness): two distinct outers whose inner fields MAY alias.
154+
// A write through `a#inner` may be observed through `b#inner`, so the post-write
155+
// value of `b#inner#count` is not provably its pre-write value.
156+
procedure chainedMayAliasFails()
157+
opaque
158+
{
159+
var a: Outer := new Outer;
160+
var b: Outer := new Outer;
161+
var i: Inner := new Inner;
162+
a#inner := i;
163+
b#inner := i;
164+
a#inner#count := 5;
165+
assert b#inner#count == 0
166+
//^^^^^^^^^^^^^^^^^^^^^^^^^ error: assertion could not be proved
167+
};
168+
169+
// Negative (write isolation): a write through one chain must not be observable on
170+
// a genuinely disjoint, freshly-allocated object.
171+
procedure chainedWriteIsolationFails()
172+
opaque
173+
{
174+
var o: Outer := new Outer;
175+
var i: Inner := new Inner;
176+
var d: Inner := new Inner;
177+
o#inner := i;
178+
o#inner#count := 7;
179+
assert d#count == 7
180+
//^^^^^^^^^^^^^^^^^^^ error: assertion could not be proved
181+
};
182+
#end

0 commit comments

Comments
 (0)