Skip to content

Commit 6c27f2a

Browse files
Parser: bare-ident dict keys, optional semicolons, chained index, runtime fixes
- parse_dict: `{foo: val}` now treats bare ident keys as string literals (JS-style), fixing "Undefined variable: store" in lib/cache.omc - eat_semi(): optional semicolons everywhere in statement position (17 sites) - ChainedIndex / ChainedIndexAssignment AST variants for `a[b][c]` read/write - IndexAssignment: handle both array (int) and dict (string) targets - arr_push: accept any expr evaluating to array, not just bare Variable (enables arr_push(dict["key"], v)) - to_str: alias for to_string builtin (used throughout stdlib cache.omc) - wrapping_add/sub/mul: prevent debug-mode overflow panics in arithmetic ops - All changes propagated to canonical.rs, compiler.rs, formatter.rs, code_intel.rs, omnimcode-js - test_substrate_embed_builtins.omc: 15/15 passing; full suite 1126/1126 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7d2b4b2 commit 6c27f2a

9 files changed

Lines changed: 374 additions & 50 deletions

File tree

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Tests for substrate_embed, str_similarity, sleep, file_ls, omc_eval_file
2+
# Run with: omc --test examples/tests/test_substrate_embed_builtins.omc
3+
4+
fn assert_eq(actual, expected, msg) {
5+
if actual != expected {
6+
test_record_failure(str_concat(msg, ": expected ", to_string(expected), " got ", to_string(actual)));
7+
}
8+
}
9+
10+
fn assert_true(cond, msg) {
11+
if !cond { test_record_failure(msg); }
12+
}
13+
14+
fn assert_false(cond, msg) {
15+
if cond { test_record_failure(msg); }
16+
}
17+
18+
fn assert_near(a, b, tol, msg) {
19+
h diff = a - b
20+
if diff < 0.0 { diff = 0.0 - diff }
21+
if diff > tol { test_record_failure(str_concat(msg, ": |", to_string(a), " - ", to_string(b), "| > ", to_string(tol))); }
22+
}
23+
24+
# ── substrate_embed ───────────────────────────────────────────────────────────
25+
26+
fn test_substrate_embed_returns_array() {
27+
h v = substrate_embed("hello world", 16)
28+
assert_eq(type_of(v), "array", "substrate_embed returns array")
29+
}
30+
31+
fn test_substrate_embed_correct_dims() {
32+
h v = substrate_embed("test", 8)
33+
assert_eq(arr_len(v), 8, "substrate_embed respects dims arg")
34+
}
35+
36+
fn test_substrate_embed_normalized() {
37+
h v = substrate_embed("normalize me", 16)
38+
h dot = 0.0
39+
h i = 0
40+
while i < arr_len(v) {
41+
h x = v[i]
42+
dot = dot + x * x
43+
i = i + 1
44+
}
45+
assert_near(dot, 1.0, 0.01, "substrate_embed is L2-normalized")
46+
}
47+
48+
fn test_substrate_embed_deterministic() {
49+
h a = substrate_embed("hello", 8)
50+
h b = substrate_embed("hello", 8)
51+
h match_all = true
52+
h i = 0
53+
while i < arr_len(a) {
54+
if a[i] != b[i] { match_all = false }
55+
i = i + 1
56+
}
57+
assert_true(match_all, "substrate_embed is deterministic")
58+
}
59+
60+
fn test_substrate_embed_differs_for_different_texts() {
61+
h a = substrate_embed("apple", 8)
62+
h b = substrate_embed("orange", 8)
63+
h same = true
64+
h i = 0
65+
while i < arr_len(a) {
66+
if a[i] != b[i] { same = false }
67+
i = i + 1
68+
}
69+
assert_false(same, "substrate_embed differs for different texts")
70+
}
71+
72+
# ── str_similarity ─────────────────────────────────────────────────────────────
73+
74+
fn test_str_similarity_self_is_one() {
75+
h sim = str_similarity("hello world", "hello world")
76+
assert_near(sim, 1.0, 0.01, "str_similarity of identical strings is ~1.0")
77+
}
78+
79+
fn test_str_similarity_in_range() {
80+
h sim = str_similarity("apple", "orange")
81+
assert_true(sim >= -1.0, "str_similarity >= -1")
82+
assert_true(sim <= 1.0, "str_similarity <= 1")
83+
}
84+
85+
fn test_str_similarity_symmetric() {
86+
h s1 = str_similarity("hello world", "world hello")
87+
h s2 = str_similarity("world hello", "hello world")
88+
assert_near(s1, s2, 0.001, "str_similarity is symmetric")
89+
}
90+
91+
# ── sleep ─────────────────────────────────────────────────────────────────────
92+
93+
fn test_sleep_returns_null() {
94+
h result = sleep(0)
95+
assert_eq(result, null, "sleep(0) returns null")
96+
}
97+
98+
fn test_sleep_short_delay() {
99+
h t0 = now_ms()
100+
sleep(50)
101+
h t1 = now_ms()
102+
h elapsed = t1 - t0
103+
assert_true(elapsed >= 40, "sleep(50) waits at least ~40ms")
104+
}
105+
106+
# ── file_ls ───────────────────────────────────────────────────────────────────
107+
108+
fn test_file_ls_returns_array() {
109+
h files = file_ls("examples/tests")
110+
assert_eq(type_of(files), "array", "file_ls returns array")
111+
}
112+
113+
fn test_file_ls_nonempty() {
114+
h files = file_ls("examples/tests")
115+
assert_true(arr_len(files) > 0, "file_ls finds files in examples/tests")
116+
}
117+
118+
fn test_file_ls_contains_known_file() {
119+
h files = file_ls("examples/tests")
120+
h found = false
121+
h i = 0
122+
while i < arr_len(files) {
123+
if files[i] == "test_substrate_embed_builtins.omc" { found = true }
124+
i = i + 1
125+
}
126+
assert_true(found, "file_ls finds this test file itself")
127+
}
128+
129+
fn test_file_ls_sorted() {
130+
h files = file_ls("examples/tests")
131+
h sorted = true
132+
h i = 1
133+
while i < arr_len(files) {
134+
if files[i] < files[i - 1] { sorted = false }
135+
i = i + 1
136+
}
137+
assert_true(sorted, "file_ls returns sorted results")
138+
}
139+
140+
# ── omc_eval_file ─────────────────────────────────────────────────────────────
141+
142+
fn test_omc_eval_file_loads_functions() {
143+
omc_eval_file("examples/lib/cache.omc")
144+
h cache = lru_new(10)
145+
lru_put(cache, "key", "value")
146+
h got = lru_get(cache, "key")
147+
assert_eq(got, "value", "omc_eval_file loads lib file and makes functions available")
148+
}

