-
-
Notifications
You must be signed in to change notification settings - Fork 376
Expand file tree
/
Copy path.plan
More file actions
155 lines (114 loc) · 7.26 KB
/
.plan
File metadata and controls
155 lines (114 loc) · 7.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# Plan: Unwrap 1-Field Structs During MIR→LIR Lowering
## Root Cause
The 8 List.fold record-accumulator bugs all involve records like `{total: Dec}` — a 1-field struct wrapping a Dec (i128). The dev backend's calling convention handles `struct_` layouts differently from scalar/i128 layouts (no aarch64 even-register alignment for multi-reg structs, different register save/restore paths). A 1-field struct is semantically identical to its inner type in memory, so it should never exist as a `struct_` layout.
## Design
During MIR→LIR lowering, unwrap 1-field records and single-tag single-payload unions so that:
- `{total: Dec}` gets layout `dec`, not `struct_(size=16, fields=[dec])`
- `[Foo(Blah)]` gets layout of `Blah`, not `tag_union(...)` (this already works for tag unions)
- Record field access on a 1-field record becomes a no-op (pass the value through)
- Record destructuring on a 1-field record binds directly to the value
- Record construction of a 1-field record is just the inner expression
- `Str.inspect` still renders `{ total: 10 }` because it dispatches on the **monotype** (which is still `.record`), not the layout
## Changes
### 1. `layoutFromRecord()` — unwrap 1-field records (MirToLir.zig ~line 178)
When `fields.len == 1`, return the inner field's layout directly instead of calling `putRecord`:
```zig
fn layoutFromRecord(self: *Self, record: anytype) !layout.Idx {
const fields = self.mir_store.monotype_store.getFields(record.fields);
if (fields.len == 0) return .zst;
if (fields.len == 1) return self.layoutFromMonotype(fields[0].type_idx);
// ... existing multi-field path
}
```
### 2. `lowerRecord()` — unwrap 1-field record construction (MirToLir.zig ~line 444)
When the monotype has 1 field, just lower and return the single field expression directly (no `struct_` wrapper):
```zig
fn lowerRecord(self: *Self, rec: anytype, mono_idx: Monotype.Idx, region: Region) !LirExprId {
const mir_fields = self.mir_store.getExprSpan(rec.fields);
if (mir_fields.len == 0) { ... }
if (mir_fields.len == 1) return self.lowerExpr(mir_fields[0]);
// ... existing multi-field path
}
```
### 3. `lowerRecordAccess()` — unwrap 1-field field access (MirToLir.zig ~line 873)
When the record monotype has 1 field, field access is a no-op — return the lowered record expression itself:
```zig
fn lowerRecordAccess(self: *Self, ra: anytype, mir_expr_id: MIR.ExprId, region: Region) !LirExprId {
const struct_mono = self.mir_store.typeOf(ra.record);
const mono = self.mir_store.monotype_store.getMonotype(struct_mono);
if (mono == .record) {
const fields = self.mir_store.monotype_store.getFields(mono.record.fields);
if (fields.len == 1) return self.lowerExpr(ra.record);
}
// ... existing multi-field path
}
```
### 4. Record destructure pattern — unwrap 1-field (MirToLir.zig ~line 1252)
When the record monotype has 1 field, the destructure pattern becomes just the single inner pattern:
```zig
.record_destructure => |rd| blk: {
const mir_patterns = self.mir_store.getPatternSpan(rd.destructs);
if (mir_patterns.len == 1) {
break :blk try self.lowerPattern(mir_patterns[0]);
}
// ... existing multi-field path
}
```
### 5. `inspectRecord()` — handle unwrapped 1-field records (MirToLir.zig ~line 1812)
This is the critical part for preserving Str.inspect output. When there's 1 field, the layout is NOT a struct anymore, so we can't do `struct_access`. Instead, the value IS the field value directly:
```zig
fn inspectRecord(self: *Self, value_expr: LirExprId, record: anytype, mono_idx: Monotype.Idx, region: Region) !LirExprId {
const fields = self.mir_store.monotype_store.getFields(record.fields);
if (fields.len == 0) return self.emitStrLiteral("{}", region);
if (fields.len == 1) {
// 1-field record: layout is unwrapped, so value_expr IS the field value
// Still render as "{ fieldname: <inspected_value> }"
const field = fields[0];
const field_name = self.getIdentText(field.name) orelse "?";
const label = try std.fmt.allocPrint(self.allocator, "{{ {s}: ", .{field_name});
defer self.allocator.free(label);
const save = self.scratch_lir_expr_ids.items.len;
defer self.scratch_lir_expr_ids.shrinkRetainingCapacity(save);
try self.scratch_lir_expr_ids.append(self.allocator, try self.emitStrLiteral(label, region));
const inspected = try self.expandStrInspect(value_expr, field.type_idx, region);
try self.scratch_lir_expr_ids.append(self.allocator, inspected);
try self.scratch_lir_expr_ids.append(self.allocator, try self.emitStrLiteral(" }", region));
const parts = self.scratch_lir_expr_ids.items[save..];
const span = try self.lir_store.addExprSpan(parts);
return self.lir_store.addExpr(.{ .str_concat = span }, region);
}
// ... existing multi-field path (unchanged)
}
```
### 6. Debug assertion in `putRecord` (store.zig ~line 350)
Add an assertion that putRecord is never called with exactly 1 field:
```zig
pub fn putRecord(self: *Self, ..., field_layouts: []const Layout, ...) !Idx {
std.debug.assert(field_layouts.len != 1); // 1-field records should be unwrapped by lowering
// ... existing code
}
```
### 7. Similar treatment for 1-field tuples (optional, check if needed)
`layoutFromTuple` and `lowerTuple` / `lowerTupleAccess` / `tuple_destructure` may need the same treatment. Check if `(x,)` single-element tuples exist in Roc and handle them if so.
## What About Single-Tag Single-Payload Unions?
`layoutFromTagUnion` (line 219-224) ALREADY unwraps these:
```zig
if (tags.len == 1) {
const payloads = ...;
if (payloads.len == 1) return self.layoutFromMonotype(payloads[0]);
}
```
And `lowerTag` already emits just the payload for single-tag single-payload (line 536-576).
Tag union destructuring also handles this. So no changes needed there.
## Other Situations to Consider
1. **Record update syntax** (`{..acc, field: val}`): Desugared to full record construction at MIR level, so `lowerRecord` handles it. With 1-field, the update is just the new value.
2. **Record equality** (`==`): After unwrapping, comparing `{total: Dec} == {total: Dec}` becomes comparing `Dec == Dec`, which uses the correct i128 equality path.
3. **Record in tag union payloads**: e.g. `Ok({total: Dec})` — the 1-field record layout is unwrapped to Dec, so the tag union payload is just Dec. This should work naturally.
4. **Record as function parameter/return**: After unwrapping, `{total: Dec}` is passed as Dec (i128), getting proper aarch64 alignment and i128 handling.
5. **Refcounting**: If the single field is refcounted (e.g. `{name: Str}`), the unwrapped layout is `str`, which has correct refcounting. No special handling needed.
6. **REPL rendering** (eval.zig `formatWithTypes`): The REPL renderer also uses the type system (nominal types) to determine rendering, not layouts. 1-field records will still render as `{ field: value }` because the type information is preserved.
## Expected Impact
- Fixes all 8 List.fold record-accumulator failures (the 1-field Dec record cases)
- The 2-field record tests already pass (32-byte structs work in the current codegen)
- No impact on Str.inspect rendering (still shows `{ total: 10 }`)
- Cleaner generated code (no struct wrapping for trivial records)