Skip to content

Commit fc118a4

Browse files
JSON parse + stringify: json_parse / json_stringify via serde_json
Fourth piece of the Python-ergonomics catch-up. Wraps serde_json (new workspace dep) behind two builtins covering the common Python json module surface: json_parse(text) -> Value JSON null -> Value::Null JSON bool -> Value::Bool JSON number -> HInt (when representable as i64) or HFloat JSON string -> Value::String JSON array -> Value::Array JSON object -> Value::Dict (BTreeMap; keys lexicographically ordered) Recursive; throws on parse error. json_stringify(value, pretty?=false) -> string Inverse mapping. Pretty=1 enables 2-space indented multi-line output (matches Python json.dumps(indent=2)). Singularity / Function / Circuit values stringify to their display form (lossy by design — no clean JSON representation). NaN / Inf coerce to JSON null (JSON spec doesn't allow them). Round-trip invariant: for any pure-data Value (no Function, no Singularity), json_parse(json_stringify(v)) yields a structurally equal Value. Numeric exactness is preserved within i64/f64 range. Helpers added at module scope in interpreter.rs: - json_to_value(serde_json::Value) -> Value - value_to_json(&Value) -> serde_json::Value Both pub(crate) so other interpreter sites (e.g. future Python- JSON bridge work via py_embed) can reuse them. Tests (examples/tests/test_json.omc — 17 tests, all pass): - Parse: int, float, string, bool, null, array (incl. empty, mixed types), object, nested object-in-array-in-object - Stringify: int, string (with quotes), array (compact), dict (alphabetically ordered keys) - Round-trip: flat array, deeply nested object with mixed types - Pretty-printing: indent=1 produces multi-line output Cargo.toml: serde_json = \"1.0\" (workspace dep) added to omnimcode-core. Regression: 8 exception + 10 f-string + 10 regex + 57 substrate + 70 builtins + 18 harmonic libs + 16 heal + 17 new JSON = 206 OMC tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 0fbaf4d commit fc118a4

4 files changed

Lines changed: 228 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/tests/test_json.omc

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Tests for json_parse / json_stringify.
2+
#
3+
# Round-trip invariants: parse(stringify(v)) == v for any pure-data
4+
# Value (HInt, HFloat, String, Bool, Null, Array, Dict). Functions
5+
# and Singularity values stringify to their display form (lossy).
6+
7+
fn assert_eq(actual, expected, msg) {
8+
if actual != expected {
9+
test_record_failure(msg + ": expected " + to_string(expected) + " got " + to_string(actual));
10+
}
11+
}
12+
13+
fn assert_true(cond, msg) {
14+
if !cond { test_record_failure(msg); }
15+
}
16+
17+
# ---- Primitive parsing ----
18+
19+
fn test_parse_int() {
20+
h v = json_parse("42");
21+
assert_eq(v, 42, "int parses to HInt");
22+
}
23+
24+
fn test_parse_float() {
25+
h v = json_parse("3.14");
26+
assert_true(v > 3.1, "float lower bound");
27+
assert_true(v < 3.2, "float upper bound");
28+
}
29+
30+
fn test_parse_string() {
31+
h v = json_parse("\"hello\"");
32+
assert_eq(v, "hello", "string parses");
33+
}
34+
35+
fn test_parse_bool() {
36+
assert_eq(json_parse("true"), true, "true parses");
37+
assert_eq(json_parse("false"), false, "false parses");
38+
}
39+
40+
fn test_parse_null() {
41+
h v = json_parse("null");
42+
assert_eq(v, null, "null parses");
43+
}
44+
45+
# ---- Arrays ----
46+
47+
fn test_parse_array() {
48+
h v = json_parse("[1, 2, 3]");
49+
assert_eq(arr_len(v), 3, "3-elem array");
50+
assert_eq(arr_get(v, 0), 1, "first");
51+
assert_eq(arr_get(v, 2), 3, "last");
52+
}
53+
54+
fn test_parse_empty_array() {
55+
h v = json_parse("[]");
56+
assert_eq(arr_len(v), 0, "empty array");
57+
}
58+
59+
fn test_parse_mixed_array() {
60+
h v = json_parse("[1, \"two\", 3.5, true, null]");
61+
assert_eq(arr_len(v), 5, "5 mixed types");
62+
assert_eq(arr_get(v, 0), 1, "int");
63+
assert_eq(arr_get(v, 1), "two", "string");
64+
assert_eq(arr_get(v, 3), true, "bool");
65+
}
66+
67+
# ---- Objects (dicts) ----
68+
69+
fn test_parse_object() {
70+
h v = json_parse("{\"name\": \"alice\", \"age\": 30}");
71+
assert_eq(dict_get(v, "name"), "alice", "name field");
72+
assert_eq(dict_get(v, "age"), 30, "age field");
73+
}
74+
75+
fn test_parse_nested() {
76+
h v = json_parse("{\"a\": [1, 2, {\"b\": 3}]}");
77+
h a = dict_get(v, "a");
78+
assert_eq(arr_len(a), 3, "outer array");
79+
h inner = arr_get(a, 2);
80+
assert_eq(dict_get(inner, "b"), 3, "nested b");
81+
}
82+
83+
# ---- Stringify ----
84+
85+
fn test_stringify_int() {
86+
assert_eq(json_stringify(42), "42", "int stringifies");
87+
}
88+
89+
fn test_stringify_string() {
90+
assert_eq(json_stringify("hello"), "\"hello\"", "string with quotes");
91+
}
92+
93+
fn test_stringify_array() {
94+
h s = json_stringify([1, 2, 3]);
95+
assert_eq(s, "[1,2,3]", "array stringifies (no spaces)");
96+
}
97+
98+
fn test_stringify_dict() {
99+
h d = dict_new();
100+
dict_set(d, "x", 1);
101+
dict_set(d, "y", 2);
102+
# BTreeMap orders alphabetically — output is deterministic.
103+
h s = json_stringify(d);
104+
assert_eq(s, "{\"x\":1,\"y\":2}", "dict stringifies sorted");
105+
}
106+
107+
# ---- Round-trip ----
108+
109+
fn test_roundtrip_array() {
110+
h v = [10, 20, 30];
111+
h back = json_parse(json_stringify(v));
112+
assert_eq(arr_len(back), 3, "len preserved");
113+
assert_eq(arr_get(back, 1), 20, "values preserved");
114+
}
115+
116+
fn test_roundtrip_nested() {
117+
h d = dict_new();
118+
dict_set(d, "users", ["alice", "bob"]);
119+
dict_set(d, "count", 2);
120+
h s = json_stringify(d);
121+
h back = json_parse(s);
122+
assert_eq(dict_get(back, "count"), 2, "count round-tripped");
123+
h users = dict_get(back, "users");
124+
assert_eq(arr_get(users, 1), "bob", "nested array round-tripped");
125+
}
126+
127+
# ---- Pretty-printing ----
128+
129+
fn test_pretty_print() {
130+
h s = json_stringify([1, 2, 3], 1);
131+
# Pretty output contains newlines.
132+
assert_true(str_contains(s, "\n"), "pretty output has newlines");
133+
}

omnimcode-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ path = "src/lib.rs"
1515
[dependencies]
1616
regex = "1.10"
1717
thiserror = "1.0"
18+
serde_json = "1.0"
1819
# Embedded CPython — required for the desktop standalone binary
1920
# (always-on py_import/py_call/etc.), optional for downstream
2021
# crates that target WASM or no_std where libpython can't link.

omnimcode-core/src/interpreter.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1915,6 +1915,7 @@ impl Interpreter {
19151915
| "str_split_lines" | "str_count" | "str_is_empty"
19161916
| "str_to_int" | "str_to_float" | "str_capitalize"
19171917
| "re_match" | "re_find" | "re_find_all" | "re_replace" | "re_split"
1918+
| "json_parse" | "json_stringify"
19181919
// Arrays
19191920
| "arr_new" | "arr_from_range" | "arr_len" | "arr_get" | "arr_set"
19201921
| "arr_push" | "arr_first" | "arr_last" | "arr_slice" | "arr_concat"
@@ -3031,6 +3032,40 @@ impl Interpreter {
30313032
// Defaults to comma separator, no header skip. Pass an explicit
30323033
// separator to handle TSV (sep="\t"), pipe-delim, etc. Pass
30333034
// skip_header=1 to drop the first line.
3035+
// ---- JSON (via serde_json) -----------------------------
3036+
"json_parse" => {
3037+
// json_parse(text) -> Value (dict, array, string, int,
3038+
// float, bool, or Null). Throws on parse error.
3039+
if args.is_empty() {
3040+
return Err("json_parse requires (text)".to_string());
3041+
}
3042+
let text = self.eval_expr(&args[0])?.to_display_string();
3043+
match serde_json::from_str::<serde_json::Value>(&text) {
3044+
Ok(v) => Ok(json_to_value(v)),
3045+
Err(e) => Err(format!("json_parse: {}", e)),
3046+
}
3047+
}
3048+
"json_stringify" => {
3049+
// json_stringify(value) -> string. Pretty-prints if a
3050+
// second arg is truthy (matches Python json.dumps(indent=2)).
3051+
if args.is_empty() {
3052+
return Err("json_stringify requires (value, pretty?)".to_string());
3053+
}
3054+
let v = self.eval_expr(&args[0])?;
3055+
let jv = value_to_json(&v);
3056+
let pretty = if args.len() >= 2 {
3057+
self.eval_expr(&args[1])?.to_int() != 0
3058+
} else { false };
3059+
let s = if pretty {
3060+
serde_json::to_string_pretty(&jv)
3061+
} else {
3062+
serde_json::to_string(&jv)
3063+
};
3064+
match s {
3065+
Ok(out) => Ok(Value::String(out)),
3066+
Err(e) => Err(format!("json_stringify: {}", e)),
3067+
}
3068+
}
30343069
"csv_parse" => {
30353070
if args.is_empty() {
30363071
return Err("csv_parse requires (text, sep?, skip_header?)".to_string());
@@ -7495,6 +7530,63 @@ fn values_equal(a: &Value, b: &Value) -> bool {
74957530

74967531
// Free function reused by quantize / quantization_ratio / mean_omni_weight.
74977532
// Snap |n| to the nearest Fibonacci attractor, preserving sign.
7533+
/// Convert a `serde_json::Value` into an OMC `Value`. JSON object →
7534+
/// `Value::Dict`, JSON array → `Value::Array`, numbers split into
7535+
/// `HInt` (when representable as i64) vs `HFloat` (everything else).
7536+
pub(crate) fn json_to_value(j: serde_json::Value) -> Value {
7537+
match j {
7538+
serde_json::Value::Null => Value::Null,
7539+
serde_json::Value::Bool(b) => Value::Bool(b),
7540+
serde_json::Value::Number(n) => {
7541+
if let Some(i) = n.as_i64() { Value::HInt(HInt::new(i)) }
7542+
else if let Some(f) = n.as_f64() { Value::HFloat(f) }
7543+
else { Value::HInt(HInt::new(0)) }
7544+
}
7545+
serde_json::Value::String(s) => Value::String(s),
7546+
serde_json::Value::Array(arr) => {
7547+
let items: Vec<Value> = arr.into_iter().map(json_to_value).collect();
7548+
Value::Array(HArray::from_vec(items))
7549+
}
7550+
serde_json::Value::Object(map) => {
7551+
let mut out = std::collections::BTreeMap::new();
7552+
for (k, v) in map {
7553+
out.insert(k, json_to_value(v));
7554+
}
7555+
Value::dict_from(out)
7556+
}
7557+
}
7558+
}
7559+
7560+
/// Convert an OMC `Value` back into a `serde_json::Value` for
7561+
/// stringification. Singularity and Function values stringify to
7562+
/// their display form (no clean JSON representation).
7563+
pub(crate) fn value_to_json(v: &Value) -> serde_json::Value {
7564+
match v {
7565+
Value::Null => serde_json::Value::Null,
7566+
Value::Bool(b) => serde_json::Value::Bool(*b),
7567+
Value::HInt(h) => serde_json::json!(h.value),
7568+
Value::HFloat(f) => {
7569+
// NaN / Inf can't be represented in JSON — coerce to null.
7570+
if f.is_finite() { serde_json::json!(*f) } else { serde_json::Value::Null }
7571+
}
7572+
Value::String(s) => serde_json::Value::String(s.clone()),
7573+
Value::Array(arr) => {
7574+
let items: Vec<serde_json::Value> = arr.items.borrow().iter()
7575+
.map(value_to_json).collect();
7576+
serde_json::Value::Array(items)
7577+
}
7578+
Value::Dict(d) => {
7579+
let mut map = serde_json::Map::new();
7580+
for (k, vv) in d.borrow().iter() {
7581+
map.insert(k.clone(), value_to_json(vv));
7582+
}
7583+
serde_json::Value::Object(map)
7584+
}
7585+
// Singularity / Function / Circuit: fall back to display string.
7586+
other => serde_json::Value::String(other.to_display_string()),
7587+
}
7588+
}
7589+
74987590
pub(crate) fn fold_to_fibonacci_const(n: i64) -> i64 {
74997591
// Substrate-routed via phi_pi_fib::fold_to_nearest_attractor.
75007592
// Was: a 15-element local Fibonacci array + linear scan.
@@ -7855,6 +7947,7 @@ pub(crate) const HEAL_BUILTIN_NAMES: &[&str] = &[
78557947
"str_split_lines", "str_count", "str_is_empty",
78567948
"str_to_int", "str_to_float", "str_capitalize",
78577949
"re_match", "re_find", "re_find_all", "re_replace", "re_split",
7950+
"json_parse", "json_stringify",
78587951
// Arrays
78597952
"arr_new", "arr_from_range", "arr_len", "arr_get", "arr_set",
78607953
"arr_push", "arr_first", "arr_last", "arr_slice", "arr_concat",

0 commit comments

Comments
 (0)