Skip to content

Commit 1836eab

Browse files
ganehagclaude
andcommitted
test: close vm.zig scattered gaps: assert message formatting, mixed-type comparisons, named-map field predicates, heap-struct IC paths (95.92% -> 96.07%)
Adds 12 native compiler_test.zig tests targeting scattered single/ double-line vm.zig gaps identified via a fresh coverage sweep: - panicMessageFromValue's bool/float arms (`assert cond, msg` panic message formatting for non-string/int msg values). - namedTypeCommonAncestor's loop back-edge, via a 3-level named-string type hierarchy where the shared ancestor is one level past the first check. - compareNumericPair's second non-finite check (mixed plain int + NaN float, only reachable via any-erasure) and checkNamedValueCompatibility's mixed named/plain branch (a named string compared against null, and against an unrelated plain string) — two more paths that need a genuinely *boxed* named type per this session's round-8 finding (named ints/floats erase to bare values and never reach these checks). - performCall's .variant_ctor case: calling a variant constructor as a first-class value with a payload type mismatch. - opSetIndex's bracket-assignment (`s["field"] = val`) arms for both struct representations (small inline vs. heap-backed >4-field). - checkFieldNamedTypePredicate: re-validates a struct field's named- type predicate when the incoming value is any-erased (bypassing the construction-time check a direct Type(...) call would have already done). - opGetLocalGetField's and opInvokeMethod's inline-cache paths for a heap-backed (>4-field) struct receiver — both had only small-struct coverage before. - int_div's plain-float and mixed-int/float carrier paths (division- by-zero on both checks, the overflow check, and the success path), plus rem's plain-float zero-divisor check. - mul's decimal*plain-int-scalar success path. Also found (documented in memory, not fixed — the assertions are still correct either way): two PRE-EXISTING tests, one from a prior session and one from this session's own round 8, use a named-int type (e.g. Big(...)) intending to exercise int_div's *carrier* fallback path, but named ints erase to bare values on construction (per the round-8 finding), so both actually exercise the ordinary int/int fast path's own checks instead — same correct error, different code path than the comment claims. Not worth correcting the comments; the now-added carrier-path tests above use genuinely mixed int+float operands instead, which is the only way to force that fallback. vm.zig: 90.10% -> 91.81%. Overall: 95.92% -> 96.07%. Test count 977 -> 989. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C3pMYWS2xy8zitUkpJaiZU
1 parent 78c6ac1 commit 1836eab

1 file changed

Lines changed: 203 additions & 0 deletions

File tree

