feat(joinir): Phase 30.x jsonir v0 snapshot tests

Implement JSON snapshot testing for JoinIR regression detection.

Implementation:
- tests/fixtures/joinir/v0_*.jsonir: 6 snapshot fixtures for minimal JoinIR cases
- src/tests/joinir_json_min.rs: Added 6 snapshot comparison tests

Fixtures:
- v0_skip_ws_min.jsonir (1338 bytes)
- v0_funcscanner_trim_min.jsonir (4484 bytes)
- v0_funcscanner_append_defs_min.jsonir (988 bytes)
- v0_stage1_usingresolver_min.jsonir (987 bytes)
- v0_stageb_body_min.jsonir (1074 bytes)
- v0_stageb_funcscanner_min.jsonir (962 bytes)

Usage:
  # Run snapshot tests
  NYASH_JOINIR_SNAPSHOT_TEST=1 cargo test --release joinir_json_v0_

  # Regenerate fixtures (after intentional changes)
  NYASH_JOINIR_SNAPSHOT_TEST=1 NYASH_JOINIR_SNAPSHOT_GENERATE=1 cargo test --release joinir_json_v0_

Purpose:
- Fix current JoinIR shape as v0 baseline (pre-generic state)
- Detect unintended regressions during genericization
- Allow intentional fixture updates when design changes

🤖 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-25 10:19:02 +09:00
parent e9c7d27a7f
commit de16ff9b7f
8 changed files with 221 additions and 1 deletions

View File

