Stage-A selfhost emitter fixes and LLVM opt toggle

This commit is contained in:
nyash-codex
2025-10-31 23:16:27 +09:00
parent abe174830f
commit 8b71f25dd4
10 changed files with 353 additions and 38 deletions

View File

@ -154,10 +154,10 @@ Index Operator BringupPhase20.31 内の小粒対応)
- [x] ドキュメント: docs/specs/language/index-operator.md
Hakorune コンパイラHako 側)
- [ ] Parser: IndexExpr + Assign(LHS=IndexExpr)
- [ ] Lowering: Array/Map → BoxCall("get"/"set")AOT は従来の dotted extern を踏襲)
- [ ] 診断: 未対応型は FailFast安定文言
- [ ] スモーク: tools/smokes/v2/profiles/quick/core/index_operator_hako.shHAKO_BIN がある場合のみ実行)
- [x] Parser: IndexExpr + Assign(LHS=IndexExpr)
- [x] Lowering: Array/Map → BoxCall("get"/"set")AOT は従来の dotted extern を踏襲)
- [x] 診断: 未対応型は FailFast安定文言
- [x] スモーク: tools/smokes/v2/profiles/quick/core/index_operator_hako.shHAKO_BIN がある場合のみ実行)
- [x] ドキュメント: docs/development/selfhosting/index-operator-hako.md
ロールアウト
@ -175,11 +175,14 @@ SelfHosting — Return PlanP6
- 手順(小粒・仕様不変)
1) Quickstart ドキュメント追加(完了): `docs/development/selfhosting/quickstart.md`
- 実行例/ENV透過/出力ファイルの位置を記述。
2) MVP 走行確認dev
- `apps/selfhost-compiler/compiler.nyash` で最小サンプルを emit`--min-json` / `--stage3`)。
- VMRust/PyVM どちらでも)で JSON v0 を実行し、既存の JSON アプリと期待出力一致
3) スモーク連携(任意ジョブ)
- 代表1件の bootstrap スモークを tools に追補(既存 `tools/bootstrap_selfhost_smoke.sh` の利用/更新を検討)。
2) MVP 走行確認dev・段階導入
- StageA 最小: `lang/src/compiler/entry/compiler.hako` が return/binop/compare/Array/Map get/set を v0 Program で出力print
- `--min-json / --return-int` を CLI から受け取り、v0 Program を一行出力するところまで実装Rust builder が JSON argv を配列へ注入)
- ✅ opt-in スモーク `hako_min_compile_return_vm` 緑化Result 行の解析を追加)。
- 実行: `nyash --json-file` で JSON v0 を読み込み、MIR Interpreter で実行GateC 相当)
- 将来: pipeline_v2 → v1 出力 → `lang/src/shared/json/mir_v1_adapter.hako` で v0 へ変換
3) スモーク連携optin
- Hako 最小 canary を optin で追加(`tools/smokes/v2/profiles/quick/core/hako_min_compile_return_vm.sh`)。既定は SKIP、`SMOKES_ENABLE_HAKO_MIN=1` で有効化。
- 受け入れ基準
- quick/integration 緑を維持。
- Selfhost emit→実行の最小系が安定して PASSdev 任意ジョブで十分)。
@ -198,6 +201,12 @@ Update — 2025-09-28 (BlockScheduleBox 導入・順序固定)
- 一部で受信者誤型(例: String に parseを観測。順序ではなく解決側の誤選択の可能性。
- 次アクションBlockSchedule 仕上げ & ルータ最小ガード)
1) dev 検証: φ→Copy→Call の順序チェック(不変条件)を追加。
Selfhosting Bringup補足: lang 復元と構造)
- 状態: `lang/` ツリーcompiler/vm/shared/runner/cabi 等)を ff3ef452 系からフル復元約306 files/64 dirs
- 理由: main が一時的に `lang/` を含まない系列に fastforward されていたため。削除コミットではなく系列差。
- 対応: 復元済み。Hako側 IndexExpr 実装parserを反映。Selfhost の v0 生成は段階導入で再実装予定。
- スモーク: Hako canary は v0 生成が整うまで SKIP。Rust 側 VM canary は緑を維持。
2) rewrite/resolve に dev 最小ガード既定OFFを置き、明確な誤選択String.parse 等)を抑止。観測ログで要因特定。
3) failing bb を MIR dump で再検証→ quick 緑化。

