Phase 33-3: If/PHI MIR pattern matching + Select lowering (minimal patterns)
Implementation:
- Implement MIR pattern matching in if_select.rs (simple/local patterns)
- Add try_lower_if_to_joinir() entry point in lowering/mod.rs
- Create comprehensive integration tests (4/4 passing)
Pattern Support:
- Simple pattern: if cond { return 1 } else { return 2 }
- Both blocks must have Return only (no instructions)
- Local pattern: if cond { x = a } else { x = b }; return x
- Each branch has exactly 1 Copy instruction
- Both branches jump to same merge block
- Merge block Returns the assigned variable
Safety Mechanisms:
- Dev toggle: NYASH_JOINIR_IF_SELECT=1 required
- Function name filter: Only IfSelectTest.* functions
- Fallback: Returns None on pattern mismatch
- Zero breaking changes: Existing if_phi path untouched
Tests (4/4 PASS):
- test_if_select_simple_pattern
- test_if_select_local_pattern
- test_if_select_disabled_by_default
- test_if_select_wrong_function_name
Files:
- Modified: src/mir/join_ir/lowering/if_select.rs (pattern matching)
- Modified: src/mir/join_ir/lowering/mod.rs (entry point)
- New: src/tests/mir_joinir_if_select.rs (integration tests)
- Modified: src/tests/mod.rs (module registration)
Phase 33-3.2/3.3 (legacy removal) pending
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -3,44 +3,227 @@
|
||||
//! 最小の if/else(副作用なし、単純な値選択)を JoinInst::Select に変換する。
|
||||
|
||||
use crate::mir::join_ir::JoinInst;
|
||||
use crate::mir::{BasicBlockId, MirFunction};
|
||||
use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
|
||||
|
||||
pub struct IfSelectLowerer {
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
/// If/Else パターンの分類
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum IfPatternType {
|
||||
/// Simple pattern: if cond { return 1 } else { return 2 }
|
||||
Simple,
|
||||
/// Local pattern: if cond { x = a } else { x = b }; return x
|
||||
Local,
|
||||
}
|
||||
|
||||
/// 検出された If/Else パターン情報
|
||||
#[derive(Debug, Clone)]
|
||||
struct IfPattern {
|
||||
pattern_type: IfPatternType,
|
||||
cond: ValueId,
|
||||
then_val: ValueId,
|
||||
else_val: ValueId,
|
||||
dst: Option<ValueId>,
|
||||
}
|
||||
|
||||
/// Branch 命令の情報
|
||||
#[derive(Debug, Clone)]
|
||||
struct IfBranch {
|
||||
cond: ValueId,
|
||||
then_block: BasicBlockId,
|
||||
else_block: BasicBlockId,
|
||||
}
|
||||
|
||||
impl IfSelectLowerer {
|
||||
pub fn new(debug: bool) -> Self {
|
||||
Self { debug }
|
||||
}
|
||||
|
||||
/// if/else が Select に lowering できるかチェック
|
||||
pub fn can_lower_to_select(&self, _func: &MirFunction, _if_block_id: BasicBlockId) -> bool {
|
||||
// パターン:
|
||||
// 1. if_block に Branch がある
|
||||
// 2. then/else ブロックが存在
|
||||
// 3. merge ブロックに 1 つの PHI がある
|
||||
// 4. PHI の incoming が then/else から来ている
|
||||
|
||||
// 実装: Phase 33-2 では保守的に false を返す(フォールバック)
|
||||
// Phase 33-3 で実装予定
|
||||
if self.debug {
|
||||
eprintln!("[joinir/if_select] can_lower_to_select: Phase 33-2 stub (always false)");
|
||||
}
|
||||
false
|
||||
pub fn can_lower_to_select(&self, func: &MirFunction, if_block_id: BasicBlockId) -> bool {
|
||||
self.find_if_pattern(func, if_block_id).is_some()
|
||||
}
|
||||
|
||||
/// if/else を Select に変換
|
||||
pub fn lower_if_to_select(
|
||||
&self,
|
||||
_func: &MirFunction,
|
||||
_if_block_id: BasicBlockId,
|
||||
func: &MirFunction,
|
||||
if_block_id: BasicBlockId,
|
||||
) -> Option<JoinInst> {
|
||||
// 実装: Phase 33-2 では None を返す(フォールバック)
|
||||
// Phase 33-3 で実装予定
|
||||
let pattern = self.find_if_pattern(func, if_block_id)?;
|
||||
|
||||
if self.debug {
|
||||
eprintln!("[joinir/if_select] lower_if_to_select: Phase 33-2 stub (always None)");
|
||||
eprintln!(
|
||||
"[IfSelectLowerer] lowering {:?} pattern to Select",
|
||||
pattern.pattern_type
|
||||
);
|
||||
}
|
||||
|
||||
// Select 命令を生成
|
||||
let dst = pattern.dst.unwrap_or(pattern.then_val);
|
||||
|
||||
Some(JoinInst::Select {
|
||||
dst,
|
||||
cond: pattern.cond,
|
||||
then_val: pattern.then_val,
|
||||
else_val: pattern.else_val,
|
||||
})
|
||||
}
|
||||
|
||||
/// MIR 関数から if/else パターンを探す
|
||||
fn find_if_pattern(
|
||||
&self,
|
||||
func: &MirFunction,
|
||||
block_id: BasicBlockId,
|
||||
) -> Option<IfPattern> {
|
||||
// 1. Block が Branch 命令で終わっているか確認
|
||||
let block = func.blocks.get(&block_id)?;
|
||||
let branch = match block.terminator.as_ref()? {
|
||||
MirInstruction::Branch {
|
||||
condition,
|
||||
then_bb,
|
||||
else_bb,
|
||||
} => IfBranch {
|
||||
cond: *condition,
|
||||
then_block: *then_bb,
|
||||
else_block: *else_bb,
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// 2. then/else ブロックの構造を確認
|
||||
let then_block = func.blocks.get(&branch.then_block)?;
|
||||
let else_block = func.blocks.get(&branch.else_block)?;
|
||||
|
||||
// 3. simple パターンのチェック
|
||||
if let Some(pattern) = self.try_match_simple_pattern(&branch, then_block, else_block) {
|
||||
if self.debug {
|
||||
eprintln!("[IfSelectLowerer] matched simple pattern");
|
||||
}
|
||||
return Some(pattern);
|
||||
}
|
||||
|
||||
// 4. local パターンのチェック
|
||||
if let Some(pattern) = self.try_match_local_pattern(func, &branch, then_block, else_block)
|
||||
{
|
||||
if self.debug {
|
||||
eprintln!("[IfSelectLowerer] matched local pattern");
|
||||
}
|
||||
return Some(pattern);
|
||||
}
|
||||
|
||||
if self.debug {
|
||||
eprintln!("[IfSelectLowerer] no pattern matched");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// simple パターン: if cond { return 1 } else { return 2 }
|
||||
fn try_match_simple_pattern(
|
||||
&self,
|
||||
branch: &IfBranch,
|
||||
then_block: &crate::mir::BasicBlock,
|
||||
else_block: &crate::mir::BasicBlock,
|
||||
) -> Option<IfPattern> {
|
||||
// then ブロックが Return だけか確認
|
||||
let then_val = match then_block.terminator.as_ref()? {
|
||||
MirInstruction::Return { value: Some(v) } => *v,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// else ブロックが Return だけか確認
|
||||
let else_val = match else_block.terminator.as_ref()? {
|
||||
MirInstruction::Return { value: Some(v) } => *v,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// 両方のブロックが命令を持たない(Return のみ)ことを確認
|
||||
if !then_block.instructions.is_empty() || !else_block.instructions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(IfPattern {
|
||||
pattern_type: IfPatternType::Simple,
|
||||
cond: branch.cond,
|
||||
then_val,
|
||||
else_val,
|
||||
dst: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// local パターン: if cond { x = a } else { x = b }; return x
|
||||
fn try_match_local_pattern(
|
||||
&self,
|
||||
func: &MirFunction,
|
||||
branch: &IfBranch,
|
||||
then_block: &crate::mir::BasicBlock,
|
||||
else_block: &crate::mir::BasicBlock,
|
||||
) -> Option<IfPattern> {
|
||||
// then ブロックが「1命令 + Jump」の形か確認
|
||||
if then_block.instructions.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// then ブロックの命令が代入(Copy)か確認
|
||||
let (dst_then, val_then) = match &then_block.instructions[0] {
|
||||
MirInstruction::Copy { dst, src } => (*dst, *src),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// then ブロックが Jump で終わるか確認
|
||||
let merge_block_id = match then_block.terminator.as_ref()? {
|
||||
MirInstruction::Jump { target } => *target,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// else ブロックも同じ構造か確認
|
||||
if else_block.instructions.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (dst_else, val_else) = match &else_block.instructions[0] {
|
||||
MirInstruction::Copy { dst, src } => (*dst, *src),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// 代入先が同じ変数か確認
|
||||
if dst_then != dst_else {
|
||||
return None;
|
||||
}
|
||||
|
||||
// else ブロックも同じ merge ブロックに Jump するか確認
|
||||
let else_merge = match else_block.terminator.as_ref()? {
|
||||
MirInstruction::Jump { target } => *target,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
if merge_block_id != else_merge {
|
||||
return None;
|
||||
}
|
||||
|
||||
// merge ブロックが「return dst」だけか確認
|
||||
let merge_block = func.blocks.get(&merge_block_id)?;
|
||||
match merge_block.terminator.as_ref()? {
|
||||
MirInstruction::Return {
|
||||
value: Some(v),
|
||||
} if *v == dst_then => {
|
||||
// OK
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
if !merge_block.instructions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(IfPattern {
|
||||
pattern_type: IfPatternType::Local,
|
||||
cond: branch.cond,
|
||||
then_val: val_then,
|
||||
else_val: val_else,
|
||||
dst: Some(dst_then),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,3 +41,61 @@ pub use skip_ws::lower_skip_ws_to_joinir;
|
||||
pub use stage1_using_resolver::lower_stage1_usingresolver_to_joinir;
|
||||
pub use stageb_body::lower_stageb_body_to_joinir;
|
||||
pub use stageb_funcscanner::lower_stageb_funcscanner_to_joinir;
|
||||
|
||||
// Phase 33: If/Else → Select lowering entry point
|
||||
use crate::mir::join_ir::JoinInst;
|
||||
use crate::mir::{BasicBlockId, MirFunction};
|
||||
|
||||
/// Phase 33-3: Try to lower if/else to JoinIR Select instruction
|
||||
///
|
||||
/// Scope:
|
||||
/// - Only applies to functions matching "IfSelectTest.*"
|
||||
/// - Requires NYASH_JOINIR_IF_SELECT=1 environment variable
|
||||
/// - Falls back to traditional if_phi on pattern mismatch
|
||||
///
|
||||
/// Returns Some(JoinInst::Select) if pattern matched, None otherwise.
|
||||
pub fn try_lower_if_to_joinir(
|
||||
func: &MirFunction,
|
||||
block_id: BasicBlockId,
|
||||
debug: bool,
|
||||
) -> Option<JoinInst> {
|
||||
// dev トグルチェック
|
||||
if !crate::config::env::joinir_if_select_enabled() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 関数名で制限(IfSelectTest.* のみ)
|
||||
if !func.signature.name.starts_with("IfSelectTest.") {
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[try_lower_if_to_joinir] skipping non-test function: {}",
|
||||
func.signature.name
|
||||
);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// if_select lowering を試行
|
||||
let lowerer = if_select::IfSelectLowerer::new(debug);
|
||||
|
||||
if !lowerer.can_lower_to_select(func, block_id) {
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[try_lower_if_to_joinir] pattern not matched for {}",
|
||||
func.signature.name
|
||||
);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
let result = lowerer.lower_if_to_select(func, block_id);
|
||||
|
||||
if result.is_some() && debug {
|
||||
eprintln!(
|
||||
"[try_lower_if_to_joinir] if_select lowering used for {}",
|
||||
func.signature.name
|
||||
);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
232
src/tests/mir_joinir_if_select.rs
Normal file
232
src/tests/mir_joinir_if_select.rs
Normal file
@ -0,0 +1,232 @@
|
||||
//! Phase 33-3: If/Else → Select lowering integration tests
|
||||
//!
|
||||
//! Tests the pattern matching and lowering of if/else to JoinIR Select instruction.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::mir::join_ir::lowering::try_lower_if_to_joinir;
|
||||
use crate::mir::join_ir::JoinInst;
|
||||
use crate::mir::{
|
||||
BasicBlock, BasicBlockId, MirFunction, MirInstruction, MirModule, ValueId,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Helper to create a simple if/else function matching the "simple" pattern
|
||||
fn create_simple_pattern_mir() -> MirFunction {
|
||||
let mut blocks = BTreeMap::new();
|
||||
|
||||
// Entry block (bb0): branch on cond
|
||||
let mut entry = BasicBlock::new(BasicBlockId::new(0));
|
||||
entry.terminator = Some(MirInstruction::Branch {
|
||||
condition: ValueId(0), // cond parameter
|
||||
then_bb: BasicBlockId::new(1),
|
||||
else_bb: BasicBlockId::new(2),
|
||||
});
|
||||
blocks.insert(BasicBlockId::new(0), entry);
|
||||
|
||||
// Then block (bb1): return 10
|
||||
// NOTE: Pattern matcher expects empty blocks (Return only)
|
||||
let mut then_block = BasicBlock::new(BasicBlockId::new(1));
|
||||
then_block.terminator = Some(MirInstruction::Return {
|
||||
value: Some(ValueId(1)), // Assumes ValueId(1) is const 10
|
||||
});
|
||||
blocks.insert(BasicBlockId::new(1), then_block);
|
||||
|
||||
// Else block (bb2): return 20
|
||||
// NOTE: Pattern matcher expects empty blocks (Return only)
|
||||
let mut else_block = BasicBlock::new(BasicBlockId::new(2));
|
||||
else_block.terminator = Some(MirInstruction::Return {
|
||||
value: Some(ValueId(2)), // Assumes ValueId(2) is const 20
|
||||
});
|
||||
blocks.insert(BasicBlockId::new(2), else_block);
|
||||
|
||||
use crate::mir::{EffectMask, MirType};
|
||||
use crate::mir::function::FunctionMetadata;
|
||||
use std::collections::HashMap;
|
||||
|
||||
MirFunction {
|
||||
signature: crate::mir::FunctionSignature {
|
||||
name: "IfSelectTest.test/1".to_string(),
|
||||
params: vec![MirType::Unknown],
|
||||
return_type: MirType::Integer,
|
||||
effects: EffectMask::PURE,
|
||||
},
|
||||
entry_block: BasicBlockId::new(0),
|
||||
blocks: blocks.into_iter().collect(),
|
||||
locals: vec![],
|
||||
params: vec![ValueId(0)],
|
||||
next_value_id: 3,
|
||||
metadata: FunctionMetadata::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create a local pattern function
|
||||
fn create_local_pattern_mir() -> MirFunction {
|
||||
let mut blocks = BTreeMap::new();
|
||||
|
||||
// Entry block (bb0): branch on cond
|
||||
let mut entry = BasicBlock::new(BasicBlockId::new(0));
|
||||
entry.terminator = Some(MirInstruction::Branch {
|
||||
condition: ValueId(0), // cond
|
||||
then_bb: BasicBlockId::new(1),
|
||||
else_bb: BasicBlockId::new(2),
|
||||
});
|
||||
blocks.insert(BasicBlockId::new(0), entry);
|
||||
|
||||
// Then block (bb1): x = 100; jump merge
|
||||
// NOTE: Pattern matcher expects exactly 1 Copy instruction
|
||||
let mut then_block = BasicBlock::new(BasicBlockId::new(1));
|
||||
then_block.instructions.push(MirInstruction::Copy {
|
||||
dst: ValueId(3), // x
|
||||
src: ValueId(10), // Assumes ValueId(10) is const 100
|
||||
});
|
||||
then_block.terminator = Some(MirInstruction::Jump {
|
||||
target: BasicBlockId::new(3),
|
||||
});
|
||||
blocks.insert(BasicBlockId::new(1), then_block);
|
||||
|
||||
// Else block (bb2): x = 200; jump merge
|
||||
// NOTE: Pattern matcher expects exactly 1 Copy instruction
|
||||
let mut else_block = BasicBlock::new(BasicBlockId::new(2));
|
||||
else_block.instructions.push(MirInstruction::Copy {
|
||||
dst: ValueId(3), // x
|
||||
src: ValueId(20), // Assumes ValueId(20) is const 200
|
||||
});
|
||||
else_block.terminator = Some(MirInstruction::Jump {
|
||||
target: BasicBlockId::new(3),
|
||||
});
|
||||
blocks.insert(BasicBlockId::new(2), else_block);
|
||||
|
||||
// Merge block (bb3): return x
|
||||
let mut merge_block = BasicBlock::new(BasicBlockId::new(3));
|
||||
merge_block.terminator = Some(MirInstruction::Return {
|
||||
value: Some(ValueId(3)),
|
||||
});
|
||||
blocks.insert(BasicBlockId::new(3), merge_block);
|
||||
|
||||
use crate::mir::{EffectMask, MirType};
|
||||
use crate::mir::function::FunctionMetadata;
|
||||
use std::collections::HashMap;
|
||||
|
||||
MirFunction {
|
||||
signature: crate::mir::FunctionSignature {
|
||||
name: "IfSelectTest.main/0".to_string(),
|
||||
params: vec![],
|
||||
return_type: MirType::Integer,
|
||||
effects: EffectMask::PURE,
|
||||
},
|
||||
entry_block: BasicBlockId::new(0),
|
||||
blocks: blocks.into_iter().collect(),
|
||||
locals: vec![],
|
||||
params: vec![],
|
||||
next_value_id: 21,
|
||||
metadata: FunctionMetadata::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if_select_simple_pattern() {
|
||||
// Set environment variable for this test
|
||||
std::env::set_var("NYASH_JOINIR_IF_SELECT", "1");
|
||||
|
||||
let func = create_simple_pattern_mir();
|
||||
let entry_block = func.entry_block;
|
||||
|
||||
// Try to lower to JoinIR
|
||||
let result = try_lower_if_to_joinir(&func, entry_block, true);
|
||||
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"Expected simple pattern to be lowered to Select"
|
||||
);
|
||||
|
||||
if let Some(JoinInst::Select {
|
||||
dst,
|
||||
cond,
|
||||
then_val,
|
||||
else_val,
|
||||
}) = result
|
||||
{
|
||||
eprintln!("✅ Simple pattern successfully lowered to Select");
|
||||
eprintln!(" dst: {:?}, cond: {:?}, then: {:?}, else: {:?}", dst, cond, then_val, else_val);
|
||||
} else {
|
||||
panic!("Expected JoinInst::Select, got {:?}", result);
|
||||
}
|
||||
|
||||
// Clean up
|
||||
std::env::remove_var("NYASH_JOINIR_IF_SELECT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if_select_local_pattern() {
|
||||
std::env::set_var("NYASH_JOINIR_IF_SELECT", "1");
|
||||
|
||||
let func = create_local_pattern_mir();
|
||||
let entry_block = func.entry_block;
|
||||
|
||||
// Try to lower to JoinIR
|
||||
let result = try_lower_if_to_joinir(&func, entry_block, true);
|
||||
|
||||
assert!(
|
||||
result.is_some(),
|
||||
"Expected local pattern to be lowered to Select"
|
||||
);
|
||||
|
||||
if let Some(JoinInst::Select {
|
||||
dst,
|
||||
cond,
|
||||
then_val,
|
||||
else_val,
|
||||
}) = result
|
||||
{
|
||||
eprintln!("✅ Local pattern successfully lowered to Select");
|
||||
eprintln!(" dst: {:?}, cond: {:?}, then: {:?}, else: {:?}", dst, cond, then_val, else_val);
|
||||
} else {
|
||||
panic!("Expected JoinInst::Select, got {:?}", result);
|
||||
}
|
||||
|
||||
std::env::remove_var("NYASH_JOINIR_IF_SELECT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if_select_disabled_by_default() {
|
||||
// Ensure environment variable is NOT set
|
||||
std::env::remove_var("NYASH_JOINIR_IF_SELECT");
|
||||
|
||||
let func = create_simple_pattern_mir();
|
||||
let entry_block = func.entry_block;
|
||||
|
||||
// Should return None when disabled
|
||||
let result = try_lower_if_to_joinir(&func, entry_block, false);
|
||||
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"Expected None when NYASH_JOINIR_IF_SELECT is not set"
|
||||
);
|
||||
|
||||
eprintln!("✅ If/Select lowering correctly disabled by default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if_select_wrong_function_name() {
|
||||
std::env::set_var("NYASH_JOINIR_IF_SELECT", "1");
|
||||
|
||||
// Create function with wrong name (not IfSelectTest.*)
|
||||
let mut func = create_simple_pattern_mir();
|
||||
func.signature.name = "WrongName.test/1".to_string();
|
||||
|
||||
let entry_block = func.entry_block;
|
||||
|
||||
// Should return None for non-IfSelectTest functions
|
||||
let result = try_lower_if_to_joinir(&func, entry_block, true);
|
||||
|
||||
assert!(
|
||||
result.is_none(),
|
||||
"Expected None for non-IfSelectTest functions"
|
||||
);
|
||||
|
||||
eprintln!("✅ Function name filter working correctly");
|
||||
|
||||
std::env::remove_var("NYASH_JOINIR_IF_SELECT");
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,7 @@ pub mod mir_funcscanner_ssa;
|
||||
pub mod mir_funcscanner_trim_min;
|
||||
pub mod mir_joinir_funcscanner_append_defs; // Phase 27.14: FuncScannerBox._append_defs JoinIR変換
|
||||
pub mod mir_joinir_funcscanner_trim; // Phase 27.1: FuncScannerBox.trim JoinIR変換
|
||||
pub mod mir_joinir_if_select; // Phase 33-3: If/Else → Select lowering tests
|
||||
pub mod mir_joinir_min; // Phase 26-H: JoinIR型定義妥当性確認
|
||||
pub mod mir_joinir_skip_ws; // Phase 27.0: minimal_ssa_skip_ws JoinIR変換
|
||||
pub mod mir_joinir_stage1_using_resolver_min; // Phase 27.12: Stage1UsingResolverBox.resolve_for_source JoinIR変換
|
||||
|
||||
Reference in New Issue
Block a user