AI integration: providers, aifunc, markers, codeswap
The AI features in v02. Replaces archive/AI_CX_SYNTAX_v1.x.md and archive/AI_OPCODES_v1.x.md.
⚠ HISTORICAL (PB v02 era, retired 2026-05-30). Sections §1–8 describe the PB-based v02 AI surface (arCodeModify,aifunc_ct/_rt,\{N:marker syntax, KwaAI library hooks, etc.). PB sources are inBackups/beta_v2.0.28_retirement_2026-05-30.7z. The active C v3 AI runtime is described in §9 below and inruntime/cx_ai_*.c. The provider/call builtins (ai_set_provider,ai_call,ai_init,generatecode,replacecode, etc.) still function in v3 via the C port of these modules;aifunc_ct/_rtand the\{N:marker syntax are also forward-ported — seeDOCS/design/UNIFIED_REGISTER_ISA.mdfor the unified VM model they now run on.
The AI surface is shaped around a single architectural principle: AI-generated bytecode lives in a separate code array (arCodeModify); the original CX code in arCode stays pristine. Dispatch flips between the two via a small per-callee or per-marker stack. See ARCHITECTURE.md for the substrate. (Note: in v3/C era, arCodeModify is implemented via the register VM codeswap mechanism — see §9.)
ai_set_provider("anthropic"); // or "openai", "google", "mistral", "cohere",
// "xai", "deepseek", "groq", "ollama", "custom"
ai_set_key("sk-..."); // optional; env var is read otherwise
ai_init(); // returns 1 if a key is resolvable
println(ai_get_error()); // last error string
The 9 cloud providers each read a conventional env var (ANTHROPIC_API_KEY, OPENAI_API_KEY, GROQ_API_KEY, etc.). Ollama is local-but-HTTP. CxLlama (a daemon-driven local llama.cpp) is reserved as provider #9 but not yet wired in v02 — see "Deferred" at the end.
#pragma ai_fallback "groq, ollama" // at compile time
ai_set_fallback("groq, ollama"); // at runtime
When the current provider's call returns "ERROR: ...", AI_MakeRequestWithFallback walks the chain provider-by-provider. State (provider/model/key/endpoint) is snapshotted before the walk and restored on success — the fallback is a one-shot rescue, not a permanent switch.
key = "task-signature-or-hash";
hit = ai_cache_get(key);
if (hit == "") {
result = ai_call(prompt);
ai_cache_put(key, result, 3600); // ttl seconds
} else { result = hit; }
req = ai_call_async(prompt); // returns request id (or <0 on error)
while (ai_async_ready(req) == 0) { ... }
result = ai_async_result(req); // blocks if still pending; auto-cleans up
Declare a function whose body the LLM writes:
aifunc_ct function add.i(a.i, b.i) {
"Return a + b."
}
aifunc_rt function clamp.i(x.i, lo.i, hi.i) {
"Return x clamped to [lo, hi]."
}
// Call site looks identical to a regular function call:
result = add(3, 7); // -> 10
y = clamp(50, 0, 100); // -> 50
The body is a single string literal = the LLM task. Both aifunc_ct and aifunc_rt are modifier keywords before function:
aifunc_ct — runs the LLM at compile time (Pass 3.5). The bytecode lands in arCodeModify before the program starts. Compile fails if the LLM/parse fails.aifunc_rt — defers the LLM call to vmInit (after compile, before the dispatch loop). Same failure semantics; abort happens before user code runs.Multi-param: any number of int / string / float / long params. Mixed types route via gFuncParamTypes (per-funcId per-arg lane).
CXOP_CALLAIA call to an aifunc emits CXOP_CALLAI fnEntryPC, nArgs, fnFirstSlot with funcId in flags. The handler:
ARCHITECTURE.md).gActiveArCodeKind to #ARCODE_MODIFY.pc = entryPC in arCodeModify.RETURN restores the caller's branch via stCallFrame\savedBranch (Phase 3b plumbing). Kept as a dedicated opcode (not a flag on CALL) so future hardware-accelerated AI runtimes can intercept this single opcode.
#pragma ai_iterations 3 // refinement passes; round 1 generates, 2..N polish
#pragma ai_momentum 2 // 0=auto, 1=det, 2=balanced, 3=low-latency, 4=creative
aifunc_ct function maxof3.i(a.i, b.i, c.i) {
"Return the largest of a, b, c."
}
Round-N prompts feed round-(N-1)'s response back to the LLM with explicit "improve, but stay within the listed opcodes (PUSHI, FETCH, STORE, ADD, ... RET)" — without that constraint, refinement tends to invent ops we don't support (DUP2, etc.).
Momentum → temperature: 0→0.7 if iter>1 else 0.0, 1→0.0, 2→0.5, 3→0.2, 4→0.9.
Inline regions where the default CX code runs first, and AI can replace the body at runtime via generatecode + replacecode.
#pragma named val
val.i = 0;
function runMarker.v() {
\{5: "Set val to a value"
val = 42; // default code, runs first
\5:}
}
runMarker();
// val = 42
generatecode("Set val to 99. Push 99 then store into val.", 5);
replacecode(5);
runMarker();
// val = 99 — AI body in arCodeModify swapped in for marker 5
\{N: (open, with "desc" after) and \N:} (close). N is a positive integer; globally unique.CXOP_MARKER_BEGIN N + the body (compiled normally into arCode) + CXOP_MARKER_END N. The marker is registered in gMarkers(N) with beginPC / endPC / desc.replacecode(N): gMarkers(N)\active = True + \modifyEntryPC set. Next pass through MARKER_BEGIN swaps to arCodeModify, runs the AI body, MARKER_RETURN unwinds via gMarkerCallStack[] back to endPC + 1 in arCode.Nesting works — peer markers each have their own slot; nested markers stack via gMarkerCallStack. Inner is bypassed when outer is replaced (AI body doesn't contain MARKER_BEGIN ops).
Optimizer remap contract: any pass that touches gnBytecodeLen must remap gMarkers\beginPC/endPC through oldToNew[]. This is wired in V2.0.3 for both optimizer_pass_dead_first_store and optimizer_pass_peephole_fusion; new passes must opt in.
| Builtin | What it does |
|---|---|
generatecode(task.s, N.i) | LLM round-trip; stashes response in gMarkerCodeBuffer. Pass "" to reuse the marker's inline description as prompt. |
replacecode(N.i) | Parses gMarkerCodeBuffer into arCodeModify; registers + activates marker N's swap. |
setcode(body.s) | Stuffs gMarkerCodeBuffer with a CX-supplied string — bypasses the LLM. Lets you write deterministic offline tests. (V2.0.4) |
clonemarker(srcId.i, dstId.i) | Clones src's active swap-body to dst (both must be declared marker sites). "Stamp out N rival brains from one template." (V2.0.4) |
Marker bodies are NOT functions: no params, no return value. The eval-stack must be in the same state at exit as at entry. Use FETCH name / STORE name to access globally declared #pragma named slots.
-> works there too (v3.187.0)An aifunc can write a C-rule at runtime and hand it to arm() — an AI authoring entity behaviour as data. Rule text spells handle metadata exactly like every other CX program:
arm(e, "{ int n = e[\"skus\"]->count; ... }") // the metadata surface, in a rule
There is nothing special to remember and nothing to translate. ->count, ->cap, ->type, ->valid and ->id all read the same in rule text as in program text, because the rule front-end lowers them through the same shared builder to the same call the sandbox already accepts.
The retired call spellings do NOT work here either. jsonSize(...), queueCount(q), listSize(xs) and the rest are CX-E1048 in rule text just as they are everywhere else (Language Reference §11). Until v3.187.0 rule text was the one exemption — because -> parses as a field read on a dereference and the rule sandbox declines that whole node class — so an AI trained on documentation older than v3.187.0 will write the call form. Regenerate the prompt, do not translate.
Why this matters more in a rule than anywhere else: a rule that fails to compile makes arm() return 0 and the rule simply never fires — there is no error at the call site. Wrong metadata spelling in rule text is behaviour that silently does nothing, in either direction.
What -> does NOT open in a rule. Only the metadata fields. e->hull is still refused (it is not a spelling of e["hull"]), pointers are still refused, and a metadata field on something that is not a container handle is still an error rather than a 0.
; Modules/ai/cx_ai-v02.pbi
Procedure.l AI_LibraryLookup(name.s, task.s, mode.l) ; today: return 0 (miss)
Procedure AI_LibraryStore(name.s, task.s, mode.l, body.s, score.l) ; today: no-op
Two pinch-points wired into AI_CreateFunc_v2 (the aifunc-decl materialiser). Today both are no-op stubs; the real subsystem will:
(name, task hash), then (eventually) P2P peers. Returns the quality score of the best match; AI_CreateFunc_v2 skips the LLM round-trip when score ≥ threshold.Why this matters — every AI feature above currently requires a paid API key. The library lets one developer's vetted output serve thousands of others without spending more tokens. Pinch-points are present so the existing AI features automatically benefit when the subsystem lands.
The pre-aifunc-decl builtin form is preserved for backward compatibility:
#pragma named val
val.i = 0;
rt_createaifunc("setval42", "Set val to 42."); // @todo VERIFY -- see note below
result = ai_call_func("setval42", 0); // dispatches into arCodeModify
src.s = "PUSHI 99\nSTORE val\nRET\n";
ai_parse_bytecode("setval99", src); // hand-built bytecode
ai_call_func("setval99", 0);
println(ai_dump_asm("setval99")); // textual disasm
@todo —ct_createaifuncdoes not exist. Verified 2026-07-23 againstruntime/cx_builtin_names.hand the compiler's special-form sites: the real family isrt_createaifunc/rt_callaifunc/rt_runaifunc/rt_destroyaifunc/rt_aifunc_count— allrt_-prefixed, noct_variant. The snippet above has been changed tort_createaifunc, but the surrounding claim that this runs the LLM at compile time is unverified and may have been the reason act_name was written. The compile-time path that definitely exists is theaifunc_ctdeclaration (§Hello, AI-Generated World in the Language Reference). Someone who knows this subsystem should confirm which of the two this example meant, rather than a guess being written in here.
rt_createaifunc / ai_call_func / ai_parse_bytecode / ai_set_named / ai_get_named / ai_dump_asm are all functional. New code should prefer the aifunc decl + markers, but cxtest 146 and Examples-legacy/AI/830-839 still exercise this path.
ai_ws_get / ai_ws_set are intentionally not ported — alfa itself deprecated them in V1.110.90 in favour of FETCH name / STORE name through gAINamedSlots. See memory entry project-ai-ws-skip.
Use setcode + replacecode to drive markers without an API key:
function runM.v() {
\{7: "default"
val = 1;
\7:}
}
runM(); // val = 1
setcode("PUSHI 99\nSTORE val\n");
replacecode(7);
runM(); // val = 99
For aifunc decls, you can plant the body via ai_parse_bytecode under the same name the decl uses internally (e.g. for aifunc_ct function mul.i(...) the body is registered as mul). But the decl path also tries to generate at compile time — so for tests, prefer setcode + replacecode + markers over the decl form.
Examples-legacy/751/752/753.../Examples/AI/ — 13 programs covering the full AI surface. All build clean (100% as of 2026-05-29). Key entries:830 test ai functions — provider switching + ai_init + cache + ai_call836 test pi ai generated + 838 test ai eval — eval + LLM round-trip840 aifunc decl multi param + 841 aifunc iter momentum — aifunc_ct/_rt declarations842 codeswap markers — setcode / replacecode / generatecode end-to-end844 game 24 ai + 845 sudoku ai + 846 dijkstra ai — non-trivial games849 asm to disk roundtrip — ai_parse_bytecode → ai_dump_asm → fwrite → fread → re-register (replaces retired cxtest 157 .ocx-roundtrip)../cxtests/ — 141–156 cover the AI surface (provider, named slots, bytecode, opcode coverage, container universals, async + cache). All PASS (100% as of 2026-05-29).The AI subsystem lives in runtime/cx_ai_*:
cx_ai_http.{h,c} — WinHTTP-backed HTTP client (POST/GET, TLS via system cert store). Linux/macOS stub returns -1 until libcurl wiring lands.cx_ai_providers.{h,c} — 10-provider table (anthropic / openai / google / mistral / cohere / xai / deepseek / groq / ollama / custom). One cx_ai_provider_chat(id, model, key, system, user, max_tokens, temp, timeout, &out) dispatch. Per-provider JSON body builders + response extractors. Auto-picks first env-var-keyed provider when none explicitly set.cx_ai_native.c — the AI bytecode parser (cx_ai_parse_bytecode_native + ai_mnemonic → an ai_func_t of AOP ops), the register-VM executor (ai_vm_exec_reg), named binding (cx_ai_bind_named / cx_ai_bind_container), codeswap markers, and the LLM/provider hooks. The AI bytecode runs on the ONE register VM (2026-06-04 fold) — the old typed-stack AOP mini-VM was retired. ai_vm_exec_reg translates the AOP bytecode 1:1 into register instructions and runs them on a fresh cx_risc_vm. Named slots can still be bound to host C globals, but instead of being copied they are accessed in place through a gVT — a cx_slot table whose ptr points at the real storage; HOSTGET/HOSTSET (and AICONT for containers) read/write it directly (no dual register). (Renamed 2026-07-31 from FETCHH/STOREH: a trailing H means HALFWORD in both RISC-V and ARM — LH/SH, LDRH/STRH — so the old names read as 16-bit loads to anything trained on a real ISA. They mean host slot, not width. This paragraph was the exposure: nothing in the compiler renders these mnemonics, so a listing never showed them, but a reader of this manual did.) Native's typed containers — raw C int64/double arrays, cx_list_int/cx_list_float, cx_map_int — are bridged in the register VM's AICONT op (cx_risc_vm.c). The VM/risc path's executor (ai_exec, cxasm.c) does the same translation, so there is one executor across both paths + codeswap.DOCS/design/UNIFIED_REGISTER_ISA.md for the one-VM design (three engines → one register VM).#pragma named accumulates across lines (was last-wins). Container forms — #pragma named array NAME.t[N], list NAME.t, map NAME.t — emit the C declaration + the bind call automatically.