plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
f0da60c
— Peter John
2026-11-02T11:22:40+05:30
docs: slices + List design (Option B, copying sub-slice append, error-union get)
docs/plans/2026-11-02-go-slices.review.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Slices and List (review plan)
|
|
2
|
+
|
|
3
|
+
**Status:** approved 2026-11-02. Decisions: Option B, a sub-slice append copies, and `get` returns an error union.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
Two sequence types with one job each:
|
|
7
|
+
- **`List[T]`**: the growable, shared, mutable object. It's a `record`, and `push` changes it in place for everyone holding it. Its API is unchanged.
|
|
8
|
+
- **`[]T`** (spelled `Slice[T]` in std): a small value view (backing array, start, length). Slicing is free and copies nothing. Writes through `set` are visible to every view of the same array.
|
|
9
|
+
|
|
10
|
+
`Buffer` and `ByteSlice` go away: `List[Byte]` is the growable byte buffer and `[]Byte` is the byte view. `Str` holds a `[]Byte`, so substrings stop copying.
|
|
11
|
+
|
|
12
|
+
```plum
|
|
13
|
+
ps := List[Particle]()
|
|
14
|
+
ps.push(Particle(x: 0.0, y: 0.0)) # shared object, as today
|
|
15
|
+
spawn(ps) # the callee's push is visible here
|
|
16
|
+
|
|
17
|
+
view := ps.slice(1, 3) # []Particle: no copy, no allocation
|
|
18
|
+
view.set(0, p) # writes into ps's storage
|
|
19
|
+
for q := range view # fast loop over a view
|
|
20
|
+
q.x
|
|
21
|
+
|
|
22
|
+
w := "hello world".sub(6, 11) # "world": shares the bytes, O(1)
|
|
23
|
+
b := List[Byte]() # was Buffer()
|
|
24
|
+
b.writeStr("hi ")
|
|
25
|
+
t := b.toStr() # copies, so later writes to b don't change t
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Layout
|
|
29
|
+
```mermaid
|
|
30
|
+
flowchart LR
|
|
31
|
+
L["List[Int] record (data, size)"] --> A["Array[Int]: 1 2 3 4 _ _"]
|
|
32
|
+
V["view = ps.slice(1, 3): header (data, start=1, len=2), lives in locals"] --> A
|
|
33
|
+
```
|
|
34
|
+
- `List[T]` stays `record List[T](data: Array[T], size: Int)`. Elements are unboxed: numbers in native arrays, structs as struct-of-arrays, records as refs.
|
|
35
|
+
- `struct Slice[T](data: Array[T], start: Int, len: Int)`, with `[]T` as its type syntax. Passing and returning it costs three values, not an allocation.
|
|
36
|
+
- `Array[Byte]` becomes packed `array<i8>` (it's `array<i32>` today), so `[]Byte`, `List[Byte]` and `Str` all share one compact byte layout.
|
|
37
|
+
|
|
38
|
+
## Rulings
|
|
39
|
+
- **Appending to a sub-slice copies.** `[]T` has `append`, which returns the new header, and it is a compile error to drop the result. `slice(i, j)` sets capacity equal to length, so the first append on a view reallocates and never overwrites its parent's elements. Reading and `set` stay shared.
|
|
40
|
+
- **`get` returns an error union:** `get(i) -> error{OutOfBounds}!T` on both `List` and `[]T`, so `xs.get(i)!`, `catch` and `try` keep working.
|
|
41
|
+
- **No `xs[i]` / `xs[a:b]` syntax:** brackets mean generics. Element access is `get`/`set`, and slicing is `slice(i, j)` on a `List`, a `[]T` or a `Str`.
|
|
42
|
+
- **`Str` becomes `struct Str(bytes: []Byte)`.**
|
|
43
|
+
- `sub`, `slice`, `trim*` and `split` parts share bytes with the original.
|
|
44
|
+
- `Str` has no setter. Every way from a mutable `List[Byte]` or `[]Byte` to a `Str` copies (`toStr`, `Str.fromBytes`), as Go's `string(b)` does.
|
|
45
|
+
- Trade-off: a small substring keeps its big source alive. `s.clone()` detaches it.
|
|
46
|
+
- **Equality:** `==` on `[]T` is a compile error (Go). Use `equals`. `Str ==` keeps comparing contents.
|
|
47
|
+
- **Removals:** `Buffer` and `ByteSlice` are removed with no aliases. `List` keeps its whole API and gains `slice(i, j) -> []T`, while `sublist` stays as the copying form.
|
|
48
|
+
|
|
49
|
+
## Language work this needs
|
|
50
|
+
1. **Structs may hold references** (an `Array[T]`, a record, another struct with refs). This lifts today's "Int/Float/Bool/Byte/struct only" rule. A ref leaf is one more wasm local; in struct-of-arrays storage it is an array of refs.
|
|
51
|
+
2. **Generic structs** (`struct Slice[T]`), one copy per instantiation like every generic. `T`'s leaves decide the backing layout.
|
|
52
|
+
3. **Struct fields stored inline in records.** Today a struct field in a record is boxed (an extra allocation per store). Once `Str` is a struct, `record User(name: Str)` would pay that box on every construction. Inline means the record's wasm struct holds the struct's leaves directly.
|
|
53
|
+
4. **`[]T` type syntax** for any `T`. Today's fixed `[]Byte` builtin goes away.
|
|
54
|
+
|
|
55
|
+
## Work, in order
|
|
56
|
+
1. Structs with reference fields: checker rule, codegen leaves, ref arrays in struct-of-arrays, tests.
|
|
57
|
+
2. Generic structs: parser, checker and per-instantiation flattening.
|
|
58
|
+
3. Struct fields stored inline in records, covering construction, field read and write, spread update, `==`, and host unwrap.
|
|
59
|
+
4. Packed `Array[Byte]`; `Slice[T]` plus the `[]T` syntax (grammar regenerated with `--disable-optimizations`); the must-use check on `append`; `List.slice`.
|
|
60
|
+
5. `Str` on `[]Byte` with zero-copy `sub`/`slice`/`trim`/`split`; update the host-boundary unwraps (cli, runtime, test harnesses).
|
|
61
|
+
6. Replace `Buffer` with `List[Byte]` and `ByteSlice` with `[]Byte` across std (`Str`, `Json`, `Base64`, `Os`, `Http`, `Uuid`, ...), then the examples and README.
|
|
62
|
+
7. Benchmarks before and after (particle loop, Json parse, string split and trim), then a final review.
|
|
63
|
+
|
|
64
|
+
## Later performance work (no API change)
|
|
65
|
+
- **Escape analysis:** a `List` that never leaves its function keeps `data` and `size` in locals, with no record allocation.
|
|
66
|
+
- **Loops:** `for x := range list` reads `data` and `size` once before the loop.
|
|
67
|
+
- **Inlining:** `get`, `set` and `push` are inlined.
|
|
68
|
+
|
|
69
|
+
## Risks
|
|
70
|
+
- **Step 1** touches struct flattening everywhere (params, returns, `T?`, error unions, varargs, closures). **Step 3** touches every record access path. These two are the bulk of the work.
|
|
71
|
+
- **`Str` as a struct** changes its host-boundary shape (cli, runtime and three test harnesses unwrap it by hand today).
|
|
72
|
+
- **Packed bytes:** `Array[Byte]` going from i32 to i8 changes the get and set codegen for byte arrays and needs its own tests.
|
|
73
|
+
- **Closures capturing a `[]T`** capture the header by value, so an `append` inside doesn't reach the outer variable. A `List` captured by a closure keeps today's shared behavior.
|