@ -230,3 +230,217 @@ fn test_all_instruction_types_json() {
eprintln!("[joinir/json] all_instruction_types test passed");
}
// ============================================================================
// Phase 30.x: jsonir v0 スナップショットテスト
// ============================================================================
//
// 目的:
// - 現状の JoinIR 形pre-generic な generic_case_a / case-specific lowering
// v0_ フィクスチャとして固定する
// - JoinIR/LoopScopeShape の汎用化中に意図しない退行を検知する
//
// 実行方法:
// NYASH_JOINIR_SNAPSHOT_TEST=1 cargo test --release joinir_json_v0_ -- --nocapture
//
// フィクスチャ初期生成:
// NYASH_JOINIR_SNAPSHOT_GENERATE=1 cargo test --release joinir_json_v0_ -- --nocapture
use crate::ast::ASTNode;
use crate::mir::join_ir::lowering::{
lower_funcscanner_append_defs_to_joinir, lower_funcscanner_trim_to_joinir,
lower_skip_ws_to_joinir, lower_stage1_usingresolver_to_joinir, lower_stageb_body_to_joinir,
lower_stageb_funcscanner_to_joinir,
};
use crate::mir::MirCompiler;
use crate::parser::NyashParser;
/// フィクスチャファイルのベースディレクトリ
const FIXTURE_DIR: &str = "tests/fixtures/joinir";
/// v0 スナップショットのケース定義
#[derive(Debug, Clone, Copy)]
enum SnapshotCase {
SkipWsMin,
FuncscannerTrimMin,
FuncscannerAppendDefsMin,
Stage1UsingresolverMin,
StagebBodyMin,
StagebFuncscannerMin,
}
impl SnapshotCase {
fn fixture_filename(&self) -> &'static str {
match self {
Self::SkipWsMin => "v0_skip_ws_min.jsonir",
Self::FuncscannerTrimMin => "v0_funcscanner_trim_min.jsonir",
Self::FuncscannerAppendDefsMin => "v0_funcscanner_append_defs_min.jsonir",
Self::Stage1UsingresolverMin => "v0_stage1_usingresolver_min.jsonir",
Self::StagebBodyMin => "v0_stageb_body_min.jsonir",
Self::StagebFuncscannerMin => "v0_stageb_funcscanner_min.jsonir",
}
}
fn source_file(&self) -> &'static str {
match self {
Self::SkipWsMin => "apps/tests/minimal_ssa_skip_ws.hako",
Self::FuncscannerTrimMin => "lang/src/compiler/tests/funcscanner_trim_min.hako",
Self::FuncscannerAppendDefsMin => "apps/tests/funcscanner_append_defs_minimal.hako",
Self::Stage1UsingresolverMin => "apps/tests/stage1_usingresolver_minimal.hako",
Self::StagebBodyMin => "apps/tests/stageb_body_extract_minimal.hako",
Self::StagebFuncscannerMin => "apps/tests/stageb_funcscanner_scan_boxes_minimal.hako",
}
}
fn name(&self) -> &'static str {
match self {
Self::SkipWsMin => "skip_ws_min",
Self::FuncscannerTrimMin => "funcscanner_trim_min",
Self::FuncscannerAppendDefsMin => "funcscanner_append_defs_min",
Self::Stage1UsingresolverMin => "stage1_usingresolver_min",
Self::StagebBodyMin => "stageb_body_min",
Self::StagebFuncscannerMin => "stageb_funcscanner_min",
}
}
}
/// ケースに対応する JoinIR JSON を生成
fn generate_joinir_json(case: SnapshotCase) -> Option<String> {
// Stage-3 parser を有効化
std::env::set_var("NYASH_PARSER_STAGE3", "1");
std::env::set_var("HAKO_PARSER_STAGE3", "1");
let src = std::fs::read_to_string(case.source_file()).ok()?;
// FuncScanner.trim は FuncScannerBox の定義が必要
let full_src = if matches!(case, SnapshotCase::FuncscannerTrimMin) {
let func_scanner_src =
std::fs::read_to_string("lang/src/compiler/entry/func_scanner.hako").ok()?;
format!("{func_scanner_src}\n\n{src}")
} else {
src
};
let ast: ASTNode = NyashParser::parse_from_string(&full_src).ok()?;
let mut mc = MirCompiler::with_options(false);
let compiled = mc.compile(ast).ok()?;
let join_module = match case {
SnapshotCase::SkipWsMin => lower_skip_ws_to_joinir(&compiled.module),
SnapshotCase::FuncscannerTrimMin => lower_funcscanner_trim_to_joinir(&compiled.module),
SnapshotCase::FuncscannerAppendDefsMin => {
lower_funcscanner_append_defs_to_joinir(&compiled.module)
}
SnapshotCase::Stage1UsingresolverMin => {
lower_stage1_usingresolver_to_joinir(&compiled.module)
}
SnapshotCase::StagebBodyMin => lower_stageb_body_to_joinir(&compiled.module),
SnapshotCase::StagebFuncscannerMin => lower_stageb_funcscanner_to_joinir(&compiled.module),
}?;
Some(join_module_to_json_string(&join_module))
}
/// スナップショット比較を実行(共通ロジック)
fn run_snapshot_test(case: SnapshotCase) {
// トグルチェック
if std::env::var("NYASH_JOINIR_SNAPSHOT_TEST").ok().as_deref() != Some("1") {
eprintln!(
"[joinir/snapshot] NYASH_JOINIR_SNAPSHOT_TEST=1 not set, skipping {}",
case.name()
);
return;
}
let fixture_path = format!("{}/{}", FIXTURE_DIR, case.fixture_filename());
// JoinIR JSON 生成
let json = match generate_joinir_json(case) {
Some(j) => j,
None => {
eprintln!(
"[joinir/snapshot] Failed to generate JoinIR for {}, skipping",
case.name()
);
return;
}
};
// フィクスチャ生成モード
if std::env::var("NYASH_JOINIR_SNAPSHOT_GENERATE").ok().as_deref() == Some("1") {
std::fs::write(&fixture_path, &json).expect("Failed to write fixture");
eprintln!(
"[joinir/snapshot] Generated fixture: {} ({} bytes)",
fixture_path,
json.len()
);
return;
}
// フィクスチャ読み込み
let fixture = match std::fs::read_to_string(&fixture_path) {
Ok(f) => f,
Err(_) => {
eprintln!(
"[joinir/snapshot] Fixture not found: {}\n\
Run with NYASH_JOINIR_SNAPSHOT_GENERATE=1 to create it.",
fixture_path
);
panic!("Fixture not found: {}", fixture_path);
}
};
// 比較
if json != fixture {
eprintln!(
"[joinir/snapshot] MISMATCH for {}\n\
--- actual ({} bytes) ---\n{}\n\
--- fixture ({} bytes) ---\n{}",
case.name(),
json.len(),
json,
fixture.len(),
fixture
);
panic!("jsonir v0 snapshot mismatch for {}", case.name());
}
eprintln!(
"[joinir/snapshot] {} matches fixture ✓",
case.name()
);
}
// ============================================================================
// 個別スナップショットテスト
// ============================================================================
#[test]
fn joinir_json_v0_skip_ws_min_matches_fixture() {
run_snapshot_test(SnapshotCase::SkipWsMin);
}
#[test]
fn joinir_json_v0_funcscanner_trim_min_matches_fixture() {
run_snapshot_test(SnapshotCase::FuncscannerTrimMin);
}
#[test]
fn joinir_json_v0_funcscanner_append_defs_min_matches_fixture() {
run_snapshot_test(SnapshotCase::FuncscannerAppendDefsMin);
}
#[test]
fn joinir_json_v0_stage1_usingresolver_min_matches_fixture() {
run_snapshot_test(SnapshotCase::Stage1UsingresolverMin);
}
#[test]
fn joinir_json_v0_stageb_body_min_matches_fixture() {
run_snapshot_test(SnapshotCase::StagebBodyMin);
}
#[test]
fn joinir_json_v0_stageb_funcscanner_min_matches_fixture() {
run_snapshot_test(SnapshotCase::StagebFuncscannerMin);
}