Skip to content

Commit a5054c9

Browse files
committed
Fix apply on infinite lazy seq (F99)
applyFn now builds a lazy cons chain for variadic fn_val targets, peeling off only fixed params and passing the rest as a seq. A threadlocal flag signals VM/TreeWalk to use the seq directly instead of re-wrapping it in a PersistentList.
1 parent 6b5668f commit a5054c9

6 files changed

Lines changed: 75 additions & 11 deletions

File tree

‎.dev/checklist.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Check at session start for items that become actionable.
1616
| ID | Item | Trigger |
1717
|------|---------------------------------------------|--------------------------------------------------------------------------|
1818
| F94 | Upstream Alignment pass | ~38 UPSTREAM-DIFF markers remain — mostly permanent design diffs (protocol→fn, Java→Zig). No further alignment expected. |
19-
| F99 | Iterative lazy-seq realization engine | D74 fixes sieve. General deep lazy-seq + apply-on-infinite still deferred. See `optimizations.md` |
19+
| F99 | Iterative lazy-seq realization engine | D74 fixes sieve. apply-on-infinite fixed (lazy variadic path). Deep lazy-seq stack overflow still deferred. See `optimizations.md` |
2020
| F102 | map/filter chunked processing | Chunked types exist, range is lazy. Optimization: use chunks in map/filter pipelines. |
2121
| F103 | Escape analysis (local scope skip GC) | Compiler detects local-only Values, skip GC tracking |
2222
| F104 | Profile-guided optimization (extend IC) | Extend inline caching beyond monomorphic |

‎.dev/memo.md‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,16 @@ Ready for next phase planning. See Task Queue below.
2929

3030
## Previous Task
3131

32-
Phase 59: Deferred cleanup & test porting (ALL DONE).
33-
59.1: Ported Ratio upstream tests (ratio literals in add/sub/mul, i48 edge cases, BigInt/Ratio coercion).
34-
59.2: Ported promote ops tests (test-arbitrary-precision-subtract, defspec generative tests covered by CLJW-ADD).
35-
Also fixed: int/long/bigint/float coercion for Ratio/BigInt/BigDecimal types.
32+
F99 fix: apply on infinite lazy seq. Added lazy variadic path to applyFn
33+
(cons chain + peel fixed params, threadlocal flag for VM/TreeWalk rest packing).
3634

3735
Previous:
3836

39-
Phase 58: clojure.core.reducers (ALL DONE, 58.1-58.6).
40-
Post-phase: 3 bug fixes (parse-boolean, catch Error, walk+records),
41-
deferred/stale comment cleanup, record __reify_type filtering (seq/keys/vals/count).
37+
Phase 59: Deferred cleanup & test porting (ALL DONE).
38+
59.1-59.2: Ratio/promote ops upstream tests, coercion fixes.
4239

4340
## Known Issues
4441

45-
- apply on infinite lazy seq realizes eagerly (F99 — apply doesn't pass rest args as seq)
4642
- binding *ns* doesn't affect read-string for auto-resolved keywords (F138)
4743

4844
## Resolved Issues (this session)

‎src/builtins/collections.zig‎

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
10121068
pub fn vectorFn(allocator: Allocator, args: []const Value) anyerror!Value {
10131069
const items = try allocator.alloc(Value, args.len);

‎src/evaluator/tree_walk.zig‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,10 @@ pub const TreeWalk = struct {
678678
const rest_args = args[fixed_count..];
679679
const rest_val: Value = if (rest_args.len == 0)
680680
Value.nil_val
681+
else if (rest_args.len == 1 and bootstrap.apply_rest_is_seq) blk2: {
682+
bootstrap.apply_rest_is_seq = false;
683+
break :blk2 rest_args[0]; // Already a seq from apply's lazy path (F99)
684+
}
681685
else blk: {
682686
const collections = @import("../runtime/collections.zig");
683687
const items = self.allocator.alloc(Value, rest_args.len) catch return error.OutOfMemory;

‎src/runtime/bootstrap.zig‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,11 @@ fn treewalkCallBridge(allocator: Allocator, fn_val: Value, args: []const Value)
996996
/// Per-thread for concurrency (Phase 48).
997997
pub threadlocal var last_thrown_exception: ?Value = null;
998998

999+
/// Flag set by apply's lazy variadic path (F99). When true, the single rest arg
1000+
/// in the next variadic call is already a seq and should not be re-wrapped in a list.
1001+
/// Consumed (reset to false) by VM/TreeWalk rest packing code.
1002+
pub threadlocal var apply_rest_is_seq: bool = false;
1003+
9991004
/// Execute a bytecode fn_val via a new VM instance.
10001005
/// Heap-allocates the VM to avoid C stack overflow from recursive
10011006
/// VM → TreeWalk → VM calls (VM struct is ~500KB due to fixed-size stack).

‎src/vm/vm.zig‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,9 @@ pub const VM = struct {
14811481
if (rest_count == 0) {
14821482
// No rest args: rest param is nil (matches Clojure/TreeWalk behavior)
14831483
self.stack[args_start + fixed] = Value.nil_val;
1484+
} else if (rest_count == 1 and bootstrap.apply_rest_is_seq) {
1485+
// Already a seq from apply's lazy path (F99) — use directly
1486+
bootstrap.apply_rest_is_seq = false;
14841487
} else {
14851488
const rest_items = self.allocator.alloc(Value, rest_count) catch return error.OutOfMemory;
14861489
for (0..rest_count) |i| {

0 commit comments

Comments
 (0)