Skip to content

Commit c832a10

Browse files
reid-spencerclaude
andcommitted
Document what rc.10-57 changed about foreach, and fix what it caught
Three changes, all verified by compiling rather than read off the commits. `foreach` now binds its element over the loop body, so the natural example comes back OUT of the precise skip it was parked under and is gated again. The control matters more than the fix: `line.nosuch` is still an Error, so the binding carries the element's TYPE and did not become a hole admitting any member. `foreach` also iterates any resolvable collection, which answers the ruling this repo asked for — the dotted `order.lines` was not a deliberate restriction. The wrapper's iterable therefore moves back onto `order`, where an author would put it, instead of being hoisted to a direct field of the state to satisfy the old rule. `foreach k, v` destructuring is new and documented in both the concepts page and the reference, with the arity errors tabulated in both directions — one name over a mapping, two over anything else. Each documented example was compiled before prose was written around it. Caught by the upgrade: `type.md`'s `mapping from Pattern(...) to DictionaryEntry` named a type that does not exist. It passed only because a mapping's VALUE type was never resolved; rc.10-57 resolves it. Same shape as the interaction-group fix — a resolver gap closing turns a silent pass into a real failure, and the example was wrong all along. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 1e4f9ba commit c832a10

4 files changed

Lines changed: 66 additions & 10 deletions

File tree

scripts/validate-riddl-examples.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -314,13 +314,14 @@ def ind(txt: str, n: int) -> str:
314314
" record ExampleOrder is { id is String, number is String,\n"
315315
" total is Natural, status is String, isPaid is Boolean,\n"
316316
" isCancelled is Boolean, isRefunded is Boolean,\n"
317-
" confirmationNumber is String, items is many ExampleLine }\n"
318-
# The state record: `foreach ... in field X` takes a DIRECT field of
319-
# the state, handled message or function input -- a dotted path such
320-
# as `order.lines` is rejected -- so the iterable lives here.
317+
" confirmationNumber is String, items is many ExampleLine,\n"
318+
" lines is many ExampleLine,\n"
319+
" prices is mapping from String to ExampleLine }\n"
320+
# `foreach` resolves any path that lands on a collection (rc.10-57),
321+
# so the iterable sits where an author would put it -- on `order` --
322+
# rather than being hoisted to a direct field of the state.
321323
" record ExampleData is { note is String, itemCount is Natural,\n"
322-
" id is String, total is Natural, balance is Natural,\n"
323-
" lines is many ExampleLine }\n"
324+
" id is String, total is Natural, balance is Natural }\n"
324325
" command ExampleCommand is { note is String, cart is ExampleData,\n"
325326
" order is ExampleOrder, orderId is String, amount is Natural,\n"
326327
" limits is ExampleLimit }\n"

sites/riddl/docs/concepts/statement.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ Alternation — a non-exhaustive match without `default` draws a
217217
RIDDL's only loop, and deliberately bounded — there is no unbounded iteration
218218
in the language:
219219

220-
<!-- riddl: skip reason="riddlc rc.10-46 does not put the loop variable in scope; see riddl task 2026-08-09-foreach-loop-variable-not-in-scope" -->
220+
<!-- riddl: in-handler -->
221221
```riddl
222222
foreach line in field order.lines {
223223
send event LineShipped(sku = line.sku) to outlet Shipments
@@ -226,7 +226,31 @@ foreach line in field order.lines {
226226

227227
The collection is a `field` reference or a `let`-bound local whose type
228228
resolves to a collection: Sequence, Set, Graph, Table, Replica, Mapping, or a
229-
cardinality wrapper such as `many` or `optional`.
229+
cardinality wrapper such as `many` or `optional`. Any path that lands on a
230+
collection will do — the field need not be a direct field of the state, as
231+
`order.lines` above shows.
232+
233+
**The element is bound over the loop body**, and it carries the element's
234+
*type*, not merely its name: `line.sku` resolves, while `line.nosuch` is an
235+
Error. The binding ends at the closing brace, so referring to it after the
236+
loop is an Error too.
237+
238+
#### Destructuring a mapping
239+
240+
A mapping has two halves, so iterating one binds **two** names — a key and a
241+
value:
242+
243+
<!-- riddl: in-handler -->
244+
```riddl
245+
foreach sku, price in field order.prices {
246+
send event LineShipped(sku = sku) to outlet Shipments
247+
}
248+
```
249+
250+
The arity is checked both ways, and each mistake has its own message:
251+
252+
- one name over a mapping — *"binds a key AND a value, so it needs two names"*
253+
- two names over anything else — *"binds a second name only over a mapping"*
230254

231255
### Send, Tell, Yield and Reply
232256

sites/riddl/docs/concepts/type.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,13 @@ A type can be defined as a mapping from one type (the key) to another type
150150
(the value). For example, here is a dictionary definition that maps a word
151151
(lower case letters) to a type named DictionaryEntry that presumably
152152
contains all the things one would find in a dictionary entry.
153-
<!-- riddl: in-domain -->
153+
<!-- riddl-prelude
154+
record DictionaryEntry is {
155+
headword is String
156+
definition is String
157+
}
158+
-->
159+
<!-- riddl: in-context -->
154160
```riddl
155161
type dictionary = mapping from Pattern("[a-z]+") to DictionaryEntry
156162
```

sites/riddl/docs/references/language-reference.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1422,7 +1422,32 @@ foreach item in myLocalCollection {
14221422

14231423
The collection is a `field` reference or a `let`-bound local whose type resolves
14241424
to a collection — Sequence, Set, Graph, Table, Replica, Mapping, or a
1425-
cardinality wrapper such as `many` or `optional`.
1425+
cardinality wrapper such as `many` or `optional`. **Any resolvable path that
1426+
lands on a collection is accepted**; it need not be a direct field of the
1427+
enclosing state, which is why `order.lines` above works.
1428+
1429+
The element name is **bound over the loop body** and carries the element's
1430+
type, so `line.sku` resolves and `line.nosuch` is an Error. The binding ends at
1431+
the closing brace.
1432+
1433+
#### Destructuring a mapping
1434+
1435+
A mapping yields a key and a value, so iterating one binds **two** names:
1436+
1437+
<!-- riddl: in-handler -->
1438+
```riddl
1439+
foreach sku, price in field order.prices {
1440+
send event LineShipped(sku = sku) to outlet Shipments
1441+
}
1442+
```
1443+
1444+
Arity is a validation rule rather than a grammatical one — both shapes parse —
1445+
and it is checked in both directions:
1446+
1447+
| Mistake | Message |
1448+
|---|---|
1449+
| one name over a mapping | *binds a key AND a value, so it needs two names* |
1450+
| two names over a non-mapping | *binds a second name only over a mapping* |
14261451

14271452
### Require Statement
14281453

0 commit comments

Comments
 (0)