diff --git a/lib/log/log.ww b/lib/log/log.ww index 09ce07fb..8c1f6e01 100644 --- a/lib/log/log.ww +++ b/lib/log/log.ww @@ -3,7 +3,7 @@ // // Surface today: // -// log.logger — vtable with a single `println` slot +// log.logger — vtable with `println` + `printfln` slots // log.stdlogger — first-field embed of logger + a `*io.stream` sink // log.new (sl: *stdlogger, sink: *io.stream) void // log.silent *logger — a logger that discards every record @@ -11,8 +11,12 @@ // log.global *logger — the dispatch target for [[println]] / [[fatal]] // log.println (args: fmt.formattable...) void // log.lprintln (log: *logger, args: fmt.formattable...) void +// log.printfln (format: str, fields: fmt.field...) void +// log.lprintfln (log: *logger, format: str, fields: fmt.field...) void // log.fatal (args: fmt.formattable...) never // log.lfatal (log: *logger, args: fmt.formattable...) never +// log.fatalf (format: str, fields: fmt.field...) never +// log.lfatalf (log: *logger, format: str, fields: fmt.field...) never // log.setlogger (log: *logger) void // // Divergences from Hare: @@ -32,10 +36,11 @@ // [[println]] / [[lprintln]] / [[setlogger]] / etc.) must call // some lib/log fn first so init runs. // -// • printfln / lprintfln / fatalf / lfatalf are skipped — they -// need a fmt {n}-placeholder parser that isn't shipped yet. The -// logger vtable carries only `println` today; the format-string -// entries graduate with the parser. +// • Param name divergence from Hare: the format-string parameter is +// `format` (not Hare's `fmt`) because ww's resolver shadows the +// `fmt` module reference inside a body when a same-name param is +// in scope (silent miscompile — CALL through str.ptr). Tracked as +// task #19; rename to `fmt` when fixed. // // Sink today is [[io.stream]] only — lib/io has no fd-backed stream // yet (Hare's `io::handle = file | int` collapses to one variant). @@ -71,15 +76,12 @@ use fmt; use io; use os; -// logger — interface for log dispatch. v1 carries a single vtable -// slot. Hare layers a `printfln` slot for format-string callbacks; -// that comes back when lib/fmt grows a {n}-placeholder parser. -// -// TODO: needs fmt {n}-placeholder parser (future task) — adds a -// `printfln: fn(l: *logger, fmt: str, args: fmt.field...) void` -// slot here. +// logger — interface for log dispatch. Two vtable slots: bare-args +// `println` (formattable-variadic) and `printfln` (format-string + +// field-variadic). Mirrors ref/hare/log/logger.ha:9. export type logger = struct { - println: fn(l: *logger, args: fmt.formattable...) void, + println: fn(l: *logger, args: fmt.formattable...) void, + printfln: fn(l: *logger, format: str, fields: fmt.field...) void, }; // stdlogger — concrete logger forwarding to a `*io.stream` sink. @@ -123,9 +125,11 @@ fn ensureinit() void = { stderrsink.write = stderrwrite; stderrsink.close = stderrclose; - _silent.println = silentprintln; + _silent.println = silentprintln; + _silent.printfln = silentprintfln; - _default.logger.println = stdprintln; + _default.logger.println = stdprintln; + _default.logger.printfln = stdprintfln; _default.sink = &stderrsink; silent = &_silent; @@ -162,18 +166,31 @@ fn stdprintln(l: *logger, args: fmt.formattable...) void = { fmt.fprintln(sl.sink, args...); }; +// stdprintfln — vtable callback for stdlogger's format-string slot. +// Mirrors ref/hare/log/logger.ha:38 log_printfln. `format` (not +// Hare's `fmt`) per the param-name divergence noted in the header. +fn stdprintfln(l: *logger, format: str, fields: fmt.field...) void = { + let sl: *stdlogger = l: *stdlogger; + fmt.fprintfln(sl.sink, format, fields...); +}; + // silentprintln — vtable callback for the silent logger. Discards // every record without dispatching through fmt; keeps silent truly // silent if fmt ever gets stateful. fn silentprintln(l: *logger, args: fmt.formattable...) void = { }; +// silentprintfln — format-string sibling of [[silentprintln]]. +// Mirrors ref/hare/log/silent.ha:15. +fn silentprintfln(l: *logger, format: str, fields: fmt.field...) void = { }; + // new — wire `sl` as a stdlogger over `sink`. Hare returns by value // (ref/hare/log/logger.ha:20); ww cgen can't return wide structs, // so we take an out-parameter pointer (same shape as memio.fixed, // bufio.init). export fn new(sl: *stdlogger, sink: *io.stream) void = { ensureinit(); - sl.logger.println = stdprintln; + sl.logger.println = stdprintln; + sl.logger.printfln = stdprintfln; sl.sink = sink; }; @@ -212,6 +229,30 @@ export fn setlogger(log: *logger) void = { global = log; }; -// TODO: needs fmt {n}-placeholder parser (future task) — adds -// `lprintfln` / `printfln` / `lfatalf` / `fatalf` here once the -// printfln vtable slot is in place. +// lprintfln — dispatch a format-string record through `log`. Hare's +// lib/log/funcs.ha counterpart at line 13. +export fn lprintfln(log: *logger, format: str, fields: fmt.field...) void = { + ensureinit(); + log.printfln(log, format, fields...); +}; + +// printfln — dispatch through the [[global]] logger. Hare's +// lib/log/funcs.ha counterpart at line 23. +export fn printfln(format: str, fields: fmt.field...) void = { + ensureinit(); + lprintfln(global, format, fields...); +}; + +// lfatalf — lprintfln to `log` then exit(255). Hare's +// lib/log/funcs.ha counterpart at line 35. +export fn lfatalf(log: *logger, format: str, fields: fmt.field...) never = { + lprintfln(log, format, fields...); + os.exit(255); +}; + +// fatalf — lprintfln to [[global]] then exit(255). Hare's +// lib/log/funcs.ha counterpart at line 52. +export fn fatalf(format: str, fields: fmt.field...) never = { + printfln(format, fields...); + os.exit(255); +}; diff --git a/lib/log/logtest.ww b/lib/log/logtest.ww index e5caeede..b09e0042 100644 --- a/lib/log/logtest.ww +++ b/lib/log/logtest.ww @@ -165,10 +165,110 @@ fn streq(a: str, b: str) bool = { if (!streq(memio.string(&mem2), "second\n")) { fail(); }; }; -// TODO subprocess: log.fatal / log.lfatal exit(255). Verifying them -// needs a fork+wait fixture so the parent can assert WEXITSTATUS == -// 255 without the test process itself terminating. lib/os doesn't -// ship process spawning yet; revisit when that lands. +// ---- lprintfln: {n}-placeholder render into a memio sink ------------- + +@test fn lprintflnbasic() void = { + let buf: [32]u8; + let mem: memio.state; + let s: io.stream; + memio.fixed(&mem, &s, buf[0:32]); + + let sl: log.stdlogger; + log.new(&sl, &s); + + log.lprintfln(&sl.logger, "x={} y={}", 42i64, "hi"); + + if (!streq(memio.string(&mem), "x=42 y=hi\n")) { fail(); }; +}; + +// ---- printfln through global: setlogger then dispatch --------------- + +// Mirrors [[setloggerswap]] but exercises the format-string path: +// install sl1 as global, printfln writes there; mem2 stays empty. +@test fn printflnglobal() void = { + let buf1: [32]u8; + let mem1: memio.state; + let s1: io.stream; + memio.fixed(&mem1, &s1, buf1[0:32]); + let sl1: log.stdlogger; + log.new(&sl1, &s1); + + let buf2: [32]u8; + let mem2: memio.state; + let s2: io.stream; + memio.fixed(&mem2, &s2, buf2[0:32]); + let sl2: log.stdlogger; + log.new(&sl2, &s2); + + log.setlogger(&sl1.logger); + log.printfln("v={}", 7i64); + if (!streq(memio.string(&mem1), "v=7\n")) { fail(); }; + if (mem2.pos != 0) { fail(); }; +}; + +// ---- silent.printfln writes nothing --------------------------------- + +// The silent logger's printfln callback discards args without touching +// fmt or any sink. Same pattern as [[silentwritesnothing]] for the +// bare-args path. +@test fn silentignoresprintfln() void = { + let buf: [16]u8; + let mem: memio.state; + let s: io.stream; + memio.fixed(&mem, &s, buf[0:16]); + + let sl: log.stdlogger; + log.new(&sl, &s); // triggers log.ensureinit; populates log.silent + + if (log.silent == nil) { fail(); }; + + log.lprintfln(log.silent, "ignored={}", 1i64); + + if (mem.pos != 0) { fail(); }; +}; + +// ---- lprintfln: indexed {N} placeholder across log → fmt seam ------- + +// The fmt parser proper is covered in fmttest; this pins that log's +// variadic forwarding propagates argv order so indexed placeholders +// resolve correctly. +@test fn lprintflnindexed() void = { + let buf: [16]u8; + let mem: memio.state; + let s: io.stream; + memio.fixed(&mem, &s, buf[0:16]); + + let sl: log.stdlogger; + log.new(&sl, &s); + + log.lprintfln(&sl.logger, "{1} {0}", "a", "b"); + + if (!streq(memio.string(&mem), "b a\n")) { fail(); }; +}; + +// ---- lprintfln: {:mods} modifier across log → fmt seam -------------- + +// Pins that mod-bearing placeholders flow through log's forwarding +// intact (parser proper covered in fmttest). +@test fn lprintflnmods() void = { + let buf: [16]u8; + let mem: memio.state; + let s: io.stream; + memio.fixed(&mem, &s, buf[0:16]); + + let sl: log.stdlogger; + log.new(&sl, &s); + + log.lprintfln(&sl.logger, "{:5}", 42i64); + + if (!streq(memio.string(&mem), " 42\n")) { fail(); }; +}; + +// TODO subprocess: log.fatal / log.lfatal / log.fatalf / log.lfatalf +// exit(255). Verifying them needs a fork+wait fixture so the parent +// can assert WEXITSTATUS == 255 without the test process itself +// terminating. lib/os doesn't ship process spawning yet; revisit +// when that lands. export fn main() i32 = { signalled = 1; defaultwiredtoglobal(); @@ -177,5 +277,10 @@ export fn main() i32 = { signalled = 4; lprintlnempty(); signalled = 5; silentwritesnothing(); signalled = 6; setloggerswap(); + signalled = 7; lprintflnbasic(); + signalled = 8; printflnglobal(); + signalled = 9; silentignoresprintfln(); + signalled = 10; lprintflnindexed(); + signalled = 11; lprintflnmods(); return 0; };