//! Everything the CLI prints. //! //! One function per output shape, each returning text plus whether that text is an //! error, and none of them touching stdout: `main.zig` decides which stream a result //! goes to and what the exit status is. These used to sit in `main.zig` alongside the //! argument parsing, which left one file holding both ends of the program. //! //! Nothing here is shared with the TUI, and that is deliberate. The two frontends //! want different things from the same engine results: this writes a full-width table //! into an allocated buffer, while `src/tui/financial.zig` draws a windowed slice of //! the same schedule into a surface at narrower widths. What they do share is the //! engine's `Money` and `Number` rendering, so the figures themselves match. //! //! The `format` prefix these functions used to carry is now the file name, so //! `format.conversion` and `format.amortization` read as what they are. const std = @import("std"); const engine = @import("engine"); /// A finished piece of output: complete text, ending in a newline, and whether it is /// an error. `main.zig` turns the flag into a stream and an exit status and prints the /// text as it stands. /// /// The newline is part of the text because some outputs are several lines and some /// are one. It used to be split: errors and multi-line results carried their own, /// single-line results relied on `main` to add one, and programmer mode did both, so /// `tally -p 1` ended in a blank line. pub const Result = struct { output: []const u8, is_error: bool, pub fn ok(output: []const u8) Result { return .{ .output = output, .is_error = false }; } /// An engine error, in the engine's words. pub fn fail(err: engine.Error) Result { return .{ .output = decoratedError(err), .is_error = true }; } /// Text that does not fit the buffer it was written into. One message for one /// condition: there used to be "buffer overflow" in eight places and "result too /// long to display" in a ninth, and the first reads like a security bug. const too_long: Result = .{ .output = "error: the result is too long to display\n", .is_error = true }; }; /// Which evaluator a CLI invocation wants. /// /// The CLI's own concept, not the engine's: the engine has `evalString` and /// `evalProgrammerString` and no notion of a mode. There are exactly two here /// because `-p` is the only mode flag; the TUI's four tabs are its own enum. pub const Mode = enum { standard, programmer }; /// How this frontend renders a number. /// /// The engine has no default and no digit constants of its own: a terminal, a /// clipboard and an Android screen want different budgets, so each frontend states /// its own (NFR-9.9, "display precision is a separate decision from compute /// precision"). These are the values the engine used to hold. const display_format: engine.Number.FormatOptions = .{ .fraction_digits = 20, .scientific_below_exponent = 15, .max_integer_digits = 40, .significant_digits = 17, .separators = true, }; /// The engine's amount formatter, so the CLI, the TUI and the engine all group /// amounts the same way. Two decimals and grouping are properties of money rather /// than of a screen, so unlike `display_format` there is no budget to pass. const money = engine.financial.money; // -- Expressions -- /// Evaluate an expression and format the result for a terminal. /// /// The programmer configuration (width, signedness, byte order) is explicit because /// the flags that set it are. Standard mode ignores it: it is fixed at 64-bit signed. /// There used to be a two-argument `evaluate` in front of this that defaulted the /// configuration, and by the end only the tests called it. pub fn evaluateWith( allocator: std.mem.Allocator, expression: []const u8, mode: Mode, config: engine.programmer.Config, buf: []u8, ) Result { if (mode == .programmer) { const result = engine.evalProgrammerString(allocator, expression, config) catch |err| { return .fail(err); }; return programmerResult(buf, result, config); } var env = engine.Environment.init(allocator); defer env.deinit(); // A standalone "to" keyword means this is a unit conversion, e.g. // "32F to C". Anything without it falls through to normal evaluation. if (engine.units.parseRequest(expression)) |maybe_request| { if (maybe_request) |request| { var value = engine.evalString(&env, allocator, request.value_text) catch |err| return .fail(err); defer value.deinit(); return conversionUnits(allocator, buf, value, request.from, request.to); } } else |err| return .fail(err); var info = engine.evalStringInfo(&env, allocator, expression) catch |err| return .fail(err); defer info.value.deinit(); const shown = info.value.render(allocator, display_format) catch |err| return .fail(err); defer shown.deinit(allocator); // The hex, octal and binary rows, when the expression used a non-decimal literal // and the answer is a whole number that fits a pattern (FR-1.9). if (info.basePattern()) |int| return standardMultiBase(buf, shown.text, int); // Into the caller's buffer, so the result does not depend on the allocator // outliving this call. return .ok(std.fmt.bufPrint(buf, "{s}\n", .{shown.text}) catch return .too_long); } /// A standard-mode result with its multi-base rows. The rows print themselves, and /// `dec_display` is the caller's allocated text rather than a slice of `buf`, so /// writing into `buf` cannot clobber it. fn standardMultiBase(buf: []u8, dec_display: []const u8, int: engine.Integer) Result { return .ok(std.fmt.bufPrint(buf, "{s}\n hex: {f}\n oct: {f}\n bin: {f}\n", .{ dec_display, int.fmt(.hex, .{}), int.fmt(.octal, .{}), int.fmt(.binary, .{}), }) catch return .too_long); } fn programmerResult(buf: []u8, result: engine.Integer, config: engine.programmer.Config) Result { return .ok(std.fmt.bufPrint(buf, \\ dec(signed): {f} \\ dec(unsigned): {f} \\ hex: {f} \\ oct: {f} \\ bin: {f} \\ , .{ result.fmt(.decimal_signed, .{}), result.fmt(.decimal_unsigned, .{}), result.fmt(.hex, .{ .endian = config.display_endian }), result.fmt(.octal, .{}), result.fmt(.binary, .{}), }) catch return .too_long); } // -- Conversions -- /// Format a unit conversion between two resolved units. /// /// The value is parsed exactly rather than through f64, so a decimal input like 2.5 /// enters the conversion without being rounded first. The units arrive resolved /// because `units.parseRequest` resolved them while reading the request, so there is /// no second lookup here and no second `UnknownUnit` path. pub fn conversion( allocator: std.mem.Allocator, buf: []u8, value_text: []const u8, from: engine.UnitDef, to: engine.UnitDef, ) Result { // Parse the value exactly rather than through f64, so a decimal input like // 2.5 enters the conversion without being rounded first. var value = engine.Number.parse(allocator, value_text) catch |err| return .fail(err); defer value.deinit(); return conversionUnits(allocator, buf, value, from, to); } /// Format a conversion of a value that has already been evaluated, which is what a /// bare expression such as `2*3 kg to lb` arrives as. /// /// Uses the exact path so terminating conversions print exactly: `12 in to ft` /// is `1`, not `0.9999999999999998`. fn conversionUnits( allocator: std.mem.Allocator, buf: []u8, value: engine.Number, from: engine.UnitDef, to: engine.UnitDef, ) Result { var converted = engine.units.convertExactUnits(allocator, value, from, to) catch |err| return .fail(err); defer converted.deinit(); const shown_in = value.render(allocator, display_format) catch |err| return .fail(err); defer shown_in.deinit(allocator); const shown_out = converted.render(allocator, display_format) catch |err| return .fail(err); defer shown_out.deinit(allocator); return .ok(std.fmt.bufPrint(buf, "{s} {s} = {s} {s}\n", .{ shown_in.text, from.name, shown_out.text, to.name, }) catch return .too_long); } // -- Amortization -- /// Render an amortization schedule as a table. The result is allocated because a /// 360-period schedule does not fit the fixed buffers the other outputs use. pub fn amortization( allocator: std.mem.Allocator, params: engine.financial.AmortizationParams, summary_only: bool, ) Result { const payment = engine.financial.amortizationPayment(params) catch |err| return amortizationError(err); const totals = engine.financial.amortizationTotals(params) catch |err| return amortizationError(err); const rows = engine.financial.amortizationSchedule(allocator, params) catch |err| return amortizationError(err); defer allocator.free(rows); var out: std.Io.Writer.Allocating = .init(allocator); defer out.deinit(); writeSchedule(&out.writer, params, payment, rows, totals, summary_only) catch |err| return switch (err) { error.Unformattable => unformattable, error.WriteFailed => .fail(error.OutOfMemory), }; return .ok(out.toOwnedSlice() catch return .fail(error.OutOfMemory)); } fn writeSchedule( w: *std.Io.Writer, params: engine.financial.AmortizationParams, payment: f64, rows: []const engine.financial.AmortizationEntry, totals: engine.financial.AmortizationTotals, summary_only: bool, ) (std.Io.Writer.Error || error{Unformattable})!void { var a: [4]Amount = undefined; try w.print("{s} at {d}% per period over {d} periods\n", .{ try a[0].of(params.principal), params.rate, params.periods }); try w.print("Payment {s} per period\n\n", .{try a[0].of(payment)}); if (!summary_only) { try w.writeAll("Period Payment Interest Principal Balance\n"); for (rows) |row| { try w.print("{d: >6} {s: >12} {s: >12} {s: >12} {s: >12}\n", .{ row.period, try a[0].of(row.payment), try a[1].of(row.interest), try a[2].of(row.principal), try a[3].of(row.balance), }); } try w.writeAll("\n"); } try w.print("Periods paid {d}\nTotal paid {s}\nTotal interest {s}\nPrincipal {s}\n", .{ totals.periods, try a[0].of(totals.paid), try a[1].of(totals.interest), try a[2].of(totals.principal), }); } /// Room for one amount's text, for the columns that pad it. Rendered rather than /// printed with `{f}` so that an amount too large to render is told apart from a /// writer that has run out of memory. const Amount = struct { buf: [64]u8, fn of(self: *Amount, value: f64) error{Unformattable}![]const u8 { return money(value).render(&self.buf) catch error.Unformattable; } }; // -- Struct layout -- const struct_layout = engine.struct_layout; const CStructParser = engine.CStructParser; /// Members a memory map can tell apart, one letter each: a-z, then A-Z. const max_keyed_members = 52; /// Whether a struct layout is drawn in color. Deciding (a terminal, `NO_COLOR`, /// `--color`) is `main`'s job; this only carries the answer. /// /// Color is a second channel, never the only one: every member keeps its letter, so /// the output means the same thing piped, pasted, or read by someone who cannot tell /// the hues apart. What color adds is that a member's bytes read as one block in the /// map, its row in the table is the same hue, and padding falls back as faint gaps. pub const Paint = enum { plain, ansi, /// The terminal's own six hues rather than fixed RGB values, so the user's theme /// decides what they look like and keeps them readable on its own background. /// Cycled by member index, so neighbouring members never share one. Each starts /// with a reset because faint persists until something clears it. const hues = [_][]const u8{ "\x1b[0;36m", // cyan "\x1b[0;33m", // yellow "\x1b[0;35m", // magenta "\x1b[0;32m", // green "\x1b[0;34m", // blue "\x1b[0;31m", // red }; /// Faint (SGR 2) rather than "bright black", which some themes (Solarized among /// them) draw in nearly the background color. const faint = "\x1b[0;2m"; const reset = "\x1b[0m"; fn member(self: Paint, index: usize) []const u8 { return self.use(hues[index % hues.len]); } fn padding(self: Paint) []const u8 { return self.use(faint); } fn end(self: Paint) []const u8 { return self.use(reset); } /// `code` when painting, nothing when plain. fn use(self: Paint, code: []const u8) []const u8 { return if (self == .ansi) code else ""; } }; /// Lay out a C struct and render it: a table of members and padding, a summary line, /// and a memory map with one letter per byte. Allocated, because a struct's table has /// no useful upper bound. pub fn structLayout( allocator: std.mem.Allocator, source: []const u8, abi: *const struct_layout.Abi, paint: Paint, ) Result { var diag: CStructParser.Diagnostic = .{}; var parsed = CStructParser.parse(allocator, source, &diag) catch |err| { return located(allocator, err, source, diag.span); }; defer parsed.deinit(); const definition = parsed.definition; var layout_diag: struct_layout.Diagnostic = .{}; var layout = struct_layout.compute(allocator, definition, abi, &layout_diag) catch |err| { const span = if (layout_diag.member) |i| definition.members[i].span else null; return located(allocator, err, source, span); }; defer layout.deinit(allocator); var scratch: std.heap.ArenaAllocator = .init(allocator); defer scratch.deinit(); const rows = tableRows(scratch.allocator(), definition, &layout) catch |err| return .fail(err); var out: std.Io.Writer.Allocating = .init(allocator); defer out.deinit(); writeLayout(&out.writer, rows, definition, &layout, paint) catch return .fail(error.OutOfMemory); const text = out.toOwnedSlice() catch |err| return .fail(err); return .{ .output = text, .is_error = false }; } /// An error, and where in the definition it is, when it is anywhere in particular: /// /// error: unknown type (typedefs and macros from elsewhere are not visible) /// line 3, column 5: uint33_t fn located(allocator: std.mem.Allocator, err: engine.Error, source: []const u8, span: ?struct_layout.Span) Result { // Running out of memory is about the machine, not a place in the input, and the // parser's diagnostic always holds a span, so this comes first. if (err == error.OutOfMemory) return .fail(err); const s = span orelse return .fail(err); const at = CStructParser.locate(source, s.start); const text = source[s.start..][0..s.len]; // One line of it, and not much of that: a directive is reported whole. const line_end = std.mem.indexOfScalar(u8, text, '\n') orelse text.len; const shown = text[0..@min(line_end, 40)]; const output = std.fmt.allocPrint(allocator, "error: {s}\n line {d}, column {d}{s}{s}\n", .{ engine.phrase(err), at.line, at.column, if (shown.len == 0) ", at the end of the input" else ": ", shown, }) catch return .fail(error.OutOfMemory); return .{ .output = output, .is_error = true }; } /// The letter a member is drawn with in the map, matching its row in the table. fn memberKey(index: usize) u8 { if (index < 26) return @intCast('a' + index); if (index < max_keyed_members) return @intCast('A' + index - 26); return ' '; } /// A count with thousands separators (NFR-7), printed with `{f}`. const Grouped = struct { n: u64, pub fn format(self: Grouped, w: *std.Io.Writer) std.Io.Writer.Error!void { var digits: [20]u8 = undefined; const len = std.fmt.printInt(&digits, self.n, 10, .lower, .{}); try engine.grouping.print(w, digits[0..len]); } }; /// A member's type as a person writes it, dimensions included: `char[16]`, /// `int[2][3]`, `double[]`. Printed with `{f}`. const TypeName = struct { member: struct_layout.Member, pub fn format(self: TypeName, w: *std.Io.Writer) std.Io.Writer.Error!void { try w.writeAll(self.member.type_name); if (self.member.flexible) try w.writeAll("[]"); for (self.member.dims) |dim| try w.print("[{d}]", .{dim}); } }; /// One line of the table, its cells rendered to text once. A column's width is /// measured from the same text that is printed, so the two cannot disagree. const Row = struct { kind: union(enum) { header, member: usize, padding }, offset: []const u8, size: []const u8, alignment: []const u8, type: []const u8, name: []const u8, }; /// The table: a header, then one row per segment of the layout, which is the same /// list the memory map draws. Allocated in `arena`, which the caller releases. fn tableRows(arena: std.mem.Allocator, def: struct_layout.Definition, layout: *const struct_layout.Layout) ![]const Row { var rows: std.ArrayList(Row) = .empty; try rows.append(arena, .{ .kind = .header, .offset = "Offset", .size = "Size", .alignment = "Align", .type = "Type", .name = "Name", }); var it = layout.segments(); while (it.next()) |seg| { const offset = try std.fmt.allocPrint(arena, "{f}", .{Grouped{ .n = seg.offset }}); const size = try std.fmt.allocPrint(arena, "{f}", .{Grouped{ .n = seg.len }}); try rows.append(arena, switch (seg.kind) { .member => |i| .{ .kind = .{ .member = i }, .offset = offset, .size = size, .alignment = try std.fmt.allocPrint(arena, "{f}", .{Grouped{ .n = layout.placements[i].alignment }}), .type = try std.fmt.allocPrint(arena, "{f}", .{TypeName{ .member = def.members[i] }}), .name = def.members[i].name, }, .padding, .tail => .{ .kind = .padding, .offset = offset, .size = size, .alignment = "", .type = "", .name = if (seg.kind == .tail) "(tail padding)" else "(padding)", }, }); } return rows.items; } /// The widest cell in each column. const Widths = struct { offset: usize = 0, size: usize = 0, alignment: usize = 0, type: usize = 0, fn of(rows: []const Row) Widths { var widths: Widths = .{}; for (rows) |row| { widths.offset = @max(widths.offset, row.offset.len); widths.size = @max(widths.size, row.size.len); widths.alignment = @max(widths.alignment, row.alignment.len); widths.type = @max(widths.type, row.type.len); } return widths; } }; /// A member row colors its key and its name, which is enough to find its bytes in the /// map without turning the table into a wall of color. A padding row is faint from /// end to end, and the header is plain. fn writeRow(w: *std.Io.Writer, row: Row, widths: Widths, paint: Paint) std.Io.Writer.Error!void { const key: u8, const key_color: []const u8, const key_end: []const u8, const name_color: []const u8 = switch (row.kind) { .header => .{ ' ', "", "", "" }, .member => |i| .{ memberKey(i), paint.member(i), paint.end(), paint.member(i) }, .padding => .{ ' ', paint.padding(), "", "" }, }; const line_end = if (row.kind == .header) "" else paint.end(); try w.print(" {[kc]s}{[key]c}{[ke]s} {[offset]s: >[ow]} {[size]s: >[sw]} {[alignment]s: >[aw]}" ++ " {[type]s: <[tw]} {[nc]s}{[name]s}{[end]s}\n", .{ .kc = key_color, .key = key, .ke = key_end, .offset = row.offset, .ow = widths.offset, .size = row.size, .sw = widths.size, .alignment = row.alignment, .aw = widths.alignment, .type = row.type, .tw = widths.type, .nc = name_color, .name = row.name, .end = line_end, }); } fn writeLayout( w: *std.Io.Writer, rows: []const Row, def: struct_layout.Definition, layout: *const struct_layout.Layout, paint: Paint, ) std.Io.Writer.Error!void { if (def.name) |name| switch (def.name_kind) { .tag => try w.print("struct {s}\n", .{name}), .typedef => try w.print("{s}\n", .{name}), } else try w.writeAll("struct (anonymous)\n"); try w.print("ABI {s}: {s}\n\n", .{ layout.abi.id, layout.abi.label }); const widths: Widths = .of(rows); for (rows) |row| try writeRow(w, row, widths, paint); // The summary: what a reader came for. try w.print("\n {f} {s}, alignment {d}, ", .{ Grouped{ .n = layout.size }, bytes(layout.size), layout.alignment }); const padding = layout.padding(); if (padding == 0) { try w.writeAll("no padding\n"); } else { try w.print("{f} {s} of padding (", .{ Grouped{ .n = padding }, bytes(padding) }); try writePercent(w, padding, layout.size); try w.print("); {f} {s} packed\n", .{ Grouped{ .n = layout.packed_size }, bytes(layout.packed_size) }); } if (layout.size == 0) return; if (def.members.len > max_keyed_members) { try w.print("\n (no memory map: more than {d} members to tell apart)\n", .{max_keyed_members}); return; } try w.writeAll("\n"); try writeMap(w, layout, paint); } fn bytes(n: u64) []const u8 { return if (n == 1) "byte" else "bytes"; } /// `part` as a percentage of `whole`, rounded half up to two places, with trailing /// zeros dropped: 6.25%, 50%, 33.33%. In integer hundredths, so no float rounding /// decides what is printed. fn writePercent(w: *std.Io.Writer, part: u64, whole: u64) std.Io.Writer.Error!void { const hundredths: u128 = (@as(u128, part) * 20000 + whole) / (@as(u128, whole) * 2); const units = hundredths / 100; const frac = hundredths % 100; if (frac == 0) { try w.print("{d}%", .{units}); } else if (frac % 10 == 0) { try w.print("{d}.{d}%", .{ units, frac / 10 }); } else { try w.print("{d}.{d:0>2}%", .{ units, frac }); } } /// The segment holding a byte, for bytes visited in increasing order. const SegmentCursor = struct { it: struct_layout.SegmentIterator, current: struct_layout.Segment, /// How many segments have been passed: which one `current` is. index: usize = 0, fn init(layout: *const struct_layout.Layout) SegmentCursor { var it = layout.segments(); const first = it.next().?; return .{ .it = it, .current = first }; } /// Zero-length segments are passed over, since no byte is in them. fn at(self: *SegmentCursor, offset: u64) struct_layout.Segment { while (self.current.offset + self.current.len <= offset) { self.current = self.it.next().?; self.index += 1; } return self.current; } }; /// Sixteen bytes a row, each drawn as its member's letter or `.` for padding, in /// groups of four. A row identical to the one above it is collapsed to `*`, as /// hexdump does, so `char buf[4096]` costs two lines rather than 256. Rows are found /// from the segments rather than walked byte by byte, so a struct the size of an /// address space costs no more than one with the same number of members. /// /// In color, a code is written only where the color changes, so a member's run of /// bytes, gaps included, is one span of one hue. fn writeMap(w: *std.Io.Writer, layout: *const struct_layout.Layout, paint: Paint) std.Io.Writer.Error!void { const per_row = 16; const size = layout.size; const rows = (size + per_row - 1) / per_row; const last_offset = (rows - 1) * per_row; var label_digits: usize = 2; while (label_digits < 16 and last_offset >> @intCast(label_digits * 4) != 0) label_digits += 1; var cursor: SegmentCursor = .init(layout); // The segment the last printed row lay wholly inside, if it did. var previous_whole: ?usize = null; var starred = false; var row: u64 = 0; while (row < rows) { const start = row * per_row; const end = @min(start + per_row, size); const seg = cursor.at(start); const whole = end - start == per_row and seg.offset + seg.len >= end; if (whole and previous_whole == cursor.index) { if (!starred) try w.writeAll(" *\n"); starred = true; // Straight to the first row this segment does not fill. row = (seg.offset + seg.len) / per_row; continue; } starred = false; previous_whole = if (whole) cursor.index else null; try w.writeAll(" 0x"); try w.printInt(start, 16, .lower, .{ .fill = '0', .width = label_digits }); var bytes_cursor = cursor; var current: []const u8 = ""; for (start..end) |offset| { const glyph: u8, const color = switch (bytes_cursor.at(offset).kind) { .member => |i| .{ memberKey(i), paint.member(i) }, .padding, .tail => .{ '.', paint.padding() }, }; try w.writeAll(if ((offset - start) % 4 == 0) " " else " "); if (!std.mem.eql(u8, color, current)) { try w.writeAll(color); current = color; } try w.writeByte(glyph); } try w.print("{s}\n", .{paint.end()}); row += 1; } } // -- Errors -- /// Turn an engine error into a CLI line: "error: \n". /// /// The phrases live once, in `engine.phrase`. This adds the prefix and the newline at /// comptime, so the strings still have static lifetime and there is no second copy of /// the wording to drift. The CLI and the TUI previously each kept their own switch /// over the whole error set; the TUI's was already missing three cases and rendered /// them as "evaluation error". /// /// `inline else` makes this exhaustive with no fallback branch: an error added /// anywhere in the engine fails to compile in `engine.phrase` rather than silently /// reading "evaluation error" here. Reached through `Result.fail`. fn decoratedError(err: engine.Error) []const u8 { return switch (err) { inline else => |e| comptime "error: " ++ engine.phrase(e) ++ "\n", }; } /// The loan terms a schedule cannot be built from, in the words of the thing the /// user actually typed. fn amortizationError(err: engine.Error) Result { return switch (err) { // The realistic causes are all one of these, and a bare "domain error" // would leave the user guessing which. engine.Error.DomainError => .{ .output = "error: check the loan terms: principal and periods must be positive, the rate cannot be negative, and the payment must at least cover the first period's interest\n", .is_error = true, }, else => .fail(err), }; } /// An amount too large to render. Reported rather than printed as a placeholder: /// the previous local formatter returned "?" for these, so a table of question /// marks came out with exit status 0. const unformattable: Result = .{ .output = "error: an amount in this schedule is too large to format\n", .is_error = true, }; // -- Tests -- const testing = std.testing; /// A unit by name, for the tests that call a formatter directly. In the real paths /// `units.parseRequest` has already resolved both units by the time a formatter sees /// them, which is why the formatters take `UnitDef` rather than names. fn unit(name: []const u8) engine.UnitDef { return engine.units.findUnit(name).?; } test "evaluate: standard arithmetic" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluateWith(arena.allocator(), "2 + 2", .standard, .{}, &buf); try testing.expect(!result.is_error); try testing.expectEqualStrings("4\n", result.output); } test "evaluate: large number has commas" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluateWith(arena.allocator(), "2^32 - 1", .standard, .{}, &buf); try testing.expect(!result.is_error); try testing.expectEqualStrings("4,294,967,295\n", result.output); } test "evaluate: function" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluateWith(arena.allocator(), "sin(pi/2) + 1", .standard, .{}, &buf); try testing.expect(!result.is_error); try testing.expectEqualStrings("2\n", result.output); } test "evaluate: explicit mul" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluateWith(arena.allocator(), "3*(4+5)", .standard, .{}, &buf); try testing.expect(!result.is_error); try testing.expectEqualStrings("27\n", result.output); } test "evaluate: programmer mode" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluateWith(arena.allocator(), "0xFF & 0x0F", .programmer, .{}, &buf); try testing.expect(!result.is_error); try testing.expect(std.mem.indexOf(u8, result.output, "15") != null); try testing.expect(std.mem.indexOf(u8, result.output, "00 00 00 00 00 00 00 0F") != null); } test "evaluate: division by zero error" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluateWith(arena.allocator(), "1/0", .standard, .{}, &buf); try testing.expect(result.is_error); try testing.expectEqualStrings("error: division by zero\n", result.output); } test "evaluate: unknown variable error" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluateWith(arena.allocator(), "xyz + 1", .standard, .{}, &buf); try testing.expect(result.is_error); try testing.expectEqualStrings("error: unknown variable\n", result.output); } test "evaluate: an expression written in another base answers in every base" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; // Standard mode, but a hex literal says the user is thinking in bases, so the // answer carries the other three rather than only decimal. const hex = evaluateWith(arena.allocator(), "0xFF + 1", .standard, .{}, &buf); try testing.expect(!hex.is_error); try testing.expectEqualStrings( \\256 \\ hex: 01 00 \\ oct: 000 400 \\ bin: 0000 0001 0000 0000 \\ , hex.output); // The rows are as wide as the value needs and no wider: 10 fits one byte, where // 256 above needed two. Standard mode has no width setting to consult, so the // value chooses. const binary = evaluateWith(arena.allocator(), "0b1010", .standard, .{}, &buf); try testing.expect(!binary.is_error); try testing.expectEqualStrings( \\10 \\ hex: 0A \\ oct: 012 \\ bin: 0000 1010 \\ , binary.output); // The decimal line is the ordinary rendering, so it still groups its digits. const octal = evaluateWith(arena.allocator(), "0o777 * 2", .standard, .{}, &buf); try testing.expect(!octal.is_error); try testing.expect(std.mem.startsWith(u8, octal.output, "1,022\n")); try testing.expect(std.mem.indexOf(u8, octal.output, "hex: 03 FE") != null); // Past 2^53 the rows agree with the decimal line above them. They came from an // f64 and read `DE AD BE EF DE AD C0 00` under the right decimal (open item 3). const wide = evaluateWith(arena.allocator(), "0xDEADBEEFDEADBEEF", .standard, .{}, &buf); try testing.expectEqualStrings( \\16,045,690,984,833,335,023 \\ hex: DE AD BE EF DE AD BE EF \\ oct: 1 572 555 756 773 653 337 357 \\ bin: 1101 1110 1010 1101 1011 1110 1110 1111 1101 1110 1010 1101 1011 1110 1110 1111 \\ , wide.output); } test "evaluate: a non-decimal literal with no whole answer stays decimal" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; // The rows show a u128 bit pattern, and neither of these has one, so the answer // is rendered plainly instead of in four bases. Standard mode is not programmer // mode: it does not wrap a negative into two's complement to have something to // print. const negative = evaluateWith(arena.allocator(), "0xFF - 0x100", .standard, .{}, &buf); try testing.expect(!negative.is_error); try testing.expectEqualStrings("-1\n", negative.output); const fractional = evaluateWith(arena.allocator(), "0xFF / 2", .standard, .{}, &buf); try testing.expect(!fractional.is_error); try testing.expectEqualStrings("127.5\n", fractional.output); } test "evaluate: multi-base rows that outgrow the buffer are refused, not truncated" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); // Enough room for the decimal answer alone, nowhere near enough for the rows. // Half a table would read as a wrong answer, so nothing is printed. var small: [8]u8 = undefined; const result = evaluateWith(arena.allocator(), "0xFF + 1", .standard, .{}, &small); try testing.expect(result.is_error); try testing.expectEqualStrings("error: the result is too long to display\n", result.output); } test "the programmer config reaches the result" { var buf: [4096]u8 = undefined; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); // 8-bit: 0xFF + 1 wraps to 0. const narrow = evaluateWith(alloc, "0xFF + 1", .programmer, .{ .width = .bits8 }, &buf); try testing.expect(!narrow.is_error); try testing.expect(std.mem.indexOf(u8, narrow.output, "dec(unsigned): 0\n") != null); // Signedness decides whether >> extends the sign. const signed = evaluateWith(alloc, "0xFF >> 1", .programmer, .{ .width = .bits8, .signedness = .signed, }, &buf); try testing.expect(std.mem.indexOf(u8, signed.output, "dec(signed): -1") != null); const unsigned = evaluateWith(alloc, "0xFF >> 1", .programmer, .{ .width = .bits8, .signedness = .unsigned, }, &buf); try testing.expect(std.mem.indexOf(u8, unsigned.output, "dec(unsigned): 127") != null); // Byte order reverses the hex row and nothing else. const little = evaluateWith(alloc, "0xDEAD", .programmer, .{ .width = .bits16, .display_endian = .little, }, &buf); try testing.expect(std.mem.indexOf(u8, little.output, "hex: AD DE") != null); const big = evaluateWith(alloc, "0xDEAD", .programmer, .{ .width = .bits16, .display_endian = .big, }, &buf); try testing.expect(std.mem.indexOf(u8, big.output, "hex: DE AD") != null); } test "conversion: km to mi" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [256]u8 = undefined; const result = conversion(arena.allocator(), &buf, "100", unit("km"), unit("mi")); try testing.expect(!result.is_error); try testing.expect(std.mem.indexOf(u8, result.output, "62.137") != null); try testing.expect(std.mem.indexOf(u8, result.output, "km") != null); try testing.expect(std.mem.indexOf(u8, result.output, "mi") != null); } test "conversion: temperature freezing point" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [256]u8 = undefined; const result = conversion(arena.allocator(), &buf, "0", unit("C"), unit("F")); try testing.expect(!result.is_error); try testing.expectEqualStrings("0 C = 32 F\n", result.output); } test "conversion: incompatible units is an error" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [256]u8 = undefined; const result = conversion(arena.allocator(), &buf, "1", unit("kg"), unit("m")); try testing.expect(result.is_error); try testing.expect(std.mem.indexOf(u8, result.output, "incompatible") != null); } test "conversion: alias resolves to canonical name in output" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [256]u8 = undefined; const result = conversion(arena.allocator(), &buf, "1", unit("kilometer"), unit("meters")); try testing.expect(!result.is_error); // Conversion output now uses the standard comma grouping (NFR-7). try testing.expectEqualStrings("1 km = 1,000 m\n", result.output); } test "conversion: exact conversion prints exactly" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [256]u8 = undefined; // The bug this task exists to fix. const feet = conversion(arena.allocator(), &buf, "12", unit("in"), unit("ft")); try testing.expect(!feet.is_error); try testing.expectEqualStrings("12 in = 1 ft\n", feet.output); // Exact affine conversion. const celsius = conversion(arena.allocator(), &buf, "98.6", unit("F"), unit("C")); try testing.expect(!celsius.is_error); try testing.expectEqualStrings("98.6 F = 37 C\n", celsius.output); // Exact fractional factor. const mps = conversion(arena.allocator(), &buf, "3.6", unit("km/h"), unit("m/s")); try testing.expect(!mps.is_error); try testing.expectEqualStrings("3.6 km/h = 1 m/s\n", mps.output); } test "evaluate: bare conversion expression without subcommand" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluateWith(arena.allocator(), "100 km to mi", .standard, .{}, &buf); try testing.expect(!result.is_error); try testing.expect(std.mem.indexOf(u8, result.output, "62.137") != null); } test "evaluate: glued bare conversion" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluateWith(arena.allocator(), "32F to C", .standard, .{}, &buf); try testing.expect(!result.is_error); try testing.expectEqualStrings("32 F = 0 C\n", result.output); } test "evaluate: conversion value may be an expression" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluateWith(arena.allocator(), "2*3 kg to g", .standard, .{}, &buf); try testing.expect(!result.is_error); try testing.expectEqualStrings("6 kg = 6,000 g\n", result.output); } test "evaluate: bare conversion with unknown unit errors" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluateWith(arena.allocator(), "100 km to smoots", .standard, .{}, &buf); try testing.expect(result.is_error); try testing.expect(std.mem.indexOf(u8, result.output, "unknown unit") != null); } test "evaluate: bare conversion with incompatible units errors" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluateWith(arena.allocator(), "1 kg to m", .standard, .{}, &buf); try testing.expect(result.is_error); try testing.expect(std.mem.indexOf(u8, result.output, "incompatible") != null); } test "evaluate: ordinary expressions are unaffected by conversion detection" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; const result = evaluateWith(arena.allocator(), "2 + 3 * 4", .standard, .{}, &buf); try testing.expect(!result.is_error); try testing.expectEqualStrings("14\n", result.output); } test "evaluate: expression containing a unit-like name still evaluates" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); var buf: [4096]u8 = undefined; // "e" is Euler's number here, not a unit, because there is no "to" keyword. const result = evaluateWith(arena.allocator(), "e", .standard, .{}, &buf); try testing.expect(!result.is_error); try testing.expect(std.mem.startsWith(u8, result.output, "2.718")); } test "amortization: an unrenderable amount is an error, not a table of marks" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); // 1e40 used to produce a full report of "?" with exit status 0. It now fits, // because the engine formatter groups the whole number. const large = amortization(arena.allocator(), .{ .principal = 1e40, .rate = 0.5, .periods = 3, }, false); try testing.expect(!large.is_error); try testing.expect(std.mem.indexOfScalar(u8, large.output, '?') == null); try testing.expect(std.mem.indexOf(u8, large.output, "10,000,000,000,000,000,000,000,000,000,000,000,000,000.00") != null); // Past the width of the formatting buffer the report is refused outright, with // a non-zero exit status, rather than printed with placeholders. const huge = amortization(arena.allocator(), .{ .principal = 1e60, .rate = 0.5, .periods = 3, }, false); try testing.expect(huge.is_error); try testing.expect(std.mem.indexOf(u8, huge.output, "too large to format") != null); try testing.expect(std.mem.indexOfScalar(u8, huge.output, '?') == null); } test "amortization: table has a row per period and correct first row" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const result = amortization(arena.allocator(), .{ .principal = 200000, .rate = 0.5, .periods = 360, }, false); try testing.expect(!result.is_error); // Header, payment, blank, column head, 360 rows, blank, 4 summary lines, each // ending in a newline like every other result. try testing.expectEqual(@as(usize, 369), std.mem.count(u8, result.output, "\n")); try testing.expect(std.mem.endsWith(u8, result.output, "\n")); try testing.expect(std.mem.indexOf(u8, result.output, "Payment 1,199.10 per period") != null); try testing.expect(std.mem.indexOf(u8, result.output, "1,000.00") != null); try testing.expect(std.mem.indexOf(u8, result.output, "199,800.90") != null); try testing.expect(std.mem.indexOf(u8, result.output, "Total interest 231,677.04") != null); } test "amortization: summary only omits the rows" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); const result = amortization(arena.allocator(), .{ .principal = 200000, .rate = 0.5, .periods = 360, }, true); try testing.expect(!result.is_error); try testing.expect(std.mem.indexOf(u8, result.output, "Period Payment") == null); try testing.expect(std.mem.indexOf(u8, result.output, "Periods paid 360") != null); } test "amortization: bad terms explain themselves" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer _ = arena.deinit(); // A payment that never covers the interest. const result = amortization(arena.allocator(), .{ .principal = 200000, .rate = 0.5, .periods = 360, .payment = 500, }, false); try testing.expect(result.is_error); try testing.expect(std.mem.indexOf(u8, result.output, "loan terms") != null); } test "every result is complete text, ending in exactly one newline" { // `main` prints a result as it stands. This held for some outputs and not others: // single-line answers relied on `main` adding a newline, and programmer mode, // which carried its own, got a second one and ended in a blank line. var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); const a = arena.allocator(); // A buffer each: the results that write into one point into it, so sharing one // would leave the early results reading the last one's text. var bufs: [8][4096]u8 = undefined; for ([_]Result{ evaluateWith(a, "2 + 2", .standard, .{}, &bufs[0]), evaluateWith(a, "0xFF + 1", .standard, .{}, &bufs[1]), evaluateWith(a, "0xFF", .programmer, .{}, &bufs[2]), evaluateWith(a, "100 km to mi", .standard, .{}, &bufs[3]), evaluateWith(a, "1 / 0", .standard, .{}, &bufs[4]), evaluateWith(a, "1 +", .programmer, .{}, &bufs[5]), conversion(a, &bufs[6], "100", engine.units.findUnit("km").?, engine.units.findUnit("mi").?), conversion(a, &bufs[7], "abc", engine.units.findUnit("km").?, engine.units.findUnit("mi").?), amortization(a, .{ .principal = 1000, .rate = 1, .periods = 3 }, false), amortization(a, .{ .principal = 1000, .rate = 1, .periods = 3 }, true), amortization(a, .{ .principal = -1, .rate = 1, .periods = 3 }, false), structLayout(a, "struct { int a; char b; }", struct_layout.default_abi, .plain), structLayout(a, "struct { int a; char b; }", struct_layout.default_abi, .ansi), structLayout(a, "struct { bad b; }", struct_layout.default_abi, .plain), }) |result| { try testing.expect(std.mem.endsWith(u8, result.output, "\n")); try testing.expect(!std.mem.endsWith(u8, result.output, "\n\n")); } } // -- Struct layout -- /// What `structLayout` prints for `source` under the profile named `abi`, which must /// lay out. Allocated in `arena`. fn rendered(arena: std.mem.Allocator, source: []const u8, abi: []const u8, paint: Paint) ![]const u8 { const result = structLayout(arena, source, struct_layout.findAbi(abi).?, paint); try testing.expect(!result.is_error); return result.output; } /// A struct with one more member than the map has letters for. fn manyMembers(arena: std.mem.Allocator) ![]const u8 { var source: std.ArrayList(u8) = .empty; try source.appendSlice(arena, "struct { char "); for (0..max_keyed_members + 1) |i| try source.print(arena, "{s}m{d}", .{ if (i == 0) "" else ", ", i }); try source.appendSlice(arena, "; }"); return source.items; } test "structLayout: the design document's example, in full" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); try testing.expectEqualStrings( \\struct hdr \\ABI sysv64: 64-bit Linux and macOS (x86-64 SysV, AArch64) \\ \\ Offset Size Align Type Name \\ a 0 4 4 uint32_t magic \\ b 4 1 1 uint8_t ver \\ 5 1 (padding) \\ c 6 2 2 uint16_t len \\ d 8 8 8 uint64_t ts \\ \\ 16 bytes, alignment 8, 1 byte of padding (6.25%); 15 bytes packed \\ \\ 0x00 a a a a b . c c d d d d d d d d \\ , try rendered(arena.allocator(), "struct hdr { uint32_t magic; uint8_t ver; uint16_t len; uint64_t ts; }", "sysv64", .plain)); } test "structLayout: arrays, a flexible member, and a map that collapses" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); try testing.expectEqualStrings( \\buf_t \\ABI sysv64: 64-bit Linux and macOS (x86-64 SysV, AArch64) \\ \\ Offset Size Align Type Name \\ a 0 4 4 int len \\ b 4 4,096 1 char[4096] data \\ c 4,100 12 2 short[2][3] grid \\ d 4,112 0 8 double[] flex \\ \\ 4,112 bytes, alignment 8, no padding \\ \\ 0x0000 a a a a b b b b b b b b b b b b \\ 0x0010 b b b b b b b b b b b b b b b b \\ * \\ 0x1000 b b b b c c c c c c c c c c c c \\ , try rendered(arena.allocator(), "typedef struct { int len; char data[4096]; short grid[2][3]; double flex[]; } buf_t;", "sysv64", .plain)); } test "structLayout: a struct under another ABI says so and lays out by its rules" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); const out = try rendered(arena.allocator(), "struct { char c; long l; }", "win64", .plain); try testing.expect(std.mem.startsWith(u8, out, "struct (anonymous)\nABI win64: 64-bit Windows (x64, ARM64)\n")); try testing.expect(std.mem.indexOf(u8, out, " b 4 4 4 long l\n") != null); try testing.expect(std.mem.indexOf(u8, out, "8 bytes, alignment 4, 3 bytes of padding (37.5%); 5 bytes packed") != null); } test "structLayout: tail padding gets a row and dots of its own" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); try testing.expect(std.mem.endsWith(u8, try rendered(arena.allocator(), "struct tail { uint64_t a; uint8_t b; };", "sysv64", .plain), \\ a 0 8 8 uint64_t a \\ b 8 1 1 uint8_t b \\ 9 7 (tail padding) \\ \\ 16 bytes, alignment 8, 7 bytes of padding (43.75%); 9 bytes packed \\ \\ 0x00 a a a a a a a a b . . . . . . . \\ )); } test "structLayout: errors name the problem and where it is" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); const Case = struct { source: []const u8, abi: []const u8 = "sysv64", output: []const u8 }; for ([_]Case{ .{ .source = "struct {\n int a;\n uint33_t b;\n}", .output = "error: unknown type (typedefs and macros from elsewhere are not visible)\n line 3, column 3: uint33_t\n", }, .{ // Errors from laying out point at the member they are about. .source = "struct { char c; __int128 big; }", .abi = "arm32", .output = "error: this type does not exist on the selected ABI\n line 1, column 27: big\n", }, .{ .source = "struct { int a;", .output = "error: unexpected end of input\n line 1, column 16, at the end of the input\n", }, .{ // A directive is reported by its first line, and not much of that. .source = "#include \"a/very/long/path/to/a/header/that/goes/on.h\"\nstruct { int a; }", .output = "error: only #pragma pack is understood, not other directives\n line 1, column 1: #include \"a/very/long/path/to/a/header/t\n", }, .{ // An error about the whole struct has nowhere in particular to point. .source = "struct empty { };", .output = "error: the struct has no members\n", }, }) |case| { const result = structLayout(arena.allocator(), case.source, struct_layout.findAbi(case.abi).?, .plain); try testing.expect(result.is_error); try testing.expectEqualStrings(case.output, result.output); } } test "structLayout: more members than letters gets no map, and says why" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); const out = try rendered(arena.allocator(), try manyMembers(arena.allocator()), "sysv64", .plain); try testing.expect(std.mem.indexOf(u8, out, " Z 51 1 1 char m51\n") != null); try testing.expect(std.mem.indexOf(u8, out, " 52 1 1 char m52\n") != null); try testing.expect(std.mem.endsWith(u8, out, "(no memory map: more than 52 members to tell apart)\n")); } test "structLayout: a map of a huge struct is as short as a small one" { // Two gigabytes of array: walking it byte by byte would not finish. var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); try testing.expect(std.mem.endsWith(u8, try rendered(arena.allocator(), "struct { char c; double d[268435456]; }", "sysv64", .plain), \\ 0x00000000 a . . . . . . . b b b b b b b b \\ 0x00000010 b b b b b b b b b b b b b b b b \\ * \\ 0x80000000 b b b b b b b b \\ )); } test "writePercent drops the zeros it does not need" { var buf: [32]u8 = undefined; for ([_]struct { part: u64, whole: u64, text: []const u8 }{ .{ .part = 1, .whole = 16, .text = "6.25%" }, .{ .part = 1, .whole = 2, .text = "50%" }, .{ .part = 1, .whole = 3, .text = "33.33%" }, .{ .part = 3, .whole = 8, .text = "37.5%" }, }) |case| { var w = std.Io.Writer.fixed(&buf); try writePercent(&w, case.part, case.whole); try testing.expectEqualStrings(case.text, w.buffered()); } } /// `text` with every ANSI escape sequence (ESC, `[`, parameters, a final letter) /// removed. fn stripAnsi(allocator: std.mem.Allocator, text: []const u8) ![]u8 { var out: std.ArrayList(u8) = .empty; var i: usize = 0; while (i < text.len) { if (text[i] == 0x1b and i + 1 < text.len and text[i + 1] == '[') { i += 2; while (i < text.len and !std.ascii.isAlphabetic(text[i])) i += 1; i += 1; continue; } try out.append(allocator, text[i]); i += 1; } return out.items; } test "structLayout: color adds escapes and nothing else" { // The letters are the key and color is a second channel, so taking the color // away must leave exactly the plain output: same rows, same columns, same map. var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); const a = arena.allocator(); for ([_][]const u8{ "struct hdr { uint32_t magic; uint8_t ver; uint16_t len; uint64_t ts; }", "struct tail { uint64_t a; uint8_t b; };", "typedef struct { int len; char data[4096]; short grid[2][3]; double flex[]; } buf_t;", "struct { char c; double d[268435456]; }", "struct { char a; char b; char c; char d; char e; char f; char g; char h; }", try manyMembers(a), }) |source| { const plain = try rendered(a, source, "sysv64", .plain); const colored = try rendered(a, source, "sysv64", .ansi); try testing.expect(std.mem.indexOfScalar(u8, plain, 0x1b) == null); try testing.expect(std.mem.indexOfScalar(u8, colored, 0x1b) != null); try testing.expectEqualStrings(plain, try stripAnsi(a, colored)); } } test "structLayout: a member's row and bytes share its hue, and padding is faint" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); const out = try rendered(arena.allocator(), "struct { char c; int i; }", "sysv64", .ansi); const first = Paint.hues[0]; const second = Paint.hues[1]; const faint = Paint.faint; const reset = Paint.reset; // In the table: the key and the name, nothing between them. try testing.expect(std.mem.indexOf(u8, out, " " ++ first ++ "a" ++ reset ++ " 0 1 1 char " ++ first ++ "c" ++ reset ++ "\n") != null); try testing.expect(std.mem.indexOf(u8, out, " " ++ second ++ "b" ++ reset ++ " 4 4 4 int " ++ second ++ "i" ++ reset ++ "\n") != null); // A padding row is faint from its first column to its end. try testing.expect(std.mem.indexOf(u8, out, " " ++ faint ++ " 1 3 (padding)" ++ reset ++ "\n") != null); // In the map, a code only where the color changes, and a reset at the end of // the row so nothing bleeds into what the terminal prints next. try testing.expect(std.mem.endsWith(u8, out, " 0x00 " ++ first ++ "a " ++ faint ++ ". . . " ++ second ++ "b b b b" ++ reset ++ "\n")); // The header, column titles and summary stay plain. try testing.expect(std.mem.startsWith(u8, out, "struct (anonymous)\nABI sysv64: 64-bit Linux and macOS (x86-64 SysV, AArch64)\n\n Offset")); try testing.expect(std.mem.indexOf(u8, out, "\n 8 bytes, alignment 4, 3 bytes of padding (37.5%); 5 bytes packed\n") != null); } test "Paint: neighbouring members never share a hue, and plain is empty" { for (0..max_keyed_members) |i| { try testing.expect(!std.mem.eql(u8, Paint.ansi.member(i), Paint.ansi.member(i + 1))); try testing.expectEqualStrings("", Paint.plain.member(i)); } try testing.expectEqualStrings("", Paint.plain.padding()); try testing.expectEqualStrings("", Paint.plain.end()); }