refactor: List is slice-backed { items: []T; cap } — directly iterable

items is now a []T slice whose .len IS the live element count (cap = allocated
capacity), so a List iterates directly: `for xs.items (e) { ... }`. A
`len :: (self) -> i64 #get => items.len` accessor keeps `xs.len` reads working;
`.len` WRITES become `.items.len`. List stays 24 bytes (`[]T`=16 + cap=8).

- list.sx: append/ensure_capacity/deinit rewritten for the slice backing. deinit
  guards the free on `cap > 0` (true ownership) and resets via explicit
  ptr=null/len=0 (a `.{}` slice assignment yields a garbage len; `.[]` is the
  empty-slice literal but can't be assigned to a generic []T — both worked around).
- Compiler coupling updated: comptime_vm makeStringList/readStringList write/read
  items as a {ptr,len} fat pointer at field 0 + cap at field 1; control_flow
  listView views an `items: []T` slice (keeps the legacy {[*]T,len} shape too).
- Migrated List `.len` writes to `.items.len` in sched.sx + ui/{render,pipeline,
  glyph_cache} + platform/{sdl3,android,uikit}.
- Snapshots: List's type-table layout changed → ~40 .ir + memory/0800 (items now
  prints as a slice) regenerated; diagnostics/1183 retargeted to a genuine
  many-pointer (xs.items is a slice now). Example memory/0840 locks for-each.
This commit is contained in:
agra
2026-06-22 11:55:19 +03:00
parent 9d3a019670
commit 5cc45a2b38
58 changed files with 59733 additions and 59479 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -15,10 +15,9 @@ sum :: (s: []i64) -> i64 {
}
main :: () -> i32 {
xs : List(i64) = .{};
xs.append(10);
xs.append(20);
r := sum(xs.items); // [*]i64 → []i64 — needs xs.items[0..xs.len]
a : [4]i64 = .[10, 20, 30, 40];
mp : [*]i64 = xx @a[0]; // a genuine many-pointer (carries no length)
r := sum(mp); // [*]i64 → []i64 — rejected; needs mp[0..len]
print("{}\n", r);
return 0;
}

View File

@@ -1,5 +1,5 @@
error: a many-pointer '[*]T' does not coerce to a slice '[]T' implicitly (it carries no length) — slice it with a length: ptr[0..len]
--> examples/diagnostics/1183-diagnostics-many-pointer-to-slice-rejected.sx:21:10
--> examples/diagnostics/1183-diagnostics-many-pointer-to-slice-rejected.sx:20:10
|
21 | r := sum(xs.items); // [*]i64 → []i64 — needs xs.items[0..xs.len]
| ^^^^^^^^^^^^^
20 | r := sum(mp); // [*]i64 → []i64 — rejected; needs mp[0..len]
| ^^^^^^^

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,43 @@
// List is `{ items: []T; cap }` — `items` is a `[]T` slice whose `.len` IS the
// live element count, so a List is directly iterable with a `for`-each, and
// `xs.len` reads the live count via a `#get` accessor. Exercises append (incl.
// a realloc past the initial cap of 4), for-each, parallel for-with-index,
// empty iteration, direct `for xs` over the List, and truncation via items.len.
#import "modules/std.sx";
main :: () -> i64 {
xs : List(i64) = .{};
i := 0;
while i < 6 { xs.append((i + 1) * 10); i = i + 1; } // 10..60; grows 4 -> 8
print("len={} cap>={}\n", xs.len, if xs.cap >= 6 then 1 else 0); // len=6 cap>=1
// for-each over the items slice
sum := 0;
for xs.items (e) { sum = sum + e; }
print("foreach sum={}\n", sum); // 210
// parallel for with index
print("indexed:");
for xs.items, 0.. (e, ix) { if ix % 2 == 0 { print(" {}@{}", e, ix); } }
print("\n"); // 10@0 30@2 50@4
// direct `for xs` over the List value (listView path)
s2 := 0;
for xs (e) { s2 = s2 + e; }
print("for-list sum={}\n", s2); // 210
// len via #get used as a loop bound + indexing
acc := 0;
j := 0;
while j < xs.len { acc = acc + xs.items[j]; j = j + 1; }
print("indexed sum={}\n", acc); // 210
// truncate to empty via items.len, then iterate (zero iterations)
xs.items.len = 0;
cnt := 0;
for xs.items (e) { cnt = cnt + 1; }
print("after trunc: len={} iters={}\n", xs.len, cnt); // len=0 iters=0
return 0;
}

