ajhahn.de
← Flash
Flash 330 lines
// fmt — string formatting and integer parsing, in pure Flash.
//
// `allocPrint` mirrors Zig's `std.fmt.allocPrint` over the verb subset the
// compiler uses — `{s}` (string) and `{d}` (decimal integer) — walking the
// format string at comptime so an unknown verb or a mismatched argument
// count is a compile error, not a runtime surprise. `bufPrint` is its
// allocation-free twin: the same format engine, but the bytes land in a
// caller buffer and a too-small buffer reports `NoSpaceLeft`. `parseInt` mirrors
// Zig's `std.fmt.parseInt`: optional leading sign, base 0 auto-detects a
// 0b / 0o / 0x prefix (case-insensitive), `_` digit separators are legal
// only between digits, and accumulation is overflow-checked. The error
// names (`Overflow`, `InvalidCharacter`) match Zig's, so a facade can
// re-route between the two implementations without touching a call site.

use "base"
use "list"

// Format `args` into a freshly allocated string the caller owns.
pub fn allocPrint(alloc base.Allocator, comptime fmt []u8, args anytype) ![]u8 {
    var out list.List(u8) = .empty
    errdefer out.deinit(alloc)
    var sink ListSink = .{ .alloc = alloc, .out = &out }
    try printEngine(&sink, fmt, args)
    return out.toOwnedSlice(alloc)
}

// Format `args` into `buf` and return the written prefix. A buffer too
// small for the formatted output reports `error.NoSpaceLeft`, matching
// Zig's `std.fmt.bufPrint`.
pub fn bufPrint(buf []mut u8, comptime fmt []u8, args anytype) ![]u8 {
    var sink BufSink = .{ .buf = buf, .pos = 0 }
    try printEngine(&sink, fmt, args)
    return buf[0..sink.pos]
}

// The format walk shared by `allocPrint` and `bufPrint`. `sink` is a
// pointer to anything with `append(u8)` and `appendSlice([]u8)`; the
// format string is validated at comptime, so the two fronts cannot
// drift in what they accept.
fn printEngine(sink anytype, comptime fmt []u8, args anytype) !void {
    comptime var i usize = 0
    comptime var arg usize = 0
    inline while i < fmt.len {
        if fmt[i] == '{' {
            if i + 2 >= fmt.len || fmt[i + 2] != '}' {
                #compileError("fmt: a format spec is a single verb: {s} or {d}")
            }
            switch fmt[i + 1] {
                's' => try sink.appendSlice(args[arg]),
                'd' => try appendDecimal(sink, args[arg]),
                else => #compileError("fmt: unsupported format verb"),
            }
            arg += 1
            i += 3
        } else {
            try sink.append(fmt[i])
            i += 1
        }
    }
    if arg != args.len {
        #compileError("fmt: argument count does not match the format string")
    }
}

// The allocating sink: bytes append to the `List(u8)` that `allocPrint`
// owns (and releases on error via its errdefer).
const ListSink = struct {
    alloc base.Allocator,
    out *mut list.List(u8),

    fn append(self *mut ListSink, c u8) !void {
        try self.out.append(self.alloc, c)
    }

    fn appendSlice(self *mut ListSink, s []u8) !void {
        try self.out.appendSlice(self.alloc, s)
    }
}

// The fixed-buffer sink: bytes land at `pos`; running out of room is
// `NoSpaceLeft`.
const BufSink = struct {
    buf []mut u8,
    pos usize,

    fn append(self *mut BufSink, c u8) !void {
        if self.pos >= self.buf.len {
            return error.NoSpaceLeft
        }
        self.buf[self.pos] = c
        self.pos += 1
    }

    fn appendSlice(self *mut BufSink, s []u8) !void {
        for c in s {
            try self.append(c)
        }
    }
}

