feat(joinir): Phase 52-53 LoopFrontendBinding JSON + Statement Handlers

Phase 52: LoopFrontendBinding JSON generation fixes
- Add receiver_to_json() for Field node structure (me.tokens)
- Add needs_me_receiver() for instance method detection
- Fix "condition" → "cond" key for JoinIR Frontend
- Add me parameter propagation in loop_patterns.rs
- Add JoinIR-compatible type fields in ast_json.rs
  - Variable → "type": "Var"
  - Literal → "type": "Int"/"Bool" (literal_to_joinir_json)
  - BinaryOp → "type": "Binary"/"Compare" (is_compare_op)
  - MethodCall → "type": "Method"

Phase 53: Statement Handler module for loop body
- NEW: stmt_handlers.rs with StatementEffect type
- Support: Local, Assignment, Print, Method, If statements
- If lowering: single variable update → Select instruction
- Remove hardcoded assert in loop_patterns.rs
- Replace with generic lower_statement() calls

Test results: 56 JoinIR tests PASS, 7 loop_frontend_binding tests PASS

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-11-29 04:42:16 +09:00
parent 6bb6f38a1c
commit e27934d91a
6 changed files with 545 additions and 53 deletions

View File

@ -79,19 +79,36 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
"static": is_static,
"override": is_override,
}),
ASTNode::Variable { name, .. } => json!({"kind":"Variable","name":name}),
ASTNode::Literal { value, .. } => json!({"kind":"Literal","value": lit_to_json(&value)}),
// Phase 52: Variable → Var ードJoinIR Frontend 互換)
ASTNode::Variable { name, .. } => json!({
"kind": "Variable",
"type": "Var", // JoinIR Frontend expects "type": "Var"
"name": name
}),
// Phase 52: Literal → Int/Bool/String ードJoinIR Frontend 互換)
ASTNode::Literal { value, .. } => literal_to_joinir_json(&value),
// Phase 52: BinaryOp → Binary/Compare ードJoinIR Frontend 互換)
ASTNode::BinaryOp {
operator,
left,
right,
..
} => json!({
} => {
let op_str = bin_to_str(&operator);
// JoinIR Frontend distinguishes between Binary (arithmetic) and Compare
let type_str = if is_compare_op(&operator) { "Compare" } else { "Binary" };
json!({
"kind": "BinaryOp",
"op": bin_to_str(&operator),
"type": type_str,
"op": op_str,
// JoinIR Frontend expects "lhs"/"rhs" not "left"/"right"
"lhs": ast_to_json(&left),
"rhs": ast_to_json(&right),
// Also keep "left"/"right" for backward compatibility
"left": ast_to_json(&left),
"right": ast_to_json(&right),
}),
})
}
ASTNode::UnaryOp {
operator, operand, ..
} => json!({
@ -99,6 +116,7 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
"op": un_to_str(&operator),
"operand": ast_to_json(&operand),
}),
// Phase 52: MethodCall → Method ードJoinIR Frontend 互換)
ASTNode::MethodCall {
object,
method,
@ -106,9 +124,14 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
..
} => json!({
"kind": "MethodCall",
"object": ast_to_json(&object),
"type": "Method", // JoinIR Frontend expects "type": "Method"
// JoinIR Frontend expects "receiver" not "object"
"receiver": ast_to_json(&object),
"object": ast_to_json(&object), // Keep for backward compatibility
"method": method,
"arguments": arguments.into_iter().map(|a| ast_to_json(&a)).collect::<Vec<_>>()
// JoinIR Frontend expects "args" not "arguments"
"args": arguments.iter().map(|a| ast_to_json(a)).collect::<Vec<_>>(),
"arguments": arguments.into_iter().map(|a| ast_to_json(&a)).collect::<Vec<_>>() // Keep for backward compatibility
}),
ASTNode::FunctionCall {
name, arguments, ..
@ -142,6 +165,28 @@ pub fn ast_to_json(ast: &ASTNode) -> Value {
})).collect::<Vec<_>>(),
"else": ast_to_json(&else_expr),
}),
// Phase 52: FieldAccess → Field ードJoinIR Frontend 互換)
ASTNode::FieldAccess { object, field, .. } => json!({
"kind": "FieldAccess",
"type": "Field", // JoinIR Frontend expects "type": "Field"
"object": ast_to_json(&object),
"field": field
}),
// Phase 52: Me → Var("me") ードJoinIR Frontend 互換)
ASTNode::Me { .. } => json!({
"kind": "Me",
"type": "Var", // JoinIR Frontend expects "type": "Var"
"name": "me"
}),
// Phase 52: New → NewBox ードJoinIR Frontend 互換)
ASTNode::New {
class, arguments, ..
} => json!({
"kind": "New",
"type": "NewBox", // JoinIR Frontend expects "type": "NewBox"
"box_name": class,
"args": arguments.into_iter().map(|a| ast_to_json(&a)).collect::<Vec<_>>()
}),
other => json!({"kind":"Unsupported","debug": format!("{:?}", other)}),
}
}
@ -459,3 +504,55 @@ fn str_to_un(s: &str) -> Option<UnaryOperator> {
_ => return None,
})
}
/// Phase 52: Check if a binary operator is a comparison operator
fn is_compare_op(op: &BinaryOperator) -> bool {
matches!(
op,
BinaryOperator::Equal
| BinaryOperator::NotEqual
| BinaryOperator::Less
| BinaryOperator::Greater
| BinaryOperator::LessEqual
| BinaryOperator::GreaterEqual
)
}
/// Phase 52: Convert a literal value to JoinIR-compatible JSON format
///
/// JoinIR Frontend expects:
/// - Integer: {"type": "Int", "value": <number>}
/// - Boolean: {"type": "Bool", "value": <bool>}
/// - String: {"type": "String", "value": <string>}
fn literal_to_joinir_json(v: &LiteralValue) -> Value {
match v {
LiteralValue::Integer(i) => json!({
"kind": "Literal",
"type": "Int", // JoinIR Frontend expects "type": "Int"
"value": i
}),
LiteralValue::Bool(b) => json!({
"kind": "Literal",
"type": "Bool", // JoinIR Frontend expects "type": "Bool"
"value": b
}),
LiteralValue::String(s) => json!({
"kind": "Literal",
"type": "String",
"value": s
}),
LiteralValue::Float(f) => json!({
"kind": "Literal",
"type": "Float",
"value": f
}),
LiteralValue::Null => json!({
"kind": "Literal",
"type": "Null"
}),
LiteralValue::Void => json!({
"kind": "Literal",
"type": "Void"
}),
}
}