View File

@@ -1 +1 @@
List__i32{items: [*]i32@0xADDR, len: 5, cap: 8}
List__i32{items: [1, 3, 4, 1, 5], cap: 8}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,6 @@
len=6 cap>=1
foreach sum=210
indexed: 10@0 30@2 50@4
for-list sum=210
indexed sum=210
after trunc: len=0 iters=0

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -400,7 +400,7 @@ impl Platform for AndroidPlatform {
}
poll_events :: (self: *AndroidPlatform) -> []Event {
self.events.len = 0;
self.events.items.len = 0;
sx_android_drain_touches(self, @self.events);
result : []Event = ---;
result.ptr = self.events.items;

View File

@@ -144,7 +144,7 @@ impl Platform for SdlPlatform {
}
poll_events :: (self: *SdlPlatform) -> []Event {
self.events.len = 0;
self.events.items.len = 0;
sdl_event : SDL_Event = .none;
while SDL_PollEvent(@sdl_event) {
if sdl_event == {

View File

@@ -379,7 +379,7 @@ impl Platform for UIKitPlatform {
result : []Event = ---;
result.ptr = self.events.items;
result.len = self.events.len;
self.events.len = 0;
self.events.items.len = 0;
result
}

View File

@@ -2,45 +2,58 @@
// directly — std.sx re-exports `List`.
#import "modules/std/core.sx";
// `items` is a `[]T` slice whose `.len` IS the live element count, so a `List`
// is directly iterable: `for xs.items (e) { ... }`. `cap` is the allocated
// capacity (`>= items.len`); the buffer spans `cap` elements, the slice spans
// the live `items.len`. The `len` `#get` accessor exposes the live count as
// `xs.len` (read), delegating to `items.len`; writes use `xs.items.len`.
List :: struct ($T: Type) {
items: [*]T = null;
len: i64 = 0;
items: []T = .[]; // empty slice ({ptr, len=0}); `.[]` is the empty-slice
// literal — `.{}` would init the slice's underlying
// {ptr,len} struct (and currently yields a garbage len).
cap: i64 = 0;
// No-paren read accessor: `xs.len` → the live element count.
len :: (self: *List(T)) -> i64 #get => self.items.len;
append :: (list: *List(T), item: T, alloc: Allocator = context.allocator) {
if list.len >= list.cap {
if list.items.len >= list.cap {
new_cap := if list.cap == 0 then 4 else list.cap * 2;
new_items : [*]T = xx alloc.alloc_bytes(new_cap * size_of(T));
if list.len > 0 {
memcpy(new_items, list.items, list.len * size_of(T));
alloc.dealloc_bytes(list.items);
new_ptr : [*]T = xx alloc.alloc_bytes(new_cap * size_of(T));
if list.items.len > 0 {
memcpy(new_ptr, list.items.ptr, list.items.len * size_of(T));
alloc.dealloc_bytes(list.items.ptr);
}
list.items = new_items;
list.items.ptr = new_ptr; // keep the live len; only the buffer moves
list.cap = new_cap;
}
list.items[list.len] = item;
list.len += 1;
list.items.ptr[list.items.len] = item; // write at the live index (within cap)
list.items.len += 1;
}
ensure_capacity :: (list: *List(T), n: i64, alloc: Allocator = context.allocator) {
if list.cap >= n { return; }
new_cap := if list.cap == 0 then 4 else list.cap;
while new_cap < n { new_cap = new_cap * 2; }
new_items : [*]T = xx alloc.alloc_bytes(new_cap * size_of(T));
if list.len > 0 {
memcpy(new_items, list.items, list.len * size_of(T));
alloc.dealloc_bytes(list.items);
new_ptr : [*]T = xx alloc.alloc_bytes(new_cap * size_of(T));
if list.items.len > 0 {
memcpy(new_ptr, list.items.ptr, list.items.len * size_of(T));
alloc.dealloc_bytes(list.items.ptr);
}
list.items = new_items;
list.items.ptr = new_ptr;
list.cap = new_cap;
}
deinit :: (list: *List(T), alloc: Allocator = context.allocator) {
if list.items != null {
alloc.dealloc_bytes(list.items);
// `cap > 0` is the ownership signal: a List holds an allocated buffer
// ONLY after a growth set `cap`. Guarding on `cap` (not `items.ptr`)
// makes deinit idempotent and safe on a never-grown / borrowed-items
// list — and `.[]` leaves a len-0 slice whose ptr need not be null.
if list.cap > 0 {
alloc.dealloc_bytes(list.items.ptr);
}
list.items = null;
list.len = 0;
list.items.ptr = null;
list.items.len = 0;
list.cap = 0;
}
}

View File

@@ -647,7 +647,7 @@ remove_timer :: (self: *Scheduler, idx: i64) {
self.timers.items[i] = self.timers.items[i + 1];
i = i + 1;
}
self.timers.len = self.timers.len - 1;
self.timers.items.len = self.timers.items.len - 1;
}
// Remove a pending sleep timer referencing fiber `f`, if any. A fiber has at
@@ -676,7 +676,7 @@ remove_io_waiter :: (self: *Scheduler, idx: i64) {
self.io_waiters.items[i] = self.io_waiters.items[i + 1];
i = i + 1;
}
self.io_waiters.len = self.io_waiters.len - 1;
self.io_waiters.items.len = self.io_waiters.items.len - 1;
}
// Remove a pending fd-waiter referencing fiber `f`, if any. A fiber has at most

View File

@@ -575,7 +575,7 @@ GlyphCache :: struct {
return; // shaped_buf already has the result
}
self.shaped_buf.len = 0;
self.shaped_buf.items.len = 0;
if text.len == 0 { return; }
if is_ascii(text) {

View File

@@ -141,7 +141,7 @@ UIPipeline :: struct {
// Reset render_tree nodes (backing is stale after arena reset)
self.render_tree.nodes.items = null;
self.render_tree.nodes.len = 0;
self.render_tree.nodes.items.len = 0;
self.render_tree.nodes.cap = 0;
push Context.{ allocator = xx build_arena, data = context.data } {

View File

@@ -47,7 +47,7 @@ RenderTree :: struct {
}
clear :: (self: *RenderTree) {
self.nodes.len = 0;
self.nodes.items.len = 0;
self.generation += 1;
}

View File

@@ -305,10 +305,14 @@ max :: (a: $T, b: T) -> T {
}
List :: struct ($T: Type) {
items: [*]T;
len: i64;
items: []T; // a slice; items.len is the live count, so a List is
cap: i64; // directly iterable: `for xs.items (e) { ... }`
append :: (self: *List(T), item: T) { ... }
// `#get` property accessor: read via no-paren field syntax (`xs.len`),
// not `xs.len()`. Takes only `self`; a real field of the same name wins.
len :: (self: *List(T)) -> i64 #get => self.items.len;
}
```

View File

@@ -2516,18 +2516,22 @@ pub const Vm = struct {
for (items, 0..) |s, i| {
try self.writeField(table, backing + i * str_size, .string, try self.makeStringValue(table, s));
}
// The List struct: field 0 = items ([*]string), 1 = len (i64), 2 = cap (i64).
// The List struct: field 0 = items (`[]string` fat pointer {ptr, len}),
// field 1 = cap (i64). `items.ptr` = backing, `items.len` = cap = n.
const addr = self.machine.allocBytes(table.typeSizeBytes(list_ty), 8);
const items_fty = table.memberType(list_ty, 0) orelse
return self.failMsg("comptime List builder: result type has no items field");
const len_fty = table.memberType(list_ty, 1) orelse
return self.failMsg("comptime List builder: result type has no len field");
const cap_fty = table.memberType(list_ty, 2) orelse
const cap_fty = table.memberType(list_ty, 1) orelse
return self.failMsg("comptime List builder: result type has no cap field");
const n: Reg = @bitCast(@as(i64, @intCast(items.len)));
try self.writeField(table, addr + fieldOffset(table, list_ty, 0), items_fty, backing);
try self.writeField(table, addr + fieldOffset(table, list_ty, 1), len_fty, n);
try self.writeField(table, addr + fieldOffset(table, list_ty, 2), cap_fty, n);
// Write the `items` slice as a {ptr, len} fat pointer at field 0.
if (items_fty.isBuiltin() or table.get(items_fty) != .slice)
return self.failMsg("comptime List builder: items field is not a slice");
const items_off = addr + fieldOffset(table, list_ty, 0);
try self.machine.writeWord(items_off, table.pointer_size, backing);
try self.machine.writeWord(items_off + table.pointer_size, 8, n);
// cap = live count (the backing is exactly `n` elements).
try self.writeField(table, addr + fieldOffset(table, list_ty, 1), cap_fty, n);
return addr;
}
@@ -2547,8 +2551,11 @@ pub const Vm = struct {
fn readStringList(self: *Vm, table: *const types.TypeTable, list_ty: TypeId, addr: Addr) Error![]const []const u8 {
if (list_ty.isBuiltin() or table.get(list_ty) != .@"struct")
return self.failMsg("comptime List reader: arg type is not a List struct");
const items_ptr = try self.machine.readWord(addr + fieldOffset(table, list_ty, 0), table.pointer_size);
const len: usize = @intCast(try self.machine.readWord(addr + fieldOffset(table, list_ty, 1), 8));
// `items` is a `[]string` fat pointer at field 0: ptr at offset 0, len
// at offset `pointer_size` (there is no separate `len` field now).
const items_off = addr + fieldOffset(table, list_ty, 0);
const items_ptr = try self.machine.readWord(items_off, table.pointer_size);
const len: usize = @intCast(try self.machine.readWord(items_off + table.pointer_size, 8));
const str_size = table.typeSizeBytes(.string);
const out = self.gpa.alloc([]const u8, len) catch return self.failMsg("comptime List reader: out of memory");
var i: usize = 0;

View File

@@ -249,14 +249,34 @@ pub fn lowerWhile(self: *Lowering, we: *const ast.WhileExpr) Ref {
return self.builder.constInt(0, .void);
}
/// View a `List(T)`-like struct (`{ items: [*]T, len, … }`) as its backing
/// `items` pointer + element type + `len`, so `for list: (x)` iterates the
/// elements. Null for anything that isn't such a struct.
/// View a `List(T)`-like struct as its backing `items` pointer + element type
/// + live length, so `for list (x)` iterates the elements. Two shapes:
/// - CURRENT `List`: `{ items: []T, cap }` — `items` is a `[]T` slice whose
/// own `.ptr`/`.len` ARE the backing pointer and live count.
/// - LEGACY: `{ items: [*]T, len, … }` — a many-pointer `items` paired with a
/// sibling `len` field (kept so a user struct of that shape still iterates).
/// Null for anything that isn't such a struct.
pub fn listView(self: *Lowering, value: Ref, ty: TypeId) ?struct { data: Ref, data_ty: TypeId, len: Ref } {
if (ty.isBuiltin()) return null;
const info = self.module.types.get(ty);
if (info != .@"struct") return null;
const items_id = self.module.types.internString("items");
// Current shape: an `items: []T` slice — view via its `.ptr`/`.len`.
for (info.@"struct".fields, 0..) |f, i| {
if (f.name == items_id and !f.ty.isBuiltin() and self.module.types.get(f.ty) == .slice) {
const slice_val = self.builder.emit(.{ .struct_get = .{ .base = value, .field_index = @intCast(i) } }, f.ty);
const elem = self.module.types.get(f.ty).slice.element;
const mp_ty = self.module.types.manyPtrTo(elem);
return .{
.data = self.builder.emit(.{ .data_ptr = .{ .operand = slice_val } }, mp_ty),
.data_ty = mp_ty,
.len = self.builder.emit(.{ .length = .{ .operand = slice_val } }, .i64),
};
}
}
// Legacy shape: `items: [*]T` + a sibling `len` field.
const len_id = self.module.types.internString("len");
var items_idx: ?u32 = null;
var items_ty: TypeId = .unresolved;