Skip to content

Commit 6e1b5f4

Browse files
Phase H.5 host integration: safe becomes a first-class OMC keyword
Until now, `safe a / b` and `safe arr_get(a, idx)` only worked inside the OMC-written self-healing compiler demos (self_healing_h4.omc, h5.omc), which carry their own OMC-side parser/AST/encoder/executor. The host Rust parser/interpreter didn't know `safe` as a keyword — it would tokenize as an unknown identifier. This integration brings `safe` into the language proper: parser.rs: Token::Safe variant + "safe" keyword recognition ast.rs: Expression::Safe(Box<Expression>) variant parser.rs: parse_expression peeks for Safe and wraps the rest. Bare `safe arr_set(buf, i, v);` statements work via the existing expression-statement fallback. interpreter.rs: eval_expr dispatches Safe by inner shape: Div(l, r) → safe_divide(l, r) arr_get(a, idx) → safe_arr_get(a, idx) arr_set(a, idx,v) → safe_arr_set(a, idx, v) Unknown shapes evaluate inner directly. compiler.rs: Same dispatch for the Rust VM bytecode path, plus type inference delegates Safe to its inner. examples/safe_keyword_host.omc — new minimal smoke test that runs without any self-healing-compiler infrastructure. Eight outputs, all correct: safe 89 / 0 → 89 compute(144, 0) [dynamic /0] → 144 compute(144, 3) → 48 safe arr_get(xs, 999) → 20 [fold(999)=610, 610%3=1] safe arr_get(xs, 1) → 20 safe arr_set(xs, 999, 99) → xs becomes [10, 99, 30] The mutation case works cleanly through tree-walk because the interpreter pattern-matches Safe(Call("arr_set", ...)) before any synthetic-arg shim runs — safe_arr_set receives the actual Expression::Variable it needs and writes back to the caller's scope. Known gap: Safe(Call("arr_set", ...)) compiled to bytecode and run through the Rust VM still routes via vm_call_builtin's synthetic-arg shim and loses the mutation. This is the same shape gap V.7c closed for arr_set with Op::ArrSetNamed; a future Op::SafeArrSetNamed would close it here too. Tree-walk works cleanly today; the named-mutation gap is documented and bounded. Regression: V.9b ✓✓✓ still passes. H.5 OMC demo file (six sub-demos) all converge. No breakage of the existing surface. The Phase H story is no longer "fork the self-healing-compiler demo file." It's "write `safe` where you'd write a runtime guard." This commit also includes the (slightly delayed) Phase H.5.1 CHANGELOG entry covering the bytecode-VM gap closure shipped in commit 1deae52. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 1deae52 commit 6e1b5f4

7 files changed

Lines changed: 163 additions & 1 deletion

File tree

CHANGELOG.md

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

55
## [Unreleased]
66

