// readline — the flibc raw line editor's pure core, ported to Flash from its // hand-written Zig (the second FlashOS module after `tokenize`). // // The kernel console stays "dumb": all line editing lives in userland as a // per-byte state machine. This port carries the pure, host-tested core — the // byte transition, the cursor-aware edit ops, and the command-history ring — // and leaves out the SVC-driven drivers (inline-asm, gated to // aarch64-freestanding) and the host test blocks, exactly as the `tokenize` // port left out its tests. // // First port to exercise the expression/statement grammar tier end to end: a // `switch` with a value list (`'\r', '\n'`), an inclusive range // (`0x20...0x7e`), an inline `if`-expression arm, and `label: { … }` block arms // whose value is a `break :blk …`; a typed union literal (`Action{ .echo = byte }`); // `[*]mut volatile u8` for the alignment-safe byte copy; struct field default values // (`len usize = 0`, `bytes [HIST_LINE_CAP]u8 = undefined`, `stash HistSlot = .{}`); // struct methods over `*State` / `*History`; and optional returns (`?[]u8`). // Doc comments are carried through verbatim. The `//` rationale comments are // Flash-source notes (ordinary comments are not re-emitted); the core lowers to // Zig whose token stream matches the reference, modulo Flash's canonical layout // (mandatory braces, expanded containers). /// Forward byte copy through a *volatile* destination (non-overlapping); returns /// the number of bytes copied (min of the two lengths). flibc payloads run with /// SCTLR_EL1.A strict alignment asserted, and the ReleaseSmall loop-idiom /// vectorizer will happily widen a plain `while (i16-byte by-value return that /// LLVM materialises with a `str q` (16-byte NEON) store through the AArch64 /// indirect-result register x8 — and that store takes an alignment data abort /// under SCTLR_EL1.A when the caller's result slot is only 8-aligned (the /// struct's natural alignment). A bare enum returns in a register: no store, no /// fault. The whole-line replace carries no payload here; replaceLine is void /// and the driver captures the pre-swap extent itself. `.none` means the op was /// a no-op at a boundary (buffer full, cursor at an edge) and nothing is drawn. pub const Edit = enum { none, /// A byte was inserted at the cursor: repaint buf[pos-1..len], then step /// the cursor back (len-pos) columns to sit just after the new byte. insert, /// The byte before the cursor was removed: backspace, repaint buf[pos..len], /// blank the vacated last column, step back (len-pos+1) columns. delete, /// Cursor moved one column left (a bare backspace, no erase). left, /// Cursor moved one column right (re-emit the byte it stepped over). right, } /// Line editor state. `buf` is the caller-provided fixed-size buffer /// (rule 1 — no realloc); `len` is the committed-byte count and `pos` the /// cursor offset, with the invariant `pos <= len <= buf.len`. Submission /// yields `buf[0..len]`. Plain `step`/`readline` are append-only and ignore /// `pos`; the cursor ops below back `readlineEdit`. pub const State = struct { buf []mut u8, len usize = 0, pos usize = 0, pub fn init(buf []mut u8) State { return .{ .buf = buf } } pub fn slice(self *State) []u8 { return self.buf[0..self.len] } /// Insert `c` at the cursor, shifting the tail right. No-op when full. pub fn insertAt(self *mut State, c u8) Edit { if self.len >= self.buf.len { return .none } const v [*]mut volatile u8 = #ptrCast(self.buf.ptr) // alignment-safe, see copyBytes var i = self.len while i > self.pos { v[i] = v[i - 1] i -= 1 } v[self.pos] = c self.len += 1 self.pos += 1 return .insert } /// Delete the byte before the cursor, shifting the tail left. No-op at col 0. pub fn backspace(self *mut State) Edit { if self.pos == 0 { return .none } const v [*]mut volatile u8 = #ptrCast(self.buf.ptr) // alignment-safe, see copyBytes for i in self.pos..self.len { v[i - 1] = v[i] } self.len -= 1 self.pos -= 1 return .delete } /// Move the cursor one column left. No-op at col 0. pub fn moveLeft(self *mut State) Edit { if self.pos == 0 { return .none } self.pos -= 1 return .left } /// Move the cursor one column right. No-op at end of line. pub fn moveRight(self *mut State) Edit { if self.pos >= self.len { return .none } self.pos += 1 return .right } /// Replace the whole line with `s` (clipped to capacity), cursor to end. /// Backs history recall. Void (not Edit-returning): the driver captures the /// pre-swap extent before calling, so the redraw needs nothing back. pub fn replaceLine(self *mut State, s []u8) { self.len = copyBytes(self.buf, s) self.pos = self.len } } /// What the driver should do with a byte after `step` runs. Pure data — /// the driver translates this into sys_write_fd / return calls; tests /// inspect it directly. pub const Action = union(enum) { /// Byte consumed silently (overflow drop, ignored control char, /// ^D mid-line, or BS on empty buffer). none, /// Byte accepted into the buffer; echo this byte to fd 1. echo u8, /// One byte was popped; emit the standard "\x08 \x08" rubout. backspace, /// TAB — request completion of the current token. The completing driver /// extends the buffer in place; plain readline ignores it. complete, /// Line is complete; driver should return the buffered slice. submit, /// ^D on an empty line — driver returns Outcome.eof. eof, /// ^C — driver returns Outcome.abandoned; no echo (caller redraws). abandon, } /// Driver outcome for a full `readline` call. pub const Outcome = union(enum) { /// Submitted line; slice points into the caller-provided buffer. line []u8, /// Stream EOF — ^D on an empty line, or sys_read returned <= 0. eof, /// User cancelled the line (^C). Caller drops the buffer. abandoned, } /// One-byte state transition for the plain (append-only) editor. Pure: no /// syscalls, no allocator. `readlineEdit` does not use this — it routes bytes /// through keys.Decoder and the State cursor ops instead. pub fn step(state *mut State, byte u8) Action { return switch byte { '\r', '\n' => .submit, 0x03 => .abandon, 0x04 => if (state.len == 0) Action.eof else Action.none, 0x09 => .complete, 0x08, 0x7f => blk: { if state.len == 0 { break :blk Action.none } state.len -= 1 break :blk Action.backspace }, 0x20...0x7e => blk: { if state.len >= state.buf.len { break :blk Action.none } state.buf[state.len] = byte state.len += 1 break :blk Action{ .echo = byte } }, else => .none, } } // ---- command history (caller-owned ring; rule 1 — no allocator / no .bss) --- /// Per-entry capacity for a recorded history line. Matches fsh's LINE_MAX; a /// longer submitted line is clipped when recorded (recall still works, the /// stored copy is just shorter). Lines hold only printable bytes, so a slot /// needs no NUL terminator — `len` delimits it. pub const HIST_LINE_CAP usize = 256 /// One history slot. The caller declares an array of these on its stack and /// hands a slice to History.init; History itself never allocates. Slot bytes /// are written by `push` before they are ever read back, so an `undefined` /// array is a valid backing store (History.count gates every read). pub const HistSlot = struct { bytes [HIST_LINE_CAP]u8 = undefined, len usize = 0, } /// A fixed-capacity ring of recently submitted lines, navigated with Up/Down. /// Pure (host-tested): `older`/`newer` walk the ring and hand back the recalled /// line; the driver paints it with State.replaceLine. The in-progress line is /// stashed on the first Up so Down past the newest entry restores it. pub const History = struct { slots []mut HistSlot, stash HistSlot = .{}, head usize = 0, // ring index of the next write (mod slots.len) count usize = 0, // filled slots, saturating at slots.len nav usize = 0, // 0 = editing the live line; k = the k-th newest recalled pub fn init(slots []mut HistSlot) History { return .{ .slots = slots } } // The k-th newest entry (k in 1..count): the newest sits one behind head. fn entry(self *History, back usize) []u8 { m := self.slots.len i := (self.head + m - back) % m return self.slots[i].bytes[0..self.slots[i].len] } /// Record a submitted line and leave browse mode. A blank line and an exact /// repeat of the most-recent entry are not recorded (ignoredups). pub fn push(self *mut History, line []u8) { self.nav = 0 if self.slots.len == 0 || line.len == 0 { return } if self.count > 0 && eql(self.entry(1), line) { return } slot := &self.slots[self.head] slot.len = copyBytes(&slot.bytes, line) self.head = (self.head + 1) % self.slots.len if self.count < self.slots.len { self.count += 1 } } /// Step one entry older. The first step stashes `current` (the live, /// unsubmitted line) so `newer` can restore it. Returns the recalled line, /// or null at the oldest entry / on empty history (caller draws nothing). pub fn older(self *mut History, current []u8) ?[]u8 { if self.count == 0 { return null } if self.nav == 0 { self.stash.len = copyBytes(&self.stash.bytes, current) self.nav = 1 return self.entry(1) } if self.nav < self.count { self.nav += 1 return self.entry(self.nav) } return null } /// Step one entry newer. Returns the recalled line, the stashed live line /// when stepping off the newest entry, or null when not browsing. pub fn newer(self *mut History) ?[]u8 { if self.nav == 0 { return null } if self.nav > 1 { self.nav -= 1 return self.entry(self.nav) } self.nav = 0 return self.stash.bytes[0..self.stash.len] } /// Leave browse mode without recording a line (^C path). pub fn resetNav(self *mut History) { self.nav = 0 } fn eql(a []u8, b []u8) bool { if a.len != b.len { return false } for i in 0..a.len { if a[i] != b[i] { return false } } return true } }