fibers: Scheduler.deinit + struct-literal init cleanup

Scheduler.deinit closes the bounded leaks B1 documented: it reaps any leftover
ready fibers, frees every heap Task from go (now tracked via a task_allocs
field), frees the timers/io_waiters/task_allocs List backings, and closes the
lazily-opened kqueue fd. Terminal + idempotent; the per-spawn/go closure env
remains unfreeable (language limitation). Locked by
examples/concurrency/1820-concurrency-fiber-scheduler-deinit.sx, which exercises
every freed resource under a tracking GPA (freed by deinit: 5, kq reset to -1).

Also converts plain-struct '= ---'+field-assign init to '.{ ... }' literal init
where '---' carries no meaning: Scheduler.init, Dock.make, and the fiber
examples 1811/1813/1814/1816 (partial literals zero-fill the index-filled array
fields). Unions, '---'-feature tests, the 0154 regression, documented
generic-pack gaps, and loop/conditional inits are intentionally left on '---'.
This commit is contained in:
agra
2026-06-22 09:45:33 +03:00
parent 1e0015d6b4
commit 55ed9a248e
12 changed files with 299 additions and 53 deletions

View File

@@ -4,7 +4,35 @@ Companion to [PLAN-FIBERS.md](PLAN-FIBERS.md). Update after every step (one step
per the cadence rule). New corpus category: `18xx` concurrency.
## Last completed step
**B1.5 — END-TO-END M:1 validation — STREAM B1 COMPLETE.** A single capstone exercises the whole
**B1 follow-up — `Scheduler.deinit` (close the bounded leaks).** Post-B1 non-blocking cleanup: a
terminal `deinit` on `library/modules/std/sched.sx`'s `Scheduler` releases the resources B1 documented
as leaked. Frees, in order: (1) any fibers still enqueued ready (leak-safety net for `spawn`/`go`
without `run()``munmap` stack + free struct; a suspended off-queue fiber is unreachable, but a clean
`run()` aborts on orphans so none survive it); (2) every heap `*Task` from `go` — newly tracked via a
`task_allocs: List(*void)` field appended in `go` (the scheduler otherwise has no handle on its generic
`Task($R)`s); (3) the three `List` backings (`task_allocs`/`timers`/`io_waiters`, all grown through
`own_allocator`); (4) the lazily-opened kqueue fd (`close`, reset to `-1`). NOT freed (unchanged
language limitation): the per-`spawn`/`go` closure env (sx exposes no env-free). Idempotent (rests on
`List.deinit` nulling `items` + the `kq`/`ready_head` resets); TERMINAL contract — no scheduler-owned
handle (`*Task`, `*Fiber`, the scheduler) is usable after `deinit`.
- Added a canonical `close :: (i32) -> i32 extern libc` (matches the dedupe-canonical signature 1816
already uses) + the `task_allocs` field.
- Locked by `examples/concurrency/1820-concurrency-fiber-scheduler-deinit.sx` (aarch64-macOS `.build
{"target":"macos"}`, runs end-to-end): one run touches every freed resource — a SLEEPER (`timers`), a
pipe READER `block_on_fd` + WRITER (kqueue fd + `io_waiters`), two `go` tasks (`Task`s + `task_allocs`)
— then `deinit`. Verified by a tracking `GPA`: `freed by deinit: 5`, `live after deinit: 5` (the
RESIDUAL = the 5 documented closure envs, not a bug), `kq open after run: true` → `kq after deinit:
-1` (the genuinely-open kqueue fd is closed), `read: 3 [97 98 99]` (the fd path actually ran). Counts
captured into locals BEFORE printing (`print` allocates format temporaries through the same GPA).
- **Adversarially reviewed (worker):** no real memory-safety bug in the supported (deinit-after-`run`)
path — reap-loop reads `f.next` before freeing `f`, the three freed List backings + Tasks + kq are all
disjoint + scheduler-owned, no over-free, idempotent. The one CRITICAL it raised was a DOC contradiction
(step-(1) defensive reap vs step-(2) "post-run only"), reconciled by spelling out the terminal contract.
Its 0154-over-store concern (`.{}`→`List` writes in `init` could clobber `kq`) was PROBED and cleared:
`kq == -1` immediately after `init`, all fields clean. Suite GREEN **759/0**.
### Earlier — B1.5 — END-TO-END M:1 validation — STREAM B1 COMPLETE
A single capstone exercises the whole
colorblind pure-sx async runtime together: the M:1 scheduler (B1.5a) + suspending fiber-task async
`go`/`wait` (B1.4a) + deterministic virtual-time `sleep`/`now_ms` (B1.4b), over the `abi(.naked)`
`swap_context` on guarded `mmap` stacks (B1.0B1.3). `examples/concurrency/1817-concurrency-fiber-m1-end-to-end.sx`:
@@ -366,11 +394,14 @@ async (B1.4a: `Task($R)`/`go`/`wait`/`cancel`), deterministic virtual-time timer
`clock_ms`/`now_ms`/`sleep`, timer-driven `run`), AND real fd readiness via kqueue (B1.4c: lazy `kq`,
`io_waiters`, `block_on_fd`, run-loop Mode 2) — all over the `abi(.naked)` `swap_context` on guarded
`mmap` stacks (B1.0B1.3), reusing `std/net/kqueue.sx`. Every park path (timer sleep, fd block, raw
suspend) is balanced through `wake` (which evicts stale timer + fd waiters — the UAF guards). Locked
by `18xx` 18001817 (naked-asm, context-snapshot, blocking async, the switch + §10.7 stress gate +
guarded stacks + Win64 sibling, scheduler round-robin, suspend/wake, async go/wait/cancel, sim-timer
ordering, timer early-wake eviction, kqueue pipe I/O, and the **1817 end-to-end capstone**). Suite
GREEN **755/0**, master committed.
suspend) is balanced through `wake` (which evicts stale timer + fd waiters — the UAF guards). A terminal
`deinit` (B1 follow-up) closes the previously-documented leaks: heap `Task`s (tracked via `task_allocs`),
the `timers`/`io_waiters`/`task_allocs` List backings, and the kqueue fd; the per-`spawn`/`go` closure
env remains unfreeable (language limitation). Locked by `18xx` 18001820 (naked-asm, context-snapshot,
blocking async, the switch + §10.7 stress gate + guarded stacks + Win64 sibling, scheduler round-robin,
suspend/wake, async go/wait/cancel, sim-timer ordering, timer early-wake eviction, kqueue pipe I/O, the
**1817 end-to-end capstone**, sleep-negative/double-wait guards, and **1820 scheduler-deinit**). Suite
GREEN **759/0**, committed.
Future work (none blocking B1): a **linux epoll twin** of `block_on_fd` (mirror via `std/net/epoll`;
OS-neutral facade `std.event`) — B1.4c wired macOS kqueue only; routing the suspending async through
@@ -471,11 +502,13 @@ fibers/Io/scheduler code yet. Grounded floor facts:
## Next step
**Stream B1 is COMPLETE — no next step in this stream.** The pure-sx M:1 async runtime is feature-
complete and committed (18001817 green, 755/0). Pick up **Stream B2** (channels / structured cancel /
async stdlib) as a fresh carve (PLAN-CHANNELS.md), OR one of the documented non-blocking follow-ups:
the linux `epoll` twin of `block_on_fd`, a `Scheduler.deinit` (free the kq fd / heap Tasks / drain
leaks), `Future(void)`/`timeout` (needs issue 0150), or routing the suspending async through the
erased `context.io` for the M:N model. None of these block B1.
complete and committed (18001820 green, 759/0), now WITH a `Scheduler.deinit` closing the bounded
leaks. Pick up **Stream B2** (channels / structured cancel / async stdlib) as a fresh carve
(PLAN-CHANNELS.md), OR one of the remaining non-blocking follow-ups: the linux `epoll` twin of
`block_on_fd`, `Future(void)`/`timeout` (needs issue 0150), or routing the suspending async through the
erased `context.io` for the M:N model. (`Scheduler.deinit` — DONE, see Last completed step.) None of
these block B1. The closure-env leak survives `deinit` (no language affordance to free a closure env);
revisit if/when sx grows closure-env ownership.
**Deferred (future B1.4c sibling): the linux epoll twin of `block_on_fd`.** B1.4c wired the **macOS
kqueue** path only (the host is aarch64-macOS). The linux mirror would register interest via
@@ -521,6 +554,14 @@ incomplete); a dedicated effort; lambda workers are the idiom meanwhile.
codegen (`.unresolved` reaching LLVM emission, `src/backend/llvm/types.zig:196`). Found in the
B1.5a review; the scheduler doesn't use it (array-field index + `.*` only). Filed for its own
session: `issues/0155-scalar-pointer-index-llvm-panic.{md,sx}`.
- **✅ issue 0158 — FIXED** — a plain `union` struct-literal (`b : Overlay = .{ f = 3.14 }`) fell
through the generic struct-literal path (`getStructFields` empty for a union → malformed
`structInit`, overlapping zero-fill clobbered the member → silent `0.0`). Fix: `lowerStructLiteral`
detects a plain-union target → new `lowerUnionLiteral` (`src/ir/lower/stmt.zig`) writes each named
member into a union-sized slot via the assignment-path lvalue resolver, then loads it back.
Single-arm only (one direct member, or same-arm promoted members); overlapping/different-arm/
positional literals are diagnosed. specs.md updated. Regressions: `examples/types/0194` +
`examples/diagnostics/1191`.
- **✅ issue 0157 — FIXED** (B1.4a) — a user generic `ufcs` method whose name collides with a
stdlib re-export resolved via last-wins `fn_ast_map` with no receiver filtering → wrong overload →
`$R` unbound → LLVM panic. Fix: `selectUfcsGenericByReceiver` (`src/ir/lower/call.zig`) — most
@@ -593,6 +634,17 @@ incomplete); a dedicated effort; lambda workers are the idiom meanwhile.
trusted. `18xx` asserts program-emitted ordering contracts, not raw interleaving.
## Log
- **B1 follow-up — `Scheduler.deinit`.** Closes the bounded leaks B1 documented. Added a `task_allocs:
List(*void)` field (appended in `go` so the scheduler can reach its generic `Task($R)`s) + a canonical
`close` extern, then a terminal idempotent `deinit`: reap leftover ready fibers (`munmap` + free) →
free tracked Tasks → `List.deinit` the 3 backings → `close` the lazy kqueue fd (reset `-1`). Closure
envs stay unfreeable (documented). Probe-observed the accounting under a tracking GPA (deinit drives
live allocs 7→3 in a spawn+sleep+2×go run; residual = envs). Locked by
`1820-concurrency-fiber-scheduler-deinit.sx` (one run hits timers + kqueue fd + Tasks; `freed by
deinit: 5`, `live after deinit: 5` (env residual), `kq open after run: true`→`kq after deinit: -1`,
`read: 3 [97 98 99]`), `.build {"target":"macos"}`. Adversarial review: no real UAF/over-free in the
supported deinit-after-`run` path; reconciled a doc contradiction (terminal-contract wording); 0154
over-store concern probed + cleared (`kq == -1` right after `init`). Suite GREEN **759/0**.
- **B1.4c — real fd-readiness blocking via kqueue (macOS).** De-risked first with a no-scheduler probe
(confirmed `size_of(Kevent)==32` and the pipe→kevent roundtrip: `kq_wait` returned 1, `out.ident ==
read_fd`, `out.filter == -1`, `out.data == 1` — the struct layout reads the fd back correctly). Then