7+
### Added (Phase H.5 host-language integration: `safe` as a first-class keyword, 2026-05-14)
8+
9+
🎯 **`safe` is now a host-level OMC keyword — no self-healing-demo infrastructure required.**
10+
11+
Until now, `safe a / b` and `safe arr_get(a, idx)` only worked inside the OMC-written self-healing compiler demos (`examples/self_healing_h4.omc`, `h5.omc`), which carry their own OMC-side parser, AST, encoder, and executor. The host Rust parser/interpreter didn't know `safe` as a keyword — it would tokenize as an unknown identifier.
12+
13+
This integration brings `safe` into the language proper:
14+
15+
| Layer | Change |
16+
|---|---|
17+
| Lexer (`parser.rs`) | New `Token::Safe`; `"safe"` keyword recognized |
18+
| AST (`ast.rs`) | New `Expression::Safe(Box<Expression>)` variant |
19+
| Parser (`parser.rs`) | `parse_expression` peeks for `Token::Safe`, wraps the rest of the expression. Bare statements (`safe arr_set(buf, i, v);`) work via the existing expression-statement fallback |
20+
| Interpreter (`interpreter.rs`) | `Expression::Safe(inner)` pattern-matches the inner shape: `Div(l, r)``safe_divide(l, r)`, `Call("arr_get", ...)``safe_arr_get(...)`, `Call("arr_set", ...)``safe_arr_set(...)`; unknown shapes evaluate the inner directly |
21+
| Compiler (`compiler.rs`) | `Expression::Safe(inner)` lowers to the matching `Op::Call("safe_*", n)` for known shapes; type inference delegates to the inner expression |
22+
23+
#### Smoke test (`examples/safe_keyword_host.omc`)
24+
25+
Eight assertions, all pass on the host interpreter without any OMC-written self-healing wrapper:
26+
27+
- `safe 89 / 0 → 89`
28+
- `compute(144, 0) → 144` (dynamic zero healed)
29+
- `compute(144, 3) → 48`
30+
- `safe arr_get([10,20,30], 999) → 20` (fold(999)=610, 610%3=1)
31+
- `safe arr_get([10,20,30], 1) → 20`
32+
- `safe arr_set(xs, 999, 99)` writes xs[1]=99; xs[0] and xs[2] unchanged
33+
34+
The mutation case (the H.5 named-store fix in OMC bytecode) is naturally clean through tree-walk because the interpreter pattern-matches `Safe(Call("arr_set", [Variable(name), ...]))` before any synthetic-arg shim runs — `safe_arr_set` receives the actual `Expression::Variable(name)` it needs and writes back to the caller's scope.
35+
36+
#### What still doesn't work
37+
38+
`Safe(Call("arr_set", ...))` compiled to bytecode and run through the Rust VM lowers to `Op::Call("safe_arr_set", 3)`, which routes via `vm_call_builtin`'s synthetic-arg shim → mutation lost. This is the same gap V.7c closed for `arr_set` with `Op::ArrSetNamed`. A future `Op::SafeArrSetNamed(String)` would close it here too. Tonight's scope kept the Rust-VM bytecode path on the existing call shim — tree-walk works cleanly, the named-mutation gap is documented and bounded.
39+
40+
#### Why this matters
41+
42+
The H.4/H.5 OMC-written demos remain the architecturally pure proof — the bytecode VM rewrites and executes `safe` semantics end-to-end on the φ-math substrate. But for a developer who just wants the feature in their OMC code, it's now a one-keyword opt-in at the language level. The Phase H story is no longer "fork the self-healing-compiler demo file." It's "write `safe` where you'd write a runtime guard."
43+
44+
### Added (Phase H.5.1: close the safe arr_set bytecode-VM gap, 2026-05-14)
45+
46+
`examples/self_healing_h5.omc``safe arr_set(VAR, idx, val)` works through the OMC bytecode VM, not just under tree-walk. New `SAFE_ARR_SET_NAMED varname` opcode in the OMC-written executor mirrors V.7c's `ARR_SET_NAMED` pattern: the variable name rides on the opcode itself rather than going through `CALL_BUILTIN`'s synthetic-arg shim that copies array arguments. Encoder detects bare-VAR first-arg shape and emits the named form; executor pops idx/val, looks up array in scope, computes fold-and-mod healed index, mutates, writes back. Demo 4b verifies: `[55, 13, 0, 0, 34]` buffer state after four `safe arr_set` writes with `idx ∈ {0, 100, -1, 6}`. Six demos, six convergences.
47+
748
### Added (Phase H.5: array-bounds healing via fold_escape on the index, 2026-05-14)
849

950
🎯 **`examples/self_healing_h5.omc``safe arr_get(a, idx)` and `safe arr_set(a, idx, v)` make out-of-bounds accesses total.**

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ What this is **not**: a fast runtime, a production toolchain, a stable API, a de
4242
| Self-healing across two stages (token + AST), 5 bugs healed in one source | `examples/self_healing_h3.omc` | All four demos converge; `safe(8) → 8` on the integrated case |
4343
| User-declared runtime self-healing via `safe` keyword | `examples/self_healing_h4.omc` | `compute(144, 0) → 144` — runtime crash converted to finite answer on attractor |
4444
| Array-bounds healing — out-of-bounds reads become attractor-landing | `examples/self_healing_h5.omc` | Loop walking 8 indices off a 5-element array; every output has `φ=1.000` |
45+
| Host-level `safe` keyword — works in any OMC program, not just the self-healing demos | `examples/safe_keyword_host.omc` | `safe 89/0 → 89`, `safe arr_get(xs, 999) → 20`, `safe arr_set(xs, 999, 99)` mutates xs[1] |
4546

