Skip to content

Commit 450d068

Browse files
fix: dict_del accepts any dict expression (not just variables); fix cache.omc inner-fn closures; add 13 cache lib tests
1 parent c63d154 commit 450d068

3 files changed

Lines changed: 156 additions & 25 deletions

File tree

examples/lib/cache.omc

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
# Returns a wrapper that caches results keyed by stringified args.
77
fn memoize(f) {
88
h store = {}
9-
fn memo_wrapper(arg) {
10-
h key = to_str(arg)
9+
h memo_wrapper = fn(arg) {
10+
h key = to_string(arg)
1111
if dict_has(store, key) {
1212
return store[key]
1313
}
@@ -22,8 +22,8 @@ fn memoize(f) {
2222
# Two-argument version.
2323
fn memoize2(f) {
2424
h store = {}
25-
fn memo2_wrapper(a, b) {
26-
h key = str_concat(to_str(a), "|", to_str(b))
25+
h memo2_wrapper = fn(a, b) {
26+
h key = str_concat(to_string(a), "|", to_string(b))
2727
if dict_has(store, key) {
2828
return store[key]
2929
}
@@ -72,7 +72,7 @@ fn _lru_touch(lru, key) {
7272

7373
# lru_get(lru, key) → value or null
7474
fn lru_get(lru, key) {
75-
h k = to_str(key)
75+
h k = to_string(key)
7676
if dict_has(lru["store"], k) {
7777
_lru_touch(lru, k)
7878
lru["hits"] = lru["hits"] + 1
@@ -84,8 +84,8 @@ fn lru_get(lru, key) {
8484

8585
# lru_put(lru, key, value)
8686
fn lru_put(lru, key, value) {
87-
h k = to_str(key)
88-
if not dict_has(lru["store"], k) {
87+
h k = to_string(key)
88+
if !dict_has(lru["store"], k) {
8989
_lru_evict(lru)
9090
arr_push(lru["order"], k)
9191
} else {
@@ -112,8 +112,8 @@ fn lru_stats(lru) {
112112
# LRU-backed memoized wrapper.
113113
fn lru_memoize(f, capacity) {
114114
h cache = lru_new(capacity)
115-
fn lru_wrapper(arg) {
116-
h key = to_str(arg)
115+
h lru_wrapper = fn(arg) {
116+
h key = to_string(arg)
117117
h cached = lru_get(cache, key)
118118
if cached != null { return cached }
119119
h result = f(arg)
@@ -132,8 +132,8 @@ fn ttl_new(ttl_seconds) {
132132

133133
# ttl_get(cache, key) → value or null (returns null if expired)
134134
fn ttl_get(cache, key) {
135-
h k = to_str(key)
136-
if not dict_has(cache["store"], k) {
135+
h k = to_string(key)
136+
if !dict_has(cache["store"], k) {
137137
cache["misses"] = cache["misses"] + 1
138138
return null
139139
}
@@ -150,7 +150,7 @@ fn ttl_get(cache, key) {
150150

151151
# ttl_put(cache, key, value)
152152
fn ttl_put(cache, key, value) {
153-
h k = to_str(key)
153+
h k = to_string(key)
154154
h now = unix_time()
155155
cache["store"][k] = value
156156
cache["expires"][k] = now + cache["ttl"]
@@ -159,8 +159,8 @@ fn ttl_put(cache, key, value) {
159159
# ttl_memoize(fn, ttl_seconds) → fn
160160
fn ttl_memoize(f, ttl) {
161161
h cache = ttl_new(ttl)
162-
fn ttl_wrapper(arg) {
163-
h key = to_str(arg)
162+
h ttl_wrapper = fn(arg) {
163+
h key = to_string(arg)
164164
h cached = ttl_get(cache, key)
165165
if cached != null { return cached }
166166
h result = f(arg)
@@ -174,25 +174,24 @@ fn ttl_memoize(f, ttl) {
174174

175175
# disk_cache_get(dir, key) → string or null
176176
fn disk_cache_get(dir, key) {
177-
h safe_key = str_replace(to_str(key), "/", "_")
177+
h safe_key = str_replace(to_string(key), "/", "_")
178178
h path = str_concat(dir, "/", safe_key, ".cache")
179179
h content = file_read(path)
180-
if content == null { return null }
181180
return content
182181
}
183182

184183
# disk_cache_put(dir, key, value)
185184
fn disk_cache_put(dir, key, value) {
186-
h safe_key = str_replace(to_str(key), "/", "_")
185+
h safe_key = str_replace(to_string(key), "/", "_")
187186
h path = str_concat(dir, "/", safe_key, ".cache")
188-
file_write(path, to_str(value))
187+
write_file(path, to_string(value))
189188
}
190189

191190
# disk_memoize(fn, dir) → fn
192191
# Persists results to disk; survives process restarts.
193192
fn disk_memoize(f, dir) {
194-
fn disk_wrapper(arg) {
195-
h key = to_str(arg)
193+
h disk_wrapper = fn(arg) {
194+
h key = to_string(arg)
196195
h cached = disk_cache_get(dir, key)
197196
if cached != null { return cached }
198197
h result = f(arg)

examples/tests/test_cache_lib.omc

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Tests for examples/lib/cache.omc
2+
# Uses module import to preserve internal helper functions.
3+
4+
import "examples/lib/cache.omc" as cache
5+
6+
fn assert_eq(actual, expected, msg) {
7+
if actual != expected {
8+
test_record_failure(msg + ": expected " + to_string(expected) + " got " + to_string(actual));
9+
}
10+
}
11+
12+
fn assert_true(cond, msg) { if !cond { test_record_failure(msg); } }
13+
fn assert_null(v, msg) { if v != null { test_record_failure(msg + ": expected null, got " + to_string(v)); } }
14+
15+
# ── memoize ──────────────────────────────────────────────────────────────────
16+
17+
h _call_count = 0
18+
fn _expensive(n) {
19+
_call_count = _call_count + 1
20+
return n * n
21+
}
22+
23+
fn test_memoize_computes_correctly() {
24+
h m = cache.memoize(_expensive)
25+
assert_eq(m(5), 25, "memoize 5^2")
26+
assert_eq(m(7), 49, "memoize 7^2")
27+
}
28+
29+
fn test_memoize_caches_second_call() {
30+
_call_count = 0
31+
h m = cache.memoize(_expensive)
32+
m(4)
33+
m(4)
34+
m(4)
35+
assert_eq(_call_count, 1, "fn called only once for same arg")
36+
}
37+
38+
fn test_memoize_different_args_not_cached() {
39+
_call_count = 0
40+
h m = cache.memoize(_expensive)
41+
m(1)
42+
m(2)
43+
m(3)
44+
assert_eq(_call_count, 3, "distinct args → 3 calls")
45+
}
46+
47+
fn test_memoize2_computes_correctly() {
48+
fn _add(a, b) { return a + b }
49+
h m = cache.memoize2(_add)
50+
assert_eq(m(3, 4), 7, "memoize2 3+4")
51+
assert_eq(m(10, 20), 30, "memoize2 10+20")
52+
}
53+
54+
# ── LRU cache ─────────────────────────────────────────────────────────────────
55+
56+
fn test_lru_basic_put_get() {
57+
h lru = cache.lru_new(10)
58+
cache.lru_put(lru, "x", 42)
59+
assert_eq(cache.lru_get(lru, "x"), 42, "get after put")
60+
}
61+
62+
fn test_lru_miss_returns_null() {
63+
h lru = cache.lru_new(10)
64+
assert_null(cache.lru_get(lru, "no_such_key"), "miss → null")
65+
}
66+
67+
fn test_lru_evicts_when_full() {
68+
h lru = cache.lru_new(3)
69+
cache.lru_put(lru, "a", 1)
70+
cache.lru_put(lru, "b", 2)
71+
cache.lru_put(lru, "c", 3)
72+
cache.lru_put(lru, "d", 4)
73+
# oldest ("a") should be evicted (or "b" if "a" was touched)
74+
h s = cache.lru_stats(lru)
75+
assert_eq(s["size"], 3, "size stays at capacity")
76+
}
77+
78+
fn test_lru_stats_hits_misses() {
79+
h lru = cache.lru_new(10)
80+
cache.lru_put(lru, "k", 99)
81+
cache.lru_get(lru, "k")
82+
cache.lru_get(lru, "k")
83+
cache.lru_get(lru, "miss")
84+
h s = cache.lru_stats(lru)
85+
assert_eq(s["hits"], 2, "2 hits")
86+
assert_eq(s["misses"], 1, "1 miss")
87+
}
88+
89+
fn test_lru_memoize() {
90+
h count = 0
91+
fn _fn(n) { count = count + 1 return n * 3 }
92+
h m = cache.lru_memoize(_fn, 5)
93+
assert_eq(m(2), 6, "lru_memoize computes")
94+
m(2)
95+
m(2)
96+
assert_eq(count, 1, "subsequent calls cached")
97+
}
98+
99+
# ── TTL cache ─────────────────────────────────────────────────────────────────
100+
101+
fn test_ttl_basic() {
102+
h ttl = cache.ttl_new(3600)
103+
cache.ttl_put(ttl, "k", "hello")
104+
assert_eq(cache.ttl_get(ttl, "k"), "hello", "ttl get unexpired")
105+
}
106+
107+
fn test_ttl_miss() {
108+
h ttl = cache.ttl_new(3600)
109+
assert_null(cache.ttl_get(ttl, "nope"), "ttl miss → null")
110+
}
111+
112+
# ── batch_cached ──────────────────────────────────────────────────────────────
113+
114+
fn test_batch_cached_results() {
115+
fn _triple(n) { return n * 3 }
116+
h bc = cache.lru_new(20)
117+
h out = cache.batch_cached(_triple, [1, 2, 3], bc)
118+
assert_eq(out[0], 3, "batch[0]")
119+
assert_eq(out[1], 6, "batch[1]")
120+
assert_eq(out[2], 9, "batch[2]")
121+
}
122+
123+
fn test_batch_cached_deduplicates() {
124+
h fn_calls = 0
125+
fn _inc(n) { fn_calls = fn_calls + 1 return n }
126+
h bc = cache.lru_new(20)
127+
cache.batch_cached(_inc, [1, 2, 1, 2, 1], bc)
128+
assert_eq(fn_calls, 2, "only 2 unique keys computed")
129+
}

omnimcode-core/src/interpreter.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4804,16 +4804,19 @@ impl Interpreter {
48044804
}
48054805
"dict_del" => {
48064806
if args.len() < 2 {
4807-
return Err("dict_del requires (dict_var, key)".to_string());
4807+
return Err("dict_del requires (dict, key)".to_string());
48084808
}
48094809
let k = self.eval_expr(&args[1])?.to_display_string();
4810-
if let Expression::Variable(name) = &args[0] {
4811-
if let Some(Value::Dict(d)) = self.get_var(name) {
4810+
// Accept both a plain variable AND any expression that evaluates
4811+
// to a dict (e.g. obj["store"]). The Rc is shared, so removal
4812+
// through the evaluated reference propagates to all holders.
4813+
match self.eval_expr(&args[0])? {
4814+
Value::Dict(d) => {
48124815
d.borrow_mut().remove(&k);
4813-
return Ok(Value::Null);
4816+
Ok(Value::Null)
48144817
}
4818+
_ => Err("dict_del: first argument must be a dict".to_string()),
48154819
}
4816-
Err("dict_del: first argument must be a dict variable".to_string())
48174820
}
48184821
"dict_keys" => {
48194822
if args.is_empty() {

0 commit comments

Comments
 (0)