diff --git a/examples/json.lux b/examples/json.lux new file mode 100644 index 0000000..028a880 --- /dev/null +++ b/examples/json.lux @@ -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 {} diff --git a/src/interpreter.rs b/src/interpreter.rs index 609928c..fecd112 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -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, }, + /// 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::(&args, "Json.parse", span)?; + match serde_json::from_str::(&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 + 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 + 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 + 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 + 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 + 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 + 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> + 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 = 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> + 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 = 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::(&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::(&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::(&args, "Json.string", span)?; + Ok(EvalResult::Value(Value::Json(serde_json::Value::String(s)))) + } + + BuiltinFn::JsonArray => { + // Json.array(list: List) -> Json + let list = Self::expect_arg_1::>(&args, "Json.array", span)?; + let arr: Result, RuntimeError> = list.into_iter().map(|v| { + match v { + Value::Json(j) => Ok(j), + _ => Err(err("Json.array expects List")), + } + }).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::>(&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)))) + } } } diff --git a/src/types.rs b/src/types.rs index 4bcb552..ed66f4e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -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![ (