Something went wrong. Try again.
A general purpose .. programming language written in zig ijadux2.tngl.sh/Rinnescript
zig language
Something went wrong. Try again.
9.8 kB · 280 lines
Zig
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281//! `rinne lint` — static warnings over the real parser AST.//!//! Checks (v1)://! - unused local variables,//! - unused function parameters,//! - unreachable statements after return/break/continue.//!//! Warnings never fail the run: exit code is always 0.//! Output: `path:LINE:COL: warning: <msg>` lines + summary.const std = @import("std");const parser_mod = @import("../script/parser.zig");const Stmt = parser_mod.Stmt;const Expr = parser_mod.Expr;
pub const Warning = struct { line: usize, col: usize, msg: []const u8, // owned by the collector's allocator};
pub const Result = struct { warnings: []Warning,
pub fn deinit(self: *Result, alloc: std.mem.Allocator) void { for (self.warnings) |w| alloc.free(w.msg); alloc.free(self.warnings); }};
const Ctx = struct { alloc: std.mem.Allocator, out: std.ArrayList(Warning) = .empty,
fn add(self: *Ctx, line: usize, col: usize, comptime fmt: []const u8, args: anytype) void { const msg = std.fmt.allocPrint(self.alloc, fmt, args) catch return; self.out.append(self.alloc, .{ .line = line, .col = col, .msg = msg }) catch { self.alloc.free(msg); }; }};
/// Lints a parsed program. The AST is read-only here; caller keeps ownership.pub fn lintProgram(alloc: std.mem.Allocator, program: *const parser_mod.Program) !Result { var ctx = Ctx{ .alloc = alloc }; for (program.fn_decls) |*fd| { try lintFnDecl(&ctx, fd); } try lintBlockBody(&ctx, &program.global_stmts); return .{ .warnings = try ctx.out.toOwnedSlice(alloc) };}
fn lintFnDecl(ctx: *Ctx, fd: *const parser_mod.FnDecl) !void { // Collect identifier usage inside the body. var uses: std.StringHashMap(usize) = .init(ctx.alloc); // Keys are borrowed slices (AST identifiers / param names) owned by the // caller's program — never freed here. defer uses.deinit(); try countBlockUses(&uses, &fd.body);
// Unused params. FnParam carries no source position, so we point at the // function's first body line (col 1). const fn_line: u32 = if (fd.body.lines.len > 0) fd.body.lines[0] else 0; for (fd.params) |*p| { const n = uses.get(p.name) orelse 0; if (n == 0) ctx.add(fn_line, 1, "unused parameter '{s}'", .{p.name}); }
try lintBlockBody(ctx, &fd.body);}
fn countBlockUses(uses: *std.StringHashMap(usize), block: *const parser_mod.Block) !void { for (block.stmts) |*s| try countStmtUses(uses, s);}
fn bump(uses: *std.StringHashMap(usize), name: []const u8) void { const gop = uses.getOrPut(name) catch return; if (!gop.found_existing) { gop.key_ptr.* = name; // AST-backed slice; lives as long as the program gop.value_ptr.* = 0; } gop.value_ptr.* += 1;}
fn countExprUses(uses: *std.StringHashMap(usize), e: *const Expr) void { switch (e.*) { .identifier => |name| bump(uses, name), .binary => |b| { countExprUses(uses, b.lhs); countExprUses(uses, b.rhs); }, .unary => |u| countExprUses(uses, u.operand), .call => |c| { countExprUses(uses, c.callee); for (c.args) |*a| countExprUses(uses, a); }, .index_access => |ix| { countExprUses(uses, ix.object); countExprUses(uses, ix.index); }, .field_access => |fa| countExprUses(uses, fa.object), .array_lit => |items| for (items) |*it| countExprUses(uses, it), .struct_lit => |fields| for (fields) |*f| countExprUses(uses, &f.value), .call_directive => |cd| for (cd.args) |*a| countExprUses(uses, a), else => {}, }}
fn countStmtUses(uses: *std.StringHashMap(usize), s: *const Stmt) error{OutOfMemory}!void { switch (s.*) { .var_decl => |*v| countExprUses(uses, &v.initializer), .assignment => |*a| { countExprUses(uses, &a.target); // counts as a use (v1 simplicity) countExprUses(uses, &a.value); }, .expr_stmt => |*e| countExprUses(uses, e), .if_stmt => |*i| { countExprUses(uses, &i.condition); try countBlockUses(uses, &i.then_branch); if (i.else_branch) |*els| try countBlockUses(uses, els); }, .while_stmt => |*w| { countExprUses(uses, &w.condition); try countBlockUses(uses, &w.body); }, .for_stmt => |*f| { countExprUses(uses, &f.iter); try countBlockUses(uses, &f.body); }, .return_stmt => |r| if (r) |*rv| countExprUses(uses, rv), .defer_stmt => |*d| countExprUses(uses, d), else => {}, }}
/// Walks statements reporting unused locals and unreachable code.fn lintBlockBody(ctx: *Ctx, block: *const parser_mod.Block) !void { const stmts = block.stmts; const lines = block.lines;
for (stmts, 0..) |*s, i| { // Unused local declarations. if (s.* == .var_decl) { const v = s.var_decl; var uses: std.StringHashMap(usize) = .init(ctx.alloc); // Keys are borrowed AST slices — never freed here. defer uses.deinit(); // Count uses in the REST of the block (after this decl). for (stmts[i + 1 ..]) |*later| try countStmtUses(&uses, later); const n = uses.get(v.name) orelse 0; if (n == 0) { // Stmt unions carry no positions; Block.lines[i] has it. const line: u32 = if (i < lines.len) lines[i] else 0; ctx.add(line, 1, "unused variable '{s}'", .{v.name}); } }
// Unreachable code after terminators. if (i + 1 < stmts.len) { const terminates = switch (s.*) { .return_stmt, .break_stmt, .continue_stmt => true, else => false, }; if (terminates) { ctx.add( lines[i], 1, "unreachable code after this statement ({d} statement{s} skipped)", .{ stmts.len - i - 1, if (stmts.len - i - 1 == 1) "" else "s" }, ); break; // one report per block is enough } }
// Recurse into nested blocks. switch (s.*) { .if_stmt => |*ifn| { try lintBlockBody(ctx, &ifn.then_branch); if (ifn.else_branch) |*els| try lintBlockBody(ctx, els); }, .while_stmt => |*w| try lintBlockBody(ctx, &w.body), .for_stmt => |*f| try lintBlockBody(ctx, &f.body), .range_for => |*r| try lintBlockBody(ctx, &r.body), else => {}, } }}
/// Counts identifier uses inside an initializer that refer to the declared/// name itself (e.g. `var x = f(x)` — rare, but counts as a use).fn countSelfUses(uses: *std.StringHashMap(usize), e: *const Expr) void { _ = uses; _ = e;}
/// CLI entry: lints FILES, printing warnings to errw. Always exits 0.pub fn run( gpa: std.mem.Allocator, io: std.Io, errw: *std.Io.Writer, paths: []const []const u8,) !u8 { var total: usize = 0; for (paths) |path| { const src = std.Io.Dir.cwd().readFileAlloc(io, path, gpa, .limited(256 * 1024 * 1024)) catch |e| { errw.print("lint error: cannot read '{s}' ({s})\n", .{ path, @errorName(e) }) catch {}; continue; };
var parser = parser_mod.Parser.init(gpa, src); var program = parser.parseProgram() catch { const msg = parser.err_msg orelse "parse error"; errw.print("{s}: warning: file does not parse ({s}); skipping\n", .{ path, msg }) catch {}; continue; }; defer program.deinit(gpa);
var result = try lintProgram(gpa, &program); defer result.deinit(gpa);
for (result.warnings) |w| { errw.print("{s}:{d}:{d}: warning: {s}\n", .{ path, w.line, w.col, w.msg }) catch {}; } total += result.warnings.len; } errw.print("lint: {d} warning{s}\n", .{ total, if (total == 1) "" else "s" }) catch {}; // Non-zero when any warning was reported so CI and scripts can gate on a // clean lint (mirrors `fmt --check`, which returns 1 when reformatting). return if (total > 0) 1 else 0;}
// ---------------------------------------------------------------------------// Tests// ---------------------------------------------------------------------------
const testing = std.testing;
fn expectWarnings(src: []const u8, want: []const []const u8) !void { var parser = parser_mod.Parser.init(testing.allocator, src); var program = try parser.parseProgram(); defer program.deinit(testing.allocator);
var result = try lintProgram(testing.allocator, &program); defer result.deinit(testing.allocator);
try testing.expectEqual(want.len, result.warnings.len); for (want, result.warnings) |expected, w| { try testing.expect(std.mem.indexOf(u8, w.msg, expected) != null); }}
test "lint: unused variable and parameter" { try expectWarnings( \\fn add(a: int, b: int) int { \\ return a; \\} \\var dead = 5; \\ , &.{ "unused parameter 'b'", "unused variable 'dead'" });}
test "lint: unused local flagged, used one clean" { try expectWarnings( \\fn f() int { \\ var used = 1; \\ var unused = 2; \\ return used; \\} \\ , &.{"unused variable 'unused'"});}
test "lint: unreachable code after return" { try expectWarnings( \\fn g() int { \\ return 1; \\ var x = 3; \\} \\ , &.{"unreachable code"});}