// Append `value` in decimal through `sink`. Every integer the compiler
// formats fits an i128; a negative value prints a leading '-' with its
// magnitude carried in a u128, so even the type's own minimum cannot
// overflow on negation.
fn appendDecimal(sink anytype, value anytype) !void {
    v := #as(i128, value)
    var m u128 = 0
    if v < 0 {
        try sink.append('-')
        m = #as(u128, #intCast(-(v + 1))) + 1
    } else {
        m = #intCast(v)
    }
    if m == 0 {
        try sink.append('0')
        return
    }
    // A u128 is at most 39 decimal digits; collect them least-significant
    // first, then append in reading order.
    var digits [39]u8 = undefined
    var n usize = 0
    while m != 0 {
        digits[n] = '0' + #as(u8, #intCast(m % 10))
        m /= 10
        n += 1
    }
    while n > 0 {
        n -= 1
        try sink.append(digits[n])
    }
}

// Parse `s` as a `T` in the given base. Base 0 auto-detects a 0b / 0o /
// 0x prefix and falls back to decimal. A negative value accumulates on
// the negative side, so the type's own minimum parses without overflow —
// and a '-' on an unsigned type reports `Overflow` on the first nonzero
// digit, exactly as Zig's does.
pub fn parseInt(comptime T type, s []u8, radix u8) !T {
    if s.len == 0 {
        return error.InvalidCharacter
    }
    var start usize = 0
    var negative bool = false
    if s[0] == '+' {
        start = 1
    } else if s[0] == '-' {
        negative = true
        start = 1
    }
    var b u8 = radix
    if radix == 0 {
        b = 10
        rest := s[start..]
        if rest.len > 2 && rest[0] == '0' {
            c := rest[1]
            if c == 'b' || c == 'B' {
                b = 2
                start += 2
            } else if c == 'o' || c == 'O' {
                b = 8
                start += 2
            } else if c == 'x' || c == 'X' {
                b = 16
                start += 2
            }
        }
    }
    var x T = 0
    var last_was_digit bool = false
    for c in s[start..] {
        if c == '_' {
            if !last_was_digit {
                return error.InvalidCharacter
            }
            last_was_digit = false
            continue
        }
        d := try charToDigit(c, b)
        last_was_digit = true
        mul := #mulWithOverflow(x, #as(T, #intCast(b)))
        if mul[1] != 0 {
            return error.Overflow
        }
        if negative {
            r := #subWithOverflow(mul[0], #as(T, #intCast(d)))
            if r[1] != 0 {
                return error.Overflow
            }
            x = r[0]
        } else {
            r := #addWithOverflow(mul[0], #as(T, #intCast(d)))
            if r[1] != 0 {
                return error.Overflow
            }
            x = r[0]
        }
    }
    if !last_was_digit {
        return error.InvalidCharacter
    }
    return x
}

// The value of one digit character under `radix`, or `InvalidCharacter`.
fn charToDigit(c u8, radix u8) !u8 {
    var d u8 = 255
    if c >= '0' && c <= '9' {
        d = c - '0'
    } else if c >= 'a' && c <= 'z' {
        d = c - 'a' + 10
    } else if c >= 'A' && c <= 'Z' {
        d = c - 'A' + 10
    }
    if d >= radix {
        return error.InvalidCharacter
    }
    return d
}

test "allocPrint formats strings and decimals" {
    s := try allocPrint(base.testAlloc, "generic '{s}' expects {d} argument{s}, found {d}", .{ "List", 1, "", 2 })
    defer base.testAlloc.free(s)
    try base.expectEqualStrings("generic 'List' expects 1 argument, found 2", s)
}

test "allocPrint passes a verb-free string through" {
    s := try allocPrint(base.testAlloc, "no verbs here", .{})
    defer base.testAlloc.free(s)
    try base.expectEqualStrings("no verbs here", s)
}

test "allocPrint formats runtime integers of the compiler's widths" {
    count := #as(usize, 1000)
    bits := #as(u16, 128)
    s := try allocPrint(base.testAlloc, "budget {d}, width {d}", .{ count, bits })
    defer base.testAlloc.free(s)
    try base.expectEqualStrings("budget 1000, width 128", s)
}

test "allocPrint formats decimal boundaries" {
    const min i128 = -170141183460469231731687303715884105728
    s := try allocPrint(base.testAlloc, "{d}", .{min})
    defer base.testAlloc.free(s)
    try base.expectEqualStrings("-170141183460469231731687303715884105728", s)

    z := try allocPrint(base.testAlloc, "{d}", .{0})
    defer base.testAlloc.free(z)
    try base.expectEqualStrings("0", z)
}

