Flash 309 lines
// 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 (i<n) dst[i]=src[i]` copy into a
/// `str q` (16-byte NEON) store — which takes an alignment data abort when the
/// destination is only 8-aligned, as a history slot or the line buffer routinely
/// is. Volatile accesses are never widened or merged, so this is alignment-safe
/// by construction; the buffers are at most one line, so the byte-wise cost is
/// irrelevant. Mirrors the hand-rolled providers in src/utilc.zig and
/// flibc/mem.zig that dodge the same hazard. The cursor shift loops below cast
/// the line buffer the same way for the same reason.
fn copyBytes(dst []mut u8, src []u8) usize {
n := #min(dst.len, src.len)
const d [*]mut volatile u8 = #ptrCast(dst.ptr)
for i in 0..n {
d[i] = src[i]
}
return n
}
/// A cursor-aware edit directive returned by the State cursor ops. A plain
/// enum on purpose: a payload-carrying union is a >16-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
}
}