Skip to content

Commit 04e96da

Browse files
Lazy generators: O(1)-memory streaming via callback dispatch
The old `yield` semantics appended to a Vec, so a generator over a million values built a million-entry array before returning. New streaming mode: gen_stream / gen_count / gen_sum / gen_take install a callback on the interpreter, and every yield invokes the callback with the value. Memory is O(call-stack-depth), not O(yield-count). The callback returns 1 to continue or 0 to short-circuit; the generator unwinds via return_value sentinel. Eager yield still works when no callback is installed — existing test_generators.omc (8 cases) passes unchanged. The streaming API: gen_stream(thunk, callback) — run, callback per yield gen_count(thunk) — count yields, no storage gen_sum(thunk) — sum int yields, no storage gen_take(thunk, n) — first n into a list gen_substrate_fib(callback, max) — Fibonacci stream w/ resonance Thunks (not direct calls) keep the generator lazy: gen_stream installs the callback BEFORE invoking the thunk, so the generator body never runs without the callback in place. gen_substrate_fib is the OMC-only differentiator: a native lazy generator that streams Fibonacci attractors as HInt values, each already carrying resonance=1.0. Python's generators don't have substrate metadata and would need an itertools dance for laziness. Tests: 12 cases — count/sum/take basics, eager-fallback regression, substrate-fib stream with per-value attractor verification, early stop, and lazy sum of 1..1,000,000 = 500,000,500,000 without OOM. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 285dfb4 commit 04e96da

3 files changed

