feat: add JSON parsing and manipulation support

Add comprehensive JSON support via the Json module:
- Parse JSON strings with Json.parse() returning Result<Json, String>
- Stringify with Json.stringify() and Json.prettyPrint()
- Extract values with Json.get(), getIndex(), asString(), asInt(), etc.
- Build JSON with constructors: Json.null(), bool(), int(), string(), array(), object()
- Query with Json.isNull() and Json.keys()

Includes example at examples/json.lux demonstrating building, parsing,
and extracting JSON data with file I/O integration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 16:22:35 -05:00
parent b0f6756411
commit ef9746c2fe
3 changed files with 580 additions and 0 deletions

View File

@@ -83,6 +83,27 @@ pub enum BuiltinFn {
StringToUpper,
StringToLower,
StringSubstring,
// JSON operations
JsonParse,
JsonStringify,
JsonPrettyPrint,
JsonGet,
JsonGetIndex,
JsonAsString,
JsonAsNumber,
JsonAsInt,
JsonAsBool,
JsonAsArray,
JsonIsNull,
JsonKeys,
JsonNull,
JsonBool,
JsonNumber,
JsonInt,
JsonString,
JsonArray,
JsonObject,
}
/// Runtime value
@@ -112,6 +133,8 @@ pub enum Value {
version: u32,
value: Box<Value>,
},
/// JSON value (for JSON parsing/manipulation)
Json(serde_json::Value),
}
impl Value {
@@ -131,6 +154,7 @@ impl Value {
Value::Builtin(_) => "Function",
Value::Constructor { .. } => "Constructor",
Value::Versioned { .. } => "Versioned",
Value::Json(_) => "Json",
}
}
@@ -279,6 +303,7 @@ impl fmt::Display for Value {
} => {
write!(f, "{} @v{}", value, version)
}
Value::Json(json) => write!(f, "{}", json),
}
}
}
@@ -856,6 +881,30 @@ impl Interpreter {
("round".to_string(), Value::Builtin(BuiltinFn::MathRound)),
]));
env.define("Math", math_module);
// JSON module
let json_module = Value::Record(HashMap::from([
("parse".to_string(), Value::Builtin(BuiltinFn::JsonParse)),
("stringify".to_string(), Value::Builtin(BuiltinFn::JsonStringify)),
("prettyPrint".to_string(), Value::Builtin(BuiltinFn::JsonPrettyPrint)),
("get".to_string(), Value::Builtin(BuiltinFn::JsonGet)),
("getIndex".to_string(), Value::Builtin(BuiltinFn::JsonGetIndex)),
("asString".to_string(), Value::Builtin(BuiltinFn::JsonAsString)),
("asNumber".to_string(), Value::Builtin(BuiltinFn::JsonAsNumber)),
("asInt".to_string(), Value::Builtin(BuiltinFn::JsonAsInt)),
("asBool".to_string(), Value::Builtin(BuiltinFn::JsonAsBool)),
("asArray".to_string(), Value::Builtin(BuiltinFn::JsonAsArray)),
("isNull".to_string(), Value::Builtin(BuiltinFn::JsonIsNull)),
("keys".to_string(), Value::Builtin(BuiltinFn::JsonKeys)),
("null".to_string(), Value::Builtin(BuiltinFn::JsonNull)),
("bool".to_string(), Value::Builtin(BuiltinFn::JsonBool)),
("number".to_string(), Value::Builtin(BuiltinFn::JsonNumber)),
("int".to_string(), Value::Builtin(BuiltinFn::JsonInt)),
("string".to_string(), Value::Builtin(BuiltinFn::JsonString)),
("array".to_string(), Value::Builtin(BuiltinFn::JsonArray)),
("object".to_string(), Value::Builtin(BuiltinFn::JsonObject)),
]));
env.define("Json", json_module);
}
/// Execute a program
@@ -2243,6 +2292,278 @@ impl Interpreter {
let result: String = chars[start..end].iter().collect();
Ok(EvalResult::Value(Value::String(result)))
}
// JSON operations
BuiltinFn::JsonParse => {
let s = Self::expect_arg_1::<String>(&args, "Json.parse", span)?;
match serde_json::from_str::<serde_json::Value>(&s) {
Ok(json) => Ok(EvalResult::Value(Value::Constructor {
name: "Ok".to_string(),
fields: vec![Value::Json(json)],
})),
Err(e) => Ok(EvalResult::Value(Value::Constructor {
name: "Err".to_string(),
fields: vec![Value::String(e.to_string())],
})),
}
}
BuiltinFn::JsonStringify => {
let json = match &args[0] {
Value::Json(j) => j.clone(),
v => return Err(err(&format!("Json.stringify expects Json, got {}", v.type_name()))),
};
Ok(EvalResult::Value(Value::String(json.to_string())))
}
BuiltinFn::JsonPrettyPrint => {
let json = match &args[0] {
Value::Json(j) => j.clone(),
v => return Err(err(&format!("Json.prettyPrint expects Json, got {}", v.type_name()))),
};
match serde_json::to_string_pretty(&json) {
Ok(s) => Ok(EvalResult::Value(Value::String(s))),
Err(e) => Err(err(&format!("Json.prettyPrint error: {}", e))),
}
}
BuiltinFn::JsonGet => {
// Json.get(json, key) -> Option<Json>
if args.len() != 2 {
return Err(err("Json.get requires 2 arguments: json, key"));
}
let json = match &args[0] {
Value::Json(j) => j,
v => return Err(err(&format!("Json.get expects Json, got {}", v.type_name()))),
};
let key = match &args[1] {
Value::String(s) => s.clone(),
v => return Err(err(&format!("Json.get expects String key, got {}", v.type_name()))),
};
match json.get(&key) {
Some(v) => Ok(EvalResult::Value(Value::Constructor {
name: "Some".to_string(),
fields: vec![Value::Json(v.clone())],
})),
None => Ok(EvalResult::Value(Value::Constructor {
name: "None".to_string(),
fields: vec![],
})),
}
}
BuiltinFn::JsonGetIndex => {
// Json.getIndex(json, index) -> Option<Json>
if args.len() != 2 {
return Err(err("Json.getIndex requires 2 arguments: json, index"));
}
let json = match &args[0] {
Value::Json(j) => j,
v => return Err(err(&format!("Json.getIndex expects Json, got {}", v.type_name()))),
};
let idx = match &args[1] {
Value::Int(n) => *n as usize,
v => return Err(err(&format!("Json.getIndex expects Int index, got {}", v.type_name()))),
};
match json.get(idx) {
Some(v) => Ok(EvalResult::Value(Value::Constructor {
name: "Some".to_string(),
fields: vec![Value::Json(v.clone())],
})),
None => Ok(EvalResult::Value(Value::Constructor {
name: "None".to_string(),
fields: vec![],
})),
}
}
BuiltinFn::JsonAsString => {
// Json.asString(json) -> Option<String>
let json = match &args[0] {
Value::Json(j) => j,
v => return Err(err(&format!("Json.asString expects Json, got {}", v.type_name()))),
};
match json.as_str() {
Some(s) => Ok(EvalResult::Value(Value::Constructor {
name: "Some".to_string(),
fields: vec![Value::String(s.to_string())],
})),
None => Ok(EvalResult::Value(Value::Constructor {
name: "None".to_string(),
fields: vec![],
})),
}
}
BuiltinFn::JsonAsNumber => {
// Json.asNumber(json) -> Option<Float>
let json = match &args[0] {
Value::Json(j) => j,
v => return Err(err(&format!("Json.asNumber expects Json, got {}", v.type_name()))),
};
match json.as_f64() {
Some(n) => Ok(EvalResult::Value(Value::Constructor {
name: "Some".to_string(),
fields: vec![Value::Float(n)],
})),
None => Ok(EvalResult::Value(Value::Constructor {
name: "None".to_string(),
fields: vec![],
})),
}
}
BuiltinFn::JsonAsInt => {
// Json.asInt(json) -> Option<Int>
let json = match &args[0] {
Value::Json(j) => j,
v => return Err(err(&format!("Json.asInt expects Json, got {}", v.type_name()))),
};
match json.as_i64() {
Some(n) => Ok(EvalResult::Value(Value::Constructor {
name: "Some".to_string(),
fields: vec![Value::Int(n)],
})),
None => Ok(EvalResult::Value(Value::Constructor {
name: "None".to_string(),
fields: vec![],
})),
}
}
BuiltinFn::JsonAsBool => {
// Json.asBool(json) -> Option<Bool>
let json = match &args[0] {
Value::Json(j) => j,
v => return Err(err(&format!("Json.asBool expects Json, got {}", v.type_name()))),
};
match json.as_bool() {
Some(b) => Ok(EvalResult::Value(Value::Constructor {
name: "Some".to_string(),
fields: vec![Value::Bool(b)],
})),
None => Ok(EvalResult::Value(Value::Constructor {
name: "None".to_string(),
fields: vec![],
})),
}
}
BuiltinFn::JsonAsArray => {
// Json.asArray(json) -> Option<List<Json>>
let json = match &args[0] {
Value::Json(j) => j,
v => return Err(err(&format!("Json.asArray expects Json, got {}", v.type_name()))),
};
match json.as_array() {
Some(arr) => {
let items: Vec<Value> = arr.iter().map(|v| Value::Json(v.clone())).collect();
Ok(EvalResult::Value(Value::Constructor {
name: "Some".to_string(),
fields: vec![Value::List(items)],
}))
}
None => Ok(EvalResult::Value(Value::Constructor {
name: "None".to_string(),
fields: vec![],
})),
}
}
BuiltinFn::JsonIsNull => {
// Json.isNull(json) -> Bool
let json = match &args[0] {
Value::Json(j) => j,
v => return Err(err(&format!("Json.isNull expects Json, got {}", v.type_name()))),
};
Ok(EvalResult::Value(Value::Bool(json.is_null())))
}
BuiltinFn::JsonKeys => {
// Json.keys(json) -> Option<List<String>>
let json = match &args[0] {
Value::Json(j) => j,
v => return Err(err(&format!("Json.keys expects Json, got {}", v.type_name()))),
};
match json.as_object() {
Some(obj) => {
let keys: Vec<Value> = obj.keys().map(|k| Value::String(k.clone())).collect();
Ok(EvalResult::Value(Value::Constructor {
name: "Some".to_string(),
fields: vec![Value::List(keys)],
}))
}
None => Ok(EvalResult::Value(Value::Constructor {
name: "None".to_string(),
fields: vec![],
})),
}
}
// JSON constructors
BuiltinFn::JsonNull => {
Ok(EvalResult::Value(Value::Json(serde_json::Value::Null)))
}
BuiltinFn::JsonBool => {
let b = Self::expect_arg_1::<bool>(&args, "Json.bool", span)?;
Ok(EvalResult::Value(Value::Json(serde_json::Value::Bool(b))))
}
BuiltinFn::JsonNumber => {
let n = match &args[0] {
Value::Float(f) => serde_json::Number::from_f64(*f)
.ok_or_else(|| err("Invalid float for JSON"))?,
Value::Int(i) => serde_json::Number::from(*i),
v => return Err(err(&format!("Json.number expects Float or Int, got {}", v.type_name()))),
};
Ok(EvalResult::Value(Value::Json(serde_json::Value::Number(n))))
}
BuiltinFn::JsonInt => {
let n = Self::expect_arg_1::<i64>(&args, "Json.int", span)?;
Ok(EvalResult::Value(Value::Json(serde_json::Value::Number(serde_json::Number::from(n)))))
}
BuiltinFn::JsonString => {
let s = Self::expect_arg_1::<String>(&args, "Json.string", span)?;
Ok(EvalResult::Value(Value::Json(serde_json::Value::String(s))))
}
BuiltinFn::JsonArray => {
// Json.array(list: List<Json>) -> Json
let list = Self::expect_arg_1::<Vec<Value>>(&args, "Json.array", span)?;
let arr: Result<Vec<serde_json::Value>, RuntimeError> = list.into_iter().map(|v| {
match v {
Value::Json(j) => Ok(j),
_ => Err(err("Json.array expects List<Json>")),
}
}).collect();
Ok(EvalResult::Value(Value::Json(serde_json::Value::Array(arr?))))
}
BuiltinFn::JsonObject => {
// Json.object(entries: List<(String, Json)>) -> Json
let list = Self::expect_arg_1::<Vec<Value>>(&args, "Json.object", span)?;
let mut map = serde_json::Map::new();
for item in list {
match item {
Value::Tuple(fields) if fields.len() == 2 => {
let key = match &fields[0] {
Value::String(s) => s.clone(),
_ => return Err(err("Json.object expects (String, Json) tuples")),
};
let value = match &fields[1] {
Value::Json(j) => j.clone(),
_ => return Err(err("Json.object expects (String, Json) tuples")),
};
map.insert(key, value);
}
_ => return Err(err("Json.object expects List<(String, Json)>")),
}
}
Ok(EvalResult::Value(Value::Json(serde_json::Value::Object(map))))
}
}
}

