// Console I/O layer of flibc — `puts` and a comptime-format `printf` // on top of the unified write_fd ABI. Both build their output into a // stack-resident 256-byte buffer, then emit it to fd 1 (stdout) in a // single length-carrying syscall. Output longer than 255 bytes is // silently truncated; the demo programs ship are well below that bound. // // Format spec is a deliberate subset of C printf: // %% — literal '%' // %s — null-terminated string ([*:0]const u8) // %d / %i — signed decimal (any int that fits in i64) // %u — unsigned decimal (any int that fits in u64) // %x — lowercase hex (any int that fits in u64) // %c — single byte // Width / precision / padding are not supported — this is demoware- // grade by design; richer formatting belongs to future fsh / // coreutils work once a real userland exercises it. use "syscalls" as sys const BUF_LEN usize = 256 /// puts — write a null-terminated string followed by a newline. Mirrors /// the C library shape (line-buffered, sentinel-terminated input). pub fn puts(s [*:0]u8) void { var buf [BUF_LEN]u8 = undefined var pos usize = 0 // Same BUF_LEN-1 truncation bound printf enforces: copy the input, // append the newline, then flush `pos` bytes in one syscall. buf_put_zstr(&buf, &pos, s) buf_put_byte(&buf, &pos, '\n') _ = sys.write_fd(1, &buf, pos) } /// printf(fmt, .{args...}) — format and emit. The format string is /// walked at comptime so the dispatch on each spec resolves to a /// straight call into the matching `buf_put_*` helper at codegen time; /// no runtime parser, no jump table. /// /// Output longer than BUF_LEN-1 (255 bytes) is silently truncated. /// This matches the demo-grade scope of flibc; production code /// requiring large prints should use a more robust IO layer. pub fn printf(comptime fmt []u8, args anytype) void { var buf [BUF_LEN]u8 = undefined var pos usize = 0 // Scale evaluation quota linearly with format string length; the // + 1000 covers the comptime walker's fixed per-spec overhead. #setEvalBranchQuota(8 * fmt.len + 1000) comptime var i usize = 0 comptime var arg_idx usize = 0 inline while i < fmt.len { const c = fmt[i] if c == '%' && i + 1 < fmt.len { const spec = fmt[i + 1] if spec == '%' { buf_put_byte(&buf, &pos, '%') } else { emit_spec(&buf, &pos, spec, args[arg_idx]) arg_idx += 1 } i += 2 } else { buf_put_byte(&buf, &pos, c) i += 1 } } const len = if (pos < BUF_LEN) pos else BUF_LEN - 1 _ = sys.write_fd(1, &buf, len) } // Dispatch a single arg-consuming spec. Inline so each call site is // resolved at the printf comptime walk — `spec` is comptime and only // the matching arm contributes to runtime code generation. Wrapping // the args[arg_idx] read in a separate inline fn (rather than a // switch inside printf) keeps the comptime tuple index away from // arg-less specs (`%%`, literals): if no arg-consuming spec runs in a // given iteration, args is never indexed at all. inline fn emit_spec(buf *mut [BUF_LEN]u8, pos *mut usize, comptime spec u8, arg anytype) void { switch spec { 's' => buf_put_zstr(buf, pos, arg), 'd', 'i' => buf_put_signed(buf, pos, #intCast(arg)), 'u' => buf_put_unsigned(buf, pos, #intCast(arg)), 'x' => buf_put_hex(buf, pos, #intCast(arg)), 'c' => buf_put_byte(buf, pos, #intCast(arg)), else => #compileError("flibc.printf: unsupported %" ++ &[_]u8{spec}), } } // Saturating byte append — silently drops overflow past BUF_LEN-1 so // the trailing slot stays free for the '\0' that printf writes before // flushing. Truncating-on-overflow matches the demo-grade scope; the // alternative (a syscall-per-flush mid-format) would push complexity // disproportionate to the use case. fn buf_put_byte(buf *mut [BUF_LEN]u8, pos *mut usize, c u8) void { if pos.* < BUF_LEN - 1 { buf[pos.*] = c pos.* += 1 } } fn buf_put_zstr(buf *mut [BUF_LEN]u8, pos *mut usize, s [*:0]u8) void { var k usize = 0 while s[k] != 0 { buf_put_byte(buf, pos, s[k]) k += 1 } } fn buf_put_signed(buf *mut [BUF_LEN]u8, pos *mut usize, val i64) void { if val < 0 { buf_put_byte(buf, pos, '-') // i64.min would overflow `-val`; bitcast to u64 to recover the // magnitude in two's complement (-i64.min == i64.min reinterpret). // Branchless and avoids the IntegerOverflow runtime check. const mag u64 = #bitCast(-(val +% 1)) buf_put_unsigned(buf, pos, mag + 1) } else { buf_put_unsigned(buf, pos, #intCast(val)) } } fn buf_put_unsigned(buf *mut [BUF_LEN]u8, pos *mut usize, val u64) void { if val == 0 { buf_put_byte(buf, pos, '0') return } // u64.max is 20 decimal digits; 20 + slack rounds to the nearest // power-of-two stack slot. var tmp [20]u8 = undefined var n usize = 0 var v = val while v > 0 { tmp[n] = #intCast('0' + (v % 10)) n += 1 v /= 10 } while n > 0 { n -= 1 buf_put_byte(buf, pos, tmp[n]) } } fn buf_put_hex(buf *mut [BUF_LEN]u8, pos *mut usize, val u64) void { if val == 0 { buf_put_byte(buf, pos, '0') return } const digits = "0123456789abcdef" var tmp [16]u8 = undefined var n usize = 0 var v = val while v > 0 { tmp[n] = digits[#as(usize, #intCast(v & 0xf))] n += 1 v >>= 4 } while n > 0 { n -= 1 buf_put_byte(buf, pos, tmp[n]) } }