Lines changed: 418 additions & 7 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Lazy generators — streaming yield via callback.
2+
#
3+
# Previous behaviour: every `yield` appended to a Vec, materializing
4+
# the entire sequence before returning. A generator of a billion
5+
# values consumed memory for a billion values.
6+
#
7+
# Now: gen_stream / gen_take / gen_sum / gen_count install a callback.
8+
# Each `yield v` invokes the callback with v; the callback returns 1
9+
# to continue or 0 to short-circuit. Memory is O(call-stack-depth),
10+
# not O(yield-count). The existing eager `for x in fib() { ... }`
11+
# still works (no callback installed → eager fallback).
12+
#
13+
# Substrate-aligned bonus: gen_substrate_fib is a native lazy
14+
# generator that streams Fibonacci attractors with substrate metadata
15+
# carried per value. Python can't do either property naturally.
16+
17+
fn assert_eq(actual, expected, msg) {
18+
if actual != expected {
19+
test_record_failure(msg + ": expected " + to_string(expected) + " got " + to_string(actual));
20+
}
21+
}
22+
23+
fn assert_true(cond, msg) {
24+
if !cond { test_record_failure(msg); }
25+
}
26+
27+
# ---- A small generator that yields 1..n. Used by every test below. ----
28+
29+
fn count_to(n) {
30+
h i = 1;
31+
while i <= n {
32+
yield i;
33+
i = i + 1;
34+
}
35+
}
36+
37+
# ---- gen_count: walk a generator without storing anything ----
38+
39+
fn test_gen_count_basic() {
40+
h n = gen_count(fn() { return count_to(100); });
41+
assert_eq(n, 100, "count_to(100) yields 100 values");
42+
}
43+
44+
fn test_gen_count_large() {
45+
# 100_000 values — would be a 100k-entry Vec in eager mode.
46+
# In streaming mode this is O(1) memory.
47+
h n = gen_count(fn() { return count_to(100000); });
48+
assert_eq(n, 100000, "100k yields counted, no materialisation");
49+
}
50+
51+
# ---- gen_sum: accumulate without storing ----
52+
53+
fn test_gen_sum_basic() {
54+
h s = gen_sum(fn() { return count_to(10); });
55+
# 1+2+...+10 = 55
56+
assert_eq(s, 55, "sum 1..10 = 55");
57+
}
58+
59+
fn test_gen_sum_large() {
60+
h s = gen_sum(fn() { return count_to(1000); });
61+
# 1+2+...+1000 = 500500
62+
assert_eq(s, 500500, "sum 1..1000 = 500500");
63+
}
64+
65+
# ---- gen_take: pull first n values ----
66+
67+
fn test_gen_take_first_5() {
68+
h xs = gen_take(fn() { return count_to(1000000); }, 5);
69+
assert_eq(arr_len(xs), 5, "took 5 values");
70+
assert_eq(arr_get(xs, 0), 1, "first = 1");
71+
assert_eq(arr_get(xs, 4), 5, "fifth = 5");
72+
}
73+
74+
fn test_gen_take_more_than_available() {
75+
# Asking for 100 from a generator that yields 5 → returns what's there
76+
h xs = gen_take(fn() { return count_to(5); }, 100);
77+
assert_eq(arr_len(xs), 5, "got all 5 available");
78+
assert_eq(arr_get(xs, 4), 5, "last = 5");
79+
}
80+
81+
# ---- gen_stream: arbitrary user-provided callback ----
82+
83+
fn test_gen_stream_with_inline_lambda() {
84+
# Sum the first 7 squares: 1+4+9+16+25+36+49 = 140
85+
h acc = [0];
86+
h taken = [0];
87+
gen_stream(
88+
fn() { return count_to(1000); },
89+
fn(v) {
90+
if arr_get(taken, 0) >= 7 { return 0; }
91+
arr_set(acc, 0, arr_get(acc, 0) + v * v);
92+
arr_set(taken, 0, arr_get(taken, 0) + 1);
93+
return 1;
94+
}
95+
);
96+
assert_eq(arr_get(acc, 0), 140, "sum of first 7 squares = 140");
97+
}
98+
99+
# ---- Eager fallback still works (regression) ----
100+
101+
fn test_eager_generator_still_returns_array() {
102+
# No gen_stream — pure yield should materialise as before.
103+
h all = count_to(5);
104+
assert_eq(arr_len(all), 5, "eager array length");
105+
assert_eq(arr_get(all, 0), 1, "eager arr[0] = 1");
106+
assert_eq(arr_get(all, 4), 5, "eager arr[4] = 5");
107+
}
108+
109+
# ---- Substrate-native lazy Fibonacci generator ----
110+
111+
fn test_substrate_fib_stream() {
112+
# Stream Fibonacci values <= 100 ; should be [0,1,1,2,3,5,8,13,21,34,55,89] = 12 values
113+
h collected = [];
114+
gen_substrate_fib(
115+
fn(v) {
116+
arr_push(collected, v);
117+
return 1;
118+
},
119+
100
120+
);
121+
assert_eq(arr_len(collected), 12, "12 Fibonacci ≤ 100");
122+
assert_eq(arr_get(collected, 0), 0, "first = 0");
123+
assert_eq(arr_get(collected, 11), 89, "last ≤ 100 = 89");
124+
}
125+
126+
fn test_substrate_fib_each_is_attractor() {
127+
# Every yielded value should be a Fibonacci attractor (substrate-
128+
# aligned), so is_attractor(v) is 1 for all of them.
129+
h all_attractors = [1];
130+
gen_substrate_fib(
131+
fn(v) {
132+
if is_attractor(v) == 0 {
133+
arr_set(all_attractors, 0, 0);
134+
}
135+
return 1;
136+
},
137+
1000
138+
);
139+
assert_eq(arr_get(all_attractors, 0), 1, "every yielded value is on-attractor");
140+
}
141+
142+
fn test_substrate_fib_early_stop() {
143+
# Stop after 5 yields even though max=1000.
144+
h count = [0];
145+
gen_substrate_fib(
146+
fn(v) {
147+
arr_set(count, 0, arr_get(count, 0) + 1);
148+
if arr_get(count, 0) >= 5 { return 0; }
149+
return 1;
150+
},
151+
1000
152+
);
153+
assert_eq(arr_get(count, 0), 5, "stopped at 5");
154+
}
155+
156+
# ---- Memory pressure demo: gen_sum over 1M without OOM ----
157+
158+
fn test_lazy_million() {
159+
h s = gen_sum(fn() { return count_to(1000000); });
160+
# Sum 1..1M = 500000500000
161+
assert_eq(s, 500000500000, "lazily summed 1..1M");
162+
}

omnimcode-core/src/compiler.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,11 @@ impl Compiler {
199199
| "tape_neg" | "tape_pow_int"
200200
| "tape_exp" | "tape_sin" | "tape_cos"
201201
| "tape_relu" | "tape_sigmoid" | "tape_tanh"
202-
| "tape_matmul" | "tape_sum" | "tape_mean" => Some("int"),
202+
| "tape_matmul" | "tape_sum" | "tape_mean"
203+
// Lazy generators: gen_stream/gen_count/gen_sum
204+
// return int (success/count/accumulator).
205+
| "gen_stream" | "gen_count" | "gen_sum"
206+
| "gen_substrate_fib" => Some("int"),
203207
"pow" | "sqrt" | "log" | "log2" | "log10"
204208
| "exp" | "sin" | "cos" | "tan" | "asin" | "acos"
205209
| "atan" | "atan2" | "hypot" | "lerp"
@@ -262,6 +266,8 @@ impl Compiler {
262266
// 2D array primitives (Track 2 — 2026-05-16)
263267
| "arr_matmul" | "arr_transpose"
264268
| "arr_eye" | "arr_zeros_2d"
269+
// Lazy generator collector: returns array
270+
| "gen_take"
265271
// Forward-mode autograd duals (Track 2 — 2026-05-16)
266272
| "dual" | "dual_add" | "dual_sub"
267273
| "dual_mul" | "dual_div" | "dual_neg"

0 commit comments

Comments
 (0)