Skip to content

Commit 007bc4e

Browse files
Phase 2: JSON parser+serializer in OMC (forcing-function demo)
A 220-line JSON subset parser written entirely in OMC. Validates the Phase 1 ergonomics push under real-program pressure: dict literals/get/set, string + concat, try/catch with descriptive parse errors, recursive descent via mutable-closure cursor (peek/advance/ eof state captured in shared env), HOFs (arr_map(parts, json_stringify) for the recursive serializer). Round-trips a representative config doc; catches malformed inputs with positional error messages. Identical output under tree-walk and VM. Once OMC has json_parse/json_stringify as user-space code, every future program can read/write structured config without adding more host-side builtins. Subset: objects, arrays, strings (with \n \t \" \\ \r escapes), numbers (int + decimal, no exponent), bools, null. Known limits: no scientific notation, no unicode escapes, no comments, no trailing commas. Real-world usage would extend the cursor and parse_value arms — the structure is straightforward to grow. Findings (worth lifting into language-level fixes later): * Long if/elif/else chains nest hard because OMC has no `else if` syntax — `else { if ... { } }` works but is ugly. * No string switch/match makes char-by-char dispatch verbose (is_digit could be a 1-line lookup if we had `match`). * Strings are byte-indexed via str_slice but the parser only ever touches ASCII so this isn't biting yet — would for non-ASCII input. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 9637c8b commit 007bc4e

1 file changed

Lines changed: 312 additions & 0 deletions

File tree

examples/json.omc

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
# =============================================================================
2+
# JSON parser and serializer — written in OMC
3+
# =============================================================================
4+
# Subset of RFC 8259 sufficient for config / interchange use:
5+
# - objects {"k": v, ...} → Value::Dict
6+
# - arrays [a, b, c] → Value::Array
7+
# - strings "..." with \n \t \" \\ \r escapes
8+
# - numbers (int, float; no exponent notation)
9+
# - booleans true / false
10+
# - null
11+
#
12+
# Not supported: scientific notation, unicode escapes, comments,
13+
# trailing commas. A real implementation would handle these — left as
14+
# known-limitations for the demo.
15+
#
16+
# The parser uses a mutable-closure cursor pattern: the lexer state
17+
# (source, position) is captured inside `make_cursor()` and exposed
18+
# via [peek, advance, eof, pos] closures. This is the same pattern
19+
# the log_analyzer counter table used; it's how OMC carries object-
20+
# oriented state without classes.
21+
#
22+
# Run:
23+
# ./target/release/omnimcode-standalone examples/json.omc
24+
# OMC_VM=1 ./target/release/omnimcode-standalone examples/json.omc
25+
# =============================================================================
26+
27+
# ---- Cursor: encapsulated lexer state via mutable closures ----------------
28+
29+
fn make_cursor(src) {
30+
h pos = 0;
31+
h len = str_len(src);
32+
h peek = fn() {
33+
if pos >= len { return ""; }
34+
return str_slice(src, pos, pos + 1);
35+
};
36+
h advance = fn() {
37+
h c = "";
38+
if pos < len {
39+
c = str_slice(src, pos, pos + 1);
40+
}
41+
pos = pos + 1;
42+
return c;
43+
};
44+
h eof = fn() {
45+
if pos >= len { return 1; }
46+
return 0;
47+
};
48+
h get_pos = fn() { return pos; };
49+
h slice_from = fn(start) { return str_slice(src, start, pos); };
50+
return [peek, advance, eof, get_pos, slice_from];
51+
}
52+
53+
# Cursor accessor sugar
54+
fn c_peek(c) { h f = arr_get(c, 0); return f(); }
55+
fn c_advance(c) { h f = arr_get(c, 1); return f(); }
56+
fn c_eof(c) { h f = arr_get(c, 2); return f(); }
57+
fn c_pos(c) { h f = arr_get(c, 3); return f(); }
58+
fn c_slice(c, start) { h f = arr_get(c, 4); return f(start); }
59+
60+
# ---- Skip whitespace -------------------------------------------------------
61+
62+
fn skip_ws(c) {
63+
while c_eof(c) == 0 {
64+
h ch = c_peek(c);
65+
if ch == " " { c_advance(c); }
66+
else { if ch == "\n" { c_advance(c); }
67+
else { if ch == "\t" { c_advance(c); }
68+
else { if ch == "\r" { c_advance(c); }
69+
else { return 0; } } } }
70+
}
71+
return 0;
72+
}
73+
74+
# ---- Parse helpers ---------------------------------------------------------
75+
76+
fn is_digit(ch) {
77+
if ch == "0" { return 1; }
78+
if ch == "1" { return 1; }
79+
if ch == "2" { return 1; }
80+
if ch == "3" { return 1; }
81+
if ch == "4" { return 1; }
82+
if ch == "5" { return 1; }
83+
if ch == "6" { return 1; }
84+
if ch == "7" { return 1; }
85+
if ch == "8" { return 1; }
86+
if ch == "9" { return 1; }
87+
return 0;
88+
}
89+
90+
fn parse_string(c) {
91+
h q = c_advance(c); # consume opening quote
92+
if q != "\"" {
93+
error(concat_many("expected string at pos ", c_pos(c)));
94+
}
95+
h out = "";
96+
while c_eof(c) == 0 {
97+
h ch = c_advance(c);
98+
if ch == "\"" { return out; }
99+
if ch == "\\" {
100+
h esc = c_advance(c);
101+
if esc == "n" { out = out + "\n"; }
102+
else { if esc == "t" { out = out + "\t"; }
103+
else { if esc == "r" { out = out + "\r"; }
104+
else { if esc == "\"" { out = out + "\""; }
105+
else { if esc == "\\" { out = out + "\\"; }
106+
else { error(concat_many("bad escape: \\", esc)); } } } } }
107+
}
108+
else {
109+
out = out + ch;
110+
}
111+
}
112+
error("unterminated string");
113+
return "";
114+
}
115+
116+
fn parse_number(c) {
117+
h start = c_pos(c);
118+
h is_float = 0;
119+
if c_peek(c) == "-" { c_advance(c); }
120+
while c_eof(c) == 0 {
121+
h ch = c_peek(c);
122+
if is_digit(ch) == 1 { c_advance(c); }
123+
else { if ch == "." { c_advance(c); is_float = 1; }
124+
else { return _finalize_number(c_slice(c, start), is_float); } }
125+
}
126+
return _finalize_number(c_slice(c, start), is_float);
127+
}
128+
129+
fn _finalize_number(s, is_float) {
130+
if is_float == 1 {
131+
return to_float(s);
132+
}
133+
return to_int(s);
134+
}
135+
136+
fn parse_literal(c, word, value) {
137+
h n = str_len(word);
138+
h start = c_pos(c);
139+
h i = 0;
140+
while i < n {
141+
if c_advance(c) != str_slice(word, i, i + 1) {
142+
error(concat_many("expected literal '", word, "' at pos ", start));
143+
}
144+
i = i + 1;
145+
}
146+
return value;
147+
}
148+
149+
# ---- Recursive descent ----------------------------------------------------
150+
151+
fn parse_value(c) {
152+
skip_ws(c);
153+
if c_eof(c) == 1 { error("unexpected end of input"); }
154+
h ch = c_peek(c);
155+
if ch == "{" { return parse_object(c); }
156+
if ch == "[" { return parse_array(c); }
157+
if ch == "\"" { return parse_string(c); }
158+
if ch == "t" { return parse_literal(c, "true", 1); }
159+
if ch == "f" { return parse_literal(c, "false", 0); }
160+
if ch == "n" { return parse_literal(c, "null", 0); } # OMC has no Null literal in expressions; encode as 0 sentinel for now
161+
if is_digit(ch) == 1 { return parse_number(c); }
162+
if ch == "-" { return parse_number(c); }
163+
error(concat_many("unexpected char '", ch, "' at pos ", c_pos(c)));
164+
return 0;
165+
}
166+
167+
fn parse_object(c) {
168+
c_advance(c); # consume {
169+
h out = {};
170+
skip_ws(c);
171+
if c_peek(c) == "}" { c_advance(c); return out; }
172+
while c_eof(c) == 0 {
173+
skip_ws(c);
174+
h key = parse_string(c);
175+
skip_ws(c);
176+
if c_advance(c) != ":" {
177+
error(concat_many("expected ':' after key at pos ", c_pos(c)));
178+
}
179+
skip_ws(c);
180+
h val = parse_value(c);
181+
dict_set(out, key, val);
182+
skip_ws(c);
183+
h next = c_advance(c);
184+
if next == "}" { return out; }
185+
if next != "," {
186+
error(concat_many("expected ',' or '}' in object at pos ", c_pos(c)));
187+
}
188+
}
189+
error("unterminated object");
190+
return out;
191+
}
192+
193+
fn parse_array(c) {
194+
c_advance(c); # consume [
195+
h out = [];
196+
skip_ws(c);
197+
if c_peek(c) == "]" { c_advance(c); return out; }
198+
while c_eof(c) == 0 {
199+
skip_ws(c);
200+
arr_push(out, parse_value(c));
201+
skip_ws(c);
202+
h next = c_advance(c);
203+
if next == "]" { return out; }
204+
if next != "," {
205+
error(concat_many("expected ',' or ']' in array at pos ", c_pos(c)));
206+
}
207+
}
208+
error("unterminated array");
209+
return out;
210+
}
211+
212+
fn json_parse(src) {
213+
h c = make_cursor(src);
214+
h v = parse_value(c);
215+
skip_ws(c);
216+
return v;
217+
}
218+
219+
# ---- Serializer -----------------------------------------------------------
220+
# Re-emits Values back to JSON. Strings get their special chars escaped.
221+
# Dicts and arrays are recursive. Numbers go raw via to_display_string
222+
# (already produces "42" not "HInt(42, ...)" thanks to Phase 1).
223+
224+
fn json_escape(s) {
225+
h out = "";
226+
h n = str_len(s);
227+
h i = 0;
228+
while i < n {
229+
h ch = str_slice(s, i, i + 1);
230+
if ch == "\"" { out = out + "\\\""; }
231+
else { if ch == "\\" { out = out + "\\\\"; }
232+
else { if ch == "\n" { out = out + "\\n"; }
233+
else { if ch == "\t" { out = out + "\\t"; }
234+
else { if ch == "\r" { out = out + "\\r"; }
235+
else { out = out + ch; } } } } }
236+
i = i + 1;
237+
}
238+
return out;
239+
}
240+
241+
fn json_stringify(v) {
242+
h t = type_of(v);
243+
if t == "string" { return concat_many("\"", json_escape(v), "\""); }
244+
if t == "int" { return concat_many("", v); }
245+
if t == "float" { return concat_many("", v); }
246+
if t == "bool" {
247+
if v == 1 { return "true"; }
248+
return "false";
249+
}
250+
if t == "null" { return "null"; }
251+
if t == "array" {
252+
h parts = arr_map(v, json_stringify);
253+
return concat_many("[", arr_join(parts, ","), "]");
254+
}
255+
if t == "dict" {
256+
h ks = dict_keys(v);
257+
h pairs = [];
258+
h i = 0;
259+
while i < arr_len(ks) {
260+
h k = arr_get(ks, i);
261+
h vv = dict_get(v, k);
262+
arr_push(pairs, concat_many("\"", json_escape(k), "\":", json_stringify(vv)));
263+
i = i + 1;
264+
}
265+
return concat_many("{", arr_join(pairs, ","), "}");
266+
}
267+
return "null";
268+
}
269+
270+
# ---- Demo ------------------------------------------------------------------
271+
272+
println("=== JSON parse + stringify demo ===========================");
273+
println("");
274+
275+
# Round-trip a config-shaped document
276+
h src = "{\"name\":\"omc\",\"version\":2,\"features\":[\"closures\",\"dicts\",\"try-catch\"],\"perf\":{\"vm_speedup\":2.4,\"hof_speedup\":2.22}}";
277+
278+
println(concat_many("input: ", src));
279+
280+
h parsed = json_parse(src);
281+
println(concat_many("parsed: ", parsed));
282+
283+
h restringified = json_stringify(parsed);
284+
println(concat_many("output: ", restringified));
285+
286+
println("");
287+
println("=== Pulling values out of the parsed structure =============");
288+
println(concat_many(" name: ", dict_get(parsed, "name")));
289+
println(concat_many(" version: ", dict_get(parsed, "version")));
290+
h feats = dict_get(parsed, "features");
291+
println(concat_many(" feature count: ", arr_len(feats)));
292+
println(concat_many(" first feature: ", arr_get(feats, 0)));
293+
h perf = dict_get(parsed, "perf");
294+
println(concat_many(" vm_speedup: ", dict_get(perf, "vm_speedup")));
295+
296+
println("");
297+
println("=== Error handling (try/catch) =============================");
298+
try {
299+
h bad = json_parse("{\"a\": 1, \"b\":}");
300+
println(concat_many("should not reach: ", bad));
301+
} catch err {
302+
println(concat_many("caught parse error: ", err));
303+
}
304+
305+
try {
306+
h bad = json_parse("{\"a\": tru}");
307+
} catch err {
308+
println(concat_many("caught: ", err));
309+
}
310+
311+
println("");
312+
println("=== Done ===================================================");

0 commit comments

Comments
 (0)