Something went wrong. Try again.
A general purpose .. programming language written in zig ijadux2.tngl.sh/Rinnescript
zig language
Something went wrong. Try again.
12 kB · 328 lines
Zig
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329//! `rinne fmt` — a safety-first, comment-preserving code formatter.//!//! Line-based by design: the formatter never reorders or merges lines, so//! comments and format strings survive byte-for-byte. Per line it://! - strips trailing whitespace,//! - splits code from trailing `//` comment at the first // outside quotes,//! - re-indents to the current brace depth (counted outside strings),//! - collapses 3+ blank lines into one.//!//! `--check` exits 1 when a file would change (CI-friendly).const std = @import("std");
pub const Error = error{ OutOfMemory, FileOpenFailed,} || std.Io.Reader.Error;
/// Formats `src` and returns the result (caller frees with `alloc`)./// Pure function — no I/O — so tests run in-memory.pub fn formatSource(alloc: std.mem.Allocator, src: []const u8) Error![]u8 { var out: std.ArrayList(u8) = .empty; errdefer out.deinit(alloc);
var depth: usize = 0; var blank_run: usize = 0; // Continuation indentation: when the previous code line ends with an open // construct (binary operator, `(`, `[`, `,`, `=`, `:`), the following // lines get one extra indent level until the construct closes. This keeps // multi-line expressions aligned instead of collapsing them to brace depth. var cont_open = false;
var it = std.mem.splitScalar(u8, src, '\n'); while (it.next()) |raw_line| { const line = trimSpace(raw_line);
if (line.len == 0) { blank_run += 1; if (blank_run <= 2) out.append(alloc, '\n') catch return Error.OutOfMemory; continue; } blank_run = 0;
const split = splitComment(line); const code = trimSpace(split.code); const comment = split.comment;
// Indentation for non-empty code; bare comments keep one level. // A line that OPENS with a closing brace sits one level out. var eff_depth: usize = if (std.mem.startsWith(u8, code, "}") and depth > 0) depth - 1 else depth; // A continuation line (previous code line ended with an open token) is // indented one extra level, except a `}` that already returns to brace // depth-1 (e.g. the end of a multi-line expression inside a block). if (cont_open and !std.mem.startsWith(u8, code, "}")) { eff_depth += 1; } const pad = eff_depth * 4; try writeIndent(&out, alloc, pad); if (code.len != 0) { out.appendSlice(alloc, code) catch return Error.OutOfMemory; if (comment.len != 0) out.append(alloc, ' ') catch return Error.OutOfMemory; } if (comment.len != 0) { out.appendSlice(alloc, "//") catch return Error.OutOfMemory; out.appendSlice(alloc, comment) catch return Error.OutOfMemory; } out.append(alloc, '\n') catch return Error.OutOfMemory;
// Depth update counts braces outside string literals of this line. depth = updateDepth(depth, code); // Continuation state carries into the next line unless this line ends // with an open token that keeps the expression pending. cont_open = code.len != 0 and endsWithOpen(code); }
// Idempotency: exactly one trailing newline regardless of input. while (out.items.len != 0 and out.items[out.items.len - 1] == '\n') { _ = out.pop(); } if (out.items.len != 0) { out.append(alloc, '\n') catch return Error.OutOfMemory; }
return out.toOwnedSlice(alloc);}
fn trimSpace(s: []const u8) []const u8 { return std.mem.trim(u8, s, " \t\r");}
/// True when the trimmed code segment ends with an "open" construct that/// implies a following line is a continuation: a binary operator, comma,/// assignment, colon (map/type), or an unclosed `(`/`[`. Braces are handled by/// brace depth separately, so they are not considered here. A trailing `(` or/// `[` that is itself unclosed counts as open regardless of the char after it.fn endsWithOpen(code: []const u8) bool { const c = if (code.len == 0) 0 else code[code.len - 1]; if (c == '(' or c == '[') return true; switch (c) { '+', '-', '*', '/', '%', '=', ',', ':', '&', '|', '^', '<', '>', '!', '.', '?' => return true, else => {}, } return false;}
fn writeIndent(out: *std.ArrayList(u8), alloc: std.mem.Allocator, pad: usize) Error!void { var i: usize = 0; while (i < pad) : (i += 1) out.append(alloc, ' ') catch return Error.OutOfMemory;}
/// Returns the index of the first `//` outside string literals, plus the/// remainder after it (without leading spaces). `comment` is empty when none.fn splitComment(line: []const u8) struct { code: []const u8, comment: []const u8 } { var in_str = false; var i: usize = 0; while (i < line.len) : (i += 1) { const c = line[i]; if (in_str) { if (c == '\\') { i += 1; // skip escaped char } else if (c == '"') { in_str = false; } continue; } if (c == '"') { in_str = true; continue; } if (c == '/' and i + 1 < line.len and line[i + 1] == '/') { return .{ .code = line[0..i], .comment = line[i + 2 ..] }; } } return .{ .code = line, .comment = "" };}
/// Adjusts brace depth using only the code segment (comments excluded).fn updateDepth(depth: usize, code: []const u8) usize { var d = depth; var in_str = false; var i: usize = 0; while (i < code.len) : (i += 1) { const c = code[i]; if (in_str) { if (c == '\\') { i += 1; } else if (c == '"') { in_str = false; } continue; } switch (c) { '"' => in_str = true, '{' => d += 1, '}' => d -|= 1, else => {}, } } return d;}
/// CLI entry: formats FILE in place, or checks without writing under --check.pub fn run( gpa: std.mem.Allocator, io: std.Io, errw: *std.Io.Writer, check_only: bool, path: []const u8,) !u8 { const src = std.Io.Dir.cwd().readFileAlloc(io, path, gpa, .limited(256 * 1024 * 1024)) catch |e| { errw.print("fmt error: cannot read '{s}' ({s})\n", .{ path, @errorName(e) }) catch {}; return error.CompileFailed; };
const formatted = try formatSource(gpa, src); defer gpa.free(formatted);
if (!std.mem.eql(u8, formatted, src)) { if (check_only) { errw.print("{s}: would be reformatted\n", .{path}) catch {}; return 1; } std.Io.Dir.cwd().writeFile(io, .{ .sub_path = path, .data = formatted }) catch |e| { errw.print("fmt error: cannot write '{s}' ({s})\n", .{ path, @errorName(e) }) catch {}; return error.CompileFailed; }; } return 0;}
// ---------------------------------------------------------------------------// Regression tests — pinning CURRENT behavior of formatSource.// These are characterization tests: they assert what the code DOES today,// not what it should do. If a test fails after a change, the change altered// observable behavior — review carefully.// ---------------------------------------------------------------------------
const testing = std.testing;
test "fmt: idempotency — format(format(x)) == format(x)" { const src = \\fn main() int { \\ var x: int = 1; // a comment \\ // standalone \\ var s: str = "// not a comment"; \\ if x > 0 { \\ return x; \\ } \\ return 0; \\} \\ \\ \\ ; const once = try formatSource(testing.allocator, src); defer testing.allocator.free(once); const twice = try formatSource(testing.allocator, once); defer testing.allocator.free(twice); try testing.expectEqualStrings(once, twice);}
test "fmt: exactly one trailing newline — none in input" { const src = "hello"; const expected = "hello\n"; const result = try formatSource(testing.allocator, src); defer testing.allocator.free(result); try testing.expectEqualStrings(expected, result);}
test "fmt: exactly one trailing newline — several in input" { const src = "hello\n\n\n\n"; const expected = "hello\n"; const result = try formatSource(testing.allocator, src); defer testing.allocator.free(result); try testing.expectEqualStrings(expected, result);}
test "fmt: reindent — single nesting level (wrong indent corrected)" { const src = "{\n a;\n}\n"; const expected = "{\n a;\n}\n"; const result = try formatSource(testing.allocator, src); defer testing.allocator.free(result); try testing.expectEqualStrings(expected, result);}
test "fmt: reindent — double nesting level (wrong indent corrected)" { const src = "{\n {\n b;\n }\n}\n"; const expected = "{\n {\n b;\n }\n}\n"; const result = try formatSource(testing.allocator, src); defer testing.allocator.free(result); try testing.expectEqualStrings(expected, result);}
test "fmt: comment split — // inside string literal survives untouched" { const src = "var s: str = \"// not a comment\";\n"; const expected = "var s: str = \"// not a comment\";\n"; const result = try formatSource(testing.allocator, src); defer testing.allocator.free(result); try testing.expectEqualStrings(expected, result);}
test "fmt: comment split — real trailing comment preserved" { const src = "a // comment\n"; const expected = "a // comment\n"; const result = try formatSource(testing.allocator, src); defer testing.allocator.free(result); try testing.expectEqualStrings(expected, result);}
test "fmt: blank-line collapsing — 4 blank lines become 2" { const src = "a\n\n\n\nb\n"; const expected = "a\n\n\nb\n"; const result = try formatSource(testing.allocator, src); defer testing.allocator.free(result); try testing.expectEqualStrings(expected, result);}
test "fmt: blank-line collapsing — 2 blank lines preserved" { const src = "a\n\n\nb\n"; const expected = "a\n\n\nb\n"; const result = try formatSource(testing.allocator, src); defer testing.allocator.free(result); try testing.expectEqualStrings(expected, result);}
test "fmt: blank-line collapsing — 0 blank lines preserved" { const src = "a\nb\n"; const expected = "a\nb\n"; const result = try formatSource(testing.allocator, src); defer testing.allocator.free(result); try testing.expectEqualStrings(expected, result);}
test "fmt: continuation indent — multi-line expression keeps alignment" { const src = "const x = 1 +\n 2 +\n 3;\n"; const expected = "const x = 1 +\n 2 +\n 3;\n"; const result = try formatSource(testing.allocator, src); defer testing.allocator.free(result); try testing.expectEqualStrings(expected, result);}
test "fmt: continuation indent — idempotent across a re-run" { const src = "const x = 1 +\n 2 +\n 3;\n"; const once = try formatSource(testing.allocator, src); defer testing.allocator.free(once); const twice = try formatSource(testing.allocator, once); defer testing.allocator.free(twice); try testing.expectEqualStrings(once, twice);}
test "fmt: continuation inside a block uses block+continuation depth" { const src = "fn f() int {\n const y = v +\n w;\n return y;\n}\n"; const expected = "fn f() int {\n const y = v +\n w;\n return y;\n}\n"; const result = try formatSource(testing.allocator, src); defer testing.allocator.free(result); try testing.expectEqualStrings(expected, result);}
test "fmt: string with a trailing binary-ish char is not a continuation" { // `"+"` and `"+"` in a string must not trigger cont_open on the next line. const src = "var s: str = \"+\";\nreturn 0;\n"; const expected = "var s: str = \"+\";\nreturn 0;\n"; const result = try formatSource(testing.allocator, src); defer testing.allocator.free(result); try testing.expectEqualStrings(expected, result);}