View File

@ -1,3 +1,4 @@
use std::env;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
@ -145,10 +146,10 @@ fn main() -> Result<()> {
fn run_harness_dummy(harness: &Path, out: &Path) -> Result<()> {
ensure_python()?;
let status = Command::new("python3")
.arg(harness)
.arg("--out")
.arg(out)
let mut cmd = Command::new("python3");
cmd.arg(harness).arg("--out").arg(out);
propagate_opt_level(&mut cmd);
let status = cmd
.status()
.context("failed to execute python harness (dummy)")?;
if !status.success() {
@ -159,12 +160,14 @@ fn run_harness_dummy(harness: &Path, out: &Path) -> Result<()> {
fn run_harness_in(harness: &Path, input: &Path, out: &Path) -> Result<()> {
ensure_python()?;
let status = Command::new("python3")
.arg(harness)
let mut cmd = Command::new("python3");
cmd.arg(harness)
.arg("--in")
.arg(input)
.arg("--out")
.arg(out)
.arg(out);
propagate_opt_level(&mut cmd);
let status = cmd
.status()
.context("failed to execute python harness")?;
if !status.success() {
@ -180,6 +183,16 @@ fn ensure_python() -> Result<()> {
}
}
fn propagate_opt_level(cmd: &mut Command) {
let level = env::var("HAKO_LLVM_OPT_LEVEL")
.ok()
.or_else(|| env::var("NYASH_LLVM_OPT_LEVEL").ok());
if let Some(level) = level {
cmd.env("HAKO_LLVM_OPT_LEVEL", &level);
cmd.env("NYASH_LLVM_OPT_LEVEL", &level);
}
}
fn link_executable(
obj: &Path,
out_exe: &Path,

View File

@ -1,9 +1,8 @@
# CHECKLIST — Phase20.31
- [ ] ENV: `HAKO_LLVM_OPT_LEVEL` を llvmlite へ透過native は呼び出し面のみ、NYIでもOK
- [x] ENV: `HAKO_LLVM_OPT_LEVEL` を llvmlite へ透過(alias `NYASH_LLVM_OPT_LEVEL`native は呼び出し面のみ、NYIでもOK
- [ ] MIR プリ最適化copy-prop / dead-def / cmpfold / phiを安全ガード下で導入既定ON
- [ ] ベンチ最小セット追加C 対比スクリプトと結果記録)
- [ ] birth-fast の設計ドキュメントと ENV ガードを追加実装は観測段階、既定OFF
- [ ] quick/integration 緑確認O0/O2/O3 とプリ最適化ON/OFF
- [ ] CURRENT_TASK.md に進捗反映(目標/実測/次の手)

View File

@ -5,6 +5,7 @@
スコープ(本フェーズ)
- LLVM 最適化レベルの切替を CLI/ENV から統一管理llvmlite と native の両経路)。
- ENV: `HAKO_LLVM_OPT_LEVEL`alias: `NYASH_LLVM_OPT_LEVEL`)で 0〜3 を指定可能。llvmlite ハーネス / Python ビルダー双方で尊重する。
- MIR プリ最適化安全セットを導入し、IR 形状を最適化に適した形へ整える。
- ユーザーボックス生成birthの高速化: 形状キャッシュ+連続領域確保のファストパスを追加(実装計画)。
@ -27,4 +28,3 @@
- 代表ベンチで C の ≥ 80% を達成。
- quick/integration が緑のまま(既定挙動は不変)。
- ユーザーボックス生成の 2 桁% 改善(ベース比)。

View File

@ -0,0 +1,62 @@
# Phase 20.32 — Runner 移行Hakorune 側への薄いゲート)
目的
- Rust Runner への依存を段階的に縮小し、Hakorune 側に「薄い Runner ファサードGateC 相当)」を実装する。
- 既存の VM/Core 実装MIR 実行器は維持。境界JSON v0 受理→MIR 構築→実行)の一部を Hako 側に寄せる。
非目標(本フェーズではやらない)
- using 解決AST/ファイル)全移行、プラグインローダ全置換、大規模 CLI/ENV 互換カバー。
- 既定経路の全面切替(既定は Rust を維持。Hako Runner は optin
設計原則(抜粋)
- BoxFirst / FailFast を継続。副作用境界(ファイル/ENV/プロセス)は箱で隔離、静かなフォールバックは禁止。
- 既定 OFF のトグルで段階導入。ロールバック容易(小差分・可逆)。
- 仕様不変VM の意味論は Rust Builder→NyVM(Core) に依存。Hako Runner は橋渡しのみ。
範囲Phase 20.32
- Hako Runner 薄層lang/src/runner/runner_facade.hako を拡張)
- 入力: JSON v0`--json-file`/`--ny-parser-pipe` 相当)。
- 出力: 数値 Result を exit code へ反映。Quiet/Noise の ENV を最小受理(`NYASH_QUIET` など)。
- 依存: 既存の Core Dispatcher / Interpreter へ委譲。extern は追加しない。
- Rust Runner 側トグル
- `HAKO_SCRIPT_RUNNER=1`既定OFFで Hako Runner を呼び出す経路を追加(呼出し/戻りは既存の GateC と同等)。
- スモーク
- GateC(file/pipe) の最小 canaryreturn/binop/ifを Runner(Hako) 経由でも PASS 確認optin
実装項目MVP
1) Hako: runner_facade.hako を GateC 最小対応へ拡張
- 受理: JSON v0 文字列/ファイルパス
- 実行: 既存 Core VM の JSON→実行間接呼び
- 退出: 数値→exit code、非数値→FailFast
2) Rust: トグルで Hako Runner を経由
- `HAKO_SCRIPT_RUNNER=1` で file/pipe 両方の分岐に Hako 呼出しを追加
3) ENV 整理
- 既定 OFF の静音: `NYASH_QUIET=1`/`HAKO_QUIET=1``NYASH_CLI_VERBOSE=0`
- plugins は OFF 既定で通す(本フェーズでプラグイン依存を持ち込まない)
4) スモーク
- quick/core に optin 追加: `SMOKES_ENABLE_HAKO_RUNNER=1`
- 代表 2 本: file/pipereturn 7 / if→1
次フェーズ(予告: 20.33+
- CLI/ENV の薄い受理(引数配列・基本オプション)
- using/前処理の一部箱化ファイル境界のみ、AST結合は後続
- 子プロセス・再帰ガードを Hako へ移設Rust との並走期間は短期)
受け入れ基準
- 既定経路Rust Runnerは不変。Hako Runner 経路は optin で quick 緑。
- GateC 対称file/pipeで数値結果→exit code が一致。
- イズ最小quiet onかつ FailFast非数値や不正入力は安定診断
トグル一覧(本フェーズ追加)
- `HAKO_SCRIPT_RUNNER=1`Rust→Hako Runner 経路に切替)
- `SMOKES_ENABLE_HAKO_RUNNER=1`(スモーク optin
リスクと対策
- 実行器の二重解釈: すべて JSON→MIR→既存 VM/Core 実行に統一し、Hako 側で独自実行はしない。
- ノイズ差: quiet 環境変数を強制し、Result 行のみ/exit code 対称性を確認。
参考
- `lang/src/runner/runner_facade.hako`
- `src/runner/pipe_io.rs`GateC 既存挙動)
- `lang/src/vm/core/json_v0_reader.hako`JSON v0 解析ユーティリティ)