View File

@@ -37,9 +37,7 @@ append :: (sh: *Shared, v: i64) {
}
main :: () -> i64 {
sh : Shared = ---;
sh.n = 0;
sh.done[0] = 0; sh.done[1] = 0; sh.done[2] = 0;
sh : Shared = .{ n = 0 }; // seq[] + done[] zero-filled
s := sched.Scheduler.init();
ps := @s;

View File

@@ -32,8 +32,7 @@ Log :: struct { seq: [16]i64; n: i64; }
rec :: (l: *Log, v: i64) { l.seq[l.n] = v; l.n = l.n + 1; }
main :: () -> i64 {
lg : Log = ---;
lg.n = 0;
lg : Log = .{ n = 0 }; // seq[] zero-filled
s := sched.Scheduler.init();
ps := @s;

View File

@@ -43,8 +43,7 @@ Log :: struct { ids: [16]i64; ts: [16]i64; n: i64; }
rec :: (l: *Log, id: i64, t: i64) { l.ids[l.n] = id; l.ts[l.n] = t; l.n = l.n + 1; }
main :: () -> i64 {
lg : Log = ---;
lg.n = 0;
lg : Log = .{ n = 0 }; // ids[] + ts[] zero-filled
s := sched.Scheduler.init();
ps := @s;

View File

@@ -41,10 +41,7 @@ S :: struct {
}
main :: () -> i64 {
st : S = ---;
st.wrote = false;
st.read_n = 0;
st.read_done = false;
st : S = .{ wrote = false, read_n = 0, read_done = false }; // bytes[] zero-filled
fds : [2]i32 = ---;
if pipe(@fds[0]) != 0 {

View File

@@ -0,0 +1,124 @@
// Stream B1 (fibers) — `Scheduler.deinit` releases the scheduler's owned heap
// + fd resources, closing the documented bounded leaks (kq fd / heap Tasks /
// List backings). Verified by a tracking `GPA`: deinit drives the live
// allocation count DOWN, and resets the kqueue fd to -1.
//
// Scenario (one run that touches every freed resource):
// - a SLEEPER fiber `sleep(5)`s → exercises the `timers` List
// - a READER fiber `block_on_fd`s a pipe → exercises the kqueue fd + the
// `io_waiters` List
// - a WRITER fiber writes 3 bytes → makes the pipe readable
// - two `go` tasks compute 42 / 7 → exercise the heap `Task`s +
// the `task_allocs` List
// After `run()` drains all of it, `deinit()` frees: the 2 heap Tasks, the
// `timers` / `io_waiters` / `task_allocs` List backings, and CLOSES the kqueue
// fd (resetting `kq` to -1). The Fibers were already reaped during `run()`.
//
// WHAT IT PROVES (the contract; numbers below are the snapshot):
// - `freed by deinit: N` — live allocations reclaimed by `deinit` (> 0).
// - `live after deinit` — the RESIDUAL. This is NOT zero and NOT a bug: it is
// exactly the documented closure-env leak — one heap env per `spawn`/`go`
// that sx cannot free (the runtime has no name for the env pointer). deinit
// reclaims everything it CAN; the env residual is a language limitation.
// - `kq open after run: 1` then `kq after deinit: -1` — the lazily-opened
// kqueue fd was genuinely open after the fd round and is closed by deinit.
// - `read: 3 [97 98 99]` — the fd path actually ran (reader blocked, woke via
// kqueue, read 'a' 'b' 'c'), so the kq we close is a real, used fd.
//
// Counts are captured into locals BEFORE any `print` — `print` itself allocates
// format temporaries through the same GPA, which would otherwise pollute the
// reading.
//
// aarch64-macOS-pinned (`.build {"target":"macos"}`, matches host → runs
// end-to-end): sched.sx's switch asm + the kqueue path are per-arch/Apple.
#import "modules/std.sx";
sched :: #import "modules/std/sched.sx";
// Raw libc fd primitives — canonical signatures (the extern dedupe rejects a
// divergent re-binding of the same C symbol). `close` matches sched.sx's own.
pipe :: (fds: *i32) -> i32 extern libc "pipe";
read :: (fd: i32, buf: [*]u8, count: usize) -> isize extern libc "read";
write :: (fd: i32, buf: [*]u8, count: usize) -> isize extern libc "write";
close :: (fd: i32) -> i32 extern libc "close";
S :: struct {
read_n: i64;
bytes: [8]u8;
read_done: bool;
}
main :: () -> i64 {
st : S = .{ read_n = 0, read_done = false }; // bytes[] zero-filled; read() fills it
fds : [2]i32 = ---;
if pipe(@fds[0]) != 0 {
print("1820: pipe() failed\n");
return 1;
}
read_fd := fds[0];
write_fd := fds[1];
// Captured under the GPA scope; printed after it closes.
after_run : i64 = 0;
after_deinit : i64 = 0;
kq_open_run : bool = false;
kq_after : i32 = 0;
gpa := mem.GPA.init();
push Context.{ allocator = xx gpa, data = null } {
s := sched.Scheduler.init();
ps := @s; pst := @st;
// SLEEPER — arms a virtual-time timer, then parks.
ps.spawn(() => { ps.sleep(5); });
// READER — blocks on the empty pipe until kqueue reports it readable.
mk_reader :: (ps: *sched.Scheduler, pst: *S, rfd: i32) {
ps.spawn(() => {
ps.block_on_fd(rfd, true);
n := read(rfd, xx @pst.bytes[0], xx 3);
pst.read_n = xx n;
pst.read_done = true;
});
}
// WRITER — writes 'a' 'b' 'c', making the pipe readable.
mk_writer :: (ps: *sched.Scheduler, wfd: i32) {
ps.spawn(() => {
buf : [3]u8 = ---;
buf[0] = xx 97; buf[1] = xx 98; buf[2] = xx 99;
write(wfd, xx @buf[0], xx 3);
});
}
mk_reader(ps, pst, read_fd);
mk_writer(ps, write_fd);
// Two async tasks — heap Tasks tracked for deinit to free.
ps.go(() -> i64 => 42);
ps.go(() -> i64 => 7);
ps.run();
after_run = gpa.alloc_count;
kq_open_run = s.kq >= 0;
ps.deinit();
after_deinit = gpa.alloc_count;
kq_after = s.kq;
}
print("read: {} [", st.read_n);
i := 0;
while i < st.read_n {
if i > 0 { print(" "); }
print("{}", st.bytes[i]);
i = i + 1;
}
print("]\n");
print("freed by deinit: {}\n", after_run - after_deinit);
print("live after deinit: {}\n", after_deinit);
print("kq open after run: {}\n", kq_open_run);
print("kq after deinit: {}\n", kq_after);
close(read_fd);
close(write_fd);
return 0;
}

View File

@@ -0,0 +1 @@
{ "target": "macos" }

View File

@@ -0,0 +1,5 @@
read: 3 [97 98 99]
freed by deinit: 5
live after deinit: 5
kq open after run: true
kq after deinit: -1

View File

@@ -32,6 +32,10 @@ kqb :: #import "modules/std/net/kqueue.sx";
mmap :: (addr: *void, len: i64, prot: i32, flags: i32, fd: i32, off: i64) -> *void extern libc "mmap";
mprotect :: (addr: *void, len: i64, prot: i32) -> i32 extern libc "mprotect";
munmap :: (addr: *void, len: i64) -> i32 extern libc "munmap";
// Canonical libc `close` signature `(i32) -> i32` — must match any other
// binding in the program (the extern dedupe rejects a divergent one). Used by
// `Scheduler.deinit` to close the lazily-opened kqueue fd.
close :: (fd: i32) -> i32 extern libc "close";
abort :: () -> noreturn extern libc "abort";
PROT_NONE :: 0;
@@ -109,33 +113,36 @@ Scheduler :: struct {
// `block_on_fd` opens it, so a pure-compute /
// virtual-timer scheduler never opens a kqueue
// fd (no leak for the common case). Once opened it
// lives for the scheduler's lifetime; there is no
// deinit yet, so it leaks one fd at program exit
// (bounded, harmless — same class as the spawn
// env / go Task leaks documented above).
// lives for the scheduler's lifetime; `deinit`
// closes it (and resets this back to -1).
io_waiters: List(IoWaiter); // fibers parked on fd readiness, grown through
// `own_allocator` (long-lived-container rule: a
// waiter outlives the `block_on_fd` call's scope).
// --- deinit bookkeeping: heap Tasks allocated by `go` --------------------
task_allocs: List(*void); // every heap `*Task` from `go`, recorded so
// `deinit` can free them. The scheduler does not
// otherwise know its Tasks (they are generic
// `Task($R)` handed back to the caller); without
// this list they would leak. Grown through
// `own_allocator` (a Task outlives the `go` call).
// Construct a scheduler BY VALUE (allocator value-return convention).
// Captures the current `context.allocator` into `own_allocator` — fibers and
// their heap `Fiber` structs outlive their spawn scope, so all internal
// allocation must go through this captured (long-lived) allocator, not
// whatever transient one happens to be current at a later call.
init :: () -> Scheduler {
s : Scheduler = ---;
s.current = null;
s.ready_head = null;
s.ready_tail = null;
s.own_allocator = context.allocator;
s.next_id = 0;
s.n_spawned = 0;
s.n_suspended = 0;
s.clock_ms = 0;
s.timers = .{};
s.kq = -1; // lazy: opened by the first block_on_fd
s.io_waiters = .{};
return s;
// Literal init (by value). `sched_ctx` is intentionally unnamed — the
// partial literal zero-fills it, and it is written by the first
// `swap_context` before ever being read. `kq = -1` is the lazy sentinel
// (opened by the first `block_on_fd`).
return Scheduler.{
current = null, ready_head = null, ready_tail = null,
own_allocator = context.allocator,
next_id = 0, n_spawned = 0, n_suspended = 0,
clock_ms = 0, timers = .{}, kq = -1, io_waiters = .{}, task_allocs = .{}
};
}
// Spawn a fiber running `body`. Heap-allocates the `Fiber` and a guarded
@@ -445,6 +452,65 @@ Scheduler :: struct {
abort();
}
}
// Release the scheduler's owned resources. TERMINAL: the scheduler is dead
// after this — no scheduler-owned handle (the `*Task`s returned by `go`, a
// `*Fiber` from `spawn`, the scheduler itself) may be used afterward; doing
// so is a use-after-free, the universal deinit contract. Idempotent: a
// second `deinit` is a no-op (it rests on `List.deinit` nulling `items` +
// zeroing `len`, and on `kq`/`ready_head` being reset below).
//
// Call AFTER `run()` has returned: a clean `run()` leaves the ready queue
// empty and aborts loudly on any orphaned suspend, so nothing is mid-flight
// and every `task_allocs` entry is a COMPLETED task (safe to free). Frees,
// in order:
// 1. any fibers still enqueued ready — a leak-SAFETY NET for the misuse
// path (`spawn`/`go` without a following `run()`, or after it returned),
// NOT a blessed reuse pattern: reaping a `go`'s fiber here while step (2)
// frees its paired `*Task` is self-consistent ONLY because the contract
// already forbade touching those handles post-`deinit`. A suspended
// (off-queue) fiber is unreachable from here, but a clean `run()` never
// leaves one (it aborts on an orphaned suspend);
// 2. every heap `*Task` from `go` (recorded in `task_allocs`);
// 3. the three `List` backings (`task_allocs`, `timers`, `io_waiters`),
// each grown through `own_allocator`;
// 4. the kqueue fd, if `block_on_fd` ever opened it (lazy `-1` otherwise).
//
// NOT freed (documented language limitation, unchanged): one closure env per
// `spawn`/`go`. The env is heap-allocated at the closure-literal site and sx
// exposes no way to free it (the scheduler cannot name the env pointer), so
// it leaks until program exit — bounded by the spawn/go count, invisible
// under the default GPA. Freeing it needs a closure-env-ownership affordance.
deinit :: (self: *Scheduler) {
// (1) Reap leftover ready fibers: unmap the stack, free the Fiber.
f := self.ready_head;
while f != null {
nxt := f.next;
munmap(f.stack_region, f.stack_len);
self.own_allocator.dealloc_bytes(xx f);
f = nxt;
}
self.ready_head = null;
self.ready_tail = null;
// (2) Free every heap Task allocated by `go`.
i := 0;
while i < self.task_allocs.len {
self.own_allocator.dealloc_bytes(self.task_allocs.items[i]);
i = i + 1;
}
// (3) Free the List backings (all grown through `own_allocator`).
self.task_allocs.deinit(self.own_allocator);
self.timers.deinit(self.own_allocator);
self.io_waiters.deinit(self.own_allocator);
// (4) Close the kqueue fd if it was ever opened (lazy: -1 if never used).
if self.kq >= 0 {
close(self.kq);
self.kq = -1;
}
}
}
// --- the context switch (naked) + first-entry trampoline -------------------
@@ -719,6 +785,9 @@ go :: ufcs (self: *Scheduler, work: Closure() -> $R) -> *Task($R) {
t.waiter = null;
t.sched = self;
t.canceled = 0;
// Record the heap Task so `deinit` can free it (the scheduler otherwise has
// no handle on its generic Tasks). Long-lived: a Task outlives this call.
self.task_allocs.append(xx t, self.own_allocator);
self.spawn(() => {
// Cooperative cancel: skip the work entirely if cancel already landed
// before this fiber was scheduled (saves the compute + side effects). A

View File

@@ -472,20 +472,20 @@ Dock :: struct {
on_dock: ?Closure(i64, DockZone);
make :: (interaction: *DockInteraction, delta_time: *f32) -> Dock {
d : Dock = ---;
d.children = List(ViewChild).{};
d.alignments = List(Alignment).{};
d.interaction = interaction;
d.delta_time = delta_time;
d.background = null;
d.corner_radius = 0.0;
d.hint_size = 40.0;
d.hint_color = Color.rgba(77, 153, 255, 153);
d.hint_active_color = Color.rgba(77, 153, 255, 230);
d.preview_color = Color.rgba(77, 153, 255, 64);
d.enable_corners = true;
d.on_dock = null;
d
Dock.{
children = List(ViewChild).{},
alignments = List(Alignment).{},
interaction = interaction,
delta_time = delta_time,
background = null,
corner_radius = 0.0,
hint_size = 40.0,
hint_color = Color.rgba(77, 153, 255, 153),
hint_active_color = Color.rgba(77, 153, 255, 230),
preview_color = Color.rgba(77, 153, 255, 64),
enable_corners = true,
on_dock = null
}
}
add_panel :: (self: *Dock, panel: DockPanel) {