// The failing-allocator sweep body: `checkAllAllocationFailures` fails
// each allocation `allocPrint` makes in turn; every induced OOM must
// surface as an error with the partial output released.
fn allocPrintSweep(alloc base.Allocator) !void {
    s := try allocPrint(alloc, "expects {d} argument{s}, found {d}", .{ 1, "", 2 })
    defer alloc.free(s)
    try base.expectEqualStrings("expects 1 argument, found 2", s)
}

test "allocPrint survives failure at every allocation point" {
    try base.checkAllAllocationFailures(base.testAlloc, allocPrintSweep, .{})
}

test "bufPrint formats into a caller buffer" {
    var buf [64]u8 = undefined
    s := try bufPrint(buf[0..], "generic '{s}' expects {d} argument{s}, found {d}", .{ "List", 1, "", 2 })
    try base.expectEqualStrings("generic 'List' expects 1 argument, found 2", s)
}

test "bufPrint fills the buffer exactly and passes a verb-free string through" {
    var buf [4]u8 = undefined
    s := try bufPrint(buf[0..], "{d}", .{1234})
    try base.expectEqualStrings("1234", s)
    t := try bufPrint(buf[0..], "ok", .{})
    try base.expectEqualStrings("ok", t)
}

test "bufPrint formats negatives and zero" {
    var buf [8]u8 = undefined
    s := try bufPrint(buf[0..], "{d}", .{-5})
    try base.expectEqualStrings("-5", s)
    z := try bufPrint(buf[0..], "{d}", .{0})
    try base.expectEqualStrings("0", z)
}

test "bufPrint reports NoSpaceLeft when the output does not fit" {
    var buf [4]u8 = undefined
    try base.expectError(error.NoSpaceLeft, bufPrint(buf[0..], "{s}", .{"hello"}))
    try base.expectError(error.NoSpaceLeft, bufPrint(buf[0..], "12345", .{}))
    try base.expectError(error.NoSpaceLeft, bufPrint(buf[0..0], "x", .{}))
}

test "parseInt parses decimal with sign and separators" {
    try base.expectEqual(123, try parseInt(u16, "123", 10))
    try base.expectEqual(65535, try parseInt(u16, "65535", 10))
    try base.expectEqual(5, try parseInt(u16, "+5", 10))
    try base.expectEqual(-5, try parseInt(i128, "-5", 10))
    try base.expectEqual(10, try parseInt(u16, "1_0", 10))
}

test "parseInt base 0 auto-detects the prefix" {
    try base.expectEqual(255, try parseInt(i128, "0xff", 0))
    try base.expectEqual(255, try parseInt(i128, "0XFF", 0))
    try base.expectEqual(15, try parseInt(i128, "0o17", 0))
    try base.expectEqual(5, try parseInt(i128, "0b101", 0))
    try base.expectEqual(10, try parseInt(i128, "10", 0))
    try base.expectEqual(0, try parseInt(i128, "0", 0))
}

test "parseInt rejects empty, bad digits, and stray separators" {
    try base.expectError(error.InvalidCharacter, parseInt(u16, "", 10))
    try base.expectError(error.InvalidCharacter, parseInt(u16, "12a", 10))
    try base.expectError(error.InvalidCharacter, parseInt(u16, "_8", 10))
    try base.expectError(error.InvalidCharacter, parseInt(u16, "1_", 10))
    try base.expectError(error.InvalidCharacter, parseInt(i128, "0x", 0))
    try base.expectError(error.InvalidCharacter, parseInt(i128, "0x_1", 0))
    try base.expectError(error.InvalidCharacter, parseInt(u16, "8", 8))
}

test "parseInt reports overflow, including a '-' on an unsigned type" {
    try base.expectError(error.Overflow, parseInt(u16, "70000", 10))
    try base.expectError(error.Overflow, parseInt(u16, "-5", 10))
}

test "parseInt reaches the i128 boundaries" {
    try base.expectEqual(-170141183460469231731687303715884105728, try parseInt(i128, "-170141183460469231731687303715884105728", 10))
    try base.expectEqual(170141183460469231731687303715884105727, try parseInt(i128, "170141183460469231731687303715884105727", 10))
    try base.expectError(error.Overflow, parseInt(i128, "170141183460469231731687303715884105728", 10))
}