4647
Run any of these with the binary built from this repo:
4748

examples/safe_keyword_host.omc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# =============================================================================
2+
# Host-level `safe` keyword demo
3+
# =============================================================================
4+
# As of Phase H.5 (host-language integration), the `safe` keyword is part of
5+
# OMC the language — recognised by the Rust parser, AST, and interpreter
6+
# directly. No self-healing compiler infrastructure (H.1-H.5 OMC-written
7+
# demo files) is required; any OMC program can use `safe` as a one-keyword
8+
# opt-in to runtime self-healing semantics.
9+
#
10+
# Three supported shapes today:
11+
# safe a / b → safe_divide(a, b)
12+
# safe arr_get(a, idx) → safe_arr_get(a, idx)
13+
# safe arr_set(a, idx, v) → safe_arr_set(a, idx, v)
14+
#
15+
# This file is a minimal smoke test. Run with:
16+
# ./target/release/omnimcode-standalone examples/safe_keyword_host.omc
17+
# =============================================================================
18+
19+
# --- safe divide ---
20+
21+
print(safe 89 / 0); # 89: fold(0)=1, 89/1=89
22+
23+
fn compute(a, b) { return safe a / b; }
24+
print(compute(144, 0)); # 144: dynamic zero healed at runtime
25+
print(compute(144, 3)); # 48: in-bounds division unchanged
26+
27+
# --- safe array read out of bounds ---
28+
29+
h xs = [10, 20, 30];
30+
print(safe arr_get(xs, 999)); # 20: fold(999)=610, 610%3=1, xs[1]
31+
print(safe arr_get(xs, 1)); # 20: in-bounds, no rewrite needed
32+
33+
# --- safe array write out of bounds ---
34+
35+
safe arr_set(xs, 999, 99); # writes xs[fold(999) % 3] = xs[1] = 99
36+
print(arr_get(xs, 0)); # 10: unchanged
37+
print(arr_get(xs, 1)); # 99: the write landed here
38+
print(arr_get(xs, 2)); # 30: unchanged

omnimcode-core/src/ast.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,17 @@ pub enum Expression {
113113
// Harmonic operations
114114
Resonance(Box<Expression>),
115115
Fold(Box<Expression>),
116+
117+
// H.5: user-declared runtime self-healing intent.
118+
// `safe <expr>` wraps an expression in self-healing semantics.
119+
// The interpreter pattern-matches the inner expression at eval
120+
// time and routes to the appropriate ONN primitive:
121+
// safe a / b → safe_divide(a, b)
122+
// safe arr_get(a, idx) → safe_arr_get(a, idx)
123+
// safe arr_set(a, idx, v) → safe_arr_set(a, idx, v)
124+
// Other shapes fall through to evaluating the inner expression
125+
// directly (no-op), reserving the slot for future runtime guards.
126+
Safe(Box<Expression>),
116127
}
117128

118129
impl Expression {

omnimcode-core/src/compiler.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ impl Compiler {
134134
})
135135
}
136136
Expression::Index { .. } => None,
137+
// H.5: `safe <expr>` evaluates to the same type as the inner
138+
// expression after self-healing dispatch. For Div the result is
139+
// int-or-float same as Div itself; for arr_get/arr_set the
140+
// result mirrors the wrapped call. Delegating to the inner
141+
// gives the right answer in every supported shape.
142+
Expression::Safe(inner) => self.infer_type(inner),
137143
}
138144
}
139145

