diff --git a/docs/private b/docs/private index 6523cdc8..1840731c 160000 --- a/docs/private +++ b/docs/private @@ -1 +1 @@ -Subproject commit 6523cdc838a302cbbad413c14128c6fb312f7508 +Subproject commit 1840731c685509f4874650c1454ba621b9987d76 diff --git a/src/mir/join_ir/json.rs b/src/mir/join_ir/json.rs new file mode 100644 index 00000000..4fc64f6b --- /dev/null +++ b/src/mir/join_ir/json.rs @@ -0,0 +1,398 @@ +//! JoinIR JSON シリアライザ (jsonir v0) +//! +//! JoinModule を JSON 形式でシリアライズする。 +//! 用途: デバッグ、テスト、将来の selfhost JoinIR ロワーの参照フォーマット。 +//! +//! 仕様: docs/private/roadmap2/phases/phase-30-final-joinir-world/joinir_json.md + +use std::io::Write; + +use super::{ + BinOpKind, CompareOp, ConstValue, JoinFunction, JoinInst, JoinModule, MirLikeInst, VarId, +}; + +/// JoinModule を JSON としてシリアライズする +/// +/// # Example +/// ```ignore +/// let mut output = Vec::new(); +/// write_join_module_as_json(&module, &mut output)?; +/// let json_str = String::from_utf8(output)?; +/// ``` +pub fn write_join_module_as_json(module: &JoinModule, out: &mut W) -> std::io::Result<()> { + write!(out, "{{")?; + write!(out, "\"version\":0")?; + + // entry + match module.entry { + Some(entry_id) => write!(out, ",\"entry\":{}", entry_id.0)?, + None => write!(out, ",\"entry\":null")?, + } + + // functions + write!(out, ",\"functions\":[")?; + let mut first_func = true; + for func in module.functions.values() { + if !first_func { + write!(out, ",")?; + } + first_func = false; + write_function(func, out)?; + } + write!(out, "]")?; + + write!(out, "}}")?; + Ok(()) +} + +fn write_function(func: &JoinFunction, out: &mut W) -> std::io::Result<()> { + write!(out, "{{")?; + write!(out, "\"id\":{}", func.id.0)?; + write!(out, ",\"name\":\"{}\"" , escape_json_string(&func.name))?; + + // params + write!(out, ",\"params\":[")?; + for (i, param) in func.params.iter().enumerate() { + if i > 0 { + write!(out, ",")?; + } + write!(out, "{}", param.0)?; + } + write!(out, "]")?; + + // exit_cont + match func.exit_cont { + Some(cont_id) => write!(out, ",\"exit_cont\":{}", cont_id.0)?, + None => write!(out, ",\"exit_cont\":null")?, + } + + // body + write!(out, ",\"body\":[")?; + for (i, inst) in func.body.iter().enumerate() { + if i > 0 { + write!(out, ",")?; + } + write_inst(inst, out)?; + } + write!(out, "]")?; + + write!(out, "}}")?; + Ok(()) +} + +fn write_inst(inst: &JoinInst, out: &mut W) -> std::io::Result<()> { + match inst { + JoinInst::Call { + func, + args, + k_next, + dst, + } => { + write!(out, "{{\"type\":\"call\"")?; + write!(out, ",\"func\":{}", func.0)?; + write!(out, ",\"args\":[")?; + for (i, arg) in args.iter().enumerate() { + if i > 0 { + write!(out, ",")?; + } + write!(out, "{}", arg.0)?; + } + write!(out, "]")?; + match k_next { + Some(k) => write!(out, ",\"k_next\":{}", k.0)?, + None => write!(out, ",\"k_next\":null")?, + } + match dst { + Some(d) => write!(out, ",\"dst\":{}", d.0)?, + None => write!(out, ",\"dst\":null")?, + } + write!(out, "}}")?; + } + JoinInst::Jump { cont, args, cond } => { + write!(out, "{{\"type\":\"jump\"")?; + write!(out, ",\"cont\":{}", cont.0)?; + write!(out, ",\"args\":[")?; + for (i, arg) in args.iter().enumerate() { + if i > 0 { + write!(out, ",")?; + } + write!(out, "{}", arg.0)?; + } + write!(out, "]")?; + match cond { + Some(c) => write!(out, ",\"cond\":{}", c.0)?, + None => write!(out, ",\"cond\":null")?, + } + write!(out, "}}")?; + } + JoinInst::Ret { value } => { + write!(out, "{{\"type\":\"ret\"")?; + match value { + Some(v) => write!(out, ",\"value\":{}", v.0)?, + None => write!(out, ",\"value\":null")?, + } + write!(out, "}}")?; + } + JoinInst::Compute(mir_like) => { + write!(out, "{{\"type\":\"compute\",\"op\":")?; + write_mir_like_inst(mir_like, out)?; + write!(out, "}}")?; + } + } + Ok(()) +} + +fn write_mir_like_inst(inst: &MirLikeInst, out: &mut W) -> std::io::Result<()> { + match inst { + MirLikeInst::Const { dst, value } => { + write!(out, "{{\"kind\":\"const\"")?; + write!(out, ",\"dst\":{}", dst.0)?; + match value { + ConstValue::Integer(n) => { + write!(out, ",\"value_type\":\"integer\"")?; + write!(out, ",\"value\":{}", n)?; + } + ConstValue::Bool(b) => { + write!(out, ",\"value_type\":\"bool\"")?; + write!(out, ",\"value\":{}", b)?; + } + ConstValue::String(s) => { + write!(out, ",\"value_type\":\"string\"")?; + write!(out, ",\"value\":\"{}\"", escape_json_string(s))?; + } + ConstValue::Null => { + write!(out, ",\"value_type\":\"null\"")?; + write!(out, ",\"value\":null")?; + } + } + write!(out, "}}")?; + } + MirLikeInst::BinOp { dst, op, lhs, rhs } => { + write!(out, "{{\"kind\":\"binop\"")?; + write!(out, ",\"dst\":{}", dst.0)?; + write!(out, ",\"op\":\"{}\"", binop_to_str(*op))?; + write!(out, ",\"lhs\":{}", lhs.0)?; + write!(out, ",\"rhs\":{}", rhs.0)?; + write!(out, "}}")?; + } + MirLikeInst::Compare { dst, op, lhs, rhs } => { + write!(out, "{{\"kind\":\"compare\"")?; + write!(out, ",\"dst\":{}", dst.0)?; + write!(out, ",\"op\":\"{}\"", compare_to_str(*op))?; + write!(out, ",\"lhs\":{}", lhs.0)?; + write!(out, ",\"rhs\":{}", rhs.0)?; + write!(out, "}}")?; + } + MirLikeInst::BoxCall { + dst, + box_name, + method, + args, + } => { + write!(out, "{{\"kind\":\"boxcall\"")?; + match dst { + Some(d) => write!(out, ",\"dst\":{}", d.0)?, + None => write!(out, ",\"dst\":null")?, + } + write!(out, ",\"box\":\"{}\"", escape_json_string(box_name))?; + write!(out, ",\"method\":\"{}\"", escape_json_string(method))?; + write!(out, ",\"args\":[")?; + for (i, arg) in args.iter().enumerate() { + if i > 0 { + write!(out, ",")?; + } + write!(out, "{}", arg.0)?; + } + write!(out, "]")?; + write!(out, "}}")?; + } + } + Ok(()) +} + +fn binop_to_str(op: BinOpKind) -> &'static str { + match op { + BinOpKind::Add => "add", + BinOpKind::Sub => "sub", + BinOpKind::Mul => "mul", + BinOpKind::Div => "div", + BinOpKind::Or => "or", + BinOpKind::And => "and", + } +} + +fn compare_to_str(op: CompareOp) -> &'static str { + match op { + CompareOp::Lt => "lt", + CompareOp::Le => "le", + CompareOp::Gt => "gt", + CompareOp::Ge => "ge", + CompareOp::Eq => "eq", + CompareOp::Ne => "ne", + } +} + +/// JSON 文字列のエスケープ +fn escape_json_string(s: &str) -> String { + let mut escaped = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '"' => escaped.push_str("\\\""), + '\\' => escaped.push_str("\\\\"), + '\n' => escaped.push_str("\\n"), + '\r' => escaped.push_str("\\r"), + '\t' => escaped.push_str("\\t"), + c if c.is_control() => { + escaped.push_str(&format!("\\u{:04x}", c as u32)); + } + c => escaped.push(c), + } + } + escaped +} + +/// JoinModule を JSON 文字列として返す(テスト用ヘルパー) +pub fn join_module_to_json_string(module: &JoinModule) -> String { + let mut output = Vec::new(); + write_join_module_as_json(module, &mut output).expect("JSON serialization failed"); + String::from_utf8(output).expect("Invalid UTF-8 in JSON output") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mir::join_ir::{JoinContId, JoinFuncId}; + use crate::mir::ValueId; + + #[test] + fn test_empty_module() { + let module = JoinModule::new(); + let json = join_module_to_json_string(&module); + assert!(json.contains("\"version\":0")); + assert!(json.contains("\"entry\":null")); + assert!(json.contains("\"functions\":[]")); + } + + #[test] + fn test_simple_function() { + let mut module = JoinModule::new(); + let mut func = JoinFunction::new(JoinFuncId::new(0), "test".to_string(), vec![ValueId(100)]); + func.body.push(JoinInst::Ret { + value: Some(ValueId(100)), + }); + module.add_function(func); + module.entry = Some(JoinFuncId::new(0)); + + let json = join_module_to_json_string(&module); + assert!(json.contains("\"entry\":0")); + assert!(json.contains("\"name\":\"test\"")); + assert!(json.contains("\"params\":[100]")); + assert!(json.contains("\"type\":\"ret\"")); + assert!(json.contains("\"value\":100")); + } + + #[test] + fn test_const_instruction() { + let mut module = JoinModule::new(); + let mut func = JoinFunction::new(JoinFuncId::new(0), "main".to_string(), vec![]); + func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: ValueId(1), + value: ConstValue::Integer(42), + })); + module.add_function(func); + + let json = join_module_to_json_string(&module); + assert!(json.contains("\"kind\":\"const\"")); + assert!(json.contains("\"value_type\":\"integer\"")); + assert!(json.contains("\"value\":42")); + } + + #[test] + fn test_binop_instruction() { + let mut module = JoinModule::new(); + let mut func = JoinFunction::new(JoinFuncId::new(0), "main".to_string(), vec![]); + func.body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: ValueId(3), + op: BinOpKind::Add, + lhs: ValueId(1), + rhs: ValueId(2), + })); + module.add_function(func); + + let json = join_module_to_json_string(&module); + assert!(json.contains("\"kind\":\"binop\"")); + assert!(json.contains("\"op\":\"add\"")); + assert!(json.contains("\"lhs\":1")); + assert!(json.contains("\"rhs\":2")); + } + + #[test] + fn test_call_instruction() { + let mut module = JoinModule::new(); + let mut func = JoinFunction::new(JoinFuncId::new(0), "main".to_string(), vec![]); + func.body.push(JoinInst::Call { + func: JoinFuncId::new(1), + args: vec![ValueId(100), ValueId(101)], + k_next: Some(JoinContId::new(5)), + dst: Some(ValueId(200)), + }); + module.add_function(func); + + let json = join_module_to_json_string(&module); + assert!(json.contains("\"type\":\"call\"")); + assert!(json.contains("\"func\":1")); + assert!(json.contains("\"args\":[100,101]")); + assert!(json.contains("\"k_next\":5")); + assert!(json.contains("\"dst\":200")); + } + + #[test] + fn test_jump_instruction() { + let mut module = JoinModule::new(); + let mut func = JoinFunction::new(JoinFuncId::new(0), "main".to_string(), vec![]); + func.body.push(JoinInst::Jump { + cont: JoinContId::new(3), + args: vec![ValueId(10)], + cond: Some(ValueId(5)), + }); + module.add_function(func); + + let json = join_module_to_json_string(&module); + assert!(json.contains("\"type\":\"jump\"")); + assert!(json.contains("\"cont\":3")); + assert!(json.contains("\"cond\":5")); + } + + #[test] + fn test_boxcall_instruction() { + let mut module = JoinModule::new(); + let mut func = JoinFunction::new(JoinFuncId::new(0), "main".to_string(), vec![]); + func.body.push(JoinInst::Compute(MirLikeInst::BoxCall { + dst: Some(ValueId(10)), + box_name: "StringBox".to_string(), + method: "length".to_string(), + args: vec![ValueId(1)], + })); + module.add_function(func); + + let json = join_module_to_json_string(&module); + assert!(json.contains("\"kind\":\"boxcall\"")); + assert!(json.contains("\"box\":\"StringBox\"")); + assert!(json.contains("\"method\":\"length\"")); + } + + #[test] + fn test_string_escaping() { + let mut module = JoinModule::new(); + let mut func = JoinFunction::new(JoinFuncId::new(0), "main".to_string(), vec![]); + func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: ValueId(1), + value: ConstValue::String("hello\nworld\"test".to_string()), + })); + module.add_function(func); + + let json = join_module_to_json_string(&module); + assert!(json.contains("\\n")); + assert!(json.contains("\\\"")); + } +} diff --git a/src/mir/join_ir/mod.rs b/src/mir/join_ir/mod.rs index 155e1654..036d08db 100644 --- a/src/mir/join_ir/mod.rs +++ b/src/mir/join_ir/mod.rs @@ -30,6 +30,9 @@ pub mod lowering; // Phase 29 L-5.2: Progress verification pub mod verify; +// Phase 30.x: JSON serialization (jsonir v0) +pub mod json; + // Re-export lowering functions for backward compatibility pub use lowering::{ lower_funcscanner_trim_to_joinir, lower_min_loop_to_joinir, lower_skip_ws_to_joinir, diff --git a/src/tests/joinir_json_min.rs b/src/tests/joinir_json_min.rs new file mode 100644 index 00000000..0d3d84a3 --- /dev/null +++ b/src/tests/joinir_json_min.rs @@ -0,0 +1,232 @@ +//! JoinIR JSON シリアライズテスト (Phase 30.x) +//! +//! 手動構築した JoinIR を JSON に変換し、構造の妥当性を検証する。 + +use crate::mir::join_ir::json::join_module_to_json_string; +use crate::mir::join_ir::{ + BinOpKind, CompareOp, ConstValue, JoinContId, JoinFuncId, JoinFunction, JoinInst, JoinModule, + MirLikeInst, +}; +use crate::mir::ValueId; + +/// 手動構築した JoinModule の JSON 出力テスト(NYASH_JOINIR_EXPERIMENT 不要) +#[test] +fn test_manual_joinir_json() { + // skip_ws 相当の JoinIR を手動で構築 + let mut module = JoinModule::new(); + + // skip 関数: skip(s) -> i + let mut skip_func = JoinFunction::new( + JoinFuncId::new(0), + "skip".to_string(), + vec![ValueId(3000)], // s + ); + + // i_init = 0 + skip_func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: ValueId(3001), + value: ConstValue::Integer(0), + })); + + // n = s.length() + skip_func.body.push(JoinInst::Compute(MirLikeInst::BoxCall { + dst: Some(ValueId(3002)), + box_name: "StringBox".to_string(), + method: "length".to_string(), + args: vec![ValueId(3000)], + })); + + // loop_step(s, i_init, n) - 末尾呼び出し + skip_func.body.push(JoinInst::Call { + func: JoinFuncId::new(1), + args: vec![ValueId(3000), ValueId(3001), ValueId(3002)], + k_next: None, + dst: None, + }); + + module.add_function(skip_func); + + // loop_step 関数: loop_step(s, i, n) -> i + let mut loop_step = JoinFunction::new( + JoinFuncId::new(1), + "loop_step".to_string(), + vec![ValueId(3100), ValueId(3101), ValueId(3102)], // s, i, n + ); + + // cond = i < n + loop_step.body.push(JoinInst::Compute(MirLikeInst::Compare { + dst: ValueId(3103), + op: CompareOp::Lt, + lhs: ValueId(3101), + rhs: ValueId(3102), + })); + + // 条件分岐(簡略化: 条件付き ret) + loop_step.body.push(JoinInst::Ret { + value: Some(ValueId(3101)), + }); + + module.add_function(loop_step); + module.entry = Some(JoinFuncId::new(0)); + + // JSON に変換 + let json = join_module_to_json_string(&module); + + // 構造チェック + assert!(json.contains("\"version\":0")); + assert!(json.contains("\"entry\":0")); + assert!(json.contains("\"name\":\"skip\"")); + assert!(json.contains("\"name\":\"loop_step\"")); + + // 命令チェック + assert!(json.contains("\"kind\":\"const\"")); + assert!(json.contains("\"kind\":\"boxcall\"")); + assert!(json.contains("\"kind\":\"compare\"")); + assert!(json.contains("\"type\":\"call\"")); + assert!(json.contains("\"type\":\"ret\"")); + + // 値チェック + assert!(json.contains("\"value_type\":\"integer\"")); + assert!(json.contains("\"value\":0")); + assert!(json.contains("\"box\":\"StringBox\"")); + assert!(json.contains("\"method\":\"length\"")); + assert!(json.contains("\"op\":\"lt\"")); + + eprintln!("[joinir/json] manual JoinIR JSON output:"); + eprintln!("{}", json); +} + +/// 全命令タイプの JSON 出力をカバーするテスト +#[test] +fn test_all_instruction_types_json() { + let mut module = JoinModule::new(); + let mut func = JoinFunction::new(JoinFuncId::new(0), "all_types".to_string(), vec![]); + + // Const - Integer + func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: ValueId(1), + value: ConstValue::Integer(42), + })); + + // Const - Bool + func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: ValueId(2), + value: ConstValue::Bool(true), + })); + + // Const - String + func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: ValueId(3), + value: ConstValue::String("hello".to_string()), + })); + + // Const - Null + func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: ValueId(4), + value: ConstValue::Null, + })); + + // BinOp - all types + for (i, op) in [ + BinOpKind::Add, + BinOpKind::Sub, + BinOpKind::Mul, + BinOpKind::Div, + BinOpKind::Or, + BinOpKind::And, + ] + .iter() + .enumerate() + { + func.body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: ValueId(10 + i as u32), + op: *op, + lhs: ValueId(1), + rhs: ValueId(1), + })); + } + + // Compare - all types + for (i, op) in [ + CompareOp::Lt, + CompareOp::Le, + CompareOp::Gt, + CompareOp::Ge, + CompareOp::Eq, + CompareOp::Ne, + ] + .iter() + .enumerate() + { + func.body.push(JoinInst::Compute(MirLikeInst::Compare { + dst: ValueId(20 + i as u32), + op: *op, + lhs: ValueId(1), + rhs: ValueId(1), + })); + } + + // BoxCall + func.body.push(JoinInst::Compute(MirLikeInst::BoxCall { + dst: Some(ValueId(30)), + box_name: "TestBox".to_string(), + method: "test_method".to_string(), + args: vec![ValueId(1), ValueId(2)], + })); + + // Call + func.body.push(JoinInst::Call { + func: JoinFuncId::new(1), + args: vec![ValueId(1)], + k_next: Some(JoinContId::new(2)), + dst: Some(ValueId(40)), + }); + + // Jump (conditional) + func.body.push(JoinInst::Jump { + cont: JoinContId::new(3), + args: vec![ValueId(1)], + cond: Some(ValueId(2)), + }); + + // Jump (unconditional) + func.body.push(JoinInst::Jump { + cont: JoinContId::new(4), + args: vec![], + cond: None, + }); + + // Ret with value + func.body.push(JoinInst::Ret { + value: Some(ValueId(1)), + }); + + module.add_function(func); + + // JSON に変換 + let json = join_module_to_json_string(&module); + + // 全 BinOp タイプをチェック + assert!(json.contains("\"op\":\"add\"")); + assert!(json.contains("\"op\":\"sub\"")); + assert!(json.contains("\"op\":\"mul\"")); + assert!(json.contains("\"op\":\"div\"")); + assert!(json.contains("\"op\":\"or\"")); + assert!(json.contains("\"op\":\"and\"")); + + // 全 Compare タイプをチェック + assert!(json.contains("\"op\":\"lt\"")); + assert!(json.contains("\"op\":\"le\"")); + assert!(json.contains("\"op\":\"gt\"")); + assert!(json.contains("\"op\":\"ge\"")); + assert!(json.contains("\"op\":\"eq\"")); + assert!(json.contains("\"op\":\"ne\"")); + + // 全 ConstValue タイプをチェック + assert!(json.contains("\"value_type\":\"integer\"")); + assert!(json.contains("\"value_type\":\"bool\"")); + assert!(json.contains("\"value_type\":\"string\"")); + assert!(json.contains("\"value_type\":\"null\"")); + + eprintln!("[joinir/json] all_instruction_types test passed"); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index e977c86b..31cdd8c9 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -6,6 +6,7 @@ pub mod identical_exec; pub mod identical_exec_collections; pub mod identical_exec_instance; pub mod identical_exec_string; +pub mod joinir_json_min; // Phase 30.x: JoinIR JSON シリアライズテスト pub mod joinir_runner_min; // Phase 27.2: JoinIR 実行器 A/B 比較テスト pub mod joinir_runner_standalone; // Phase 27-shortterm S-3.2: JoinIR Runner 単体テスト pub mod joinir_vm_bridge_skip_ws; // Phase 27-shortterm S-4.4: JoinIR → Rust VM Bridge A/B Test