diff --git a/src/vm/mod.rs b/src/vm/mod.rs index 8581c713..c0a55be6 100644 --- a/src/vm/mod.rs +++ b/src/vm/mod.rs @@ -671,12 +671,10 @@ pub(crate) fn is_tree_bridge_eligible(b: crate::builtins::Builtin, argc: usize) // OP_CALL_DYN + a dedicated finalizer opcode (OP_GRP_BY_KEY / // OP_UNIQ_BY_KEY / OP_SRT_BY_KEY) or, where no finalizer is needed, // a tail OP_WRAPOK (mapr). See the matching arms in the compiler. - // ct fn xs / ct fn ctx xs — count by predicate. Same bridge - // contract as flt 2/3; tree interpreter handles the user-fn - // callback via ACTIVE_AST_PROGRAM. Named `ct` to avoid - // `cnt`-as-continue keyword reservation. - (Builtin::Ct, 2) => true, - (Builtin::Ct, 3) => true, + // ct fn xs / ct fn ctx xs lifted to native VM dispatch in PR C + // of ILO-45. Inline counter loop, no finalizer opcode needed — + // the bool typecheck mirrors flt's and OP_ADDK_N handles the + // per-true increment on the numeric-accumulator fast path. // rsrt fn xs (2-arg, ascending key) lifted to native dispatch via // OP_RSRT_BY_KEY in PR B of ILO-45. // mapr fn xs was on the bridge through Phase 2 PR3; PR 3b lifts it @@ -5252,6 +5250,115 @@ impl RegCompiler { self.next_reg = acc_reg + 1; return acc_reg; } + // ct fn xs / ct fn ctx xs → count by predicate. Same + // bool-typecheck shape as flt, but the accumulator is + // a number (incremented on true) instead of a list. + // Lifted off the tree-bridge in PR C of ILO-45. + // + // No finalizer opcode needed — the loop produces the + // result directly. Result is a Number; auto-unwrap is + // rejected by verify upstream (ct doesn't return Result). + (Builtin::Ct, 2) | (Builtin::Ct, 3) => { + let has_ctx = args.len() == 3; + let fn_reg = self.compile_expr(&args[0]); + let ctx_reg = if has_ctx { + Some(self.compile_expr(&args[1])) + } else { + None + }; + let xs_reg = + self.compile_expr(if has_ctx { &args[2] } else { &args[1] }); + + // Counter starts at 0. Number-only register so the + // OP_ADDK_N hot path applies on every iteration. + let counter_reg = self.alloc_reg(); + let zero_ki = self.current.add_const(Value::Number(0.0)); + self.emit_abx(OP_LOADK, counter_reg, zero_ki); + self.reg_is_num[counter_reg as usize] = true; + let one_ki = self.current.add_const(Value::Number(1.0)); + + let idx_reg = self.alloc_reg(); + self.emit_abx(OP_LOADK, idx_reg, zero_ki); + self.reg_is_num[idx_reg as usize] = true; + + let item_reg = self.alloc_reg(); + let nil_ki = self.current.add_const(Value::Nil); + self.emit_abx(OP_LOADK, item_reg, nil_ki); + + // OP_CALL_DYN ABI: res + N contiguous args. + let res_reg = self.alloc_reg(); + self.emit_abx(OP_LOADK, res_reg, nil_ki); + let arg0_reg = self.alloc_reg(); + assert!( + arg0_reg == res_reg + 1, + "ct HOF: arg0 reg must follow result reg contiguously" + ); + self.emit_abx(OP_LOADK, arg0_reg, nil_ki); + let arg1_reg = if has_ctx { + let r = self.alloc_reg(); + assert!( + r == arg0_reg + 1, + "ct HOF: arg1 reg must follow arg0 reg contiguously" + ); + self.emit_abx(OP_LOADK, r, nil_ki); + Some(r) + } else { + None + }; + + // Scratch for bool typecheck. + let isb_reg = self.alloc_reg(); + self.emit_abx(OP_LOADK, isb_reg, nil_ki); + + let _loop_top = self.current.code.len(); + self.emit_abc(OP_FOREACHPREP, item_reg, xs_reg, idx_reg); + let exit_jump_a = self.emit_jmp_placeholder(); + + let body_top = self.current.code.len(); + self.emit_abc(OP_MOVE, arg0_reg, item_reg, 0); + let argc = if let (Some(cr), Some(a1)) = (ctx_reg, arg1_reg) { + self.emit_abc(OP_MOVE, a1, cr, 0); + 2 + } else { + 1 + }; + self.emit_abc(OP_CALL_DYN, res_reg, fn_reg, argc); + + // Predicate must return bool. Same error shape as flt. + self.emit_abc(OP_ISBOOL, isb_reg, res_reg, 0); + let typeok_jump = self.emit_jmpt(isb_reg); + let err_text_ki = self.current.add_const(Value::Text(Arc::new( + "ct: predicate must return bool".to_string(), + ))); + self.emit_abx(OP_LOADK, arg0_reg, err_text_ki); + self.emit_abc(OP_WRAPERR, arg0_reg, arg0_reg, 0); + self.emit_abc(OP_PANIC_UNWRAP, 0, arg0_reg, 0); + self.current.patch_jump(typeok_jump); + + // On true: counter += 1. On false: skip. + let skip_inc_jump = self.emit_jmpf(res_reg); + if one_ki <= 255 { + self.emit_abc(OP_ADDK_N, counter_reg, counter_reg, one_ki as u8); + } else { + // K-table overflow fallback: load 1 then OP_ADD. + let one_reg = self.alloc_reg(); + self.emit_abx(OP_LOADK, one_reg, one_ki); + self.emit_abc(OP_ADD, counter_reg, counter_reg, one_reg); + } + self.current.patch_jump(skip_inc_jump); + + self.emit_abc(OP_FOREACHNEXT, item_reg, xs_reg, idx_reg); + let exit_jump_b = self.emit_jmp_placeholder(); + self.emit_jump_to(body_top); + + self.current.patch_jump(exit_jump_a); + self.current.patch_jump(exit_jump_b); + + // Counter is a Number; keep its numeric flag so + // downstream arithmetic stays on the fast path. + self.next_reg = counter_reg + 1; + return counter_reg; + } // flatmap fn xs → native HOF loop. The fn returns a list // per element; we splice each result list into the accumulator // via an inner FOREACHPREP/NEXT over the call result. diff --git a/tests/aot-baselines/obj-baselines.tsv b/tests/aot-baselines/obj-baselines.tsv index 847a92d5..8f6bbab6 100644 --- a/tests/aot-baselines/obj-baselines.tsv +++ b/tests/aot-baselines/obj-baselines.tsv @@ -19,7 +19,7 @@ cli-text-arg parse-or-default d4a099303082ee5986c4f5446ae87f0e31c9735ecebb23fedd cond-vs-ret fall 4f4e83f2e6ea1c5f8c21bce475a51f4d947883198eecc434e80f24e219f43f3f cranelift-error-span firstn bee672100e11cb5953b50e680aa6d89f6ea16252913a70ff052702ff19c5e439 cranelift-panic-fallback main 3d151059c932496a7ee15e2a42a0a9be29d770ccc94ecfbc7f108d0ac2b8e8d8 -ct-count-by-predicate pcount dd344245874ca6a650f326465740115f61df54694b45f70f2a3cfb3e3e0082ba +ct-count-by-predicate pcount 3f842765873cecb67e6058248a30a8a8dea54c10b5999184c16e860dece10a9d cumsum running 38ddf3532d63d72c021ed33ca25cad932b4344e66863936e1461c04fe012fb8f dot-index frst 2d8ebd7e5da1c5dfacc014a117756bf562926cd4083effbce0e4dc621d29d8de dot-paren-hint plus1 3fd865901660eb54d2976c159a70e8723405537602ac488000644043b7e7d00e @@ -102,7 +102,7 @@ range basic efc1ee9fca3fbf4834e25ffb2cf5c01613f5277519f4aa7848d02fa8690d55da range-call-bounds sum-indices 39943ef00fbbe7c805e2c42859c5cfc4d77da240de25bb84d61f5dfdb7657fd6 range-expr skip-first-two 951e20ae0945b6f563b0f662d9d95f2cc626d6fef1ce79d68e22ea1dabd94fa8 recursion fac 5b6e1f94c774aec065d6cb375a21e150225afd519bb5820432a348c0ea77e903 -reserved-names main 5d69d8d08353d1956f1a8c015447170506df0bbee6fc9794db851ccc9f2d6a4e +reserved-names main a1f2ac05591850056380fe39a03053d9dfa620d1fa38e42bb7fbdaf4442dd668 results div 1a94699652cc076651aade6687c2e3c1e520773441ebca23101da833c3b65e5b rndn mc-mean-ok 68af8800432c45a24546ce74dc9baf5a5d1a918d6d056ca8becc761c938e88dc rsrt top-nums 2a0df2ccb528951cea1b96eb08814ced85133fac4417f8c50d2e4f1c954e3447