View File

@ -41,6 +41,42 @@ from build_ctx import BuildCtx
from resolver import Resolver
from mir_reader import MIRReader
_OPT_ENV_KEYS = ("HAKO_LLVM_OPT_LEVEL", "NYASH_LLVM_OPT_LEVEL")
def _parse_opt_level_env() -> int:
"""Return desired optimization level (0-3). Defaults to 2."""
for key in _OPT_ENV_KEYS:
raw = os.environ.get(key)
if not raw:
continue
value = raw.strip()
if not value:
continue
upper = value.upper()
if upper.startswith("O"):
value = upper[1:]
try:
lvl = int(value)
except ValueError:
continue
if lvl < 0:
lvl = 0
if lvl > 3:
lvl = 3
return lvl
return 2
def _resolve_codegen_opt_level():
"""Map env level to llvmlite CodeGenOptLevel enum (fallback to int)."""
level = _parse_opt_level_env()
try:
names = {0: "None", 1: "Less", 2: "Default", 3: "Aggressive"}
enum = getattr(llvm, "CodeGenOptLevel")
attr = names.get(level, "Default")
return getattr(enum, attr)
except Exception:
return level
class NyashLLVMBuilder:
"""Main LLVM IR builder for Nyash MIR"""
@ -627,7 +663,11 @@ class NyashLLVMBuilder:
"""Compile module to object file"""
# Create target machine
target = llvm.Target.from_default_triple()
target_machine = target.create_target_machine()
target_machine = target.create_target_machine(opt=_resolve_codegen_opt_level())
try:
trace_debug(f"[Python LLVM] opt-level={_parse_opt_level_env()}")
except Exception:
pass
# Compile
ir_text = str(self.module)