View File

@ -173,6 +173,17 @@ impl super::MirBuilder {
// Phase 50: Generate Local declarations from binding
let (i_local, acc_local, n_local) = binding.generate_local_declarations();
// Phase 52: Check if `me` receiver is needed
// Instance methods (like print_tokens) need `me` to be passed as a parameter
let params: Vec<serde_json::Value> = if binding.needs_me_receiver() {
if debug {
eprintln!("[cf_loop/joinir] Adding 'me' to params (instance method)");
}
vec![serde_json::json!("me")]
} else {
vec![]
};
// Step 2: Construct JSON v0 format with "defs" array
// The function is named "simple" to match JoinIR Frontend's pattern matching
// Phase 50: Include i/acc/n Local declarations to satisfy JoinIR Frontend expectations
@ -180,7 +191,7 @@ impl super::MirBuilder {
"defs": [
{
"name": "simple",
"params": [],
"params": params,
"body": {
"type": "Block",
"body": [
@ -190,7 +201,7 @@ impl super::MirBuilder {
n_local,
{
"type": "Loop",
"condition": condition_json,
"cond": condition_json, // JoinIR Frontend expects "cond" not "condition"
"body": body_json
},
// Return the accumulator (or null for side-effect loops)

View File

@ -71,6 +71,16 @@ pub enum LoopPattern {
}
impl LoopFrontendBinding {
/// Phase 52: Check if `me` receiver is needed for this loop
///
/// Returns true if any external_ref starts with "me" (e.g., "me.tokens")
/// This indicates an instance method that needs access to `me`.
pub fn needs_me_receiver(&self) -> bool {
self.external_refs
.iter()
.any(|r| r == "me" || r.starts_with("me."))
}
/// print_tokens 専用のバインディングを生成
///
/// print_tokens の構造:
@ -135,6 +145,44 @@ impl LoopFrontendBinding {
None
}
/// Phase 52: ドット区切りの変数名を Field ノード構造に変換
///
/// 例: "me.tokens" → {"type": "Field", "object": {"type": "Var", "name": "me"}, "field": "tokens"}
/// 例: "arr" → {"type": "Var", "name": "arr"}
fn receiver_to_json(receiver: &str) -> serde_json::Value {
use serde_json::json;
if let Some(dot_pos) = receiver.find('.') {
// ドット区切りがある場合 → Field ノードに分解
let object_name = &receiver[..dot_pos];
let field_name = &receiver[dot_pos + 1..];
// ネストしたフィールドアクセス (e.g., "a.b.c") は再帰的に処理
if field_name.contains('.') {
// "me.tokens.inner" → Field(Field(Var("me"), "tokens"), "inner")
let inner_receiver = Self::receiver_to_json(field_name);
json!({
"type": "Field",
"object": { "type": "Var", "name": object_name },
"field": inner_receiver
})
} else {
// 単一レベルのフィールドアクセス (e.g., "me.tokens")
json!({
"type": "Field",
"object": { "type": "Var", "name": object_name },
"field": field_name
})
}
} else {
// ドットなし → 単純な Var ノード
json!({
"type": "Var",
"name": receiver
})
}
}
/// JoinIR Frontend 用の JSON v0 Local 宣言を生成
///
/// Returns: (i_local, acc_local, n_local) の JSON Value タプル
@ -142,6 +190,7 @@ impl LoopFrontendBinding {
/// Note: JoinIR Frontend expects specific type names:
/// - "Int" for integer literals (with "value" field)
/// - "Var" for variable references (with "name" field)
/// - "Field" for field access (with "object", "field" fields) - Phase 52
/// - "Method" for method calls (with "receiver", "method", "args" fields)
/// - "NewBox" for box instantiation
pub fn generate_local_declarations(
@ -201,14 +250,15 @@ impl LoopFrontendBinding {
})
}
BoundExpr::MethodCall { receiver, method } => {
// メソッド呼び出しを評価print_tokens の me.tokens.length() 等)
// JoinIR Frontend expects "Method" type
// Phase 52: メソッド呼び出しを評価print_tokens の me.tokens.length() 等)
// receiver が "me.tokens" のようにドット区切りの場合は Field ノードに分解
let receiver_json = Self::receiver_to_json(receiver);
json!({
"type": "Local",
"name": "n",
"expr": {
"type": "Method",
"receiver": { "type": "Var", "name": receiver },
"receiver": receiver_json,
"method": method,
"args": []
}
@ -349,4 +399,40 @@ mod tests {
assert_eq!(n_local["expr"]["type"], "Var");
assert_eq!(n_local["expr"]["name"], "n");
}
// Phase 52: receiver_to_json のテスト
#[test]
fn test_receiver_to_json_simple_var() {
let json = LoopFrontendBinding::receiver_to_json("arr");
assert_eq!(json["type"], "Var");
assert_eq!(json["name"], "arr");
}
#[test]
fn test_receiver_to_json_field_access() {
let json = LoopFrontendBinding::receiver_to_json("me.tokens");
assert_eq!(json["type"], "Field");
assert_eq!(json["object"]["type"], "Var");
assert_eq!(json["object"]["name"], "me");
assert_eq!(json["field"], "tokens");
}
#[test]
fn test_print_tokens_n_local_has_field() {
// Phase 52: print_tokens の n (me.tokens.length()) が Field ノードを使っているか確認
let binding = LoopFrontendBinding::for_print_tokens();
let (_i_local, _acc_local, n_local) = binding.generate_local_declarations();
// n = me.tokens.length() の構造を確認
assert_eq!(n_local["name"], "n");
assert_eq!(n_local["expr"]["type"], "Method");
assert_eq!(n_local["expr"]["method"], "length");
// receiver が Field ノードであることを確認
let receiver = &n_local["expr"]["receiver"];
assert_eq!(receiver["type"], "Field");
assert_eq!(receiver["object"]["type"], "Var");
assert_eq!(receiver["object"]["name"], "me");
assert_eq!(receiver["field"], "tokens");
}
}

View File

@ -220,12 +220,22 @@ impl AstToJoinIrLowerer {
let acc_init = ctx.get_var("acc").expect("acc must be initialized");
let n_param = ctx.get_var("n").expect("n must be parameter");
// Phase 52: Get me from context if it was registered as a param
let me_param = ctx.get_var("me");
let loop_result = ctx.alloc_var();
// Phase 52: Include me in args when present
let entry_call_args = if let Some(me_id) = me_param {
vec![me_id, i_init, acc_init, n_param]
} else {
vec![i_init, acc_init, n_param]
};
let mut entry_body = init_insts;
entry_body.push(JoinInst::Call {
func: loop_step_id,
args: vec![i_init, acc_init, n_param],
args: entry_call_args,
k_next: None,
dst: Some(loop_result),
});
@ -244,11 +254,34 @@ impl AstToJoinIrLowerer {
};
// loop_step 関数: (i, acc, n) → Jump(k_exit, cond=!(i<n)) → body → Call(loop_step)
let step_i = crate::mir::ValueId(0);
let step_acc = crate::mir::ValueId(1);
let step_n = crate::mir::ValueId(2);
// Phase 52: Check if "me" is present in original params (for instance methods)
let has_me = params
.iter()
.any(|p| p.as_str() == Some("me"));
let mut step_ctx = ExtractCtx::new(3);
// Adjust ValueIds based on whether "me" is present
// If "me" is present, it takes slot 0, and i/acc/n shift to 1/2/3
let (step_i, step_acc, step_n, step_me) = if has_me {
(
crate::mir::ValueId(1),
crate::mir::ValueId(2),
crate::mir::ValueId(3),
Some(crate::mir::ValueId(0)),
)
} else {
(
crate::mir::ValueId(0),
crate::mir::ValueId(1),
crate::mir::ValueId(2),
None,
)
};
let num_step_params = if has_me { 4 } else { 3 };
let mut step_ctx = ExtractCtx::new(num_step_params);
if let Some(me_id) = step_me {
step_ctx.register_param("me".to_string(), me_id);
}
step_ctx.register_param("i".to_string(), step_i);
step_ctx.register_param("acc".to_string(), step_acc);
step_ctx.register_param("n".to_string(), step_n);
@ -281,23 +314,13 @@ impl AstToJoinIrLowerer {
cond: Some(exit_cond),
});
// loop body を処理(Jump で抜けなかった場合のみ実行される
// Phase 53: loop body を処理(汎用 statement handler を使用
// Jump で抜けなかった場合のみ実行される
for body_stmt in loop_body_stmts {
assert_eq!(
body_stmt["type"].as_str(),
Some("Local"),
"Loop body must contain only Local statements"
);
let var_name = body_stmt["name"]
.as_str()
.expect("Local must have 'name'")
.to_string();
let expr = &body_stmt["expr"];
let (var_id, insts) = self.extract_value(expr, &mut step_ctx);
let (insts, _effect) = self.lower_statement(body_stmt, &mut step_ctx);
loop_step_body.extend(insts);
step_ctx.register_param(var_name, var_id);
// Note: lower_statement 内で ctx.register_param() が呼ばれるため、
// ここでの追加登録は不要
}
// body 処理後の i_next, acc_next を取得
@ -309,10 +332,17 @@ impl AstToJoinIrLowerer {
.expect("acc must be updated in loop body");
// loop_step を再帰的に Call末尾再帰
// Phase 52: Include me in args when present
let recurse_args = if let Some(me_id) = step_me {
vec![me_id, i_next, acc_next, step_n]
} else {
vec![i_next, acc_next, step_n]
};
let recurse_result = step_ctx.alloc_var();
loop_step_body.push(JoinInst::Call {
func: loop_step_id,
args: vec![i_next, acc_next, step_n],
args: recurse_args,
k_next: None,
dst: Some(recurse_result),
});
@ -321,10 +351,17 @@ impl AstToJoinIrLowerer {
value: Some(recurse_result),
});
// Phase 52: Include me in params when present
let loop_step_params = if let Some(me_id) = step_me {
vec![me_id, step_i, step_acc, step_n]
} else {
vec![step_i, step_acc, step_n]
};
let loop_step_func = JoinFunction {
id: loop_step_id,
name: format!("{}_loop_step", func_name),
params: vec![step_i, step_acc, step_n],
params: loop_step_params,
body: loop_step_body,
exit_cont: None,
};
@ -495,28 +532,16 @@ impl AstToJoinIrLowerer {
cond: Some(break_cond_var),
});
// loop body を処理(Break の後の Local 命令群
// Phase 53: loop body を処理(汎用 statement handler を使用
// Break の後のステートメント群
for body_stmt in loop_body {
// If + Break はスキップJump で処理済み)
if body_stmt["type"].as_str() == Some("If") {
continue;
}
assert_eq!(
body_stmt["type"].as_str(),
Some("Local"),
"Loop body must contain only Local statements after If"
);
let var_name = body_stmt["name"]
.as_str()
.expect("Local must have 'name'")
.to_string();
let expr = &body_stmt["expr"];
let (var_id, insts) = self.extract_value(expr, &mut step_ctx);
let (insts, _effect) = self.lower_statement(body_stmt, &mut step_ctx);
loop_step_body.extend(insts);
step_ctx.register_param(var_name, var_id);
}
// body 処理後の i_next, acc_next を取得

View File

@ -32,6 +32,7 @@ mod if_return;
mod loop_patterns;
mod nested_if;
mod read_quoted;
mod stmt_handlers;
#[cfg(test)]
mod tests;

View File

@ -0,0 +1,272 @@
//! Phase 53: Statement Handler モジュール
//!
//! ループ本体の各ステートメントタイプを JoinIR に変換する。
//!
//! ## 対応ステートメント
//!
//! - `Local`: 変数宣言 + 初期化
//! - `Assignment`: 変数への代入
//! - `Print`: 出力(副作用)
//! - `If`: 条件分岐
//! - `Method`: メソッド呼び出し(式文として)
use super::{AstToJoinIrLowerer, ExtractCtx, JoinInst};
use crate::mir::join_ir::MirLikeInst;
use crate::mir::ValueId;
/// ステートメントの効果(変数更新 or 副作用のみ)
#[derive(Debug, Clone)]
pub enum StatementEffect {
/// 変数を更新Assignment/Local
VarUpdate { name: String, value_id: ValueId },
/// 副作用のみPrint
SideEffect,
/// 効果なし(空の If など)
None,
}
impl AstToJoinIrLowerer {
/// Phase 53: ステートメントを JoinIR に変換
///
/// 対応タイプ: Local, Assignment, Print, If, Method
pub fn lower_statement(
&mut self,
stmt: &serde_json::Value,
ctx: &mut ExtractCtx,
) -> (Vec<JoinInst>, StatementEffect) {
let stmt_type = stmt["type"]
.as_str()
.expect("Statement must have 'type' field");
match stmt_type {
"Local" => self.lower_local_stmt(stmt, ctx),
"Assignment" => self.lower_assignment_stmt(stmt, ctx),
"Print" => self.lower_print_stmt(stmt, ctx),
"Method" => self.lower_method_stmt(stmt, ctx),
"If" => self.lower_if_stmt_in_loop(stmt, ctx),
other => panic!(
"Unsupported statement type in loop body: {}. \
Expected: Local, Assignment, Print, Method, If",
other
),
}
}
/// Local ステートメント: `local x = expr`
fn lower_local_stmt(
&mut self,
stmt: &serde_json::Value,
ctx: &mut ExtractCtx,
) -> (Vec<JoinInst>, StatementEffect) {
let var_name = stmt["name"]
.as_str()
.expect("Local must have 'name'")
.to_string();
let expr = &stmt["expr"];
let (value_id, insts) = self.extract_value(expr, ctx);
ctx.register_param(var_name.clone(), value_id);
(
insts,
StatementEffect::VarUpdate {
name: var_name,
value_id,
},
)
}
/// Assignment ステートメント: `x = expr`
///
/// Phase 53-2: `i = i + 1` などの代入文を処理
fn lower_assignment_stmt(
&mut self,
stmt: &serde_json::Value,
ctx: &mut ExtractCtx,
) -> (Vec<JoinInst>, StatementEffect) {
let target = stmt["target"]
.as_str()
.expect("Assignment must have 'target'")
.to_string();
let expr = &stmt["expr"];
let (value_id, insts) = self.extract_value(expr, ctx);
ctx.register_param(target.clone(), value_id);
(
insts,
StatementEffect::VarUpdate {
name: target,
value_id,
},
)
}
/// Print ステートメント: `print(expr)`
///
/// Phase 53-3: ConsoleBox.print 呼び出しに変換
fn lower_print_stmt(
&mut self,
stmt: &serde_json::Value,
ctx: &mut ExtractCtx,
) -> (Vec<JoinInst>, StatementEffect) {
let expr = &stmt["expr"];
let (arg_id, mut insts) = self.extract_value(expr, ctx);
// print は BoxCall として実装ConsoleBox.print
let result_id = ctx.alloc_var();
insts.push(JoinInst::Compute(MirLikeInst::BoxCall {
dst: Some(result_id),
box_name: "ConsoleBox".to_string(),
method: "print".to_string(),
args: vec![arg_id],
}));
(insts, StatementEffect::SideEffect)
}
/// Method ステートメント(式文として): `obj.method(args)`
///
/// 戻り値を捨てるメソッド呼び出しprint など)
fn lower_method_stmt(
&mut self,
stmt: &serde_json::Value,
ctx: &mut ExtractCtx,
) -> (Vec<JoinInst>, StatementEffect) {
// extract_value で Method を評価(戻り値は捨てる)
let (_, insts) = self.extract_value(stmt, ctx);
(insts, StatementEffect::SideEffect)
}
/// If ステートメント(ループ内): `if cond { then } else { else }`
///
/// Phase 53-4: ガードパターン対応
///
/// 単純な変数更新のみの場合は Select 命令に変換し、
/// 複雑な場合はパニックPhase 54 で対応予定)
fn lower_if_stmt_in_loop(
&mut self,
stmt: &serde_json::Value,
ctx: &mut ExtractCtx,
) -> (Vec<JoinInst>, StatementEffect) {
let cond_expr = &stmt["cond"];
let then_body = stmt["then"].as_array();
let else_body = stmt["else"].as_array();
// 条件を評価
let (cond_id, mut insts) = self.extract_value(cond_expr, ctx);
// 単純なケース: then/else が空または単一の変数更新
let then_stmts = then_body.map(|v| v.as_slice()).unwrap_or(&[]);
let else_stmts = else_body.map(|v| v.as_slice()).unwrap_or(&[]);
// ケース 1: 空の If条件チェックのみ
if then_stmts.is_empty() && else_stmts.is_empty() {
return (insts, StatementEffect::None);
}
// ケース 2: 単一変数更新 → Select に変換
if let (Some(then_update), None) =
(Self::extract_single_var_update(then_stmts), Self::extract_single_var_update(else_stmts))
{
// then のみ更新、else は元の値を維持
let (var_name, then_expr) = then_update;
// then の式を評価
let (then_val, then_insts) = self.extract_value(then_expr, ctx);
insts.extend(then_insts);
// else は元の値
let else_val = ctx
.get_var(&var_name)
.expect(&format!("Variable '{}' must exist for If/else", var_name));
// Select: cond ? then_val : else_val
let result_id = ctx.alloc_var();
insts.push(JoinInst::Select {
dst: result_id,
cond: cond_id,
then_val,
else_val,
});
ctx.register_param(var_name.clone(), result_id);
return (
insts,
StatementEffect::VarUpdate {
name: var_name,
value_id: result_id,
},
);
}
// ケース 3: 両方に単一変数更新(同じ変数)
if let (Some((then_name, then_expr)), Some((else_name, else_expr))) = (
Self::extract_single_var_update(then_stmts),
Self::extract_single_var_update(else_stmts),
) {
if then_name == else_name {
let (then_val, then_insts) = self.extract_value(then_expr, ctx);
insts.extend(then_insts);
let (else_val, else_insts) = self.extract_value(else_expr, ctx);
insts.extend(else_insts);
let result_id = ctx.alloc_var();
insts.push(JoinInst::Select {
dst: result_id,
cond: cond_id,
then_val,
else_val,
});
ctx.register_param(then_name.clone(), result_id);
return (
insts,
StatementEffect::VarUpdate {
name: then_name,
value_id: result_id,
},
);
}
}
// ケース 4: 複雑なケースPhase 54 で対応)
panic!(
"Complex If statement in loop body not yet supported (Phase 54). \
then: {} stmts, else: {} stmts",
then_stmts.len(),
else_stmts.len()
);
}
/// ステートメント配列から単一の変数更新を抽出
///
/// Returns: Some((変数名, 式)) if 単一の Local/Assignment
fn extract_single_var_update(
stmts: &[serde_json::Value],
) -> Option<(String, &serde_json::Value)> {
if stmts.len() != 1 {
return None;
}
let stmt = &stmts[0];
let stmt_type = stmt["type"].as_str()?;
match stmt_type {
"Local" => {
let name = stmt["name"].as_str()?.to_string();
let expr = &stmt["expr"];
Some((name, expr))
}
"Assignment" => {
let name = stmt["target"].as_str()?.to_string();
let expr = &stmt["expr"];
Some((name, expr))
}
_ => None,
}
}
}