omnimcode-core/src/ast.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ pub enum Statement {
4949
index: Expression,
5050
value: Expression,
5151
},
52+
// a[key1][key2] = value — covers dict[key][subkey] and arr[i][field]
53+
ChainedIndexAssignment {
54+
name: String,
55+
first_index: Expression,
56+
second_index: Expression,
57+
value: Expression,
58+
},
5259
If {
5360
condition: Expression,
5461
then_body: Vec<Statement>,
@@ -217,6 +224,10 @@ pub enum Expression {
217224
name: String,
218225
index: Box<Expression>,
219226
},
227+
ChainedIndex {
228+
object: Box<Expression>,
229+
index: Box<Expression>,
230+
},
220231

221232
// Binary operations
222233
Add(Box<Expression>, Box<Expression>),

omnimcode-core/src/canonical.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@ fn rename_stmt(stmt: &Statement, scope: &mut Scope) -> Statement {
179179
value: new_value,
180180
}
181181
}
182+
Statement::ChainedIndexAssignment { name, first_index, second_index, value } => {
183+
Statement::ChainedIndexAssignment {
184+
name: scope.resolve(name),
185+
first_index: rename_expr(first_index, scope),
186+
second_index: rename_expr(second_index, scope),
187+
value: rename_expr(value, scope),
188+
}
189+
}
182190
Statement::If { condition, then_body, elif_parts, else_body } => {
183191
let new_cond = rename_expr(condition, scope);
184192
// Each branch gets its own scope so a var declared in one
@@ -327,6 +335,10 @@ fn rename_expr(expr: &Expression, scope: &Scope) -> Expression {
327335
name: scope.resolve(name),
328336
index: Box::new(rename_expr(index, scope)),
329337
},
338+
Expression::ChainedIndex { object, index } => Expression::ChainedIndex {
339+
object: Box::new(rename_expr(object, scope)),
340+
index: Box::new(rename_expr(index, scope)),
341+
},
330342

331343
Expression::Array(items) => Expression::Array(
332344
items.iter().map(|e| rename_expr(e, scope)).collect(),

omnimcode-core/src/code_intel.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ fn collect_expr_calls(e: &Expression, out: &mut BTreeSet<String>) {
188188
Expression::Array(items) => for i in items { collect_expr_calls(i, out); }
189189
Expression::Dict(pairs) => for (k, v) in pairs { collect_expr_calls(k, out); collect_expr_calls(v, out); }
190190
Expression::Index { index, .. } => collect_expr_calls(index, out),
191+
Expression::ChainedIndex { object, index } => { collect_expr_calls(object, out); collect_expr_calls(index, out); }
191192
Expression::Add(a, b) | Expression::Sub(a, b) | Expression::Mul(a, b) | Expression::Div(a, b) | Expression::Mod(a, b)
192193
| Expression::Eq(a, b) | Expression::Ne(a, b) | Expression::Lt(a, b) | Expression::Le(a, b) | Expression::Gt(a, b) | Expression::Ge(a, b)
193194
| Expression::And(a, b) | Expression::Or(a, b)
@@ -290,6 +291,7 @@ pub fn ast_size(source: &str) -> Result<i64, String> {
290291
Expression::Array(items) => for i in items { walk_e(i, count); }
291292
Expression::Dict(pairs) => for (k, v) in pairs { walk_e(k, count); walk_e(v, count); }
292293
Expression::Index { index, .. } => walk_e(index, count),
294+
Expression::ChainedIndex { object, index } => { walk_e(object, count); walk_e(index, count); }
293295
Expression::Add(a, b) | Expression::Sub(a, b) | Expression::Mul(a, b) | Expression::Div(a, b) | Expression::Mod(a, b)
294296
| Expression::Eq(a, b) | Expression::Ne(a, b) | Expression::Lt(a, b) | Expression::Le(a, b) | Expression::Gt(a, b) | Expression::Ge(a, b)
295297
| Expression::And(a, b) | Expression::Or(a, b)

omnimcode-core/src/compiler.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ impl Compiler {
318318
}
319319
})
320320
}
321-
Expression::Index { .. } => None,
321+
Expression::Index { .. } | Expression::ChainedIndex { .. } => None,
322322
// H.5: `safe <expr>` evaluates to the same type as the inner
323323
// expression after self-healing dispatch. For Div the result is
324324
// int-or-float same as Div itself; for arr_get/arr_set the
@@ -388,6 +388,11 @@ impl Compiler {
388388
self.compile_expr(index)?;
389389
self.emit(Op::ArrayIndex);
390390
}
391+
Expression::ChainedIndex { object, index } => {
392+
self.compile_expr(object)?;
393+
self.compile_expr(index)?;
394+
self.emit(Op::ArrayIndex);
395+
}
391396
Expression::Array(items) => {
392397
for item in items {
393398
self.compile_expr(item)?;
@@ -826,6 +831,10 @@ impl Compiler {
826831
self.compile_expr(index)?;
827832
self.emit(Op::ArrayIndexAssign(name.clone()));
828833
}
834+
Statement::ChainedIndexAssignment { .. } => {
835+
// Fall through to interpreter for chained index assignment.
836+
return Err("ChainedIndexAssignment: not supported in bytecode VM, use tree-walk".to_string());
837+
}
829838
Statement::If {
830839
condition,
831840
then_body,

omnimcode-core/src/formatter.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ fn format_stmt(stmt: &Statement, level: usize, out: &mut String) {
7070
format_expr(value, out);
7171
out.push_str(";\n");
7272
}
73+
Statement::ChainedIndexAssignment { name, first_index, second_index, value } => {
74+
out.push_str(name);
75+
out.push('[');
76+
format_expr(first_index, out);
77+
out.push_str("][");
78+
format_expr(second_index, out);
79+
out.push_str("] = ");
80+
format_expr(value, out);
81+
out.push_str(";\n");
82+
}
7383
Statement::If { condition, then_body, elif_parts, else_body } => {
7484
out.push_str("if ");
7585
format_expr(condition, out);
@@ -300,6 +310,12 @@ fn format_expr(expr: &Expression, out: &mut String) {
300310
format_expr(index, out);
301311
out.push(']');
302312
}
313+
Expression::ChainedIndex { object, index } => {
314+
format_expr(object, out);
315+
out.push('[');
316+
format_expr(index, out);
317+
out.push(']');
318+
}
303319
Expression::Array(items) => {
304320
out.push('[');
305321
for (i, e) in items.iter().enumerate() {

0 commit comments

Comments
 (0)