Files
hakorune/docs/development/current/main/phases/phase-129

Phase 129: Materialize join_k Continuation + LLVM Parity

Goal

  • Materialize join_k as a real JoinFunction (not just a concept)
  • Achieve VM+LLVM EXE parity for Phase 128 if-only partial assign pattern
  • Verify structural properties: join_k exists, PHI禁止 (no PHI in Normalized)

Background

Phase 128 added basic Assign(int literal) support to Normalized builder, but:

  • join_k was mentioned in comments but not actually materialized
  • The If lowering doesn't generate a join continuation
  • post-if statements (e.g., return x after if) aren't supported in Normalized path

Phase 129 fixes this by actually generating join_k as a JoinFunction.

Design

join_k Continuation Pattern

if cond {
  x = 2  // then: env_then = env with x=2
} else {
  // else: env_else = env (unchanged)
}
print(x)  // post-if: needs env from both branches

Normalized Lowering:

then:
  x = Const(2)
  env_then[x] = new_vid
  TailCall(join_k, env_then)

else:
  // no assignment
  env_else = env (original)
  TailCall(join_k, env_else)

join_k(env_phi):
  // env_phi is the merged environment
  // post-if statements use env_phi
  print(env_phi[x])
  Ret

Key Properties (SSOT)

  1. join_k is a JoinFunction: Not a concept, but actual function in JoinModule
  2. then/else end with TailCall(join_k): Not Ret, not direct post-if
  3. env merging is explicit: env_phi parameter receives merged environment
  4. PHI禁止: No PHI instructions in Normalized (env update + continuation only)

Structural Verification

verify_normalized_structure() enforces:

  • If node exists → join_k exists (as JoinFunction)
  • then/else don't end with Ret (they tail-call join_k)
  • join_k has env parameter (for merged environment)
  • No PHI instructions (structural check)

Scope

In-Scope (Phase 129)

  • If-only patterns (no loops)
  • Assign(int literal) RHS only (Phase 128 baseline)
  • post-if statements: print/return (minimal set)
  • VM + LLVM EXE parity (both must work)

Out-of-Scope

  • Loop patterns (Phase 130+)
  • Assign(complex expr) RHS (Phase 130+)
  • Nested if (Phase 130+)

Implementation Plan

P0: LLVM EXE parity baseline (Phase 128)

  • Add phase128_if_only_partial_assign_normalized_llvm_exe.sh
  • Verify VM+LLVM parity for Phase 128 baseline
  • Regression: phase103, phase118

Status: DONE (commit e7ad3d31b)

P1-B: Materialize join_k (if-as-last)

Target:

  • src/mir/control_tree/normalized_shadow/if_as_last_join_k.rs
  • orchestrator: src/mir/control_tree/normalized_shadow/builder.rs
  • structure verification: src/mir/control_tree/normalized_shadow/normalized_verifier.rs
  • contract parity: src/mir/control_tree/normalized_shadow/parity_contract.rs

What is supported:

  • if-only, and the if is the last statement (Phase 129-B)
  • then/else: TailCall(join_k) with env argument (PHI禁止)

Status: DONE (Phase 129-B: fixture + VM smoke PASS)

P2 (Phase 129-C): Post-if support (return-var)

New fixture: apps/tests/phase129_if_only_post_if_return_var_min.hako

x=1; flag=1; if flag==1 { x=2 }; print(x); return "OK"

Expected: 2 (x updated in then branch)

VM smoke: phase129_if_only_post_if_return_var_vm.sh

  • NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1
  • Verify join_k → post_k continuation works

Implementation:

  • src/mir/control_tree/normalized_shadow/post_if_post_k.rs (new, 392 lines)
  • Post-if lowering with post_k continuation
  • join_k merges env from then/else → TailCall(post_k, merged_env)
  • post_k executes post-if statements → Ret

Status: DONE (Phase 129-C complete)

  • Fixture + VM smoke PASS
  • Runs through Normalized post_k path for return x pattern
  • Structure verification enforces join_k → post_k → Ret
  • Note: Current fixture phase129_if_only_post_if_return_var_min.hako has print(x); return "OK" which falls back to legacy (print not in Phase 129-C scope). Simplified test with return x confirmed to use post_k path

P3: Documentation

  • This README (DONE)
  • Update 10-Now.md / 01-JoinIR-Selfhost-INDEX.md / 30-Backlog.md

Acceptance Criteria

  • P0: Phase 128 LLVM EXE smoke passes
  • P1-B: join_k materialized for if-as-last (then/else tail-call join_k)
  • P1-B: verify_normalized_structure enforces join_k tailcall + PHI禁止
  • P2 (setup): fixture + VM smoke exist
  • P2 (behavior): post-if path runs via Normalized (no fallback)
  • P2 (Phase 129-C): post_k continuation implemented and verified
  • P3: Documentation updated
  • Regression: phase128, phase129-B all PASS
  • cargo test --lib PASS (1155 tests)

Verification Commands

# Unit tests
cargo test --lib

# Smoke tests (baseline)
bash tools/smokes/v2/profiles/integration/apps/phase128_if_only_partial_assign_normalized_vm.sh
bash tools/smokes/v2/profiles/integration/apps/phase128_if_only_partial_assign_normalized_llvm_exe.sh
bash tools/smokes/v2/profiles/integration/apps/phase129_join_k_as_last_vm.sh

# Regression
bash tools/smokes/v2/profiles/integration/apps/phase103_if_only_llvm_exe.sh
bash tools/smokes/v2/profiles/integration/apps/phase118_loop_nested_if_merge_vm.sh

Feedback Points

Box-First Modularization

  • lower_if_node: Currently ~70 lines, could be split into:
    • generate_join_k_function (create JoinFunction)
    • lower_if_branches (then/else tail calls)
    • Single responsibility principle

Fail-Fast Opportunities

  • verify_normalized_structure should fail early if:
    • If exists but no join_k found
    • then/else don't tail-call join_k
    • join_k has no env parameter

Legacy Findings

  • Current verify_branch_is_return_literal: Too restrictive for Phase 129
    • Should allow Assign statements in branches (Phase 128 already supports)
    • Should verify tail-call to join_k (not just Return)
  • Phase 128: If-only partial assign (baseline)
  • Phase 113: If-only partial assign parity (StepTree path)
  • Phase 121-127: StepTree→Normalized foundation
  • Phase 130+: Loop patterns, complex RHS

Notes

  • Dev-only: joinir_dev_enabled() + HAKO_JOINIR_STRICT=1 required
  • PHI禁止: Normalized path doesn't use PHI (env update + continuation instead)
  • join_k naming: Could use join_k, k_join, or auto-generate unique ID