Skip to content

Commit ac4948e

Browse files
Phase L+M: resonance caching + typed HIR with specialized dispatch
## Phase L — Resonance / portal caching New unary_cache_pass in bytecode_opt.rs. Precomputes pure-unary harmonic ops on constants at compile time, before the constant folder so chained arithmetic sees a single constant: LoadConst(N); Resonance → LoadConst(precomputed_float) LoadConst(N); Fold1 → LoadConst(snapped_int) LoadConst(N); IsFibonacci → LoadConst(1 or 0) LoadConst(N); Fibonacci → LoadConst(fib(N)) LoadConst(N); HimScore → LoadConst(precomputed_float) LoadConst(N); Neg → LoadConst(-N) LoadConst(N); BitNot → LoadConst(!N) LoadConst(B); Not → LoadConst(!B) Mixed example: res(89) + 0.5 folds in two passes — cache res(89) -> 1.0, then fold 1.0 + 0.5 -> 1.5, collapsing two ops to a single LoadConst. New stats counter unary_calls_cached. Aggregated into total via a new accumulate() helper (previously each stat was added by hand, easy to miss). ## Phase M — Typed HIR with specialized dispatch Compiler now tracks variable types as it lowers AST to bytecode. TypeTag = Option<&'static str> over "int" / "float" / "string" / "bool" / "array" (None = couldn't prove statically; runtime polymorphism applies as before). Sources of type info: - Typed function parameters: fn add(x: int, y: int) - Return-type annotations: fn foo() -> int - Variable decls inferred from value: h x = 89 ⇒ int - Arithmetic on known-typed operands: int + int ⇒ int - Comparisons / bitwise: always bool / int - Built-in call sites: large catalog of fixed return types (fibonacci -> int, sqrt -> float, str_uppercase -> string, etc.) New typed-fast-path opcodes that skip the runtime is_float() check: Op::AddInt, Op::SubInt, Op::MulInt Op::AddFloat, Op::SubFloat, Op::MulFloat Compiler emits these in place of polymorphic Op::Add etc. when BOTH operands' inferred types match. The constant folder learned to fold the typed variants too — `1 + 2 + 3` with both operands int still folds end-to-end. CompiledFunction gained param_types: Vec<Option<String>> and return_type: Option<String> so cross-function type info survives into the compiled module (useful for future passes and debugging). ## Consistency fix Tree-walk is_fibonacci() now returns HInt(0)/HInt(1) instead of Bool, matching the VM's Op::IsFibonacci and the canonical Python OMC idiom `if is_fibonacci(x) == 1`. Both paths now agree on the wire format. ## Tests 125 passing across the workspace (was 118). +7 new unit tests in bytecode_opt::tests for the caching pass: - caches_resonance_of_constant - caches_phi_fold_of_constant - caches_fibonacci_of_constant - caches_is_fibonacci_of_constant - caches_unary_minus_of_constant - caches_bitnot_of_constant - chains_unary_cache_then_constant_fold ## Compatibility Canonical sweep still 22/30 in both tree-walk and VM. No regressions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 888779a commit ac4948e

7 files changed

