@@ -929,15 +929,45 @@ pub fn applyFn(allocator: Allocator, args: []const Value) anyerror!Value {
929929
930930 const f = args [0 ];
931931 const last_arg = args [args .len - 1 ];
932+ const middle_count = args .len - 2 ; // exclude f and last_arg
933+
934+ // For variadic fn_val, avoid realizing infinite seqs (F99).
935+ // Build a lazy cons chain (like JVM's list*) and peel off only fixed params.
936+ if (f .tag () == .fn_val ) {
937+ if (findVariadicFixedCount (f .asFn ())) | fixed_count | {
938+ // Build arg_seq = list*(middle_args, last_arg) via cons chain
939+ var arg_seq = if (last_arg == Value .nil_val ) Value .nil_val else last_arg ;
940+ var i = middle_count ;
941+ while (i > 0 ) {
942+ i -= 1 ;
943+ const cell = try allocator .create (value_mod .Cons );
944+ cell .* = .{ .first = args [1 + i ], .rest = arg_seq };
945+ arg_seq = Value .initCons (cell );
946+ }
947+
948+ // Take exactly fixed_count items from arg_seq
949+ const call_args = try allocator .alloc (Value , fixed_count + 1 );
950+ for (0.. fixed_count ) | j | {
951+ call_args [j ] = try firstFn (allocator , &.{arg_seq });
952+ arg_seq = try restFn (allocator , &.{arg_seq });
953+ }
954+
955+ // Remaining seq is the rest param (lazy!) — convert to seq or nil
956+ call_args [fixed_count ] = try seqFn (allocator , &.{arg_seq });
957+
958+ // Signal VM/TreeWalk that the rest arg is already a seq (F99)
959+ bootstrap .apply_rest_is_seq = true ;
960+ return bootstrap .callFnVal (allocator , f , call_args );
961+ }
962+ }
932963
933- // Collect spread args from last collection
964+ // Non-variadic or non-fn_val: collect all items eagerly (existing path)
934965 const spread_items : []const Value = if (last_arg == Value .nil_val )
935966 &.{}
936967 else
937968 try collectSeqItems (allocator , last_arg );
938969
939970 // Build final args: middle args + spread items
940- const middle_count = args .len - 2 ; // exclude f and last_arg
941971 const total = middle_count + spread_items .len ;
942972 const call_args = try allocator .alloc (Value , total );
943973 if (middle_count > 0 ) {
@@ -1008,6 +1038,32 @@ pub fn applyFn(allocator: Allocator, args: []const Value) anyerror!Value {
10081038 };
10091039}
10101040
1041+ /// Find the fixed param count of a variadic arity, if any.
1042+ /// Returns null if the function has no variadic arity.
1043+ fn findVariadicFixedCount (fn_obj : * const value_mod.Fn ) ? usize {
1044+ const FnProto = @import ("../compiler/chunk.zig" ).FnProto ;
1045+ const FnNode = @import ("../analyzer/node.zig" ).FnNode ;
1046+
1047+ if (fn_obj .kind == .bytecode ) {
1048+ const primary : * const FnProto = @ptrCast (@alignCast (fn_obj .proto ));
1049+ if (primary .variadic ) return primary .arity - | 1 ;
1050+ if (fn_obj .extra_arities ) | extras | {
1051+ for (extras ) | extra | {
1052+ const p : * const FnProto = @ptrCast (@alignCast (extra ));
1053+ if (p .variadic ) return p .arity - | 1 ;
1054+ }
1055+ }
1056+ } else {
1057+ // TreeWalk Closure: first field is *const FnNode (avoid importing tree_walk.zig
1058+ // to prevent circular dependency: bootstrap → collections → tree_walk → bootstrap)
1059+ const fn_node_ptr : * const * const FnNode = @ptrCast (@alignCast (fn_obj .proto ));
1060+ for (fn_node_ptr .* .arities ) | a | {
1061+ if (a .variadic ) return a .params .len - 1 ;
1062+ }
1063+ }
1064+ return null ;
1065+ }
1066+
10111067/// (vector & items) — creates a vector from arguments.
10121068pub fn vectorFn (allocator : Allocator , args : []const Value ) anyerror ! Value {
10131069 const items = try allocator .alloc (Value , args .len );
0 commit comments