lib: fmt fprint family over io.handle; remove the fdsink workaround (#5)
Graduates the fmt fprint family (fprint/fprintf/fprintln/fprintfln + internal putbytes/writeone/format*) from io.stream to io.handle, so a file (fd) prints directly through io.write's file-arm (commit-1). Removes the fdsink placeholder -- the fake-stream-vtable-over-os.write shim that stood in for the missing handle. The 8 stdio wrappers route over os.STD{OUT,ERR}_FILENO (new i32 filenos in lib/os; os is the import floor, so it can't hold an io.file-typed handle like Hare's os::stdout_file -- consumers cast i32 to io.file). Migrates the fd-shim sentinel tests 777/780/781 to fprint-over-handle as their headers designed, cstage-only per the pre-existing #209 (fmt is wwstage-uncompilable). Regenerates the 6 os-embedding combined.ww.
This commit is contained in:
@@ -29,7 +29,7 @@ Signatures mirror Hare too, modulo:
|
||||
- Call-site variadic sugar matches Hare. `fn f(args: T...)` declares
|
||||
a Hare-style variadic; call sites either gather N args into a
|
||||
fresh `[]T` (`fmt.println(42, "hi", true)`) or forward an existing
|
||||
slice with `xs...` (`fdprintln(fd, args...)`). The bare `T...` form
|
||||
slice with `xs...` (`fprintln(h, args...)`). The bare `T...` form
|
||||
in tagged unions still means spread-flatten (`(...inner | E)`);
|
||||
the two uses don't overlap because `T...` only attaches to a
|
||||
*param* decl. `lib/fmt` ships both the print family (`print` /
|
||||
|
||||
180
lib/fmt/fmt.ww
180
lib/fmt/fmt.ww
@@ -1,35 +1,23 @@
|
||||
// fmt — formatting writers. Mirrors Hare's lib/fmt subset. Project #94
|
||||
// fold-eFinal.
|
||||
//
|
||||
// Two sinks behind one surface, the distinction baked into the name:
|
||||
// fold-eFinal; io fold-2 (#5) graduated the sink to [[io.handle]].
|
||||
//
|
||||
// fprint / fprintln / fprintf / fprintfln
|
||||
// write to an [[io.stream]] (= `*io.vtable`) —
|
||||
// Hare's primary surface. Errors via [[io.error]].
|
||||
// fdprint / fdprintln / fdprintf / fdprintfln
|
||||
// write to a raw fd via [[os.write]], wrapped in a
|
||||
// stack-resident [[fd_ctx]] vtable. NO Hare
|
||||
// counterpart — a pre-handle shim. Hare routes
|
||||
// fmt's fd sinks through `io::handle = (io::file
|
||||
// | int)` (ref/hare/fmt/wrappers.ha:9-25); ww has
|
||||
// no handle sum yet, so these stand in until io
|
||||
// fold-2 (#5) lands the handle port, at which
|
||||
// point each fdNNN folds into fNNN-over-handle and
|
||||
// the fd-prefixed names disappear.
|
||||
// write to an [[io.handle]] (= `(io.file |
|
||||
// io.stream)`) — Hare's primary surface
|
||||
// (ref/hare/fmt/print.ha:13). Errors via
|
||||
// [[io.error]]. A file handle (raw fd) and a stream
|
||||
// (`*io.vtable`) both flow in; [[io.write]]
|
||||
// dispatches per arm.
|
||||
//
|
||||
// The process-stdio wrappers (print/println/errorln/printf/printfln/
|
||||
// errorfln/fatal/fatalf) route through the fd family on fd 1 / fd 2.
|
||||
// errorfln/fatal/fatalf) route through the fprint family over a file
|
||||
// handle built from os.STD{OUT,ERR}_FILENO. The #5 handle convergence
|
||||
// retired the prior `fd_ctx` shim — the fake-stream vtable around
|
||||
// os.write that stood in before io.write took a handle.
|
||||
//
|
||||
// Call sites take Hare's variadic shape: `fmt.println(42, "hi", true)`
|
||||
// gathers the args into a `[]formattable` slice; wrappers forward via
|
||||
// `args...`.
|
||||
//
|
||||
// Cast workaround per #206-payoff (ken: KEEP the explicit casts; they
|
||||
// are cgen-neutral and sidestep the #214 over-acceptance surface). The
|
||||
// `(&fn_name): *io.<role>` cast at each fd_ctx vtable store is the
|
||||
// Hare-faithful minimum-touch route; the #206 cast-drop is gated on
|
||||
// #214. The fd sinks construct nomem and widen to io.error explicitly
|
||||
// rather than using `os.trywrite(...)?` for the no-handle interim.
|
||||
|
||||
package fmt;
|
||||
|
||||
@@ -87,42 +75,12 @@ fn i64dec(v: i64) str = {
|
||||
// arm when the first in-tree caller needs it.
|
||||
export type formattable = (i64 | str | bool | rune | f64);
|
||||
|
||||
// fd_ctx — vt at offset 0 for the intrusive io.stream→*fd_ctx cast.
|
||||
// Single tagged field (vt) means the multi-tagged-field struct-lit
|
||||
// drop in #207 doesn't bite; every fd wrapper stack-allocates fd_ctx
|
||||
// inside its own frame and dispatches without escaping `&c.vt`.
|
||||
export type fd_ctx = struct {
|
||||
vt: io.vtable,
|
||||
fd: i32,
|
||||
};
|
||||
|
||||
// fdsinkread — eof-only sink; the fd half of fmt is write-only. The
|
||||
// unused-`buf` parameter matches io.reader's signature.
|
||||
fn fdsinkread(s: io.stream, buf: []u8) (size | io.eof | io.error) = {
|
||||
let e: io.eof;
|
||||
return e;
|
||||
};
|
||||
|
||||
// fdsinkwrite — recover fd via intrusive cast, dispatch one os.write.
|
||||
// -errno collapses to a nomem-widened io.error. Construction-then-widen
|
||||
// (vs `os.trywrite(...)?`) for the no-handle interim.
|
||||
fn fdsinkwrite(s: io.stream, buf: []u8) (size | io.error) = {
|
||||
let c: *fd_ctx = s: *fd_ctx;
|
||||
let r: i64 = os.write(c.fd, buf.ptr, buf.len: u64);
|
||||
if (r < 0) {
|
||||
let nm: nomem;
|
||||
let e: io.error = nm;
|
||||
return e;
|
||||
};
|
||||
return r: size;
|
||||
};
|
||||
|
||||
// ---- internal stream formatters --------------------------------------
|
||||
|
||||
// putbytes — io.write(s, [ptr..ptr+n)). Internal helper; the inline
|
||||
// `let v` slice synthesis composes the (ptr, len) triple each
|
||||
// formattable arm carries.
|
||||
fn putbytes(s: io.stream, p: *u8, n: i32) (size | io.error) = {
|
||||
fn putbytes(s: io.handle, p: *u8, n: i32) (size | io.error) = {
|
||||
let v: []u8;
|
||||
v.ptr = p;
|
||||
v.len = n;
|
||||
@@ -130,7 +88,7 @@ fn putbytes(s: io.stream, p: *u8, n: i32) (size | io.error) = {
|
||||
};
|
||||
|
||||
// writeone — emit one formattable through io.write.
|
||||
fn writeone(s: io.stream, a: formattable) (size | io.error) = {
|
||||
fn writeone(s: io.handle, a: formattable) (size | io.error) = {
|
||||
match (a) {
|
||||
case let n: i64 => {
|
||||
let v: str = i64dec(n);
|
||||
@@ -365,7 +323,7 @@ fn rawlen(arg: formattable, m: *mods) i32 = {
|
||||
// shortest-G with no precision knob); base ignored on f64 (Hare too —
|
||||
// base is int-only). drew NaN/Inf signoff: nan/infinity strings emitted
|
||||
// unchanged.
|
||||
fn formatraw(s: io.stream, arg: formattable, m: *mods) (size | io.error) = {
|
||||
fn formatraw(s: io.handle, arg: formattable, m: *mods) (size | io.error) = {
|
||||
match (arg) {
|
||||
case let v: i64 => {
|
||||
let neg_flag: bool = v < 0;
|
||||
@@ -459,7 +417,7 @@ fn formatraw(s: io.stream, arg: formattable, m: *mods) (size | io.error) = {
|
||||
// counter because putbytes over memio.fixed reports a full buffer as a
|
||||
// 0-byte partial write, not io.error — looping on `total < m.width`
|
||||
// would spin on bsprintf when the sink runs out.
|
||||
fn formatone(s: io.stream, arg: formattable, m: *mods) (size | io.error) = {
|
||||
fn formatone(s: io.handle, arg: formattable, m: *mods) (size | io.error) = {
|
||||
let start: i32 = 0;
|
||||
if (m.width > 0 && m.alignment != alignment.LEFT) {
|
||||
let raw: i32 = rawlen(arg, m);
|
||||
@@ -505,7 +463,7 @@ fn formatone(s: io.stream, arg: formattable, m: *mods) (size | io.error) = {
|
||||
// for-loop would trip #18 (silent miscompile of 24B return-by-value in
|
||||
// for-loop context); inline-per-arm sidesteps it. `*mods` arm aborts
|
||||
// (parametric '%' form not implemented). Mirror print.ha:648.
|
||||
fn formatfield(s: io.stream, f: field, m: *mods) (size | io.error) = {
|
||||
fn formatfield(s: io.handle, f: field, m: *mods) (size | io.error) = {
|
||||
match (f) {
|
||||
case let v: i64 => {
|
||||
let a: formattable = v;
|
||||
@@ -541,7 +499,7 @@ fn formatfield(s: io.stream, f: field, m: *mods) (size | io.error) = {
|
||||
// the returned count, not as an error — matches [[io.write]]'s contract
|
||||
// per [[memio.fixed]]. Callers that need write-all semantics layer it
|
||||
// on top, the same way they do over raw [[io.write]].
|
||||
export fn fprint(s: io.stream, args: formattable...) (size | io.error) = {
|
||||
export fn fprint(s: io.handle, args: formattable...) (size | io.error) = {
|
||||
let total: size = 0;
|
||||
let i: i32 = 0;
|
||||
for (i < args.len) {
|
||||
@@ -564,7 +522,7 @@ export fn fprint(s: io.stream, args: formattable...) (size | io.error) = {
|
||||
|
||||
// fprintf — Hare's primary printf-family surface. Mirrors print.ha:26.
|
||||
// Returns total bytes written or io.error on the first sink failure.
|
||||
export fn fprintf(s: io.stream, fmt: str, args: field...) (size | io.error) = {
|
||||
export fn fprintf(s: io.handle, fmt: str, args: field...) (size | io.error) = {
|
||||
let total: size = 0;
|
||||
let i: i32 = 0;
|
||||
let nextimpl: i32 = 0;
|
||||
@@ -629,7 +587,7 @@ export fn fprintf(s: io.stream, fmt: str, args: field...) (size | io.error) = {
|
||||
};
|
||||
|
||||
// fprintln — fprint plus a trailing newline. Mirrors wrappers.ha.
|
||||
export fn fprintln(s: io.stream, args: formattable...) (size | io.error) = {
|
||||
export fn fprintln(s: io.handle, args: formattable...) (size | io.error) = {
|
||||
let total: size = 0;
|
||||
match (fprint(s, args...)) {
|
||||
case let n: size => { total = n; };
|
||||
@@ -643,7 +601,7 @@ export fn fprintln(s: io.stream, args: formattable...) (size | io.error) = {
|
||||
};
|
||||
|
||||
// fprintfln — fprintf plus a trailing newline. Mirrors wrappers.ha:69.
|
||||
export fn fprintfln(s: io.stream, fmt: str, args: field...) (size | io.error) = {
|
||||
export fn fprintfln(s: io.handle, fmt: str, args: field...) (size | io.error) = {
|
||||
let total: size = 0;
|
||||
match (fprintf(s, fmt, args...)) {
|
||||
case let n: size => { total = n; };
|
||||
@@ -712,113 +670,55 @@ export fn asprintf(fmt: str, args: field...) str = {
|
||||
return strings.frombytes(tight);
|
||||
};
|
||||
|
||||
// ---- fd sinks (pre-handle shim, NO Hare counterpart; #5) -------------
|
||||
|
||||
// fdprint — fdprint over a stack-resident fd_ctx io.stream. The &c.vt
|
||||
// io.stream never escapes this frame.
|
||||
export fn fdprint(fd: i32, args: formattable...) (size | io.error) = {
|
||||
let c: fd_ctx;
|
||||
c.fd = fd;
|
||||
c.vt.reader = (&fdsinkread): *io.reader;
|
||||
c.vt.writer = (&fdsinkwrite): *io.writer;
|
||||
let s: io.stream = &c.vt;
|
||||
return fprint(s, args...);
|
||||
};
|
||||
|
||||
// fdprintln — fdprint + trailing newline.
|
||||
export fn fdprintln(fd: i32, args: formattable...) (size | io.error) = {
|
||||
let c: fd_ctx;
|
||||
c.fd = fd;
|
||||
c.vt.reader = (&fdsinkread): *io.reader;
|
||||
c.vt.writer = (&fdsinkwrite): *io.writer;
|
||||
let s: io.stream = &c.vt;
|
||||
let total: size = 0;
|
||||
match (fprint(s, args...)) {
|
||||
case let n: size => { total = n; };
|
||||
case let e: io.error => return e;
|
||||
};
|
||||
match (putbytes(s, "\n".ptr, 1)) {
|
||||
case let n: size => { total += n; };
|
||||
case let e: io.error => return e;
|
||||
};
|
||||
return total;
|
||||
};
|
||||
|
||||
// fdprintf — fdprintf over a fd_ctx io.stream.
|
||||
export fn fdprintf(fd: i32, fmt: str, args: field...) (size | io.error) = {
|
||||
let c: fd_ctx;
|
||||
c.fd = fd;
|
||||
c.vt.reader = (&fdsinkread): *io.reader;
|
||||
c.vt.writer = (&fdsinkwrite): *io.writer;
|
||||
let s: io.stream = &c.vt;
|
||||
return fprintf(s, fmt, args...);
|
||||
};
|
||||
|
||||
// fdprintfln — fdprintf + trailing newline. Mirrors wrappers.ha:69.
|
||||
export fn fdprintfln(fd: i32, fmt: str, args: field...) (size | io.error) = {
|
||||
let c: fd_ctx;
|
||||
c.fd = fd;
|
||||
c.vt.reader = (&fdsinkread): *io.reader;
|
||||
c.vt.writer = (&fdsinkwrite): *io.writer;
|
||||
let s: io.stream = &c.vt;
|
||||
let total: size = 0;
|
||||
match (fprintf(s, fmt, args...)) {
|
||||
case let n: size => { total = n; };
|
||||
case let e: io.error => return e;
|
||||
};
|
||||
match (putbytes(s, "\n".ptr, 1)) {
|
||||
case let n: size => { total += n; };
|
||||
case let e: io.error => return e;
|
||||
};
|
||||
return total;
|
||||
};
|
||||
|
||||
// ---- process-stdio wrappers -----------------------------------------
|
||||
//
|
||||
// Hare routes these through os::stdout / os::stderr (io::handle); ww
|
||||
// has no fd-backed handle yet, so they route through the fd shim on
|
||||
// fd 1 / fd 2 (io fold-2, #5, collapses the fd shim). errorf / asprint /
|
||||
// bsprint omitted: the bare `error` name collides with strconv.error
|
||||
// under the driver's flat-scope concat; ship the -ln forms only.
|
||||
// Mirror ref/hare/fmt/wrappers.ha. Hare routes these through
|
||||
// os::stdout / os::stderr (io::handle); ww's os plays the sys role and
|
||||
// can't import io (import floor), so it exports the std fd NUMBERS
|
||||
// (os.STD{OUT,ERR}_FILENO) and the io.file binding is cast at the call
|
||||
// site — `os.STDOUT_FILENO: io.file` widens into the fprint handle param
|
||||
// (io fold-2, #5). errorf / asprint / bsprint omitted: the bare `error`
|
||||
// name collides with strconv.error under the driver's flat-scope concat;
|
||||
// ship the -ln forms only.
|
||||
|
||||
// print / println — wrappers.ha:78/:84 on fd 1.
|
||||
// print / println — wrappers.ha:78/:84 on stdout.
|
||||
export fn print(args: formattable...) (size | io.error) = {
|
||||
return fdprint(1, args...);
|
||||
return fprint(os.STDOUT_FILENO: io.file, args...);
|
||||
};
|
||||
|
||||
export fn println(args: formattable...) (size | io.error) = {
|
||||
return fdprintln(1, args...);
|
||||
return fprintln(os.STDOUT_FILENO: io.file, args...);
|
||||
};
|
||||
|
||||
// errorln — wrappers.ha:96 on fd 2.
|
||||
// errorln — wrappers.ha:96 on stderr.
|
||||
export fn errorln(args: formattable...) (size | io.error) = {
|
||||
return fdprintln(2, args...);
|
||||
return fprintln(os.STDERR_FILENO: io.file, args...);
|
||||
};
|
||||
|
||||
// fatal — errorln then exit(255). `never` return marks the bottom type
|
||||
// so flow-control checks treat callers as terminated. Mirrors
|
||||
// wrappers.ha:63.
|
||||
export fn fatal(args: formattable...) never = {
|
||||
fdprintln(2, args...);
|
||||
fprintln(os.STDERR_FILENO: io.file, args...);
|
||||
os.exit(255);
|
||||
};
|
||||
|
||||
// printf / printfln — wrappers.ha:10/:15 on fd 1.
|
||||
// printf / printfln — wrappers.ha:10/:15 on stdout.
|
||||
export fn printf(fmt: str, args: field...) (size | io.error) = {
|
||||
return fdprintf(1, fmt, args...);
|
||||
return fprintf(os.STDOUT_FILENO: io.file, fmt, args...);
|
||||
};
|
||||
|
||||
export fn printfln(fmt: str, args: field...) (size | io.error) = {
|
||||
return fdprintfln(1, fmt, args...);
|
||||
return fprintfln(os.STDOUT_FILENO: io.file, fmt, args...);
|
||||
};
|
||||
|
||||
// errorfln — wrappers.ha:24 on fd 2.
|
||||
// errorfln — wrappers.ha:24 on stderr.
|
||||
export fn errorfln(fmt: str, args: field...) (size | io.error) = {
|
||||
return fdprintfln(2, fmt, args...);
|
||||
return fprintfln(os.STDERR_FILENO: io.file, fmt, args...);
|
||||
};
|
||||
|
||||
// fatalf — errorfln then exit(255). Mirrors wrappers.ha:54.
|
||||
export fn fatalf(fmt: str, args: field...) never = {
|
||||
fdprintfln(2, fmt, args...);
|
||||
fprintfln(os.STDERR_FILENO: io.file, fmt, args...);
|
||||
os.exit(255);
|
||||
};
|
||||
|
||||
@@ -41,10 +41,12 @@
|
||||
// an imported module name; `use fmt; fn x(fmt: T)` is structurally
|
||||
// ambiguous under ww's `.`-for-both rule.
|
||||
//
|
||||
// Sink today is an [[io.stream]] only — lib/io has no fd-backed handle
|
||||
// yet (Hare's `io::handle = file | int`, io fold-2 #5). The default
|
||||
// logger writes to stderr via a private fd_ctx whose write callback
|
||||
// forwards to [[os.write]] on fd 2.
|
||||
// Sink today is an [[io.stream]] only. io fold-2 (#5) landed
|
||||
// [[io.handle]] = (io.file | io.stream) and graduated lib/fmt onto it,
|
||||
// but lib/log's sink graduation is a separate follow-up; until then the
|
||||
// default logger writes to stderr via a private fd-backed stream shim
|
||||
// ([[stderrsink_ctx]]) whose write callback forwards to [[os.write]] on
|
||||
// os.STDERR_FILENO.
|
||||
//
|
||||
// Cast workaround per #206-payoff (ken: KEEP the explicit casts; they
|
||||
// are cgen-neutral and sidestep the #214 over-acceptance surface). The
|
||||
@@ -85,7 +87,8 @@ export type stdlogger = struct {
|
||||
|
||||
// stderrsink_ctx — module-static state for the default logger's stderr
|
||||
// sink. vt FIRST field for the intrusive io.stream→*stderrsink_ctx cast
|
||||
// in stderrwrite. Mirrors fmt.fd_ctx; only scalars/ptrs beyond vt.
|
||||
// in stderrwrite (the same vtable-first-field shape lib/fmt used before
|
||||
// #5 graduated it onto io.handle); only scalars/ptrs beyond vt.
|
||||
type stderrsink_ctx = struct {
|
||||
vt: io.vtable,
|
||||
fd: i32,
|
||||
@@ -113,7 +116,7 @@ let initdone: i32 = 0;
|
||||
|
||||
fn ensureinit() void = {
|
||||
if (initdone != 0) { return; };
|
||||
stderrsink_ctx_g.fd = 2;
|
||||
stderrsink_ctx_g.fd = os.STDERR_FILENO;
|
||||
stderrsink_ctx_g.vt.reader = (&stderrread): *io.reader;
|
||||
stderrsink_ctx_g.vt.writer = (&stderrwrite): *io.writer;
|
||||
|
||||
|
||||
11
lib/os/os.ww
11
lib/os/os.ww
@@ -91,6 +91,17 @@ export fn exit(code: i32) void = {
|
||||
export def PATH_MAX: i32 = 4096;
|
||||
let pathbuf: [4096]u8;
|
||||
|
||||
// ref/hare/sys/+linux/types.ha:886-888. ww folds `sys` into `os`, so the
|
||||
// std fd NUMBERS live here (the sys role). Typed i32, NOT io.file as in
|
||||
// Hare's os::stdout_file (ref/hare/os/+linux/stdfd.ha:28): Hare's `os`
|
||||
// imports `io`, but ww's `os` is the import floor and must never import
|
||||
// io (lib/CLAUDE.md) — so the io.file/io.handle binding can't live here.
|
||||
// Consumers (lib/fmt's stdio wrappers) cast i32→io.file at the use site,
|
||||
// where the handle layer is already in scope.
|
||||
export def STDIN_FILENO: i32 = 0;
|
||||
export def STDOUT_FILENO: i32 = 1;
|
||||
export def STDERR_FILENO: i32 = 2;
|
||||
|
||||
fn kpath(p: str) *u8 = {
|
||||
if (p.len + 1 >= PATH_MAX) { return nil: *u8; }; // ENAMETOOLONG
|
||||
let i: i32 = 0;
|
||||
|
||||
Reference in New Issue
Block a user