diff --git a/src/core.zig b/src/core.zig index 0ed6fa2..710d139 100644 --- a/src/core.zig +++ b/src/core.zig @@ -371,6 +371,6 @@ pub const Compilation = struct { } pub fn renderErrors(self: *const Compilation) void { - self.diagnostics.renderDebug(); + self.diagnostics.renderStderr(); } }; diff --git a/src/errors.test.zig b/src/errors.test.zig index e1c1326..39e7fda 100644 --- a/src/errors.test.zig +++ b/src/errors.test.zig @@ -237,12 +237,116 @@ test "renderExtended: multi-line span renders each line with carets" { try testing.expectEqualStrings(expected, output); } -test "render: default compact style still works" { +test "renderExtended: primary bundled with one note" { + var dl = errors.DiagnosticList.init(testing.allocator, "let x = y\nlet y = z\n", "b.sx"); + defer dl.deinit(); + const p = dl.addId(.err, "undefined name", Span{ .start = 8, .end = 9 }); + dl.addNote(p, Span{ .start = 14, .end = 15 }, "declared here"); + + const output = try renderToString(&dl, testing.allocator); + defer testing.allocator.free(output); + + const expected = + \\error: undefined name + \\ --> b.sx:1:9 + \\ | + \\ 1 | let x = y + \\ | ^ + \\ + \\note: declared here + \\ --> b.sx:2:5 + \\ | + \\ 2 | let y = z + \\ | ^ + \\ + ; + try testing.expectEqualStrings(expected, output); +} + +test "renderExtended: primary bundled with help (no fix code)" { + var dl = errors.DiagnosticList.init(testing.allocator, "foo bar\n", "h.sx"); + defer dl.deinit(); + const p = dl.addId(.err, "bad thing", Span{ .start = 0, .end = 3 }); + dl.addHelp(p, null, "try something else", null); + + const output = try renderToString(&dl, testing.allocator); + defer testing.allocator.free(output); + + const expected = + \\error: bad thing + \\ --> h.sx:1:1 + \\ | + \\ 1 | foo bar + \\ | ^^^ + \\ + \\help: try something else + \\ + ; + try testing.expectEqualStrings(expected, output); +} + +test "renderExtended: help with fix-it code substitutes the line and omits arrow" { + var dl = errors.DiagnosticList.init(testing.allocator, " x := foo.value\n", "f.sx"); + defer dl.deinit(); + // primary span covers "value" at columns 12..17. + const p = dl.addId(.err, "no such field", Span{ .start = 11, .end = 16 }); + dl.addHelp(p, Span{ .start = 11, .end = 16 }, "did you mean `val`?", " x := foo.val"); + + const output = try renderToString(&dl, testing.allocator); + defer testing.allocator.free(output); + + const expected = + \\error: no such field + \\ --> f.sx:1:12 + \\ | + \\ 1 | x := foo.value + \\ | ^^^^^ + \\ + \\help: did you mean `val`? + \\ | + \\ 1 | x := foo.val + \\ | ^^^^^ + \\ + ; + try testing.expectEqualStrings(expected, output); +} + +test "renderExtended: note and help bundle in note-then-help order" { + var dl = errors.DiagnosticList.init(testing.allocator, "aaa\n", "o.sx"); + defer dl.deinit(); + const p = dl.addId(.err, "primary", Span{ .start = 0, .end = 3 }); + // Add help first, then note: rendering must still emit note before help. + dl.addHelp(p, null, "the help", null); + dl.addNote(p, Span{ .start = 0, .end = 3 }, "the note"); + + const output = try renderToString(&dl, testing.allocator); + defer testing.allocator.free(output); + + const expected = + \\error: primary + \\ --> o.sx:1:1 + \\ | + \\ 1 | aaa + \\ | ^^^ + \\ + \\note: the note + \\ --> o.sx:1:1 + \\ | + \\ 1 | aaa + \\ | ^^^ + \\ + \\help: the help + \\ + ; + try testing.expectEqualStrings(expected, output); +} + +test "render: compact style still available behind the flag" { var dl = errors.DiagnosticList.init(testing.allocator, "abc\n", "c.sx"); defer dl.deinit(); + dl.render_style = .compact; dl.add(.err, "compact error", Span{ .start = 0, .end = 3 }); - // Default render_style is .compact — preserves the existing gcc-style. var aw = std.Io.Writer.Allocating.init(testing.allocator); defer { var r = aw.writer.toArrayList(); diff --git a/src/errors.zig b/src/errors.zig index ad8ed4a..d2b9484 100644 --- a/src/errors.zig +++ b/src/errors.zig @@ -5,6 +5,7 @@ pub const Level = enum { err, warn, note, + help, }; pub const RenderStyle = enum { @@ -103,6 +104,15 @@ pub const Diagnostic = struct { message: []const u8, span: ?Span, source_file: ?[]const u8 = null, + + /// Index into `DiagnosticList.items` of the primary `err`/`warn` this + /// note/help is bundled under. `null` for a standalone primary. + primary: ?usize = null, + + /// For a `.help`: the suggested replacement source shown on the excerpt + /// line in place of the original code. `null` renders the help as a bare + /// suggestion line with no code block. + fix_code: ?[]const u8 = null, }; pub const DiagnosticList = struct { @@ -112,7 +122,7 @@ pub const DiagnosticList = struct { file_name: []const u8, current_source_file: ?[]const u8 = null, import_sources: ?*const std.StringHashMap([:0]const u8) = null, - render_style: RenderStyle = .compact, + render_style: RenderStyle = .extended, pub fn init(allocator: std.mem.Allocator, source: []const u8, file_name: []const u8) DiagnosticList { return .{ @@ -127,25 +137,75 @@ pub const DiagnosticList = struct { } pub fn add(self: *DiagnosticList, level: Level, message: []const u8, span: ?Span) void { + _ = self.addId(level, message, span); + } + + /// Append a primary diagnostic, returning its index in `items`. The index + /// is the handle passed to `addNote` / `addHelp` to bundle related + /// messages under it. On a deduplicated hit the existing index is + /// returned, so bundling still attaches to the surviving primary. + pub fn addId(self: *DiagnosticList, level: Level, message: []const u8, span: ?Span) usize { // Deduplicate: skip if same level+span+message already exists - for (self.items.items) |d| { + for (self.items.items, 0..) |d, i| { if (d.level == level and std.mem.eql(u8, d.message, message)) { const a = d.span orelse continue; const b = span orelse continue; - if (a.start == b.start and a.end == b.end) return; + if (a.start == b.start and a.end == b.end) return i; } } + const idx = self.items.items.len; self.items.append(self.allocator, .{ .level = level, .message = message, .span = span, .source_file = self.current_source_file, - }) catch {}; + }) catch return idx; + return idx; } pub fn addFmt(self: *DiagnosticList, level: Level, span: ?Span, comptime fmt: []const u8, args: anytype) void { + _ = self.addFmtId(level, span, fmt, args); + } + + pub fn addFmtId(self: *DiagnosticList, level: Level, span: ?Span, comptime fmt: []const u8, args: anytype) usize { const message = std.fmt.allocPrint(self.allocator, fmt, args) catch "diagnostic format error"; - self.add(level, message, span); + return self.addId(level, message, span); + } + + /// Attach a `note:` block to the primary diagnostic at `primary_id`. + /// Notes render after the primary, in insertion order, before any helps. + pub fn addNote(self: *DiagnosticList, primary_id: usize, span: ?Span, message: []const u8) void { + self.items.append(self.allocator, .{ + .level = .note, + .message = message, + .span = span, + .source_file = self.current_source_file, + .primary = primary_id, + }) catch {}; + } + + pub fn addNoteFmt(self: *DiagnosticList, primary_id: usize, span: ?Span, comptime fmt: []const u8, args: anytype) void { + const message = std.fmt.allocPrint(self.allocator, fmt, args) catch "diagnostic format error"; + self.addNote(primary_id, span, message); + } + + /// Attach a `help:` block to the primary diagnostic at `primary_id`. + /// When `fix_code` is non-null it is shown on the excerpt line in place of + /// the original source as a fix-it suggestion. + pub fn addHelp(self: *DiagnosticList, primary_id: usize, span: ?Span, message: []const u8, fix_code: ?[]const u8) void { + self.items.append(self.allocator, .{ + .level = .help, + .message = message, + .span = span, + .source_file = self.current_source_file, + .primary = primary_id, + .fix_code = fix_code, + }) catch {}; + } + + pub fn addHelpFmt(self: *DiagnosticList, primary_id: usize, span: ?Span, fix_code: ?[]const u8, comptime fmt: []const u8, args: anytype) void { + const message = std.fmt.allocPrint(self.allocator, fmt, args) catch "diagnostic format error"; + self.addHelp(primary_id, span, message, fix_code); } pub fn hasErrors(self: *const DiagnosticList) bool { @@ -175,11 +235,7 @@ pub const DiagnosticList = struct { pub fn renderCompact(self: *const DiagnosticList, writer: anytype) !void { for (self.items.items) |d| { - const level_str = switch (d.level) { - .err => "error", - .warn => "warning", - .note => "note", - }; + const level_str = levelStr(d.level); if (d.span) |span| { const resolved = self.resolveSourceAndFile(d); const loc = SourceLoc.compute(resolved.source, span.start); @@ -190,25 +246,47 @@ pub const DiagnosticList = struct { } } + /// Render every primary diagnostic, each followed by its bundled notes + /// (insertion order) and then its bundled helps. Bundles are separated by + /// a blank line; blocks within a bundle are too. pub fn renderExtended(self: *const DiagnosticList, writer: anytype) !void { - for (self.items.items) |d| { - try self.renderExtendedOne(writer, d); + var first = true; + for (self.items.items, 0..) |d, i| { + if (d.primary != null) continue; + if (!first) try writer.writeAll("\n"); + first = false; + try self.renderBlock(writer, d, true); + + for (self.items.items) |n| { + if (n.primary == i and n.level == .note) { + try writer.writeAll("\n"); + try self.renderBlock(writer, n, true); + } + } + for (self.items.items) |h| { + if (h.primary == i and h.level == .help) { + try writer.writeAll("\n"); + try self.renderBlock(writer, h, false); + } + } } } - fn renderExtendedOne(self: *const DiagnosticList, writer: anytype, d: Diagnostic) !void { - const level_str = switch (d.level) { - .err => "error", - .warn => "warning", - .note => "note", - }; - + /// Render one diagnostic as a header line, optional `-->` location arrow, + /// and a code excerpt with caret underline. A `.help` with `fix_code` + /// substitutes the suggested source on the excerpt line and omits the + /// arrow (`show_arrow == false`). + fn renderBlock(self: *const DiagnosticList, writer: anytype, d: Diagnostic, show_arrow: bool) !void { + const level_str = levelStr(d.level); try writer.print("{s}: {s}\n", .{ level_str, d.message }); const span = d.span orelse return; const resolved = self.resolveSourceAndFile(d); - const loc = SourceLoc.compute(resolved.source, span.start); - try writer.print(" --> {s}:{d}:{d}\n", .{ resolved.file_name, loc.line, loc.col }); + + if (show_arrow) { + const loc = SourceLoc.compute(resolved.source, span.start); + try writer.print(" --> {s}:{d}:{d}\n", .{ resolved.file_name, loc.line, loc.col }); + } var ctx = extractContext(self.allocator, resolved.source, span) catch return; defer ctx.deinit(self.allocator); @@ -220,6 +298,23 @@ pub const DiagnosticList = struct { try writeRepeated(writer, ' ', line_num_width + 1); try writer.writeAll("|\n"); + // Fix-it: show the suggested replacement on the primary span's line + // instead of the original source, with carets under the change. + if (d.fix_code) |fix| { + const ln = ctx.lines[0].line_num; + const pad = line_num_width - digitCount(ln); + try writeRepeated(writer, ' ', pad); + try writer.print("{d} | {s}\n", .{ ln, fix }); + + try writeRepeated(writer, ' ', line_num_width + 1); + try writer.writeAll("| "); + try writeRepeated(writer, ' ', ctx.start_col - 1); + const caret_count = if (ctx.end_col > ctx.start_col) ctx.end_col - ctx.start_col else 1; + try writeRepeated(writer, '^', caret_count); + try writer.writeAll("\n"); + return; + } + for (ctx.lines, 0..) |line, idx| { const pad = line_num_width - digitCount(line.line_num); try writeRepeated(writer, ' ', pad); @@ -239,6 +334,15 @@ pub const DiagnosticList = struct { } } + fn levelStr(level: Level) []const u8 { + return switch (level) { + .err => "error", + .warn => "warning", + .note => "note", + .help => "help", + }; + } + fn digitCount(n: u32) usize { if (n == 0) return 1; var count: usize = 0; @@ -252,13 +356,22 @@ pub const DiagnosticList = struct { while (i < n) : (i += 1) try writer.writeByte(byte); } + /// Render diagnostics to stderr respecting `render_style`. This is the + /// path the CLI uses; `renderDebug` remains a compact fallback. + pub fn renderStderr(self: *const DiagnosticList) void { + var aw = std.Io.Writer.Allocating.init(self.allocator); + self.render(&aw.writer) catch { + self.renderDebug(); + return; + }; + var result = aw.writer.toArrayList(); + defer result.deinit(self.allocator); + std.debug.print("{s}", .{result.items}); + } + pub fn renderDebug(self: *const DiagnosticList) void { for (self.items.items) |d| { - const level_str = switch (d.level) { - .err => "error", - .warn => "warning", - .note => "note", - }; + const level_str = levelStr(d.level); if (d.span) |span| { const resolved = self.resolveSourceAndFile(d); const loc = SourceLoc.compute(resolved.source, span.start); diff --git a/src/lsp/server.zig b/src/lsp/server.zig index 0aaef50..9975bd0 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -1749,6 +1749,7 @@ pub const Server = struct { .err => 1, .warn => 2, .note => 3, + .help => 4, }; result.append(allocator, .{ .range = range, diff --git a/tests/expected/127-import-non-transitive.txt b/tests/expected/127-import-non-transitive.txt index 14b8fe8..6a744b7 100644 --- a/tests/expected/127-import-non-transitive.txt +++ b/tests/expected/127-import-non-transitive.txt @@ -1,3 +1,17 @@ -/Users/agra/projects/sx/examples/127-import-non-transitive.sx:15:37: error: 'c_only_fn' is not visible; #import the module that declares it -/Users/agra/projects/sx/examples/127-import-non-transitive.sx:16:40: error: 'c_only_const' is not visible; #import the module that declares it -/Users/agra/projects/sx/examples/127-import-non-transitive.sx:16:40: error: unresolved 'c_only_const' (in /Users/agra/projects/sx/examples/127-import-non-transitive.sx fn main) +error: 'c_only_fn' is not visible; #import the module that declares it + --> /Users/agra/projects/sx/examples/127-import-non-transitive.sx:15:37 + | +15 | print("c_only_fn direct: {}\n", c_only_fn()); + | ^^^^^^^^^ + +error: 'c_only_const' is not visible; #import the module that declares it + --> /Users/agra/projects/sx/examples/127-import-non-transitive.sx:16:40 + | +16 | print("c_only_const direct: {}\n", c_only_const); + | ^^^^^^^^^^^^ + +error: unresolved 'c_only_const' (in /Users/agra/projects/sx/examples/127-import-non-transitive.sx fn main) + --> /Users/agra/projects/sx/examples/127-import-non-transitive.sx:16:40 + | +16 | print("c_only_const direct: {}\n", c_only_const); + | ^^^^^^^^^^^^ diff --git a/tests/expected/131-callconv-mismatch-diagnostic.txt b/tests/expected/131-callconv-mismatch-diagnostic.txt index 3a0c6ee..52cf06a 100644 --- a/tests/expected/131-callconv-mismatch-diagnostic.txt +++ b/tests/expected/131-callconv-mismatch-diagnostic.txt @@ -1 +1,5 @@ -/Users/agra/projects/sx/examples/131-callconv-mismatch-diagnostic.sx:12:42: error: call-convention mismatch: 'sx_handler' is declared with default sx convention but the target type expects callconv(.c) +error: call-convention mismatch: 'sx_handler' is declared with default sx convention but the target type expects callconv(.c) + --> /Users/agra/projects/sx/examples/131-callconv-mismatch-diagnostic.sx:12:42 + | +12 | fp : (*void) -> *void callconv(.c) = sx_handler; + | ^^^^^^^^^^ diff --git a/tests/expected/161-pack-index-oob.txt b/tests/expected/161-pack-index-oob.txt index b0a0e58..4e55b4f 100644 --- a/tests/expected/161-pack-index-oob.txt +++ b/tests/expected/161-pack-index-oob.txt @@ -1 +1,5 @@ -/Users/agra/projects/sx/examples/161-pack-index-oob.sx:14:32: error: pack index 2 out of bounds: 'args' has 1 element +error: pack index 2 out of bounds: 'args' has 1 element + --> /Users/agra/projects/sx/examples/161-pack-index-oob.sx:14:32 + | +14 | foo :: (..$args) -> $R => args[2]; + | ^ diff --git a/tests/expected/179-impl-visibility.txt b/tests/expected/179-impl-visibility.txt index d3d0181..e75909a 100644 --- a/tests/expected/179-impl-visibility.txt +++ b/tests/expected/179-impl-visibility.txt @@ -1 +1,5 @@ -/Users/agra/projects/sx/examples/./179-impl-visibility-user.sx:7:17: error: no visible xx conversion from 's64' to 'Wrap' — impl exists in another module but is not imported +error: no visible xx conversion from 's64' to 'Wrap' — impl exists in another module but is not imported + --> /Users/agra/projects/sx/examples/./179-impl-visibility-user.sx:7:17 + | + 7 | w : Wrap = xx 7; + | ^ diff --git a/tests/expected/180-impl-duplicate.txt b/tests/expected/180-impl-duplicate.txt index c307e1d..9b5f10b 100644 --- a/tests/expected/180-impl-duplicate.txt +++ b/tests/expected/180-impl-duplicate.txt @@ -1 +1,5 @@ -/Users/agra/projects/sx/examples/180-impl-duplicate.sx:20:17: error: duplicate xx conversion from 's64' to 'Wrap': impls in /Users/agra/projects/sx/examples/./180-impl-duplicate-impl-a.sx and /Users/agra/projects/sx/examples/./180-impl-duplicate-impl-b.sx +error: duplicate xx conversion from 's64' to 'Wrap': impls in /Users/agra/projects/sx/examples/./180-impl-duplicate-impl-a.sx and /Users/agra/projects/sx/examples/./180-impl-duplicate-impl-b.sx + --> /Users/agra/projects/sx/examples/180-impl-duplicate.sx:20:17 + | +20 | w : Wrap = xx 7; + | ^ diff --git a/tests/expected/181-impl-duplicate-same-file.txt b/tests/expected/181-impl-duplicate-same-file.txt index 8ea1db9..8f7ab23 100644 --- a/tests/expected/181-impl-duplicate-same-file.txt +++ b/tests/expected/181-impl-duplicate-same-file.txt @@ -1 +1,9 @@ -/Users/agra/projects/sx/examples/181-impl-duplicate-same-file.sx:15:1: error: duplicate impl 'Into' for source 's64' in /Users/agra/projects/sx/examples/181-impl-duplicate-same-file.sx +error: duplicate impl 'Into' for source 's64' in /Users/agra/projects/sx/examples/181-impl-duplicate-same-file.sx + --> /Users/agra/projects/sx/examples/181-impl-duplicate-same-file.sx:15:1 + | +15 | impl Into(MyA) for s64 { + | ^^^^^^^^^^^^^^^^^^^^^^^^ +16 | convert :: (self: s64) -> MyA { .{ v = self * 2 }; } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +17 | } + | ^ diff --git a/tests/expected/187-compile-error.txt b/tests/expected/187-compile-error.txt index 2082ca2..8612cf6 100644 --- a/tests/expected/187-compile-error.txt +++ b/tests/expected/187-compile-error.txt @@ -1 +1,5 @@ -/Users/agra/projects/sx/examples/187-compile-error.sx:12:6: error: intentional compile error from #run +error: intentional compile error from #run + --> /Users/agra/projects/sx/examples/187-compile-error.sx:12:6 + | +12 | #run compile_error("intentional compile error from #run"); + | ^^^^^^^^^^^^^ diff --git a/tests/expected/55-err-field-not-found.txt b/tests/expected/55-err-field-not-found.txt index b52ecde..86b551c 100644 --- a/tests/expected/55-err-field-not-found.txt +++ b/tests/expected/55-err-field-not-found.txt @@ -1 +1,5 @@ -/Users/agra/projects/sx/examples/55-err-field-not-found.sx:8:15: error: field 'bogus' not found on type 'Vec' +error: field 'bogus' not found on type 'Vec' + --> /Users/agra/projects/sx/examples/55-err-field-not-found.sx:8:15 + | + 8 | return xx v.bogus; + | ^^^^^^^ diff --git a/tests/expected/56-err-tuple-oob.txt b/tests/expected/56-err-tuple-oob.txt index 263caf5..8864e77 100644 --- a/tests/expected/56-err-tuple-oob.txt +++ b/tests/expected/56-err-tuple-oob.txt @@ -1 +1,5 @@ -/Users/agra/projects/sx/examples/56-err-tuple-oob.sx:6:15: error: field '42' not found on type 'tuple' +error: field '42' not found on type 'tuple' + --> /Users/agra/projects/sx/examples/56-err-tuple-oob.sx:6:15 + | + 6 | return xx t.42; + | ^^^^ diff --git a/tests/expected/57-err-dot-shorthand.txt b/tests/expected/57-err-dot-shorthand.txt index 93721de..b615a38 100644 --- a/tests/expected/57-err-dot-shorthand.txt +++ b/tests/expected/57-err-dot-shorthand.txt @@ -1 +1,5 @@ -/Users/agra/projects/sx/examples/57-err-dot-shorthand.sx:5:10: error: cannot infer enum type for '.Foo' — use an explicit type or assign to a typed variable +error: cannot infer enum type for '.Foo' — use an explicit type or assign to a typed variable + --> /Users/agra/projects/sx/examples/57-err-dot-shorthand.sx:5:10 + | + 5 | x := .Foo(1, 2); + | ^^^^ diff --git a/tests/expected/59-err-bad-variant.txt b/tests/expected/59-err-bad-variant.txt index c47cabc..5832dc7 100644 --- a/tests/expected/59-err-bad-variant.txt +++ b/tests/expected/59-err-bad-variant.txt @@ -1 +1,5 @@ -/Users/agra/projects/sx/examples/59-err-bad-variant.sx:18:14: error: no variant 'Bogus' on type 'Shape' +error: no variant 'Bogus' on type 'Shape' + --> /Users/agra/projects/sx/examples/59-err-bad-variant.sx:18:14 + | +18 | case .Bogus: (x) { print("bogus={}\n", x); } + | ^^^^^^ diff --git a/tests/expected/ffi-jni-call-11-unsupported-return-diag.txt b/tests/expected/ffi-jni-call-11-unsupported-return-diag.txt index ddc299a..8d25845 100644 --- a/tests/expected/ffi-jni-call-11-unsupported-return-diag.txt +++ b/tests/expected/ffi-jni-call-11-unsupported-return-diag.txt @@ -1 +1,5 @@ -/Users/agra/projects/sx/examples/ffi-jni-call-11-unsupported-return-diag.sx:24:14: error: JNI method 'Buf.get' returns 's8', which isn't supported by the JNI call-method lowering yet — only void/bool/s32/s64/f32/f64 and pointers are wired up +error: JNI method 'Buf.get' returns 's8', which isn't supported by the JNI call-method lowering yet — only void/bool/s32/s64/f32/f64 and pointers are wired up + --> /Users/agra/projects/sx/examples/ffi-jni-call-11-unsupported-return-diag.sx:24:14 + | +24 | _ := b.get(); + | ^^^^^ diff --git a/tests/expected/ffi-objc-dsl-04-mismatch.txt b/tests/expected/ffi-objc-dsl-04-mismatch.txt index 05dfdf7..4118ee8 100644 --- a/tests/expected/ffi-objc-dsl-04-mismatch.txt +++ b/tests/expected/ffi-objc-dsl-04-mismatch.txt @@ -1 +1,5 @@ -/Users/agra/projects/sx/examples/ffi-objc-dsl-04-mismatch.sx:18:14: error: Obj-C selector for 'SxProbeMismatch.something_extra' has 2 keyword(s) but the call passes 1 argument(s); split the sx method name on '_' so it produces exactly 1 keyword(s), or override with `#selector("...")` +error: Obj-C selector for 'SxProbeMismatch.something_extra' has 2 keyword(s) but the call passes 1 argument(s); split the sx method name on '_' so it produces exactly 1 keyword(s), or override with `#selector("...")` + --> /Users/agra/projects/sx/examples/ffi-objc-dsl-04-mismatch.sx:18:14 + | +18 | n := inst.something_extra(7); + | ^^^^^^^^^^^^^^^^^^^^