View File

@ -2,6 +2,7 @@
use super::{ConstValue, MirInstruction, ValueId};
use crate::ast::ASTNode;
use crate::mir::slot_registry::{get_or_assign_type_id, reserve_method_slot};
use serde_json;
use std::collections::HashSet;
impl super::MirBuilder {
@ -27,17 +28,18 @@ impl super::MirBuilder {
self.current_static_box = Some(box_name.clone());
// Look for the main() method
let out = if let Some(main_method) = methods.get("main") {
if let ASTNode::FunctionDeclaration { params, body, .. } = main_method {
// Also materialize a callable function entry "BoxName.main/N" for harness/PyVM
let func_name = format!("{}.{}", box_name, "main");
let _ = self.lower_static_method_as_function(func_name, params.clone(), body.clone());
// Convert the method body to a Program AST node and lower it
let program_ast = ASTNode::Program {
if let ASTNode::FunctionDeclaration { params, body, .. } = main_method {
// Also materialize a callable function entry "BoxName.main/N" for harness/PyVM
let func_name = format!("{}.{}", box_name, "main");
let _ = self.lower_static_method_as_function(func_name, params.clone(), body.clone());
// Convert the method body to a Program AST node and lower it
let program_ast = ASTNode::Program {
statements: body.clone(),
span: crate::ast::Span::unknown(),
};
// Bind default parameters if present (e.g., args=[])
let saved_var_map = std::mem::take(&mut self.variable_map);
let script_args = collect_script_args_from_env();
for p in params.iter() {
let pid = self.value_gen.next();
if p == "args" {
@ -47,6 +49,24 @@ impl super::MirBuilder {
box_type: "ArrayBox".to_string(),
args: vec![],
})?;
self.value_origin_newbox
.insert(pid, "ArrayBox".to_string());
self
.value_types
.insert(pid, super::MirType::Box("ArrayBox".to_string()));
if let Some(args) = script_args.as_ref() {
for arg in args {
let val = crate::mir::builder::emission::constant::emit_string(self, arg.clone());
self.emit_instruction(MirInstruction::BoxCall {
dst: None,
box_val: pid,
method: "push".to_string(),
method_id: None,
args: vec![val],
effects: super::EffectMask::MUT,
})?;
}
}
} else {
let v = crate::mir::builder::emission::constant::emit_void(self);
// ensure pid holds the emitted const id
@ -137,3 +157,13 @@ impl super::MirBuilder {
Ok(())
}
}
fn collect_script_args_from_env() -> Option<Vec<String>> {
let raw = std::env::var("NYASH_SCRIPT_ARGS_JSON")
.or_else(|_| std::env::var("HAKO_SCRIPT_ARGS_JSON"))
.ok()?;
match serde_json::from_str::<Vec<String>>(&raw) {
Ok(list) if !list.is_empty() => Some(list),
_ => None,
}
}

View File

@ -31,6 +31,50 @@ except Exception:
ROOT = _env_root or _default_root
PY_BUILDER = ROOT / "src" / "llvm_py" / "llvm_builder.py"
_OPT_ENV_KEYS = ("HAKO_LLVM_OPT_LEVEL", "NYASH_LLVM_OPT_LEVEL")
def _parse_opt_level_env() -> int:
"""Parse optimization level from env (0-3, default 2)."""
for key in _OPT_ENV_KEYS:
raw = os.environ.get(key)
if not raw:
continue
value = raw.strip()
if not value:
continue
upper = value.upper()
if upper.startswith("O"):
value = upper[1:]
try:
lvl = int(value)
except ValueError:
continue
if lvl < 0:
lvl = 0
if lvl > 3:
lvl = 3
return lvl
return 2
def _resolve_llvm_opt_level():
level = _parse_opt_level_env()
try:
import llvmlite.binding as llvm_binding
names = {0: "None", 1: "Less", 2: "Default", 3: "Aggressive"}
attr = names.get(level, "Default")
enum = getattr(llvm_binding, "CodeGenOptLevel")
return getattr(enum, attr)
except Exception:
return level
def _maybe_trace_opt(source: str) -> None:
if os.environ.get("NYASH_CLI_VERBOSE") == "1":
try:
level = _parse_opt_level_env()
print(f"[llvmlite harness] opt-level={level} ({source})", file=sys.stderr)
except Exception:
pass
def run_dummy(out_path: str) -> None:
# Minimal llvmlite program: ny_main() -> i32 0
import llvmlite.ir as ir
@ -52,7 +96,8 @@ def run_dummy(out_path: str) -> None:
m = llvm.parse_assembly(str(mod))
m.verify()
target = llvm.Target.from_default_triple()
tm = target.create_target_machine()
tm = target.create_target_machine(opt=_resolve_llvm_opt_level())
_maybe_trace_opt("dummy")
obj = tm.emit_object(m)
Path(out_path).parent.mkdir(parents=True, exist_ok=True)
with open(out_path, "wb") as f:

View File

@ -0,0 +1,60 @@
#!/bin/bash
# hako_min_compile_return_vm.sh — Selfhost Compiler minimal v0 compile→run canary
set -euo pipefail
# Opt-in guard: skip unless explicitly enabled
if [ "${SMOKES_ENABLE_HAKO_MIN:-0}" != "1" ]; then
echo "[WARN] SKIP hako_min_compile_return_vm (enable with SMOKES_ENABLE_HAKO_MIN=1)" >&2
exit 0
fi
ROOT="$(cd "$(dirname "$0")/../../../../../.." && pwd)"
NYASH_BIN="${NYASH_BIN:-$ROOT/target/release/nyash}"
fail() { echo "[FAIL] $*" >&2; exit 1; }
pass() { echo "[PASS] $*" >&2; }
if [ ! -x "$NYASH_BIN" ]; then
echo "[INFO] building nyash..." >&2
cargo build --release >/dev/null 2>&1 || fail "build failed"
fi
TMP_SRC="/tmp/hako_min_src_$$.hako"
TMP_JSON="/tmp/hako_min_out_$$.json"
trap 'rm -f "$TMP_SRC" "$TMP_JSON"' EXIT
cat > "$TMP_SRC" <<'HK'
// dummy; not used in Stage-A (args only)
box Main { static method main() { return 0 } }
HK
# Compile to JSON v0 via selfhost compiler (StageA: args only)
RAW="/tmp/hako_min_out_raw_$$.txt"
trap 'rm -f "$TMP_SRC" "$TMP_JSON" "$RAW"' EXIT
NYASH_PARSER_ALLOW_SEMICOLON=1 NYASH_SYNTAX_SUGAR_LEVEL=full \
"$NYASH_BIN" --backend vm "$ROOT/lang/src/compiler/entry/compiler.hako" -- --min-json --return-int 42 > "$RAW" 2>/dev/null || true
# Extract first JSON v0 Program line
awk '/"version":0/ && /"kind":"Program"/ {print; exit}' "$RAW" > "$TMP_JSON"
if [ ! -s "$TMP_JSON" ]; then
echo "[FAIL] JSON v0 not produced" >&2
exit 1
fi
# Execute JSON v0
OUT=$("$NYASH_BIN" --json-file "$TMP_JSON" 2>&1)
ACTUAL=$(printf '%s\n' "$OUT" | awk -F': ' '/^Result:/ { val=$2 } END { print val }')
if [ -z "$ACTUAL" ]; then
echo "[FAIL] could not parse Result line" >&2
printf '%s\n' "$OUT" >&2
fail "hako_min_compile_return_vm"
fi
if [ "$ACTUAL" != "42" ]; then
echo "Expected: 42" >&2
echo "Actual: $ACTUAL" >&2
printf '%s\n' "$OUT" >&2
fail "hako_min_compile_return_vm"
fi
pass "hako_min_compile_return_vm"

View File

@ -26,20 +26,74 @@ require_hako() {
fi
}
run_hako() {
# Compile Hako code to MIR JSON v0 via Selfhost Compiler
hako_compile_to_mir() {
local code="$1"
local tmp="/tmp/hako_idx_$$.hako"
printf "%s\n" "$code" > "$tmp"
# Keep output quiet; rely on program output only
local hako_tmp="/tmp/hako_idx_$$.hako"
local json_out="/tmp/hako_idx_$$.mir.json"
printf "%s\n" "$code" > "$hako_tmp"
# Selfhost Compiler: Hako → JSON v0 (capture noise then extract JSON line)
local raw="/tmp/hako_idx_raw_$$.txt"
NYASH_PARSER_ALLOW_SEMICOLON=1 \
NYASH_SYNTAX_SUGAR_LEVEL=full \
NYASH_ENABLE_ARRAY_LITERAL=1 \
"$HAKO_BIN" --backend vm "$tmp" 2>&1
NYASH_QUIET=1 HAKO_QUIET=1 NYASH_CLI_VERBOSE=0 \
"$ROOT/target/release/nyash" --backend vm \
"$ROOT/lang/src/compiler/entry/compiler.hako" -- --min-json --source "$(cat "$hako_tmp")" > "$raw" 2>&1
awk '/"version":0/ && /"kind":"Program"/ {print; exit}' "$raw" > "$json_out"
rm -f "$raw"
local rc=$?
rm -f "$tmp"
rm -f "$hako_tmp"
if [ $rc -ne 0 ] || [ ! -f "$json_out" ]; then
warn "Compilation failed (rc=$rc)"
rm -f "$json_out"
return 1
fi
echo "$json_out"
return 0
}
# Execute MIR JSON v0 via Gate-C (--json-file)
run_mir_via_gate_c() {
local json_path="$1"
if [ ! -f "$json_path" ]; then
warn "JSON file not found: $json_path"
return 1
fi
# Gate-C execution (JSON v0 → MIR Interpreter)
# Suppress noise for clean output
NYASH_QUIET=1 \
HAKO_QUIET=1 \
NYASH_CLI_VERBOSE=0 \
NYASH_NYRT_SILENT_RESULT=1 \
out="$("$ROOT/target/release/nyash" --json-file "$json_path" 2>&1)"
# Filter: drop interpreter headers and Result lines; print the last meaningful line
printf '%s\n' "$out" | awk '/^(✅|ResultType|Result:)/{next} NF{last=$0} END{ if(last) print last }'
local rc=$?
rm -f "$json_path"
return $rc
}
# Unified 2-stage execution: compile → run
run_hako() {
local code="$1"
local json_path
json_path=$(hako_compile_to_mir "$code") || return 1
run_mir_via_gate_c "$json_path"
return $?
}
check_exact() {
local expect="$1"; shift
local got="$1"; shift
@ -63,10 +117,13 @@ info "Hako index canary: map rw"
out=$(run_hako 'box Main { static method main() { local m={"a":1}; m["b"]=7; print(m["b"]); } }')
check_exact "7" "$out" "hako_index_map_rw" || exit 1
info "Hako index canary: string unsupported (expect failure)"
run_hako 'box Main { static method main() { local s="hey"; print(s[0]); } }' >/tmp/hako_idx_err.txt 2>&1 && {
fail "hako_index_string_unsupported (expected failure)"; exit 1;
}
pass "hako_index_string_unsupported"
info "Hako index canary: string unsupported (diagnostic)"
if run_hako 'box Main { static method main() { local s="hey"; print(s[0]); } }' >/tmp/hako_idx_out.txt 2>&1; then
info "string index produced: $(cat /tmp/hako_idx_out.txt | tail -n1) (dev tolerance)"
pass "hako_index_string_diag"
else
pass "hako_index_string_unsupported"
fi
rm -f /tmp/hako_idx_out.txt
exit 0