@@ -405,6 +411,40 @@ impl Compiler {
405411
}
406412
self.emit(Op::Call(name.clone(), args.len()));
407413
}
414+
Expression::Safe(inner) => {
415+
// H.5 host-level: lower `safe <expr>` to the matching
416+
// ONN primitive call. The host primitives (safe_divide,
417+
// safe_arr_get, safe_arr_set) handle the fold-and-mod /
418+
// fold-escape logic at runtime. For shapes we don't have
419+
// a primitive for, just compile the inner directly.
420+
//
421+
// KNOWN GAP: Safe(arr_set(VAR, ...)) goes through Op::Call
422+
// which routes via the vm_call_builtin shim — the mutation
423+
// is lost when run through the Rust VM. Tree-walk works
424+
// fine because the interpreter pattern-matches Safe before
425+
// any shim. A future Op::SafeArrSetNamed would close this
426+
// gap (same shape as Op::ArrSetNamed in the existing VM).
427+
match inner.as_ref() {
428+
Expression::Div(l, r) => {
429+
self.compile_expr(l)?;
430+
self.compile_expr(r)?;
431+
self.emit(Op::Call("safe_divide".to_string(), 2));
432+
}
433+
Expression::Call { name, args } if name == "arr_get" && args.len() == 2 => {
434+
for arg in args {
435+
self.compile_expr(arg)?;
436+
}
437+
self.emit(Op::Call("safe_arr_get".to_string(), 2));
438+
}
439+
Expression::Call { name, args } if name == "arr_set" && args.len() == 3 => {
440+
for arg in args {
441+
self.compile_expr(arg)?;
442+
}
443+
self.emit(Op::Call("safe_arr_set".to_string(), 3));
444+
}
445+
_ => self.compile_expr(inner)?,
446+
}
447+
}
408448
}
409449
Ok(())
410450
}

omnimcode-core/src/interpreter.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,25 @@ impl Interpreter {
517517
_ => Ok(Value::HInt(HInt::new(0))),
518518
}
519519
}
520+
Expression::Safe(inner) => {
521+
// H.5: dispatch user-declared safe semantics by inner shape.
522+
// Known shapes route to the matching ONN primitive; everything
523+
// else is evaluated unwrapped (reserves the slot for future
524+
// runtime guards on more call patterns).
525+
match inner.as_ref() {
526+
Expression::Div(l, r) => {
527+
let args = vec![(**l).clone(), (**r).clone()];
528+
self.call_function("safe_divide", &args)
529+
}
530+
Expression::Call { name, args } if name == "arr_get" && args.len() == 2 => {
531+
self.call_function("safe_arr_get", args)
532+
}
533+
Expression::Call { name, args } if name == "arr_set" && args.len() == 3 => {
534+
self.call_function("safe_arr_set", args)
535+
}
536+
_ => self.eval_expr(inner),
537+
}
538+
}
520539
}
521540
}
522541

omnimcode-core/src/parser.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ pub enum Token {
2323
As,
2424
Res,
2525
Fold,
26-
26+
Safe, // H.5 host-level support: `safe <expr>` prefix
27+
2728
// Identifiers and literals
2829
Ident(String),
2930
Number(i64),
@@ -326,6 +327,7 @@ impl Lexer {
326327
"as" => Token::As,
327328
"res" => Token::Res,
328329
"fold" => Token::Fold,
330+
"safe" => Token::Safe,
329331
"and" => Token::And,
330332
"or" => Token::Or,
331333
"not" => Token::Not,
@@ -968,6 +970,16 @@ impl Parser {
968970
}
969971

970972
fn parse_expression(&mut self) -> Result<Expression, String> {
973+
// H.5: `safe <expr>` prefix wraps the rest of the expression in
974+
// self-healing semantics. The interpreter dispatches at eval time
975+
// based on the inner shape (Div → safe_divide, arr_get → safe_arr_get,
976+
// etc). Mirrors the OMC-written parser's behaviour in
977+
// examples/self_healing_h5.omc.
978+
if self.current() == Token::Safe {
979+
self.advance();
980+
let inner = self.parse_or()?;
981+
return Ok(Expression::Safe(Box::new(inner)));
982+
}
971983
self.parse_or()
972984
}
973985

0 commit comments

Comments
 (0)