Skip to content

Commit 2b9f28f

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 24b8f04 commit 2b9f28f

6 files changed

Lines changed: 250 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
@@ -9,7 +9,7 @@ module
99
-- Laurel dialect definition, loaded from LaurelGrammar.st
1010
-- NOTE: Changes to LaurelGrammar.st are not automatically tracked by the build system.
1111
-- Update this file (e.g. this comment) to trigger a recompile after modifying LaurelGrammar.st.
12-
-- Last grammar change: renamed strConcat token to `^`; added preIncr/preDecr/postIncr/postDecr; `return` value is now Option StmtExpr (supports a valueless return).
12+
-- Last grammar change: fieldAccess now @[prec(95), leftassoc] (paren-free `c#n++` and chained `a#b#c`).
1313
public import StrataDDM.AST
1414
import StrataDDM.BuiltinDialects.Init
1515
import StrataDDM.Integration.Lean.HashCommands

Strata/Languages/Laurel/Grammar/LaurelGrammar.st

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

42-
// Field access
43-
op fieldAccess (obj: StmtExpr, field: Ident): StmtExpr => @[prec(90)] obj "#" field;
42+
// Field access.
43+
// prec(95) binds tighter than the postfix incr/decr ops (prec 90) so `c#n++`
44+
// parses paren-free as `(c#n)++`. leftassoc lets `a#b#c` left-recurse for
45+
// chained field access.
46+
op fieldAccess (obj: StmtExpr, field: Ident): StmtExpr => @[prec(95), leftassoc] obj "#" field;
4447

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

Strata/Languages/Laurel/HeapParameterization.lean

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,13 @@ where
285285
let some qualifiedName := resolveQualifiedFieldName model fieldName
286286
| return [⟨ .Hole, source ⟩]
287287

288+
-- Recurse into the target so a nested field access (`a#b#c`) has its
289+
-- inner `FieldSelect` eliminated. Without this, the un-eliminated
290+
-- `FieldSelect` reaches a downstream guard and aborts with a
291+
-- `strata-bug`. This is the single point that unblocks *both* chained
292+
-- reads and chained writes: the write arm's `recurseOne target`
293+
-- bottoms out here for any nested target.
294+
let selectTarget ← recurseOne selectTarget
288295
let valTy := (model.get fieldName).getType
289296
let readExpr := ⟨ .StaticCall "readField" [mkMd (.Var (.Local heapVar)), selectTarget, mkMd (.StaticCall qualifiedName [])], source ⟩
290297
-- Unwrap Box: apply the appropriate destructor

Strata/Languages/Laurel/Resolution.lean

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -158,18 +158,41 @@ def resolveRef (name : Identifier) (source : Option FileRange := none)
158158
modify fun s => { s with errors := s.errors.push diag }
159159
return { name with uniqueId := none }
160160

161-
/-- Extract the UserDefined type name from a resolved target expression by looking up its scope entry. -/
162-
private def targetTypeName (target : StmtExprMd) : ResolveM (Option String) := do
161+
/-- Look up the declared type of `fieldName` within the scope of the composite
162+
type named `typeName`. Returns `none` if the type or field is unknown. -/
163+
private def fieldTypeInScope (typeName : String) (fieldName : Identifier) : ResolveM (Option HighType) := do
163164
let s ← get
164-
match target.val with
165-
| .Var (.Local ref) =>
166-
match s.scope.get? ref.text with
167-
| some (_, node) =>
168-
match node.getType.val with
169-
| .UserDefined typRef => pure (some typRef.text)
170-
| _ => pure none
165+
match s.typeScopes.get? typeName with
166+
| some typeScope =>
167+
match typeScope.get? fieldName.text with
168+
| some (_, node) => pure (some node.getType.val)
171169
| none => pure none
172-
| _ => pure none
170+
| none => pure none
171+
172+
/-- Determine the user-defined type name of a target expression.
173+
Handles a local variable (`.Var (.Local _)`, scope lookup) and a chained
174+
field access (`.Var (.Field tgt f)`, recursing on `tgt` then looking the
175+
field up in the inner type's scope). `fuel` keeps the function total;
176+
chains longer than `fuel` simply stop (the conservative, no-false-positive
177+
direction, as with `underlyingBaseType`). -/
178+
private def targetTypeNameAux (fuel : Nat) (target : StmtExprMd) : ResolveM (Option String) := do
179+
match fuel with
180+
| 0 => pure none
181+
| fuel + 1 =>
182+
let ty? ← match target.val with
183+
| .Var (.Local ref) => pure ((← get).scope.get? ref.text |>.map (·.2.getType.val))
184+
| .Var (.Field tgt fieldName) =>
185+
-- Resolve the inner target's type, then look the field up in its scope.
186+
match (← targetTypeNameAux fuel tgt) with
187+
| some innerTypeName => fieldTypeInScope innerTypeName fieldName
188+
| none => pure none
189+
| _ => pure none
190+
match ty? with
191+
| some (.UserDefined typRef) => pure (some typRef.text)
192+
| _ => pure none
193+
194+
private def targetTypeName (target : StmtExprMd) : ResolveM (Option String) :=
195+
targetTypeNameAux 100 target
173196

174197
/-- Try to resolve a field name via a type scope lookup. Returns `some id` on success. -/
175198
private def resolveFieldInTypeScope (typeName : String) (fieldName : Identifier) : ResolveM (Option Identifier) := do
@@ -269,13 +292,7 @@ private def incrDecrTargetType (target : VariableMd) : ResolveM (Option HighType
269292
| none => pure none
270293
| .Field tgt fieldName =>
271294
match (← targetTypeName tgt) with
272-
| some typeName =>
273-
match s.typeScopes.get? typeName with
274-
| some typeScope =>
275-
match typeScope.get? fieldName.text with
276-
| some (_, node) => pure (some node.getType.val)
277-
| none => pure none
278-
| none => pure none
295+
| some typeName => fieldTypeInScope typeName fieldName
279296
| none => pure none
280297
| .Declare param => pure (some param.type.val)
281298

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)