Lines changed: 474 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,40 @@ All notable changes to OMNIcode will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added (Phase L + M: resonance caching + typed HIR, 2026-05-13)
8+
9+
**Phase L — Resonance / portal caching**
10+
New `unary_cache_pass` in `bytecode_opt.rs`. Folds pure-unary harmonic ops on constants at compile time, before the constant folder runs (so subsequent chained arithmetic sees a single constant):
11+
12+
- `LoadConst(N); Resonance``LoadConst(precomputed_float)``res(89)` becomes the literal `1.0`
13+
- `LoadConst(N); Fold1``LoadConst(snapped_int)``phi.fold(90)` becomes `89`
14+
- `LoadConst(N); IsFibonacci``LoadConst(1 or 0)`
15+
- `LoadConst(N); Fibonacci``LoadConst(fib(N))`
16+
- `LoadConst(N); HimScore``LoadConst(precomputed_float)`
17+
- `LoadConst(N); Neg` / `BitNot` / `Not` → precomputed inverse
18+
19+
New stats counter `unary_calls_cached`. The omnicc Python compiler calls this "resonance caching"; same semantics, scoped to bytecode. Mixed example: `res(89) + 0.5` folds in two passes — cache `res(89) → 1.0`, then fold `1.0 + 0.5 → 1.5` — collapsing two ops to a single LoadConst.
20+
21+
**Phase M — Typed HIR with specialized dispatch**
22+
23+
The compiler now tracks a `var_types: HashMap<String, &'static str>` populated from:
24+
- Typed function parameters (`fn add(x: int, y: int)`)
25+
- Return-type annotations of user-defined functions (looked up across boundaries)
26+
- Variable declarations whose value's type is statically known (`h x = 89;` ⇒ int)
27+
- Arithmetic on known-typed operands (int + int ⇒ int)
28+
- Comparisons and bitwise ops (always bool / int)
29+
- Built-in function call sites with fixed return types
30+
31+
New typed-fast-path opcodes that skip the runtime `is_float()` check:
32+
- `Op::AddInt`, `Op::SubInt`, `Op::MulInt`
33+
- `Op::AddFloat`, `Op::SubFloat`, `Op::MulFloat`
34+
35+
The compiler emits these in place of polymorphic `Op::Add` / `Op::Sub` / `Op::Mul` when **both** operands' static types match. The optimizer's constant folder also knows them — `1 + 2 + 3` with both operands int folds through the typed path, then collapses to a single constant.
36+
37+
`CompiledFunction` gained `param_types: Vec<Option<String>>` and `return_type: Option<String>` fields so cross-function type info is preserved through compilation.
38+
39+
**Tests:** +7 unit tests for resonance caching (covers res, phi.fold, is_fibonacci, fibonacci, unary minus, bitnot, chained cache+fold). 125 total tests passing (was 118).
40+
741
### Added (Phase K: bytecode optimizer, 2026-05-13)
842
New module `omnimcode-core/src/bytecode_opt.rs`. Runs after compile, before VM execution. On by default in VM mode; disable with `OMC_OPT=0`. Show stats with `OMC_OPT_STATS=1`.
943

