Skip to content

Commit fd25552

Browse files
hyperpolymathclaude
andcommitted
docs: document Rust-like #{ } record syntax (#218 step 5)
#222 (merged) made bare { = block in expression position; record/ struct construction uses the #{ } sigil. Bring the docs in line: - spec.md: record grammar production '#{' …; field_init shorthand note; operational-semantics record-construction notation; +a new callout documenting the rule and the type/decl/pattern asymmetry (type positions, struct bodies, and record patterns keep plain {}). - README.adoc, frontier-guide.adoc, migration-playbook.adoc, WHAT-MAKES-IT-BRILLIANT.md: updated all expression-position record examples to #{ }. - idaptik-hitbox / idaptik-player-hp migration lessons: updated only the AffineScript 'after' record literals; ReScript 'before' blocks left intact. Type annotations, struct/enum declarations, and record patterns left as plain {} throughout (only value construction takes #{). Pre-existing non-record parse issues in docs/guides/warmup/*.affine are unrelated and out of scope. Refs #218 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8de3e0b commit fd25552

7 files changed

Lines changed: 37 additions & 25 deletions

File tree

README.adoc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ This includes code such as:
192192
type Texture = own { id: Int, width: Int, height: Int }
193193
194194
fn load_texture(path: ref String) -{IO + Exn[LoadError]}-> own Texture {
195-
Texture { id: 42, width: 1024, height: 1024 }
195+
Texture #{ id: 42, width: 1024, height: 1024 }
196196
}
197197
198198
fn render(scene: ref Scene, texture: ref Texture) -{Render}-> () {
@@ -231,7 +231,7 @@ fn authenticate(
231231
@linear conn: own Connection[{status: Unauthenticated}]
232232
) -{Session + IO}-> Connection[{status: Authenticated, user: String}] {
233233
let creds = recv();
234-
Connection { socket: conn.socket, status: Authenticated, user: creds.user }
234+
Connection #{ socket: conn.socket, status: Authenticated, user: creds.user }
235235
}
236236
237237
fn query(
@@ -287,8 +287,8 @@ fn greet[..r](person: {name: String, ..r}) -> String {
287287
"Hello, " ++ person.name
288288
}
289289
290-
let alice = {name: "Alice", age: 30, role: "Engineer"};
291-
let bob = {name: "Bob", department: "Sales"};
290+
let alice = #{name: "Alice", age: 30, role: "Engineer"};
291+
let bob = #{name: "Bob", department: "Sales"};
292292
293293
greet(alice);
294294
greet(bob);

docs/guides/WHAT-MAKES-IT-BRILLIANT.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,9 @@ fn updateAge[R](person: {age: Int, ...R}, newAge: Int) -> {age: Int, ...R} {
183183
{...person, age: newAge} // Preserves all other fields!
184184
}
185185
186-
let alice = {name: "Alice", age: 30, city: "NYC"};
186+
let alice = #{name: "Alice", age: 30, city: "NYC"};
187187
let older = updateAge(alice, 31);
188-
// older = {name: "Alice", age: 31, city: "NYC"} ✅
188+
// older = #{name: "Alice", age: 31, city: "NYC"} ✅
189189
```
190190

191191
**Brilliance:** **Flexibility of duck typing** + **safety of static types**.

docs/guides/frontier-guide.adoc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,8 @@ type Person = {name: String, age: Int}
290290
type Company = {name: String, employees: Int, revenue: Float}
291291
292292
// Both work — the row variable absorbs the extra fields:
293-
greet(Person { name: "Alice", age: 30 })
294-
greet(Company { name: "Acme", employees: 100, revenue: 9999.0 })
293+
greet(Person #{ name: "Alice", age: 30 })
294+
greet(Company #{ name: "Acme", employees: 100, revenue: 9999.0 })
295295
----
296296

297297
Row polymorphism is also how effects compose:
@@ -465,14 +465,14 @@ type FetchError = { message: String }
465465
466466
// Open a request (produces a linear resource):
467467
fn make_request(url: String, config: ref Config) -> own Request {
468-
Request { url: url, config: config }
468+
Request #{ url: url, config: config }
469469
}
470470
471471
// Send the request (consumes it — can't send twice):
472472
fn send(req: own Request) -> Result[Response, FetchError] / IO {
473473
IO.println("Sending request to " ++ req.url);
474474
// Real implementation would use the Async effect
475-
Ok(Response { body: "OK", status: 200 })
475+
Ok(Response #{ body: "OK", status: 200 })
476476
}
477477
478478
// The return type tells you: this can fail, and it does IO.

docs/guides/lessons/migrations/idaptik-hitbox.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ fn contains(r: Rect, p: Point) -> Bool {
4444
}
4545
4646
fn from_center(c: Point, size: Size) -> Rect {
47-
{ x: c.x - size.w / 2, y: c.y - size.h / 2, w: size.w, h: size.h }
47+
#{ x: c.x - size.w / 2, y: c.y - size.h / 2, w: size.w, h: size.h }
4848
}
4949
----
5050

docs/guides/lessons/migrations/idaptik-player-hp.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,14 +178,14 @@ fn update(p: mut Player, dt_ms: Int) -> Velocity {
178178
// Knockback decay + velocity emission
179179
if p.knockback.remaining_ms > 0 {
180180
p.knockback.remaining_ms = max(0, p.knockback.remaining_ms - dt_ms)
181-
let v = Velocity { x: p.knockback.vel_x, y: p.knockback.vel_y }
181+
let v = Velocity #{ x: p.knockback.vel_x, y: p.knockback.vel_y }
182182
if p.knockback.remaining_ms <= 0 {
183183
p.knockback.vel_x = 0
184184
p.knockback.vel_y = 0
185185
}
186186
v
187187
} else {
188-
Velocity { x: 0, y: 0 }
188+
Velocity #{ x: 0, y: 0 }
189189
}
190190
}
191191
----

docs/guides/migration-playbook.adoc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ type FileBuffer = own {
252252
open_: mut Bool,
253253
}
254254
255-
fn make() -> own FileBuffer { FileBuffer { data: [], open_: true } }
255+
fn make() -> own FileBuffer { FileBuffer #{ data: [], open_: true } }
256256
257257
fn read(b: ref FileBuffer) -> String {
258258
if b.open_ { String.join(b.data, "") } else { panic("closed") }
@@ -279,7 +279,7 @@ type ClosedBuffer = own { data: Array[String] }
279279
280280
effect IO { fn log(s: String); }
281281
282-
fn make() -> own OpenBuffer { OpenBuffer { data: [] } }
282+
fn make() -> own OpenBuffer { OpenBuffer #{ data: [] } }
283283
284284
// read takes a borrow — caller keeps the buffer:
285285
fn read(b: ref OpenBuffer) -> String { String.join(b.data, "") }
@@ -291,7 +291,7 @@ fn write(b: mut OpenBuffer, s: String) -> () / IO {
291291
}
292292
293293
// close consumes Open and returns Closed — "use after close" becomes a type error:
294-
fn close(b: own OpenBuffer) -> own ClosedBuffer { ClosedBuffer { data: b.data } }
294+
fn close(b: own OpenBuffer) -> own ClosedBuffer { ClosedBuffer #{ data: b.data } }
295295
----
296296

297297
What changed:

docs/spec.md

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -401,14 +401,26 @@ primary_expr = literal
401401
| '(' expr ')'
402402
| '(' expr { ',' expr } ')' (* tuple *)
403403
| '[' [ expr { ',' expr } ] ']' (* array *)
404-
| '{' [ field_init { ',' field_init } ] [ '..' expr ] '}' (* record *)
404+
| '#{' [ field_init { ',' field_init } ] [ '..' expr ] '}' (* record *)
405405
| 'resume' '(' [ expr ] ')' ; (* effect handler resume *)
406406
407-
field_init = ident [ ':' expr ] ; (* shorthand: {x} means {x: x} *)
407+
field_init = ident [ ':' expr ] ; (* shorthand: #{x} means #{x: x} *)
408408
409409
literal = int_lit | float_lit | char_lit | string_lit | bool_lit | unit_lit ;
410410
```
411411

412+
> **Record-literal syntax (`#{ … }`).** In *expression* position a bare
413+
> `{ … }` is always a **block**; record/struct construction uses the
414+
> `#{ … }` sigil — both anonymous (`#{ x: 1, y: 2 }`) and typed
415+
> (`Point #{ x: 1, y: 2 }`). This removes the block-vs-record ambiguity
416+
> by construction (and the struct-literal-in-`if`/`match`-scrutinee
417+
> hazard). Note the asymmetry: **type** positions (`fn f(r: {x: Int, ..s})`,
418+
> `type Box[T] = own { ptr: RawPtr[T] }`), **`struct` declaration bodies**,
419+
> and **record patterns** (`match r { { x, .. } => … }`) keep the plain
420+
> `{ … }` — only value construction takes `#{`. Migrating older sources:
421+
> rewrite each expression-position record literal `{``#{`; leave
422+
> declarations, type annotations, and patterns unchanged.
423+
412424
## 2.11 Patterns
413425

414426
```ebnf
@@ -740,7 +752,7 @@ E ::= []
740752
| E e Function position
741753
| v E Argument position
742754
| let x = E in e Let binding
743-
| {ℓ₁: v₁, ..., ℓᵢ: E, ...} Record construction
755+
| #{ℓ₁: v₁, ..., ℓᵢ: E, ...} Record construction
744756
| E.ℓ Field access
745757
| E[e] Array index (array position)
746758
| v[E] Array index (index position)
@@ -870,12 +882,12 @@ fn leakResource(file: own File) -> () / Pure {
870882
```affinescript
871883
fn stackOnly() -> Int / Pure {
872884
let x = 42; // Stack
873-
let r = {a: 1, b: 2}; // Stack (doesn't escape)
885+
let r = #{a: 1, b: 2}; // Stack (doesn't escape)
874886
r.a + x
875887
}
876888
877889
fn needsHeap() -> own {x: Int} / Pure {
878-
let r = {x: 42}; // Heap - returned (escapes)
890+
let r = #{x: 42}; // Heap - returned (escapes)
879891
r
880892
}
881893
```
@@ -886,7 +898,7 @@ fn needsHeap() -> own {x: Int} / Pure {
886898
type Box[T] = own { ptr: RawPtr[T] }
887899
888900
fn box[T](value: own T) -> own Box[T] / Pure {
889-
Box { ptr: heapAlloc(value) }
901+
Box #{ ptr: heapAlloc(value) }
890902
}
891903
892904
fn unbox[T](b: own Box[T]) -> own T / Pure {
@@ -1128,12 +1140,12 @@ fn getX[..r](record: {x: Int, ..r}) -> Int / Pure {
11281140
11291141
// Add a field to any record
11301142
fn withY[..r](record: {..r}, y: Int) -> {y: Int, ..r} / Pure {
1131-
{y: y, ..record}
1143+
#{y: y, ..record}
11321144
}
11331145
11341146
// Update a field
11351147
fn mapX[..r](record: {x: Int, ..r}, f: Int -> Int / Pure) -> {x: Int, ..r} / Pure {
1136-
{x: f(record.x), ..record}
1148+
#{x: f(record.x), ..record}
11371149
}
11381150
11391151
// Remove a field
@@ -1305,7 +1317,7 @@ fn Buffer.write[cap: Nat](
13051317
where (self.len + len(data) <= cap)
13061318
{
13071319
copy(data, mut self.data[self.len..]);
1308-
Ok(Buffer { data: self.data, len: self.len + len(data) })
1320+
Ok(Buffer #{ data: self.data, len: self.len + len(data) })
13091321
}
13101322
13111323
fn Buffer.free[cap: Nat](own self: Buffer[cap]) -> () / Pure {

0 commit comments

Comments
 (0)