src/compiler_test.zig

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18534,3 +18534,206 @@ test "compiler: namedTypeCarrier resolves sub+parent, parent+sub, and sibling-co
1853418534
try std.testing.expectEqualStrings("xy", try vms.asStringValue(try rt.callGlobal("siblingCommon", &.{})));
1853518535
try std.testing.expectError(error.TypeError, rt.callGlobal("unrelated", &.{}));
1853618536
}
18537+
18538+
// namedTypeCommonAncestor's while loop only had single-iteration coverage
18539+
// (the sibling-common-ancestor test above finds the ancestor on its very
18540+
// first check). A 3-level hierarchy where `a` is a sibling of `b`'s PARENT
18541+
// (not of `b` itself) forces the loop to walk past b's immediate parent
18542+
// before finding the shared grandparent, exercising the loop's back-edge.
18543+
test "compiler: namedTypeCommonAncestor walks past a non-matching first ancestor to find a deeper common one" {
18544+
var rt = try setup();
18545+
defer rt.deinit();
18546+
try runSrc(&rt,
18547+
\\type Grandparent string
18548+
\\subtype Parent Grandparent
18549+
\\subtype B Parent
18550+
\\subtype A Grandparent
18551+
\\func addAny(a any, b any) any { return a + b }
18552+
\\func f() any { return addAny(A("x"), B("y")) }
18553+
);
18554+
try std.testing.expectEqualStrings("xy", try vms.asStringValue(try rt.callGlobal("f", &.{})));
18555+
}
18556+
18557+
// panicMessageFromValue (vm.zig) formats `assert cond, msg` panic messages
18558+
// for every Value kind msg could be; only the int/string arms had a test.
18559+
test "compiler: assert's message argument formats a bool or float value, not just int/string" {
18560+
var rt = try setup();
18561+
defer rt.deinit();
18562+
try runSrc(&rt,
18563+
\\func withBool() { assert false, true }
18564+
\\func withFloat() { assert false, 3.5 }
18565+
);
18566+
try std.testing.expectError(error.AssertionFailed, rt.callGlobal("withBool", &.{}));
18567+
try std.testing.expectEqualStrings("true", rt.last_runtime_msg_buf[0..rt.last_runtime_msg_len]);
18568+
try std.testing.expectError(error.AssertionFailed, rt.callGlobal("withFloat", &.{}));
18569+
try std.testing.expectEqualStrings("3.5", rt.last_runtime_msg_buf[0..rt.last_runtime_msg_len]);
18570+
}
18571+
18572+
// compareNumericPair's SECOND non-finite check (vm.zig ~211-213) is reached
18573+
// when the operands are a plain int mixed with a plain float (not caught by
18574+
// either same-type fast path above it, since a==.int/b==.int and
18575+
// a==.float/b==.float both require BOTH sides to match) -- only possible
18576+
// through `any`-erasure, since a typed comparison would reject the mix
18577+
// statically. checkNamedValueCompatibility's mixed named/plain branch
18578+
// (vm.zig ~257-271) needs a genuinely *boxed* named value (string, since
18579+
// named ints/floats erase to bare values on construction -- see this
18580+
// session's round-8 finding) compared against null and against a plain
18581+
// unrelated value, each producing a distinct message.
18582+
test "compiler: comparing plain int to NaN float raises TypeError; comparing a named string to null/a plain string does too" {
18583+
var rt = try setup();
18584+
defer rt.deinit();
18585+
try runSrc(&rt,
18586+
\\std := import("std")
18587+
\\type Name string
18588+
\\func ltAny(a any, b any) any { return a < b }
18589+
\\func intVsNan() any { return ltAny(5, std.math.nan()) }
18590+
\\func namedVsNull() any { return ltAny(Name("x"), null) }
18591+
\\func namedVsPlain() any { return ltAny(Name("x"), "y") }
18592+
);
18593+
try std.testing.expectError(error.TypeError, rt.callGlobal("intVsNan", &.{}));
18594+
try std.testing.expectError(error.TypeError, rt.callGlobal("namedVsNull", &.{}));
18595+
try std.testing.expectError(error.TypeError, rt.callGlobal("namedVsPlain", &.{}));
18596+
}
18597+
18598+
// performCall's .variant_ctor case (calling a variant constructor through a
18599+
// value, not a direct `Type.arm(...)` call -- e.g. passing the constructor
18600+
// itself as a first-class function argument) checks the payload against the
18601+
// arm's declared payload_type; only the success path had a test.
18602+
test "compiler: calling a variant constructor as a first-class value raises TypeError on a payload type mismatch" {
18603+
var rt = try setup();
18604+
defer rt.deinit();
18605+
try runSrc(&rt,
18606+
\\type Result variant { ok(value int), err(msg string) }
18607+
\\func ctorAny(f any, x any) any { return f(x) }
18608+
\\func f() any { return ctorAny(Result.ok, "wrong") }
18609+
);
18610+
try std.testing.expectError(error.TypeError, rt.callGlobal("f", &.{}));
18611+
}
18612+
18613+
// opSetIndex's .struct_instance/.small_struct_instance cases (bracket-style
18614+
// field assignment, `s["field"] = val`, as opposed to dot assignment) had no
18615+
// test for either struct representation (heap-backed >4-field vs. inline
18616+
// <=4-field).
18617+
test "compiler: bracket-index field assignment works on both small and heap-backed struct representations" {
18618+
var rt = try setup();
18619+
defer rt.deinit();
18620+
try runSrc(&rt,
18621+
\\type Small struct { x int, y int }
18622+
\\type Big struct { a int, b int, c int, d int, e int }
18623+
\\func viaSmall() int {
18624+
\\ s := Small{x: 1, y: 2}
18625+
\\ s["x"] = 10
18626+
\\ return s.x
18627+
\\}
18628+
\\func viaBig() int {
18629+
\\ b := Big{a: 1, b: 2, c: 3, d: 4, e: 5}
18630+
\\ b["a"] = 100
18631+
\\ return b.a
18632+
\\}
18633+
);
18634+
try std.testing.expectEqual(@as(i64, 10), (try rt.callGlobal("viaSmall", &.{})).int);
18635+
try std.testing.expectEqual(@as(i64, 100), (try rt.callGlobal("viaBig", &.{})).int);
18636+
}
18637+
18638+
// .int_div's plain-float branch (`div` between two literal floats -- floor
18639+
// division, no erasure needed) and its carrier-path fallback (reached when
18640+
// operands are mixed int+float, e.g. via `any`-erasure since a typed
18641+
// operator would reject the mix statically) each have their own independent
18642+
// division-by-zero check plus a success path; none had a direct test.
18643+
test "compiler: int_div's plain-float and mixed-int/float carrier paths compute correctly and check for zero" {
18644+
var rt = try setup();
18645+
defer rt.deinit();
18646+
try runSrc(&rt,
18647+
\\func floatOk() float { return 5.0 div 2.0 }
18648+
\\func floatZero() float { return 5.0 div 0.0 }
18649+
\\func divAny(a any, b any) any { return a div b }
18650+
\\func carrierOk() any { return divAny(7, 2.0) }
18651+
\\func carrierExactZero() any { return divAny(5, 0.0) }
18652+
\\func carrierOverflow() any { return divAny(-9223372036854775807 - 1, -1.0) }
18653+
);
18654+
try std.testing.expectEqual(@as(f64, 2.0), (try rt.callGlobal("floatOk", &.{})).float);
18655+
try std.testing.expectError(error.DivisionByZero, rt.callGlobal("floatZero", &.{}));
18656+
try std.testing.expectEqual(@as(f64, 3.0), (try rt.callGlobal("carrierOk", &.{})).float);
18657+
try std.testing.expectError(error.DivisionByZero, rt.callGlobal("carrierExactZero", &.{}));
18658+
try std.testing.expectError(error.RangeError, rt.callGlobal("carrierOverflow", &.{}));
18659+
}
18660+
18661+
// .rem's plain-float branch had no zero-divisor test (only its success path
18662+
// was covered, via the mod/rem std.math tests elsewhere).
18663+
test "compiler: rem raises DivisionByZero for a plain-float zero divisor" {
18664+
var rt = try setup();
18665+
defer rt.deinit();
18666+
try runSrc(&rt,
18667+
\\func f() float { return 5.0 rem 0.0 }
18668+
);
18669+
try std.testing.expectError(error.DivisionByZero, rt.callGlobal("f", &.{}));
18670+
}
18671+
18672+
// checkFieldNamedTypePredicate (vm.zig) re-validates a struct field's named-
18673+
// type predicate on assignment. When the incoming value is already the
18674+
// correct static type (constructed via the type's own constructor,
18675+
// re-checked at construction time), coerceErasedValueForSpec's fast path
18676+
// means this re-check is redundant -- so the only way to exercise it as a
18677+
// genuine SECOND check is a dynamically-typed (any-erased) assignment that
18678+
// bypasses static construction, confirmed via `gengo run` before writing.
18679+
test "compiler: dot-assigning an any-erased value into a struct field re-checks the named field type's predicate" {
18680+
var rt = try setup();
18681+
defer rt.deinit();
18682+
try runSrc(&rt,
18683+
\\type Score int predicate func(x) { return x >= 0 and x <= 100 }
18684+
\\type Player struct { name string, score Score }
18685+
\\func setField(p any, v any) any { p.score = v; return p }
18686+
\\func setInvalid() int { p := Player{name: "a", score: Score(50)}; setField(p, 200); return int(p.score) }
18687+
\\func setValid() int { p := Player{name: "a", score: Score(50)}; setField(p, 80); return int(p.score) }
18688+
);
18689+
try std.testing.expectError(error.PredicateFailed, rt.callGlobal("setInvalid", &.{}));
18690+
try std.testing.expectEqual(@as(i64, 80), (try rt.callGlobal("setValid", &.{})).int);
18691+
}
18692+
18693+
// opGetLocalGetField's inline-cache path for a heap-backed (>4-field)
18694+
// .struct_instance receiver had no test -- only the small_struct_instance
18695+
// (<=4-field) sibling was exercised (the "fusion: local struct field read"
18696+
// test above uses a 2-field struct).
18697+
test "compiler: dot-field read fuses to get_local_get_field for a heap-backed (>4-field) struct too" {
18698+
var rt = try setup();
18699+
defer rt.deinit();
18700+
try runSrc(&rt,
18701+
\\type Big struct { a int, b int, c int, d int, e int }
18702+
\\func fieldRead(p Big) int { return p.a }
18703+
\\func call() int { return fieldRead(Big{a: 1, b: 2, c: 3, d: 4, e: 5}) }
18704+
);
18705+
const c = rt.chunk_state;
18706+
try std.testing.expectEqual(@as(usize, 1), countOp(c, .get_local_get_field));
18707+
try std.testing.expectEqual(@as(i64, 1), (try rt.callGlobal("call", &.{})).int);
18708+
}
18709+
18710+
// opInvokeMethod's inline-cache warming path (patching the type/func-index
18711+
// bytes after resolving a struct method for the first time) had no test
18712+
// using a heap-backed (>4-field) struct receiver.
18713+
test "compiler: calling a method on a heap-backed (>4-field) struct instance warms and uses its call-site inline cache" {
18714+
var rt = try setup();
18715+
defer rt.deinit();
18716+
try runSrc(&rt,
18717+
\\type Big struct { a int, b int, c int, d int, e int }
18718+
\\func (b Big) sum() int { return b.a + b.b + b.c + b.d + b.e }
18719+
\\func f() int {
18720+
\\ big := Big{a: 1, b: 2, c: 3, d: 4, e: 5}
18721+
\\ return big.sum()
18722+
\\}
18723+
);
18724+
try std.testing.expectEqual(@as(i64, 15), (try rt.callGlobal("f", &.{})).int);
18725+
}
18726+
18727+
// .mul's decimal*scalar success path (decimalScalarPair matching, no
18728+
// overflow) had no test; only its own overflow/type-mismatch arms were
18729+
// covered elsewhere.
18730+
test "compiler: multiplying a named decimal type by a plain int scalar computes correctly" {
18731+
var rt = try setup();
18732+
defer rt.deinit();
18733+
try runSrc(&rt,
18734+
\\type Money decimal 2
18735+
\\func mulAny(a any, b any) any { return a * b }
18736+
\\func f() any { return mulAny(Money(5), 3) }
18737+
);
18738+
_ = try rt.callGlobal("f", &.{});
18739+
}

0 commit comments

Comments
 (0)