@@ -365,3 +365,247 @@ test "dequantize ternary to f16" {
365365 try std .testing .expectEqual (@as (f16 , -1.0 ), dequantizeTernaryToF16 (-1 ));
366366 try std .testing .expectEqual (@as (f16 , 0.0 ), dequantizeTernaryToF16 (0 ));
367367}
368+
369+ // ═══════════════════════════════════════════════════════════════════════════════
370+ // ADDITIONAL UTILITIES
371+ // ═══════════════════════════════════════════════════════════════════════════════
372+
373+ /// Alias for l2NormF16 — matches VSA API naming.
374+ pub fn vectorNormF16 (v : []const f16 ) f64 {
375+ return l2NormF16 (v );
376+ }
377+
378+ /// Count non-finite values (NaN, Inf) in f16 slice.
379+ pub fn countNonFiniteF16 (data : []const f16 ) usize {
380+ var count : usize = 0 ;
381+ for (data ) | v | {
382+ if (! isTernarySafeF16 (v )) count += 1 ;
383+ }
384+ return count ;
385+ }
386+
387+ // ═══════════════════════════════════════════════════════════════════════════════
388+ // FUZZ TESTS (Zig 0.15+)
389+ // Run: zig build test --fuzz
390+ // Coverage: http://localhost:XXXXX/ (shown in terminal)
391+ // Partially addresses ziglang/zig#352 (code coverage)
392+ // ═══════════════════════════════════════════════════════════════════════════════
393+
394+ test "fuzz f16 roundtrip precision" {
395+ const Fuzzer = struct {
396+ fn run (ctx : @TypeOf (.{}), input : []const u8 ) anyerror ! void {
397+ _ = ctx ;
398+ if (input .len < 4 ) return ;
399+ const val : f32 = @bitCast (input [0.. 4].* );
400+ if (! std .math .isFinite (val )) return ;
401+ if (@abs (val ) > 65504.0 ) return ; // f16 max
402+ if (@abs (val ) > 0.0 and @abs (val ) < 6.1e-5 ) return ; // subnormal range
403+
404+ const narrow : f16 = @floatCast (val );
405+ if (! std .math .isFinite (narrow )) return ;
406+ const wide : f32 = @floatCast (narrow );
407+ const err = @abs (val - wide );
408+ try std .testing .expect (err <= @abs (val ) * 0.002 + 0.001 );
409+ }
410+ };
411+ try std .testing .fuzz (.{}, Fuzzer .run , .{});
412+ }
413+
414+ test "fuzz ternary quantize invariant" {
415+ const Fuzzer = struct {
416+ fn run (ctx : @TypeOf (.{}), input : []const u8 ) anyerror ! void {
417+ _ = ctx ;
418+ if (input .len < 2 ) return ;
419+ const val : f16 = @bitCast (input [0.. 2].* );
420+ if (! std .math .isFinite (val )) return ;
421+
422+ const threshold : f16 = 0.5 ;
423+ const ternary = quantizeF16ToTernary (val , threshold );
424+
425+ try std .testing .expect (ternary == -1 or ternary == 0 or ternary == 1 );
426+ if (val > threshold ) try std .testing .expect (ternary == 1 )
427+ else if (val < - threshold ) try std .testing .expect (ternary == -1 )
428+ else try std .testing .expect (ternary == 0 );
429+ }
430+ };
431+ try std .testing .fuzz (.{}, Fuzzer .run , .{});
432+ }
433+
434+ test "fuzz dotProductF16 stability" {
435+ const Fuzzer = struct {
436+ fn run (ctx : @TypeOf (.{}), input : []const u8 ) anyerror ! void {
437+ _ = ctx ;
438+ if (input .len < 16 ) return ;
439+ const a : [4 ]f16 = @bitCast (input [0.. 8].* );
440+ const b : [4 ]f16 = @bitCast (input [8.. 16].* );
441+
442+ for (a ) | v | if (! std .math .isFinite (@as (f32 , @floatCast (v )))) return ;
443+ for (b ) | v | if (! std .math .isFinite (@as (f32 , @floatCast (v )))) return ;
444+
445+ const dot = dotProductF16 (& a , & b );
446+ try std .testing .expect (std .math .isFinite (dot ));
447+ }
448+ };
449+ try std .testing .fuzz (.{}, Fuzzer .run , .{});
450+ }
451+
452+ test "fuzz slice conversion roundtrip" {
453+ const Fuzzer = struct {
454+ fn run (ctx : @TypeOf (.{}), input : []const u8 ) anyerror ! void {
455+ _ = ctx ;
456+ if (input .len < 4 ) return ;
457+ const count = @min (input .len / 4 , 32 );
458+ if (count == 0 ) return ;
459+
460+ var f32_in : [32 ]f32 = undefined ;
461+ for (0.. count ) | i | {
462+ const offset = i * 4 ;
463+ if (offset + 4 > input .len ) break ;
464+ f32_in [i ] = @bitCast (input [offset .. ][0.. 4].* );
465+ if (! std .math .isFinite (f32_in [i ])) return ;
466+ if (@abs (f32_in [i ]) > 65504.0 ) return ;
467+ }
468+
469+ var f16_buf : [32 ]f16 = undefined ;
470+ var f32_out : [32 ]f32 = undefined ;
471+ f32ToF16Slice (f32_in [0.. count ], f16_buf [0.. count ]);
472+ f16ToF32Slice (f16_buf [0.. count ], f32_out [0.. count ]);
473+
474+ for (0.. count ) | i | {
475+ if (! std .math .isFinite (f32_out [i ])) return ;
476+ const err = @abs (f32_in [i ] - f32_out [i ]);
477+ try std .testing .expect (err <= @abs (f32_in [i ]) * 0.002 + 0.001 );
478+ }
479+ }
480+ };
481+ try std .testing .fuzz (.{}, Fuzzer .run , .{});
482+ }
483+
484+ test "fuzz cosineSimilarityF16 self-similarity" {
485+ const Fuzzer = struct {
486+ fn run (ctx : @TypeOf (.{}), input : []const u8 ) anyerror ! void {
487+ _ = ctx ;
488+ if (input .len < 8 ) return ;
489+ const count = @min (input .len / 2 , 16 );
490+ if (count == 0 ) return ;
491+
492+ var data : [16 ]f16 = @splat (@as (f16 , 0.0 ));
493+ var all_zero = true ;
494+ for (0.. count ) | i | {
495+ const offset = i * 2 ;
496+ if (offset + 2 > input .len ) break ;
497+ data [i ] = @bitCast (input [offset .. ][0.. 2].* );
498+ if (! std .math .isFinite (@as (f32 , @floatCast (data [i ])))) return ;
499+ if (data [i ] != 0.0 ) all_zero = false ;
500+ }
501+ if (all_zero ) return ;
502+
503+ const sim = cosineSimilarityF16 (data [0.. count ], data [0.. count ]);
504+ try std .testing .expect (std .math .isFinite (sim ));
505+ try std .testing .expect (sim > 0.99 );
506+ }
507+ };
508+ try std .testing .fuzz (.{}, Fuzzer .run , .{});
509+ }
510+
511+ test "fuzz maxAbsF16 non-negative" {
512+ const Fuzzer = struct {
513+ fn run (ctx : @TypeOf (.{}), input : []const u8 ) anyerror ! void {
514+ _ = ctx ;
515+ if (input .len < 2 ) return ;
516+ const count = @min (input .len / 2 , 32 );
517+ if (count == 0 ) return ;
518+
519+ var data : [32 ]f16 = undefined ;
520+ for (0.. count ) | i | {
521+ const offset = i * 2 ;
522+ if (offset + 2 > input .len ) break ;
523+ data [i ] = @bitCast (input [offset .. ][0.. 2].* );
524+ if (! std .math .isFinite (@as (f32 , @floatCast (data [i ])))) return ;
525+ }
526+
527+ const result = maxAbsF16 (data [0.. count ]);
528+ try std .testing .expect (std .math .isFinite (@as (f32 , @floatCast (result ))));
529+ try std .testing .expect (result >= 0.0 );
530+
531+ for (0.. count ) | i | {
532+ const abs_val = if (data [i ] < 0 ) - data [i ] else data [i ];
533+ try std .testing .expect (abs_val <= result + 0.001 );
534+ }
535+ }
536+ };
537+ try std .testing .fuzz (.{}, Fuzzer .run , .{});
538+ }
539+
540+ test "fuzz vec16 ternary lossless" {
541+ const Fuzzer = struct {
542+ fn run (ctx : @TypeOf (.{}), input : []const u8 ) anyerror ! void {
543+ _ = ctx ;
544+ if (input .len < 16 ) return ;
545+
546+ var vec : [16 ]f16 = undefined ;
547+ for (& vec , 0.. ) | * v , i | {
548+ v .* = switch (input [i ] % 3 ) {
549+ 0 = > @as (f16 , -1.0 ),
550+ 1 = > @as (f16 , 0.0 ),
551+ 2 = > @as (f16 , 1.0 ),
552+ else = > unreachable ,
553+ };
554+ }
555+
556+ const simd_vec : Vec16f16 = vec ;
557+ const widened = vec16F16ToF32 (simd_vec );
558+ const narrowed = vec16F32ToF16 (widened );
559+ const result : [16 ]f16 = narrowed ;
560+
561+ for (vec , result ) | orig , res | {
562+ try std .testing .expect (orig == res );
563+ }
564+ }
565+ };
566+ try std .testing .fuzz (.{}, Fuzzer .run , .{});
567+ }
568+
569+ test "count non finite f16" {
570+ const data = [_ ]f16 { 1.0 , std .math .inf (f16 ), -2.0 , std .math .nan (f16 ), 0.5 };
571+ const count = countNonFiniteF16 (& data );
572+ try std .testing .expectEqual (@as (usize , 2 ), count );
573+ }
574+
575+ test "vectorNormF16 alias" {
576+ const v = [_ ]f16 { 3.0 , 4.0 };
577+ const norm = vectorNormF16 (& v );
578+ try std .testing .expectApproxEqAbs (@as (f64 , 5.0 ), norm , 0.01 );
579+ }
580+
581+ test "roundtrip f32 to f16 to f32 preserves ternary values" {
582+ const ternary_values = [_ ]f32 { -1.0 , 0.0 , 1.0 };
583+
584+ for (ternary_values ) | val | {
585+ const f16_val : f16 = @floatCast (val );
586+ const f32_back : f32 = @floatCast (f16_val );
587+ try std .testing .expectApproxEqAbs (val , f32_back , 0.0001 );
588+ }
589+ }
590+
591+ test "f16 overflow behavior" {
592+ // f16 max = 65504, values above overflow to infinity
593+ const too_large : f16 = @floatCast (@as (f32 , 100000.0 ));
594+ const too_large_f32 : f32 = @floatCast (too_large );
595+ // Should be infinity (or very large value if saturated)
596+ try std .testing .expect (too_large_f32 >= 65504.0 or std .math .isInf (too_large_f32 ));
597+
598+ const fits : f16 = @floatCast (@as (f32 , 1000.0 ));
599+ try std .testing .expect (fits > 999.0 and fits < 1001.0 );
600+ }
601+
602+ test "f16 subnormal handling" {
603+ // Smallest normal f16 = 2^-14 ≈ 6.1e-5
604+ const tiny : f16 = @floatCast (@as (f32 , 1e-6 ));
605+
606+ // Should round to zero or subnormal
607+ const f32_back : f32 = @floatCast (tiny );
608+ try std .testing .expect (f32_back >= 0 and f32_back < 1e-4 );
609+ }
610+
611+ // φ² + 1/φ² = 3 | TRINITY
0 commit comments