View File

@@ -732,6 +732,37 @@ impl TypeEnv {
]),
);
// Add Json type (represents JSON values)
env.types.insert(
"Json".to_string(),
TypeDef::Enum(vec![
VariantDef {
name: "JsonNull".to_string(),
fields: VariantFieldsDef::Unit,
},
VariantDef {
name: "JsonBool".to_string(),
fields: VariantFieldsDef::Tuple(vec![Type::Bool]),
},
VariantDef {
name: "JsonNumber".to_string(),
fields: VariantFieldsDef::Tuple(vec![Type::Float]),
},
VariantDef {
name: "JsonString".to_string(),
fields: VariantFieldsDef::Tuple(vec![Type::String]),
},
VariantDef {
name: "JsonArray".to_string(),
fields: VariantFieldsDef::Tuple(vec![Type::List(Box::new(Type::Named("Json".to_string())))]),
},
VariantDef {
name: "JsonObject".to_string(),
fields: VariantFieldsDef::Tuple(vec![Type::List(Box::new(Type::Tuple(vec![Type::String, Type::Named("Json".to_string())])))]),
},
]),
);
// Add Console effect
env.effects.insert(
"Console".to_string(),
@@ -1191,6 +1222,122 @@ impl TypeEnv {
]);
env.bind("String", TypeScheme::mono(string_module_type));
// Json module
let json_type = Type::Named("Json".to_string());
let json_module_type = Type::Record(vec![
(
"parse".to_string(),
Type::function(
vec![Type::String],
Type::App {
constructor: Box::new(Type::Named("Result".to_string())),
args: vec![json_type.clone(), Type::String],
},
),
),
(
"stringify".to_string(),
Type::function(vec![json_type.clone()], Type::String),
),
(
"prettyPrint".to_string(),
Type::function(vec![json_type.clone()], Type::String),
),
(
"get".to_string(),
Type::function(
vec![json_type.clone(), Type::String],
Type::Option(Box::new(json_type.clone())),
),
),
(
"getIndex".to_string(),
Type::function(
vec![json_type.clone(), Type::Int],
Type::Option(Box::new(json_type.clone())),
),
),
(
"asString".to_string(),
Type::function(
vec![json_type.clone()],
Type::Option(Box::new(Type::String)),
),
),
(
"asNumber".to_string(),
Type::function(
vec![json_type.clone()],
Type::Option(Box::new(Type::Float)),
),
),
(
"asInt".to_string(),
Type::function(
vec![json_type.clone()],
Type::Option(Box::new(Type::Int)),
),
),
(
"asBool".to_string(),
Type::function(
vec![json_type.clone()],
Type::Option(Box::new(Type::Bool)),
),
),
(
"asArray".to_string(),
Type::function(
vec![json_type.clone()],
Type::Option(Box::new(Type::List(Box::new(json_type.clone())))),
),
),
(
"isNull".to_string(),
Type::function(vec![json_type.clone()], Type::Bool),
),
(
"keys".to_string(),
Type::function(
vec![json_type.clone()],
Type::List(Box::new(Type::String)),
),
),
// Constructors for building JSON
(
"null".to_string(),
Type::function(vec![], json_type.clone()),
),
(
"bool".to_string(),
Type::function(vec![Type::Bool], json_type.clone()),
),
(
"number".to_string(),
Type::function(vec![Type::Float], json_type.clone()),
),
(
"int".to_string(),
Type::function(vec![Type::Int], json_type.clone()),
),
(
"string".to_string(),
Type::function(vec![Type::String], json_type.clone()),
),
(
"array".to_string(),
Type::function(vec![Type::List(Box::new(json_type.clone()))], json_type.clone()),
),
(
"object".to_string(),
Type::function(
vec![Type::List(Box::new(Type::Tuple(vec![Type::String, json_type.clone()])))],
json_type.clone(),
),
),
]);
env.bind("Json", TypeScheme::mono(json_module_type));
// Option module
let option_module_type = Type::Record(vec![
(