omnimcode-core/src/bytecode.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ pub enum Op {
5252
Mod,
5353
Neg,
5454

55+
// Typed fast-path arithmetic: skip the runtime is_float() check when the
56+
// compiler proves both operands are int-typed. Emitted by Phase M's HIR.
57+
AddInt,
58+
SubInt,
59+
MulInt,
60+
// Typed fast-path arithmetic for floats (both operands provably float).
61+
AddFloat,
62+
SubFloat,
63+
MulFloat,
64+
5565
Eq,
5666
Ne,
5767
Lt,
@@ -112,6 +122,12 @@ pub enum Op {
112122
pub struct CompiledFunction {
113123
pub name: String,
114124
pub params: Vec<String>,
125+
/// Optional type annotation per parameter ("int" / "float" / "string" / "bool" / etc.)
126+
/// Phase M: used by the compiler to specialize arithmetic on known-int args.
127+
pub param_types: Vec<Option<String>>,
128+
/// Optional return-type annotation. Used by the type-inference helper
129+
/// when a call's return type is statically known.
130+
pub return_type: Option<String>,
115131
pub ops: Vec<Op>,
116132
pub constants: Vec<Const>,
117133
}
@@ -129,6 +145,8 @@ impl Default for Module {
129145
main: CompiledFunction {
130146
name: "__main__".to_string(),
131147
params: Vec::new(),
148+
param_types: Vec::new(),
149+
return_type: None,
132150
ops: Vec::new(),
133151
constants: Vec::new(),
134152
},

omnimcode-core/src/bytecode_opt.rs

Lines changed: 194 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ pub struct OptStats {
1717
pub dead_loads_removed: usize,
1818
pub double_nots_collapsed: usize,
1919
pub double_negs_collapsed: usize,
20+
/// Pure-unary ops on constants folded: res(89), phi.fold(N), fibonacci(N),
21+
/// is_fibonacci(N), HimScore(N), -N, !N, ~N, etc.
22+
pub unary_calls_cached: usize,
2023
}
2124

2225
impl OptStats {
@@ -25,6 +28,7 @@ impl OptStats {
2528
+ self.dead_loads_removed
2629
+ self.double_nots_collapsed
2730
+ self.double_negs_collapsed
31+
+ self.unary_calls_cached
2832
}
2933
}
3034

@@ -34,6 +38,10 @@ pub fn optimize_function(func: &mut CompiledFunction) -> OptStats {
3438
// Run passes until a fixpoint is reached. In practice 2-3 iterations.
3539
loop {
3640
let before = stats.total();
41+
// Resonance caching FIRST — turns `LoadConst(89); Resonance` into a
42+
// single constant, which the constant folder can then absorb into
43+
// surrounding arithmetic.
44+
unary_cache_pass(func, &mut stats);
3745
constant_fold_pass(func, &mut stats);
3846
dead_load_pass(func, &mut stats);
3947
double_unary_pass(func, &mut stats);
@@ -98,6 +106,83 @@ fn dead_load_pass(func: &mut CompiledFunction, stats: &mut OptStats) {
98106
}
99107
}
100108

109+
/// Cache pure-unary harmonic ops on constants:
110+
/// LoadConst(N); Resonance → LoadConst(precomputed_float)
111+
/// LoadConst(N); Fold1 → LoadConst(snapped_int)
112+
/// LoadConst(N); IsFibonacci → LoadConst(1 or 0)
113+
/// LoadConst(N); Fibonacci → LoadConst(fib(N))
114+
/// LoadConst(N); HimScore → LoadConst(precomputed_float)
115+
/// LoadConst(N); Neg → LoadConst(-N)
116+
/// LoadConst(N); BitNot → LoadConst(!N)
117+
/// LoadConst(B); Not → LoadConst(!B)
118+
///
119+
/// These are pure functions of a single constant — they cannot fail and
120+
/// cannot observe runtime state. The omnicc Python compiler calls this
121+
/// "resonance caching"; same idea, scoped to bytecode.
122+
fn unary_cache_pass(func: &mut CompiledFunction, stats: &mut OptStats) {
123+
let n = func.ops.len();
124+
if n < 2 {
125+
return;
126+
}
127+
for i in 0..(n - 1) {
128+
let const_idx = match &func.ops[i] {
129+
Op::LoadConst(idx) => *idx,
130+
_ => continue,
131+
};
132+
let c = match func.constants.get(const_idx) {
133+
Some(c) => c.clone(),
134+
None => continue,
135+
};
136+
let result = match (&func.ops[i + 1], &c) {
137+
(Op::Resonance, Const::Int(n)) => {
138+
Some(Const::Float(crate::value::HInt::compute_resonance(*n)))
139+
}
140+
(Op::Resonance, Const::Float(f)) => Some(Const::Float(
141+
crate::value::HInt::compute_resonance(*f as i64),
142+
)),
143+
(Op::Fold1, Const::Int(n)) => Some(Const::Int(fold_to_fib_const(*n))),
144+
(Op::Fold1, Const::Float(f)) => Some(Const::Int(fold_to_fib_const(*f as i64))),
145+
(Op::IsFibonacci, Const::Int(n)) => {
146+
Some(Const::Int(if crate::value::is_fibonacci(*n) { 1 } else { 0 }))
147+
}
148+
(Op::Fibonacci, Const::Int(n)) => {
149+
Some(Const::Int(crate::value::fibonacci(*n)))
150+
}
151+
(Op::HimScore, Const::Int(n)) => {
152+
Some(Const::Float(crate::value::HInt::compute_him(*n)))
153+
}
154+
(Op::Neg, Const::Int(n)) => Some(Const::Int(-*n)),
155+
(Op::Neg, Const::Float(f)) => Some(Const::Float(-*f)),
156+
(Op::BitNot, Const::Int(n)) => Some(Const::Int(!*n)),
157+
(Op::Not, Const::Bool(b)) => Some(Const::Bool(!*b)),
158+
(Op::Not, Const::Int(n)) => Some(Const::Bool(*n == 0)),
159+
_ => None,
160+
};
161+
if let Some(folded) = result {
162+
let new_idx = func.constants.len();
163+
func.constants.push(folded);
164+
func.ops[i] = Op::Nop;
165+
func.ops[i + 1] = Op::LoadConst(new_idx);
166+
stats.unary_calls_cached += 1;
167+
}
168+
}
169+
}
170+
171+
fn fold_to_fib_const(n: i64) -> i64 {
172+
let fibs: [i64; 15] = [0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610];
173+
let abs_val = n.abs();
174+
let mut nearest = fibs[0];
175+
let mut min_dist = abs_val;
176+
for &f in &fibs {
177+
let d = (f - abs_val).abs();
178+
if d < min_dist {
179+
min_dist = d;
180+
nearest = f;
181+
}
182+
}
183+
if n < 0 { -nearest } else { nearest }
184+
}
185+
101186
/// Collapse `Not; Not` (and similar double-unary ops) to no-op.
102187
fn double_unary_pass(func: &mut CompiledFunction, stats: &mut OptStats) {
103188
let n = func.ops.len();
@@ -131,9 +216,9 @@ fn fold_binary(a: &Const, b: &Const, op: &Op) -> Option<Const> {
131216
let af = const_to_float(a)?;
132217
let bf = const_to_float(b)?;
133218
return match op {
134-
Op::Add => Some(Const::Float(af + bf)),
135-
Op::Sub => Some(Const::Float(af - bf)),
136-
Op::Mul => Some(Const::Float(af * bf)),
219+
Op::Add | Op::AddFloat => Some(Const::Float(af + bf)),
220+
Op::Sub | Op::SubFloat => Some(Const::Float(af - bf)),
221+
Op::Mul | Op::MulFloat => Some(Const::Float(af * bf)),
137222
Op::Div => {
138223
if bf == 0.0 {
139224
None // can't fold div-by-zero (produces Singularity)
@@ -153,9 +238,9 @@ fn fold_binary(a: &Const, b: &Const, op: &Op) -> Option<Const> {
153238
let ai = const_to_int(a)?;
154239
let bi = const_to_int(b)?;
155240
match op {
156-
Op::Add => Some(Const::Int(ai.wrapping_add(bi))),
157-
Op::Sub => Some(Const::Int(ai.wrapping_sub(bi))),
158-
Op::Mul => Some(Const::Int(ai.wrapping_mul(bi))),
241+
Op::Add | Op::AddInt => Some(Const::Int(ai.wrapping_add(bi))),
242+
Op::Sub | Op::SubInt => Some(Const::Int(ai.wrapping_sub(bi))),
243+
Op::Mul | Op::MulInt => Some(Const::Int(ai.wrapping_mul(bi))),
159244
Op::Div => {
160245
if bi == 0 {
161246
None
@@ -204,21 +289,21 @@ fn const_to_float(c: &Const) -> Option<f64> {
204289

205290
pub fn optimize_module(module: &mut Module) -> OptStats {
206291
let mut total = OptStats::default();
207-
let stats_main = optimize_function(&mut module.main);
208-
total.constants_folded += stats_main.constants_folded;
209-
total.dead_loads_removed += stats_main.dead_loads_removed;
210-
total.double_nots_collapsed += stats_main.double_nots_collapsed;
211-
total.double_negs_collapsed += stats_main.double_negs_collapsed;
292+
accumulate(&mut total, optimize_function(&mut module.main));
212293
for (_, func) in module.functions.iter_mut() {
213-
let s = optimize_function(func);
214-
total.constants_folded += s.constants_folded;
215-
total.dead_loads_removed += s.dead_loads_removed;
216-
total.double_nots_collapsed += s.double_nots_collapsed;
217-
total.double_negs_collapsed += s.double_negs_collapsed;
294+
accumulate(&mut total, optimize_function(func));
218295
}
219296
total
220297
}
221298

299+
fn accumulate(total: &mut OptStats, s: OptStats) {
300+
total.constants_folded += s.constants_folded;
301+
total.dead_loads_removed += s.dead_loads_removed;
302+
total.double_nots_collapsed += s.double_nots_collapsed;
303+
total.double_negs_collapsed += s.double_negs_collapsed;
304+
total.unary_calls_cached += s.unary_calls_cached;
305+
}
306+
222307
#[cfg(test)]
223308
mod tests {
224309
use super::*;
@@ -307,4 +392,97 @@ mod tests {
307392
.any(|c| matches!(c, Const::Bool(true))),
308393
);
309394
}
395+
396+
// ----- Phase L: resonance / portal caching -----
397+
398+
#[test]
399+
fn caches_resonance_of_constant() {
400+
// res(89) on a constant — 89 is Fibonacci so resonance = 1.0
401+
let (m, stats) = compile_and_opt("h x = res(89);");
402+
assert!(stats.unary_calls_cached >= 1);
403+
assert!(m
404+
.main
405+
.constants
406+
.iter()
407+
.any(|c| matches!(c, Const::Float(f) if (f - 1.0).abs() < 1e-9)));
408+
}
409+
410+
#[test]
411+
fn caches_phi_fold_of_constant() {
412+
// phi.fold(90) → 89 (snap to nearest Fibonacci)
413+
let (m, stats) = compile_and_opt("h x = phi.fold(90);");
414+
assert!(stats.unary_calls_cached >= 1);
415+
assert!(m
416+
.main
417+
.constants
418+
.iter()
419+
.any(|c| matches!(c, Const::Int(89))));
420+
}
421+
422+
#[test]
423+
fn caches_fibonacci_of_constant() {
424+
let (m, stats) = compile_and_opt("h x = fibonacci(10);");
425+
assert!(stats.unary_calls_cached >= 1);
426+
assert!(m
427+
.main
428+
.constants
429+
.iter()
430+
.any(|c| matches!(c, Const::Int(55))));
431+
}
432+
433+
#[test]
434+
fn caches_is_fibonacci_of_constant() {
435+
let (m, stats) = compile_and_opt("h x = is_fibonacci(89);");
436+
assert!(stats.unary_calls_cached >= 1);
437+
assert!(m
438+
.main
439+
.constants
440+
.iter()
441+
.any(|c| matches!(c, Const::Int(1))));
442+
443+
let (m2, stats2) = compile_and_opt("h x = is_fibonacci(90);");
444+
assert!(stats2.unary_calls_cached >= 1);
445+
assert!(m2
446+
.main
447+
.constants
448+
.iter()
449+
.any(|c| matches!(c, Const::Int(0))));
450+
}
451+
452+
#[test]
453+
fn caches_unary_minus_of_constant() {
454+
let (m, stats) = compile_and_opt("h x = -42;");
455+
assert!(stats.unary_calls_cached >= 1 || stats.constants_folded >= 1);
456+
// -42 should appear as a constant after folding (the parser desugars
457+
// unary minus to `0 - 42`, which the constant folder reduces).
458+
assert!(m
459+
.main
460+
.constants
461+
.iter()
462+
.any(|c| matches!(c, Const::Int(-42))));
463+
}
464+
465+
#[test]
466+
fn caches_bitnot_of_constant() {
467+
let (m, stats) = compile_and_opt("h x = ~0;");
468+
assert!(stats.unary_calls_cached >= 1);
469+
assert!(m
470+
.main
471+
.constants
472+
.iter()
473+
.any(|c| matches!(c, Const::Int(-1))));
474+
}
475+
476+
#[test]
477+
fn chains_unary_cache_then_constant_fold() {
478+
// res(89) folds to 1.0, then `1.0 + 0.5` folds to 1.5.
479+
let (m, stats) = compile_and_opt("h x = res(89) + 0.5;");
480+
assert!(stats.unary_calls_cached >= 1);
481+
assert!(stats.constants_folded >= 1, "should fold the chained add");
482+
assert!(m
483+
.main
484+
.constants
485+
.iter()
486+
.any(|c| matches!(c, Const::Float(f) if (f - 1.5).abs() < 1e-9)));
487+
}
310488
}

0 commit comments

Comments
 (0)