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

112
examples/json.lux Normal file
View File

@@ -0,0 +1,112 @@
// JSON example - demonstrates JSON parsing and manipulation
//
// This script parses JSON, extracts values, and builds new JSON structures
fn main(): Unit with {Console, File} = {
Console.print("=== Lux JSON Example ===")
Console.print("")
// First, build some JSON programmatically
Console.print("=== Building JSON ===")
Console.print("")
let name = Json.string("Alice")
let age = Json.int(30)
let active = Json.bool(true)
let scores = Json.array([Json.int(95), Json.int(87), Json.int(92)])
let person = Json.object([("name", name), ("age", age), ("active", active), ("scores", scores)])
Console.print("Built JSON:")
let pretty = Json.prettyPrint(person)
Console.print(pretty)
Console.print("")
// Stringify to a compact string
let jsonStr = Json.stringify(person)
Console.print("Compact: " + jsonStr)
Console.print("")
// Write to file and read back to test parsing
File.write("/tmp/test.json", jsonStr)
Console.print("Written to /tmp/test.json")
Console.print("")
// Read and parse from file
Console.print("=== Parsing JSON ===")
Console.print("")
let content = File.read("/tmp/test.json")
Console.print("Read from file: " + content)
Console.print("")
match Json.parse(content) {
Ok(json) => {
Console.print("Parse succeeded!")
Console.print("")
// Get string field
Console.print("Extracting fields:")
match Json.get(json, "name") {
Some(nameJson) => match Json.asString(nameJson) {
Some(n) => Console.print(" name: " + n),
None => Console.print(" name: (not a string)")
},
None => Console.print(" name: (not found)")
}
// Get int field
match Json.get(json, "age") {
Some(ageJson) => match Json.asInt(ageJson) {
Some(a) => Console.print(" age: " + toString(a)),
None => Console.print(" age: (not an int)")
},
None => Console.print(" age: (not found)")
}
// Get bool field
match Json.get(json, "active") {
Some(activeJson) => match Json.asBool(activeJson) {
Some(a) => Console.print(" active: " + toString(a)),
None => Console.print(" active: (not a bool)")
},
None => Console.print(" active: (not found)")
}
// Get array field
match Json.get(json, "scores") {
Some(scoresJson) => match Json.asArray(scoresJson) {
Some(arr) => {
Console.print(" scores: " + toString(List.length(arr)) + " items")
// Get first score
match Json.getIndex(scoresJson, 0) {
Some(firstJson) => match Json.asInt(firstJson) {
Some(first) => Console.print(" first score: " + toString(first)),
None => Console.print(" first score: (not an int)")
},
None => Console.print(" (no first element)")
}
},
None => Console.print(" scores: (not an array)")
},
None => Console.print(" scores: (not found)")
}
Console.print("")
// Get the keys
Console.print("Object keys:")
match Json.keys(json) {
Some(ks) => Console.print(" " + String.join(ks, ", ")),
None => Console.print(" (not an object)")
}
},
Err(e) => Console.print("Parse error: " + e)
}
Console.print("")
Console.print("=== JSON Null Check ===")
let nullVal = Json.null()
Console.print("Is null value null? " + toString(Json.isNull(nullVal)))
Console.print("Is person null? " + toString(Json.isNull(person)))
}
let result = run main() with {}

View File

@@ -83,6 +83,27 @@ pub enum BuiltinFn {
StringToUpper, StringToUpper,
StringToLower, StringToLower,
StringSubstring, StringSubstring,
// JSON operations
JsonParse,
JsonStringify,
JsonPrettyPrint,
JsonGet,
JsonGetIndex,
JsonAsString,
JsonAsNumber,
JsonAsInt,
JsonAsBool,
JsonAsArray,
JsonIsNull,
JsonKeys,
JsonNull,
JsonBool,
JsonNumber,
JsonInt,
JsonString,
JsonArray,
JsonObject,
} }
/// Runtime value /// Runtime value
@@ -112,6 +133,8 @@ pub enum Value {
version: u32, version: u32,
value: Box<Value>, value: Box<Value>,
}, },
/// JSON value (for JSON parsing/manipulation)
Json(serde_json::Value),
} }
impl Value { impl Value {
@@ -131,6 +154,7 @@ impl Value {
Value::Builtin(_) => "Function", Value::Builtin(_) => "Function",
Value::Constructor { .. } => "Constructor", Value::Constructor { .. } => "Constructor",
Value::Versioned { .. } => "Versioned", Value::Versioned { .. } => "Versioned",
Value::Json(_) => "Json",
} }
} }
@@ -279,6 +303,7 @@ impl fmt::Display for Value {
} => { } => {
write!(f, "{} @v{}", value, version) write!(f, "{} @v{}", value, version)
} }
Value::Json(json) => write!(f, "{}", json),
} }
} }
} }
@@ -856,6 +881,30 @@ impl Interpreter {
("round".to_string(), Value::Builtin(BuiltinFn::MathRound)), ("round".to_string(), Value::Builtin(BuiltinFn::MathRound)),
])); ]));
env.define("Math", math_module); 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 /// Execute a program
@@ -2243,6 +2292,278 @@ impl Interpreter {
let result: String = chars[start..end].iter().collect(); let result: String = chars[start..end].iter().collect();
Ok(EvalResult::Value(Value::String(result))) 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 // Add Console effect
env.effects.insert( env.effects.insert(
"Console".to_string(), "Console".to_string(),
@@ -1191,6 +1222,122 @@ impl TypeEnv {
]); ]);
env.bind("String", TypeScheme::mono(string_module_type)); 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 // Option module
let option_module_type = Type::Record(vec![ let option_module_type = Type::Record(vec![
( (