diff --git a/array/array_test.mbt b/array/array_test.mbt index a443a4d616..b5da057eb4 100644 --- a/array/array_test.mbt +++ b/array/array_test.mbt @@ -18,3 +18,10 @@ test "zip_with" { let right = [10, 20] inspect(@array.zip_with(left, right, (a, b) => a + b), content="[11, 22]") } + +///| +test "zip_with left shorter" { + let left = [1, 2] + let right = [10, 20, 30] + inspect(@array.zip_with(left, right, (a, b) => a + b), content="[11, 22]") +} diff --git a/bench/bench_test.mbt b/bench/bench_test.mbt index 06ed6f1fc9..4bf854673a 100644 --- a/bench/bench_test.mbt +++ b/bench/bench_test.mbt @@ -34,3 +34,30 @@ test "bench buffer writes with multiple samples" { bench.bench(() => ignore(2 + 2), name="second", count=1) inspect(bench.dump_summaries().contains(","), content="true") } + +///| +fn busy_wait_us(target_us : Double) -> Unit { + let start = @bench.monotonic_clock_start() + let mut elapsed = 0.0 + while elapsed < target_us { + elapsed = @bench.monotonic_clock_end(start) + } +} + +///| +test "bench slow samples clamp batch size and winsorize" { + let mut calls = 0 + let summary = @bench.single_bench( + () => { + let is_slow = calls % 2 == 0 + calls = calls + 1 + if is_slow { + busy_wait_us(120000.0) + } + }, + count=4, + ) + let json = ToJson::to_json(summary).stringify() + inspect(json.contains("\"batch_size\""), content="true") + inspect(json.contains("\"runs\""), content="true") +} diff --git a/bigint/bigint_test.mbt b/bigint/bigint_test.mbt index 1e9033d448..38339f065d 100644 --- a/bigint/bigint_test.mbt +++ b/bigint/bigint_test.mbt @@ -41,6 +41,12 @@ test "bit_length zero" { inspect(zero.bit_length(), content="0") } +///| +test "bit_length default" { + let zero = @bigint.BigInt::default() + inspect(zero.bit_length(), content="0") +} + ///| test "add" { let a = @bigint.BigInt::from_int64(123456789012345678L) diff --git a/builtin/array_test.mbt b/builtin/array_test.mbt index 0aef8f19d0..65a95704a4 100644 --- a/builtin/array_test.mbt +++ b/builtin/array_test.mbt @@ -217,6 +217,33 @@ test "array_sort_by_pred_skip_duplicates" { assert_eq(arr[31], 9) } +///| +struct HeapSortProbe { + value : Int +} + +///| +impl Eq for HeapSortProbe with equal(_self, _other) -> Bool { + false +} + +///| +impl Compare for HeapSortProbe with compare(_self, _other) -> Int { + 1 +} + +///| +test "array_sort heap fallback" { + // Force unbalanced partitions to exercise the heap sort fallback. + let arr : FixedArray[HeapSortProbe] = FixedArray::make(64, { value: 0 }) + for i in 0.. 0, content="true") +} + ///| test "combine string" { let hash = v => { diff --git a/builtin/to_string_test.mbt b/builtin/to_string_test.mbt index 1aa8de46a2..e2d0d02c14 100644 --- a/builtin/to_string_test.mbt +++ b/builtin/to_string_test.mbt @@ -120,6 +120,15 @@ test "to_string runtime zero paths" { inspect(zero.to_int64().to_string(radix=2), content="0") } +///| +#cfg(target="js") +test "to_string js wrappers" { + inspect((255).to_string(radix=16), content="ff") + inspect(255U.to_string(radix=16), content="ff") + inspect(255L.to_string(radix=16), content="ff") + inspect(255UL.to_string(radix=16), content="ff") +} + ///| test "panic UInt64::to_string invalid radix" { let arr = Array::new() diff --git a/coverage/coverage_gaps.md b/coverage/coverage_gaps.md new file mode 100644 index 0000000000..7d52665d4d --- /dev/null +++ b/coverage/coverage_gaps.md @@ -0,0 +1,93 @@ +# Coverage gaps (black-box only) + +This note documents remaining uncovered lines after the black-box test pass and +why they are currently out of reach. + +Last coverage run (wasm-gc): +- `moon coverage analyze -- -f summary` + +## JS coverage blocked (compiler ICE) +Attempting JS coverage currently crashes the compiler: +- Command: `moon test --target js --enable-coverage` +- moonc: v0.6.37+0b3e4ae80 +- Error: assertion failure in `moonc.ml` during link-core + +This blocks coverage of JS-only code paths listed below. + +## Target-specific or host-dependent +- JS-only functions or externs (blocked by JS ICE): + - `builtin/fixedarray.mbt:1633` `FixedArray::copy` (#cfg target=js) + - `builtin/hasher.mbt:85` `seed` via `random_seed()` (#cfg target=js) + - `float/methods.mbt:886,927` JS conversions (#cfg target=js) + - `int16/int16.mbt:213,219` JS conversions (#cfg target=js) + - `builtin/to_string.mbt:339,353,699,717` JS wrappers (#cfg target=js) +- WASM host dependent: + - `env/env_wasm.mbt:102` `current_dir_internal` returns `None` only if the + host returns an empty string; not controllable from tests. + +## Invariant / defensive branches (unreachable by design) +- `bigint/bigint_nonjs.mbt:1816` `len == 0` guard; BigInt invariants keep + `len > 0` for public values. +- `encoding/utf16/decode.mbt:53,78,133,161` `ch > 0x10FFFF` is impossible for + valid surrogate pairs; the guard is defensive. +- `builtin/to_string.mbt:204,216,436,448` `hex_count*`/`radix_count*` zero paths + are bypassed by early return in `to_string` when value is zero. +- `string/regex/internal/regexp/internal/unicode/case_folding.mbt:27` odd-length + DATA guard should be unreachable (data is even-length). +- `json/parse.mbt:61,86,89,114` `abort("unreachable")` arms in parser token + state machine. +- `string/regex/internal/regexp/internal/vm/impl.mbt:201` panic for unexpected + instruction; unreachable under valid compilation. + +## Private helpers (not callable from black-box tests) +- `hashmap/utils.mbt:17-25` `_debug_entries` is private. +- `hashset/hashset.mbt:462-493` `_debug_entries` and `MyString` helpers are + private to the package. +- `immut/array/tree.mbt` internal `abort` branches and helper paths. +- `immut/array/tree_utils.mbt` internal helpers. +- `immut/hashmap/HAMT.mbt` internal mutation paths. +- `immut/hashset/HAMT.mbt` internal mutation paths. +- `immut/priority_queue/priority_queue.mbt` internal rebalance paths. +- `immut/sorted_set/immutable_set.mbt` internal balancing paths. + +## Algorithmic edge cases not hit by current black-box inputs +These are reachable in principle, but require very specific input patterns that +are hard to trigger without white-box hooks or dedicated generators: +- `builtin/array_sort_impl.mbt:253` heap sort fallback when quicksort limit + reaches zero. Attempted a degenerate `Compare` ordering in + `builtin/array_test.mbt`, but the fallback is still not observed. +- `builtin/linked_hash_map.mbt:1004,1011,1021,1024` view comparison branches + not hit by current `Map::get_from_string/get_from_bytes` cases. +- `builtin/string_methods.mbt:1215,1265` defensive break on final segment in + `replace_all` loops. +- `double/internal/ryu/ryu.mbt:229,255,435` carry/rounding branches. +- `double/scalbn.mbt:17,20,23,26,29,32,35,38,42,43` extreme exponent scaling. +- `math/log_double_nonjs.mbt:256,264,266` `ln_1p` edge-case branches; crafted + inputs still miss the `hu == 0` fast-paths due to early-return guards. +- `math/pow.mbt:229,231` subnormal base handling in float pow. Tried + `Float::reinterpret_from_int(1)` with exponent `3.0`, but wasm-gc still + bypasses the subnormal branch (likely subnormal flush-to-zero). +- `math/pow_double_nonjs.mbt:330,332` subnormal base handling in double pow. + Tried `1UL.reinterpret_as_double()` with exponent `3.0`, still no hit. +- `math/prime.mbt:287,323` rare primality branches. Tried a deterministic + `Rand` with `128017` and base `54`; still no hit on the `z == 1` branch. +- `math/trig_double_nonjs.mbt:433,461,512,582,635,640,663,664,665,666,689,714, + 721,722,725,755,759,762,780,783,790,798,802,806,811,870,915,919,921,925` + specialized rounding and edge-case paths. +- `quickcheck/splitmix/random.mbt:86-87` `next_positive_int` special cases for + `-2147483648` and `0` need specific RNG states. +- `sorted_set/set.mbt:223,238,239,240,248` join/rotation variants. +- `strconv/decimal.mbt:282,359,360,483,484,497,498,506` rounding/truncation + paths that require crafted decimal inputs. +- `list/list.mbt:242,279,376,555,722,735,777,878,929,967,980,1194,1258,1319, + 1370,1543,1578,1801` specialized list edge cases. +- `string/regex/internal/regexp/internal/parse/parse.mbt:495,583,624,625` + parser error branches for specific invalid regex patterns. Built runtime + patterns with `\f`/`\v` and escaped range endpoints, but these lines still + report uncovered. + +## Follow-ups +- Once JS coverage works again, re-run `moon test --target js --enable-coverage` + to cover JS-only branches. +- For the algorithmic edge cases, consider targeted property tests or + generated inputs if white-box tests are allowed in the future. diff --git a/double/double_test.mbt b/double/double_test.mbt index 92ce2eb652..1f9bf247fd 100644 --- a/double/double_test.mbt +++ b/double/double_test.mbt @@ -176,3 +176,14 @@ test "Double::is_neg_inf" { test "min equal to neg max" { assert_true(min_value == -max_value) } + +///| + +///| +#warnings("-deprecated") +test "pow extreme exponents" { + let big = 2.0.pow(4000.0) + inspect(big.is_pos_inf(), content="true") + let small = 2.0.pow(-4000.0) + inspect(small == 0.0, content="true") +} diff --git a/float/float_test.mbt b/float/float_test.mbt index 6d6abb4488..fb918a45bf 100644 --- a/float/float_test.mbt +++ b/float/float_test.mbt @@ -79,6 +79,13 @@ test "Hash" { ) } +///| +#cfg(target="js") +test "Float::from_int64 and from_uint64 on js" { + inspect(Float::from_uint64(42UL), content="42") + inspect(Float::from_int64(-42L), content="-42") +} + ///| test "Float::is_inf/special_values" { inspect(@float.infinity.is_inf(), content="true") diff --git a/int16/int16_test.mbt b/int16/int16_test.mbt index c80a4877e1..40e10ffce1 100644 --- a/int16/int16_test.mbt +++ b/int16/int16_test.mbt @@ -504,3 +504,12 @@ test "Int16 int64 conversions" { let back = Int16::from_int64(as64) inspect(back.to_int(), content="123") } + +///| +#cfg(target="js") +test "Int16 int64 conversions on js" { + inspect(Int16::from_int64(42), content="42") + inspect(Int16::from_int64(-42), content="-42") + inspect(Int16::to_int64(123), content="123") + inspect(Int16::to_int64(-123), content="-123") +} diff --git a/math/log_test.mbt b/math/log_test.mbt index fff3bc3e96..2ed96e1bbd 100644 --- a/math/log_test.mbt +++ b/math/log_test.mbt @@ -97,6 +97,17 @@ test "ln" { assert_eq(@math.ln(5.562684646268003e-309), -709.782712893384) } +///| +test "ln mantissa edge cases" { + assert_eq(@math.ln(1.0), 0.0) + let near_one = 0x3ff0000000000001UL.reinterpret_as_double() + let res_one = @math.ln(near_one) + assert_true((res_one - 2.220446049250313e-16).abs() < 1.0e-20) + let near_two = 0x4000000000000001UL.reinterpret_as_double() + let res_two = @math.ln(near_two) + assert_true((res_two - 0.6931471805599453).abs() < 1.0e-12) +} + ///| test "lnf subnormal input" { let sub = Float::reinterpret_from_uint(1U) @@ -123,6 +134,17 @@ test "ln_1p small and large values" { assert_true(large_res > 0.0) } +///| +test "ln_1p mantissa edge cases" { + let step = 2.98023223876953125e-8 + let res_step = @math.ln_1p(step) + assert_true((res_step - step).abs() < 1.0e-12) + let near_two = 1.0 + step + let res_near_two = @math.ln_1p(near_two) + assert_true((res_near_two - 0.6931471805599453).abs() < 1.0e-7) + assert_true((@math.ln_1p(1.0) - 0.6931471805599453).abs() < 1.0e-15) +} + ///| test "ln_1p mid-range branches" { let mid = @math.ln_1p(0.1) diff --git a/math/pow_test.mbt b/math/pow_test.mbt index 8929484fc8..8a66619c95 100644 --- a/math/pow_test.mbt +++ b/math/pow_test.mbt @@ -1220,3 +1220,17 @@ test "scalbnf exponent clamp" { assert_true(@math.scalbnf(1.0, 500).is_pos_inf()) inspect(@math.scalbnf(1.0, -500), content="0") } + +///| +test "powf subnormal base" { + let subnormal = Float::reinterpret_from_int(1) + let result = @math.powf(subnormal, 3.0) + assert_true(result == 0.0) +} + +///| +test "pow subnormal double base" { + let subnormal = 1UL.reinterpret_as_double() + let result = @math.pow(subnormal, 3.0) + assert_true(result == 0.0) +} diff --git a/math/prime_test.mbt b/math/prime_test.mbt index d7ce57998f..c440aafa39 100644 --- a/math/prime_test.mbt +++ b/math/prime_test.mbt @@ -81,6 +81,33 @@ test "@math.is_probable_prime" { inspect(@math.is_probable_prime(509033161N, r), content="false") } +///| +struct SeqSource { + values : Array[UInt64] + mut idx : Int +} + +///| +impl @random.Source for SeqSource with next(self) -> UInt64 { + let idx = self.idx + self.idx += 1 + if idx < self.values.length() { + self.values[idx] + } else { + 0UL + } +} + +///| +test "miller rabin detects z == 1 branch" { + let gen_check : SeqSource = { values: [0UL, 0UL, 52UL], idx: 0 } + let rand_check = @random.Rand::new(generator=gen_check as &@random.Source) + inspect(rand_check.bigint(17), content="52") + let gen : SeqSource = { values: [0UL, 0UL, 52UL], idx: 0 } + let rand = @random.Rand::new(generator=gen as &@random.Source) + assert_false(@math.is_probable_prime(128017N, rand, iters=1)) +} + ///| test "@math.probable_prime" { let rand = @random.Rand::new() diff --git a/string/additional_coverage_test.mbt b/string/additional_coverage_test.mbt index 1e2f2a9009..d8e2bcfa7c 100644 --- a/string/additional_coverage_test.mbt +++ b/string/additional_coverage_test.mbt @@ -81,3 +81,15 @@ test "get_char invalid surrogate pair" { let v = s[:] inspect(v.get_char(0), content="None") } + +///| +test "replace_all on view with matches" { + let view = "foo bar foo".view() + inspect(view.replace_all(old="foo", new="baz"), content="baz bar baz") +} + +///| +test "replace_all on string with matches" { + let s = "foo foo" + inspect(s.replace_all(old="foo", new="x"), content="x x") +} diff --git a/string/regex/regex_test.mbt b/string/regex/regex_test.mbt index 9e1f0b6449..13a738a23f 100644 --- a/string/regex/regex_test.mbt +++ b/string/regex/regex_test.mbt @@ -21,6 +21,56 @@ test "named group missing" { ///| test "escape form feed" { - let regex = @regex.compile("\\f") + let chars = Array::make(2, 'x') + chars[0] = '\\' + chars[1] = 'f' + let regex = @regex.compile(String::from_array(chars)) inspect(regex.execute("\u{c}") is Some(_), content="true") } + +///| +test "char class escape form feed and vertical tab" { + let chars = Array::make(6, 'x') + chars[0] = '[' + chars[1] = '\\' + chars[2] = 'f' + chars[3] = '\\' + chars[4] = 'v' + chars[5] = ']' + let pattern = String::from_array(chars) + let regex = @regex.compile(pattern) + assert_true(regex.execute("\u{c}") is Some(_)) + assert_true(regex.execute("\u{b}") is Some(_)) +} + +///| +test "char class range escape endpoints" { + let chars_v = Array::make(7, 'x') + chars_v[0] = '[' + chars_v[1] = '\\' + chars_v[2] = 't' + chars_v[3] = '-' + chars_v[4] = '\\' + chars_v[5] = 'v' + chars_v[6] = ']' + let range_v = @regex.compile(String::from_array(chars_v)) + assert_true(range_v.execute("\u{b}") is Some(_)) + let chars_f = Array::make(7, 'x') + chars_f[0] = '[' + chars_f[1] = '\\' + chars_f[2] = 'v' + chars_f[3] = '-' + chars_f[4] = '\\' + chars_f[5] = 'f' + chars_f[6] = ']' + let range_f = @regex.compile(String::from_array(chars_f)) + assert_true(range_f.execute("\u{c}") is Some(_)) +} + +///| +test "compile trailing backslash" { + let chars = Array::make(1, 'x') + chars[0] = '\\' + let result = try? @regex.compile(String::from_array(chars)) + assert_true(result is Err(_)) +}