docs/ci: selfhost bootstrap/exe-first workflows; add ny-llvmc scaffolding + JSON v0 schema validation; plan: unify to Nyash ABI v2 (no backwards compat)
This commit is contained in:
10
src/cli.rs
10
src/cli.rs
@ -58,6 +58,8 @@ pub struct CliConfig {
|
||||
// Phase-15: JSON IR v0 bridge
|
||||
pub ny_parser_pipe: bool,
|
||||
pub json_file: Option<String>,
|
||||
// GC mode (dev; forwarded to env as NYASH_GC_MODE)
|
||||
pub gc_mode: Option<String>,
|
||||
// Build system (MVP)
|
||||
pub build_path: Option<String>,
|
||||
pub build_app: Option<String>,
|
||||
@ -105,6 +107,12 @@ impl CliConfig {
|
||||
.value_name("FILE")
|
||||
.index(1)
|
||||
)
|
||||
.arg(
|
||||
Arg::new("gc")
|
||||
.long("gc")
|
||||
.value_name("{auto,rc+cycle,minorgen,stw,rc,off}")
|
||||
.help("Select GC mode (default: rc+cycle)")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("parser")
|
||||
.long("parser")
|
||||
@ -456,6 +464,7 @@ impl CliConfig {
|
||||
cli_verbose: matches.get_flag("verbose"),
|
||||
run_task: matches.get_one::<String>("run-task").cloned(),
|
||||
load_ny_plugins: matches.get_flag("load-ny-plugins"),
|
||||
gc_mode: matches.get_one::<String>("gc").cloned(),
|
||||
parser_ny: matches
|
||||
.get_one::<String>("parser")
|
||||
.map(|s| s == "ny")
|
||||
@ -516,6 +525,7 @@ impl Default for CliConfig {
|
||||
cli_verbose: false,
|
||||
run_task: None,
|
||||
load_ny_plugins: false,
|
||||
gc_mode: None,
|
||||
parser_ny: false,
|
||||
ny_parser_pipe: false,
|
||||
json_file: None,
|
||||
|
||||
@ -211,6 +211,41 @@ pub fn gc_trace_level() -> u8 {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- GC mode and instrumentation ----
|
||||
/// Return current GC mode string (auto default = "rc+cycle").
|
||||
/// Allowed: "auto", "rc+cycle", "minorgen", "stw", "rc", "off"
|
||||
pub fn gc_mode() -> String {
|
||||
match std::env::var("NYASH_GC_MODE").ok() {
|
||||
Some(m) if !m.trim().is_empty() => m,
|
||||
_ => "rc+cycle".to_string(),
|
||||
}
|
||||
}
|
||||
/// Brief metrics emission (text)
|
||||
pub fn gc_metrics() -> bool {
|
||||
std::env::var("NYASH_GC_METRICS").ok().as_deref() == Some("1")
|
||||
}
|
||||
/// JSON metrics emission (single line)
|
||||
pub fn gc_metrics_json() -> bool {
|
||||
std::env::var("NYASH_GC_METRICS_JSON").ok().as_deref() == Some("1")
|
||||
}
|
||||
/// Leak diagnostics on exit
|
||||
pub fn gc_leak_diag() -> bool {
|
||||
std::env::var("NYASH_GC_LEAK_DIAG").ok().as_deref() == Some("1")
|
||||
}
|
||||
/// Optional allocation threshold; if Some(n) and exceeded, print warning
|
||||
pub fn gc_alloc_threshold() -> Option<u64> {
|
||||
std::env::var("NYASH_GC_ALLOC_THRESHOLD").ok()?.parse().ok()
|
||||
}
|
||||
|
||||
/// Run a collection every N safepoints (if Some)
|
||||
pub fn gc_collect_sp_interval() -> Option<u64> {
|
||||
std::env::var("NYASH_GC_COLLECT_SP").ok()?.parse().ok()
|
||||
}
|
||||
/// Run a collection when allocated bytes since last >= N (if Some)
|
||||
pub fn gc_collect_alloc_bytes() -> Option<u64> {
|
||||
std::env::var("NYASH_GC_COLLECT_ALLOC").ok()?.parse().ok()
|
||||
}
|
||||
|
||||
// ---- Rewriter flags (optimizer transforms)
|
||||
pub fn rewrite_debug() -> bool {
|
||||
std::env::var("NYASH_REWRITE_DEBUG").ok().as_deref() == Some("1")
|
||||
|
||||
@ -162,6 +162,28 @@ pub mod handles {
|
||||
pub fn len() -> usize {
|
||||
REG.with(|cell| cell.borrow().map.len())
|
||||
}
|
||||
/// Tally handles by NyashBox type name (best-effort)
|
||||
pub fn type_tally() -> Vec<(String, usize)> {
|
||||
use std::collections::HashMap;
|
||||
REG.with(|cell| {
|
||||
let reg = cell.borrow();
|
||||
let mut map: HashMap<String, usize> = HashMap::new();
|
||||
for (_h, obj) in reg.map.iter() {
|
||||
let tn = obj.type_name().to_string();
|
||||
*map.entry(tn).or_insert(0) += 1;
|
||||
}
|
||||
let mut v: Vec<(String, usize)> = map.into_iter().collect();
|
||||
v.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
|
||||
v
|
||||
})
|
||||
}
|
||||
/// Snapshot current handle objects (Arc clones)
|
||||
pub fn snapshot_arcs() -> Vec<Arc<dyn crate::box_trait::NyashBox>> {
|
||||
REG.with(|cell| {
|
||||
let reg = cell.borrow();
|
||||
reg.map.values().cloned().collect()
|
||||
})
|
||||
}
|
||||
|
||||
// Scope management: track and clear handles created within a JIT call
|
||||
pub fn begin_scope() {
|
||||
|
||||
@ -51,7 +51,8 @@ def lower_atomic_op(
|
||||
resolver=None,
|
||||
preds=None,
|
||||
block_end_values=None,
|
||||
bb_map=None
|
||||
bb_map=None,
|
||||
ctx=None,
|
||||
) -> None:
|
||||
"""
|
||||
Lower atomic operations
|
||||
@ -66,7 +67,12 @@ def lower_atomic_op(
|
||||
ordering: Memory ordering
|
||||
"""
|
||||
# Get pointer
|
||||
if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
|
||||
if ctx is not None:
|
||||
try:
|
||||
ptr = ctx.resolver.resolve_ptr(ptr_vid, builder.block, ctx.preds, ctx.block_end_values, ctx.vmap)
|
||||
except Exception:
|
||||
ptr = vmap.get(ptr_vid)
|
||||
elif resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
|
||||
ptr = resolver.resolve_ptr(ptr_vid, builder.block, preds, block_end_values, vmap)
|
||||
else:
|
||||
ptr = vmap.get(ptr_vid)
|
||||
@ -85,7 +91,12 @@ def lower_atomic_op(
|
||||
elif op == "store":
|
||||
# Atomic store
|
||||
if val_vid is not None:
|
||||
if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
|
||||
if ctx is not None:
|
||||
try:
|
||||
val = ctx.resolver.resolve_i64(val_vid, builder.block, ctx.preds, ctx.block_end_values, ctx.vmap, ctx.bb_map)
|
||||
except Exception:
|
||||
val = vmap.get(val_vid, ir.Constant(ir.IntType(64), 0))
|
||||
elif resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
|
||||
val = resolver.resolve_i64(val_vid, builder.block, preds, block_end_values, vmap, bb_map)
|
||||
else:
|
||||
val = vmap.get(val_vid, ir.Constant(ir.IntType(64), 0))
|
||||
@ -94,7 +105,12 @@ def lower_atomic_op(
|
||||
elif op == "add":
|
||||
# Atomic add (fetch_add)
|
||||
if val_vid is not None:
|
||||
if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
|
||||
if ctx is not None:
|
||||
try:
|
||||
val = ctx.resolver.resolve_i64(val_vid, builder.block, ctx.preds, ctx.block_end_values, ctx.vmap, ctx.bb_map)
|
||||
except Exception:
|
||||
val = ir.Constant(ir.IntType(64), 1)
|
||||
elif resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
|
||||
val = resolver.resolve_i64(val_vid, builder.block, preds, block_end_values, vmap, bb_map)
|
||||
else:
|
||||
val = ir.Constant(ir.IntType(64), 1)
|
||||
|
||||
@ -5,6 +5,7 @@ Core of Nyash's "Everything is Box" philosophy
|
||||
|
||||
import llvmlite.ir as ir
|
||||
from typing import Dict, List, Optional, Any
|
||||
from instructions.safepoint import insert_automatic_safepoint
|
||||
|
||||
def _declare(module: ir.Module, name: str, ret, args):
|
||||
for f in module.functions:
|
||||
@ -68,6 +69,13 @@ def lower_boxcall(
|
||||
i64 = ir.IntType(64)
|
||||
i8 = ir.IntType(8)
|
||||
i8p = i8.as_pointer()
|
||||
# Insert a safepoint around potential heavy boxcall sites (pre-call)
|
||||
try:
|
||||
import os
|
||||
if os.environ.get('NYASH_LLVM_AUTO_SAFEPOINT', '1') == '1':
|
||||
insert_automatic_safepoint(builder, module, "boxcall")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Short-hands with ctx (backward-compatible fallback)
|
||||
r = resolver
|
||||
|
||||
@ -6,6 +6,7 @@ Handles regular function calls (not BoxCall or ExternCall)
|
||||
import llvmlite.ir as ir
|
||||
from typing import Dict, List, Optional, Any
|
||||
from trace import debug as trace_debug
|
||||
from instructions.safepoint import insert_automatic_safepoint
|
||||
|
||||
def lower_call(
|
||||
builder: ir.IRBuilder,
|
||||
@ -45,6 +46,13 @@ def lower_call(
|
||||
bb_map = ctx.bb_map
|
||||
except Exception:
|
||||
pass
|
||||
# Insert an automatic safepoint after the function call
|
||||
try:
|
||||
import os
|
||||
if os.environ.get('NYASH_LLVM_AUTO_SAFEPOINT', '1') == '1':
|
||||
insert_automatic_safepoint(builder, module, "function_call")
|
||||
except Exception:
|
||||
pass
|
||||
# Short-hands with ctx (backward-compatible fallback)
|
||||
r = resolver
|
||||
p = preds
|
||||
|
||||
@ -4,6 +4,7 @@ Lowering helpers for while-control flow (regular structured)
|
||||
|
||||
from typing import List, Dict, Any
|
||||
import llvmlite.ir as ir
|
||||
from instructions.safepoint import insert_automatic_safepoint
|
||||
|
||||
def lower_while_regular(
|
||||
builder: ir.IRBuilder,
|
||||
@ -57,6 +58,13 @@ def lower_while_regular(
|
||||
else:
|
||||
cond_val = ir.Constant(i1, 0)
|
||||
|
||||
# Insert a safepoint at loop header to allow cooperative GC
|
||||
try:
|
||||
import os
|
||||
if os.environ.get('NYASH_LLVM_AUTO_SAFEPOINT', '1') == '1':
|
||||
insert_automatic_safepoint(cbuild, builder.block.parent.module, "loop_header")
|
||||
except Exception:
|
||||
pass
|
||||
cbuild.cbranch(cond_val, body_bb, exit_bb)
|
||||
|
||||
# Body block
|
||||
@ -77,4 +85,3 @@ def lower_while_regular(
|
||||
|
||||
# Continue at exit
|
||||
builder.position_at_end(exit_bb)
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ Minimal mapping for NyRT-exported symbols (console/log family等)
|
||||
|
||||
import llvmlite.ir as ir
|
||||
from typing import Dict, List, Optional, Any
|
||||
from instructions.safepoint import insert_automatic_safepoint
|
||||
|
||||
def lower_externcall(
|
||||
builder: ir.IRBuilder,
|
||||
@ -197,3 +198,10 @@ def lower_externcall(
|
||||
vmap[dst_vid] = ir.Constant(i64, 0)
|
||||
else:
|
||||
vmap[dst_vid] = result
|
||||
# Insert an automatic safepoint after externcall
|
||||
try:
|
||||
import os
|
||||
if os.environ.get('NYASH_LLVM_AUTO_SAFEPOINT', '1') == '1':
|
||||
insert_automatic_safepoint(builder, module, "extern_call")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -7,6 +7,7 @@ import os
|
||||
import llvmlite.ir as ir
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Tuple, List, Optional, Any
|
||||
from instructions.safepoint import insert_automatic_safepoint
|
||||
|
||||
@dataclass
|
||||
class LoopFormContext:
|
||||
@ -53,7 +54,8 @@ def lower_while_loopform(
|
||||
bb_map: Dict[int, ir.Block],
|
||||
resolver=None,
|
||||
preds=None,
|
||||
block_end_values=None
|
||||
block_end_values=None,
|
||||
ctx=None,
|
||||
) -> bool:
|
||||
"""
|
||||
Lower a while loop using LoopForm structure
|
||||
@ -72,9 +74,22 @@ def lower_while_loopform(
|
||||
builder.position_at_end(lf.preheader)
|
||||
builder.branch(lf.header)
|
||||
|
||||
# Header: Evaluate condition
|
||||
# Header: Evaluate condition (insert a safepoint at loop header)
|
||||
builder.position_at_end(lf.header)
|
||||
if resolver is not None and preds is not None and block_end_values is not None:
|
||||
try:
|
||||
import os
|
||||
if os.environ.get('NYASH_LLVM_AUTO_SAFEPOINT', '1') == '1':
|
||||
insert_automatic_safepoint(builder, func.module, "loop_header")
|
||||
except Exception:
|
||||
pass
|
||||
if ctx is not None:
|
||||
try:
|
||||
cond64 = ctx.resolver.resolve_i64(condition_vid, builder.block, ctx.preds, ctx.block_end_values, ctx.vmap, ctx.bb_map)
|
||||
zero64 = ir.IntType(64)(0)
|
||||
cond = builder.icmp_unsigned('!=', cond64, zero64)
|
||||
except Exception:
|
||||
cond = vmap.get(condition_vid, ir.Constant(ir.IntType(1), 0))
|
||||
elif resolver is not None and preds is not None and block_end_values is not None:
|
||||
cond64 = resolver.resolve_i64(condition_vid, builder.block, preds, block_end_values, vmap, bb_map)
|
||||
zero64 = ir.IntType(64)(0)
|
||||
cond = builder.icmp_unsigned('!=', cond64, zero64)
|
||||
|
||||
@ -776,7 +776,7 @@ class NyashLLVMBuilder:
|
||||
for inst in term_ops:
|
||||
try:
|
||||
import os
|
||||
trace_debug(f"[llvm-py] term op: {inst.get('op')} dst={inst.get('dst')} cond={inst.get('cond')}")
|
||||
trace_debug(f"[llvm-py] term op: {inst.get('op')} dst={inst.get('dst')} cond={inst.get('cond')}")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
@ -950,7 +950,8 @@ class NyashLLVMBuilder:
|
||||
self.loop_count += 1
|
||||
if not lower_while_loopform(builder, func, cond, body,
|
||||
self.loop_count, self.vmap, self.bb_map,
|
||||
self.resolver, self.preds, self.block_end_values):
|
||||
self.resolver, self.preds, self.block_end_values,
|
||||
getattr(self, 'ctx', None)):
|
||||
# Fallback to regular while (structured)
|
||||
try:
|
||||
self.resolver._owner_lower_instruction = self.lower_instruction
|
||||
@ -975,58 +976,10 @@ class NyashLLVMBuilder:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _lower_while_regular(self, builder: ir.IRBuilder, inst: Dict[str, Any], func: ir.Function):
|
||||
"""Fallback regular while lowering"""
|
||||
# Create basic blocks: cond -> body -> cond, and exit
|
||||
cond_vid = inst.get("cond")
|
||||
body_insts = inst.get("body", [])
|
||||
|
||||
cur_bb = builder.block
|
||||
cond_bb = func.append_basic_block(name=f"while{self.loop_count}_cond")
|
||||
body_bb = func.append_basic_block(name=f"while{self.loop_count}_body")
|
||||
exit_bb = func.append_basic_block(name=f"while{self.loop_count}_exit")
|
||||
|
||||
# Jump from current to cond
|
||||
builder.branch(cond_bb)
|
||||
|
||||
# Cond block
|
||||
cbuild = ir.IRBuilder(cond_bb)
|
||||
try:
|
||||
# Resolve against the condition block to localize dominance
|
||||
cond_val = self.resolver.resolve_i64(cond_vid, cbuild.block, self.preds, self.block_end_values, self.vmap, self.bb_map)
|
||||
except Exception:
|
||||
cond_val = self.vmap.get(cond_vid)
|
||||
if cond_val is None:
|
||||
cond_val = ir.Constant(self.i1, 0)
|
||||
# Normalize to i1
|
||||
if hasattr(cond_val, 'type'):
|
||||
if isinstance(cond_val.type, ir.IntType) and cond_val.type.width == 64:
|
||||
zero64 = ir.Constant(self.i64, 0)
|
||||
cond_val = cbuild.icmp_unsigned('!=', cond_val, zero64, name="while_cond_i1")
|
||||
elif isinstance(cond_val.type, ir.PointerType):
|
||||
nullp = ir.Constant(cond_val.type, None)
|
||||
cond_val = cbuild.icmp_unsigned('!=', cond_val, nullp, name="while_cond_p1")
|
||||
elif isinstance(cond_val.type, ir.IntType) and cond_val.type.width == 1:
|
||||
# already i1
|
||||
pass
|
||||
else:
|
||||
# Fallback: treat as false
|
||||
cond_val = ir.Constant(self.i1, 0)
|
||||
else:
|
||||
cond_val = ir.Constant(self.i1, 0)
|
||||
|
||||
cbuild.cbranch(cond_val, body_bb, exit_bb)
|
||||
|
||||
# Body block
|
||||
bbuild = ir.IRBuilder(body_bb)
|
||||
# Allow nested lowering of body instructions within this block
|
||||
self._lower_instruction_list(bbuild, body_insts, func)
|
||||
# Ensure terminator: if not terminated, branch back to cond
|
||||
if bbuild.block.terminator is None:
|
||||
bbuild.branch(cond_bb)
|
||||
|
||||
# Continue at exit
|
||||
builder.position_at_end(exit_bb)
|
||||
# NOTE: regular while lowering is implemented in
|
||||
# instructions/controlflow/while_.py::lower_while_regular and invoked
|
||||
# from NyashLLVMBuilder.lower_instruction(). This legacy helper is removed
|
||||
# to avoid divergence between two implementations.
|
||||
|
||||
def _lower_instruction_list(self, builder: ir.IRBuilder, insts: List[Dict[str, Any]], func: ir.Function):
|
||||
"""Lower a flat list of instructions using current builder and function."""
|
||||
|
||||
@ -5,11 +5,161 @@ PHI wiring helpers
|
||||
- finalize_phis: Wire PHI incomings using end-of-block snapshots and resolver
|
||||
|
||||
These operate on the NyashLLVMBuilder instance to keep changes minimal.
|
||||
|
||||
Refactor note: core responsibilities are split into smaller helpers so they
|
||||
can be unit-tested in isolation.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import llvmlite.ir as ir
|
||||
|
||||
# ---- Small helpers (analyzable/testable) ----
|
||||
|
||||
def _collect_produced_stringish(blocks: List[Dict[str, Any]]) -> Dict[int, bool]:
|
||||
produced_str: Dict[int, bool] = {}
|
||||
for block_data in blocks:
|
||||
for inst in block_data.get("instructions", []) or []:
|
||||
try:
|
||||
opx = inst.get("op")
|
||||
dstx = inst.get("dst")
|
||||
if dstx is None:
|
||||
continue
|
||||
is_str = False
|
||||
if opx == "const":
|
||||
v = inst.get("value", {}) or {}
|
||||
t = v.get("type")
|
||||
if t == "string" or (isinstance(t, dict) and t.get("kind") in ("handle", "ptr") and t.get("box_type") == "StringBox"):
|
||||
is_str = True
|
||||
elif opx in ("binop", "boxcall", "externcall"):
|
||||
t = inst.get("dst_type")
|
||||
if isinstance(t, dict) and t.get("kind") == "handle" and t.get("box_type") == "StringBox":
|
||||
is_str = True
|
||||
if is_str:
|
||||
produced_str[int(dstx)] = True
|
||||
except Exception:
|
||||
pass
|
||||
return produced_str
|
||||
|
||||
def analyze_incomings(blocks: List[Dict[str, Any]]) -> Dict[int, Dict[int, List[Tuple[int, int]]]]:
|
||||
"""Return block_phi_incomings map: block_id -> { dst_vid -> [(decl_b, v_src), ...] }"""
|
||||
result: Dict[int, Dict[int, List[Tuple[int, int]]]] = {}
|
||||
for block_data in blocks:
|
||||
bid0 = block_data.get("id", 0)
|
||||
for inst in block_data.get("instructions", []) or []:
|
||||
if inst.get("op") == "phi":
|
||||
try:
|
||||
dst0 = int(inst.get("dst"))
|
||||
incoming0 = inst.get("incoming", []) or []
|
||||
except Exception:
|
||||
dst0 = None; incoming0 = []
|
||||
if dst0 is None:
|
||||
continue
|
||||
try:
|
||||
result.setdefault(int(bid0), {})[dst0] = [(int(b), int(v)) for (v, b) in incoming0]
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
def ensure_phi(builder, block_id: int, dst_vid: int, bb: ir.Block) -> ir.Instruction:
|
||||
"""Ensure a PHI placeholder exists at the block head for dst_vid and return it."""
|
||||
b = ir.IRBuilder(bb)
|
||||
try:
|
||||
b.position_at_start(bb)
|
||||
except Exception:
|
||||
pass
|
||||
# Prefer predeclared PHIs (e.g., from if-merge prepass)
|
||||
predecl = getattr(builder, 'predeclared_ret_phis', {}) if hasattr(builder, 'predeclared_ret_phis') else {}
|
||||
phi = predecl.get((int(block_id), int(dst_vid))) if predecl else None
|
||||
if phi is not None:
|
||||
builder.vmap[dst_vid] = phi
|
||||
return phi
|
||||
# Reuse current if it is a PHI in the correct block
|
||||
cur = builder.vmap.get(dst_vid)
|
||||
try:
|
||||
if cur is not None and hasattr(cur, 'add_incoming') and getattr(getattr(cur, 'basic_block', None), 'name', None) == bb.name:
|
||||
return cur
|
||||
except Exception:
|
||||
pass
|
||||
# Create a new placeholder
|
||||
ph = b.phi(builder.i64, name=f"phi_{dst_vid}")
|
||||
builder.vmap[dst_vid] = ph
|
||||
return ph
|
||||
|
||||
def _build_succs(preds: Dict[int, List[int]]) -> Dict[int, List[int]]:
|
||||
succs: Dict[int, List[int]] = {}
|
||||
for to_bid, from_list in (preds or {}).items():
|
||||
for fr in from_list:
|
||||
succs.setdefault(fr, []).append(to_bid)
|
||||
return succs
|
||||
|
||||
def _nearest_pred_on_path(succs: Dict[int, List[int]], preds_list: List[int], decl_b: int, target_bid: int) -> Optional[int]:
|
||||
from collections import deque
|
||||
q = deque([decl_b])
|
||||
visited = set([decl_b])
|
||||
parent: Dict[int, Any] = {decl_b: None}
|
||||
while q:
|
||||
cur = q.popleft()
|
||||
if cur == target_bid:
|
||||
par = parent.get(target_bid)
|
||||
return par if par in preds_list else None
|
||||
for nx in succs.get(cur, []):
|
||||
if nx not in visited:
|
||||
visited.add(nx)
|
||||
parent[nx] = cur
|
||||
q.append(nx)
|
||||
return None
|
||||
|
||||
def wire_incomings(builder, block_id: int, dst_vid: int, incoming: List[Tuple[int, int]]):
|
||||
"""Wire PHI incoming edges for (block_id, dst_vid) using declared (decl_b, v_src) pairs."""
|
||||
bb = builder.bb_map.get(block_id)
|
||||
if bb is None:
|
||||
return
|
||||
phi = ensure_phi(builder, block_id, dst_vid, bb)
|
||||
# Normalize predecessor list
|
||||
preds_raw = [p for p in builder.preds.get(block_id, []) if p != block_id]
|
||||
seen = set()
|
||||
preds_list: List[int] = []
|
||||
for p in preds_raw:
|
||||
if p not in seen:
|
||||
preds_list.append(p)
|
||||
seen.add(p)
|
||||
succs = _build_succs(builder.preds)
|
||||
# Precompute a non-self initial source for self-carry
|
||||
init_src_vid = None
|
||||
for (_bd0, vs0) in incoming:
|
||||
try:
|
||||
vi = int(vs0)
|
||||
except Exception:
|
||||
continue
|
||||
if vi != int(dst_vid):
|
||||
init_src_vid = vi
|
||||
break
|
||||
chosen: Dict[int, ir.Value] = {}
|
||||
for (b_decl, v_src) in incoming:
|
||||
try:
|
||||
bd = int(b_decl); vs = int(v_src)
|
||||
except Exception:
|
||||
continue
|
||||
pred_match = _nearest_pred_on_path(succs, preds_list, bd, block_id)
|
||||
if pred_match is None:
|
||||
continue
|
||||
if vs == int(dst_vid) and init_src_vid is not None:
|
||||
vs = int(init_src_vid)
|
||||
try:
|
||||
val = builder.resolver._value_at_end_i64(vs, pred_match, builder.preds, builder.block_end_values, builder.vmap, builder.bb_map)
|
||||
except Exception:
|
||||
val = None
|
||||
if val is None:
|
||||
val = ir.Constant(builder.i64, 0)
|
||||
chosen[pred_match] = val
|
||||
for pred_bid, val in chosen.items():
|
||||
pred_bb = builder.bb_map.get(pred_bid)
|
||||
if pred_bb is None:
|
||||
continue
|
||||
phi.add_incoming(val, pred_bb)
|
||||
|
||||
# ---- Public API (used by llvm_builder) ----
|
||||
|
||||
def setup_phi_placeholders(builder, blocks: List[Dict[str, Any]]):
|
||||
"""Predeclare PHIs and collect incoming metadata for finalize_phis.
|
||||
|
||||
@ -18,183 +168,55 @@ def setup_phi_placeholders(builder, blocks: List[Dict[str, Any]]):
|
||||
values eagerly to help downstream resolvers choose correct intrinsics.
|
||||
"""
|
||||
try:
|
||||
# Pass A: collect producer stringish hints per value-id
|
||||
produced_str: Dict[int, bool] = {}
|
||||
for block_data in blocks:
|
||||
for inst in block_data.get("instructions", []) or []:
|
||||
try:
|
||||
opx = inst.get("op")
|
||||
dstx = inst.get("dst")
|
||||
if dstx is None:
|
||||
continue
|
||||
is_str = False
|
||||
if opx == "const":
|
||||
v = inst.get("value", {}) or {}
|
||||
t = v.get("type")
|
||||
if t == "string" or (isinstance(t, dict) and t.get("kind") in ("handle","ptr") and t.get("box_type") == "StringBox"):
|
||||
is_str = True
|
||||
elif opx in ("binop","boxcall","externcall"):
|
||||
t = inst.get("dst_type")
|
||||
if isinstance(t, dict) and t.get("kind") == "handle" and t.get("box_type") == "StringBox":
|
||||
is_str = True
|
||||
if is_str:
|
||||
produced_str[int(dstx)] = True
|
||||
except Exception:
|
||||
pass
|
||||
# Pass B: materialize PHI placeholders and record incoming metadata
|
||||
builder.block_phi_incomings = {}
|
||||
produced_str = _collect_produced_stringish(blocks)
|
||||
builder.block_phi_incomings = analyze_incomings(blocks)
|
||||
# Materialize placeholders and propagate stringish tags
|
||||
for block_data in blocks:
|
||||
bid0 = block_data.get("id", 0)
|
||||
bb0 = builder.bb_map.get(bid0)
|
||||
for inst in block_data.get("instructions", []) or []:
|
||||
if inst.get("op") == "phi":
|
||||
try:
|
||||
dst0 = int(inst.get("dst"))
|
||||
incoming0 = inst.get("incoming", []) or []
|
||||
except Exception:
|
||||
dst0 = None; incoming0 = []
|
||||
if dst0 is None:
|
||||
continue
|
||||
# Record incoming metadata for finalize_phis
|
||||
try:
|
||||
builder.block_phi_incomings.setdefault(bid0, {})[dst0] = [
|
||||
(int(b), int(v)) for (v, b) in incoming0
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
# Ensure placeholder exists at block head
|
||||
if bb0 is not None:
|
||||
b0 = ir.IRBuilder(bb0)
|
||||
try:
|
||||
b0.position_at_start(bb0)
|
||||
except Exception:
|
||||
pass
|
||||
existing = builder.vmap.get(dst0)
|
||||
is_phi = False
|
||||
try:
|
||||
is_phi = hasattr(existing, 'add_incoming')
|
||||
except Exception:
|
||||
is_phi = False
|
||||
if not is_phi:
|
||||
ph0 = b0.phi(builder.i64, name=f"phi_{dst0}")
|
||||
builder.vmap[dst0] = ph0
|
||||
# Tag propagation: if explicit dst_type marks string or any incoming was produced as string-ish, tag dst
|
||||
try:
|
||||
dst_type0 = inst.get("dst_type")
|
||||
mark_str = isinstance(dst_type0, dict) and dst_type0.get("kind") == "handle" and dst_type0.get("box_type") == "StringBox"
|
||||
if not mark_str:
|
||||
for (_b_decl_i, v_src_i) in incoming0:
|
||||
try:
|
||||
if produced_str.get(int(v_src_i)):
|
||||
mark_str = True; break
|
||||
except Exception:
|
||||
pass
|
||||
if mark_str and hasattr(builder.resolver, 'mark_string'):
|
||||
builder.resolver.mark_string(int(dst0))
|
||||
except Exception:
|
||||
pass
|
||||
if inst.get("op") != "phi":
|
||||
continue
|
||||
try:
|
||||
dst0 = int(inst.get("dst"))
|
||||
incoming0 = inst.get("incoming", []) or []
|
||||
except Exception:
|
||||
dst0 = None; incoming0 = []
|
||||
if dst0 is None or bb0 is None:
|
||||
continue
|
||||
_ = ensure_phi(builder, bid0, dst0, bb0)
|
||||
# Tag propagation
|
||||
try:
|
||||
dst_type0 = inst.get("dst_type")
|
||||
mark_str = isinstance(dst_type0, dict) and dst_type0.get("kind") == "handle" and dst_type0.get("box_type") == "StringBox"
|
||||
if not mark_str:
|
||||
for (_b_decl_i, v_src_i) in incoming0:
|
||||
try:
|
||||
if produced_str.get(int(v_src_i)):
|
||||
mark_str = True; break
|
||||
except Exception:
|
||||
pass
|
||||
if mark_str and hasattr(builder.resolver, 'mark_string'):
|
||||
builder.resolver.mark_string(int(dst0))
|
||||
except Exception:
|
||||
pass
|
||||
# Definition hint: PHI defines dst in this block
|
||||
try:
|
||||
builder.def_blocks.setdefault(int(dst0), set()).add(int(bid0))
|
||||
except Exception:
|
||||
pass
|
||||
# Sync to resolver
|
||||
try:
|
||||
builder.resolver.block_phi_incomings = builder.block_phi_incomings
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def finalize_phis(builder):
|
||||
"""Finalize PHIs declared in JSON by wiring incoming edges at block heads.
|
||||
Uses resolver._value_at_end_i64 to materialize values at predecessor ends,
|
||||
ensuring casts/boxing are inserted in predecessor blocks (dominance-safe)."""
|
||||
# Build succ map for nearest-predecessor mapping
|
||||
succs: Dict[int, List[int]] = {}
|
||||
for to_bid, from_list in (builder.preds or {}).items():
|
||||
for fr in from_list:
|
||||
succs.setdefault(fr, []).append(to_bid)
|
||||
Uses resolver._value_at_end_i64 to materialize values at predecessor ends.
|
||||
"""
|
||||
for block_id, dst_map in (getattr(builder, 'block_phi_incomings', {}) or {}).items():
|
||||
bb = builder.bb_map.get(block_id)
|
||||
if bb is None:
|
||||
continue
|
||||
b = ir.IRBuilder(bb)
|
||||
try:
|
||||
b.position_at_start(bb)
|
||||
except Exception:
|
||||
pass
|
||||
for dst_vid, incoming in (dst_map or {}).items():
|
||||
# Ensure placeholder exists at block head
|
||||
# Prefer predeclared ret-phi when available and force using it.
|
||||
predecl = getattr(builder, 'predeclared_ret_phis', {}) if hasattr(builder, 'predeclared_ret_phis') else {}
|
||||
phi = predecl.get((int(block_id), int(dst_vid))) if predecl else None
|
||||
if phi is not None:
|
||||
builder.vmap[dst_vid] = phi
|
||||
else:
|
||||
phi = builder.vmap.get(dst_vid)
|
||||
need_local_phi = False
|
||||
try:
|
||||
if not (phi is not None and hasattr(phi, 'add_incoming')):
|
||||
need_local_phi = True
|
||||
else:
|
||||
bb_of_phi = getattr(getattr(phi, 'basic_block', None), 'name', None)
|
||||
if bb_of_phi != bb.name:
|
||||
need_local_phi = True
|
||||
except Exception:
|
||||
need_local_phi = True
|
||||
if need_local_phi:
|
||||
phi = b.phi(builder.i64, name=f"phi_{dst_vid}")
|
||||
builder.vmap[dst_vid] = phi
|
||||
# Wire incoming per CFG predecessor; map src_vid when provided
|
||||
preds_raw = [p for p in builder.preds.get(block_id, []) if p != block_id]
|
||||
# Deduplicate while preserving order
|
||||
seen = set()
|
||||
preds_list: List[int] = []
|
||||
for p in preds_raw:
|
||||
if p not in seen:
|
||||
preds_list.append(p)
|
||||
seen.add(p)
|
||||
# Helper: find the nearest immediate predecessor on a path decl_b -> ... -> block_id
|
||||
def nearest_pred_on_path(decl_b: int):
|
||||
from collections import deque
|
||||
q = deque([decl_b])
|
||||
visited = set([decl_b])
|
||||
parent: Dict[int, Any] = {decl_b: None}
|
||||
while q:
|
||||
cur = q.popleft()
|
||||
if cur == block_id:
|
||||
par = parent.get(block_id)
|
||||
return par if par in preds_list else None
|
||||
for nx in succs.get(cur, []):
|
||||
if nx not in visited:
|
||||
visited.add(nx)
|
||||
parent[nx] = cur
|
||||
q.append(nx)
|
||||
return None
|
||||
# Precompute a non-self initial source (if present) to use for self-carry cases
|
||||
init_src_vid = None
|
||||
for (b_decl0, v_src0) in incoming:
|
||||
try:
|
||||
vs0 = int(v_src0)
|
||||
except Exception:
|
||||
continue
|
||||
if vs0 != int(dst_vid):
|
||||
init_src_vid = vs0
|
||||
break
|
||||
# Pre-resolve declared incomings to nearest immediate predecessors
|
||||
chosen: Dict[int, ir.Value] = {}
|
||||
for (b_decl, v_src) in incoming:
|
||||
try:
|
||||
bd = int(b_decl); vs = int(v_src)
|
||||
except Exception:
|
||||
continue
|
||||
pred_match = nearest_pred_on_path(bd)
|
||||
if pred_match is None:
|
||||
continue
|
||||
if vs == int(dst_vid) and init_src_vid is not None:
|
||||
vs = int(init_src_vid)
|
||||
try:
|
||||
val = builder.resolver._value_at_end_i64(vs, pred_match, builder.preds, builder.block_end_values, builder.vmap, builder.bb_map)
|
||||
except Exception:
|
||||
val = None
|
||||
if val is None:
|
||||
# As a last resort, zero
|
||||
val = ir.Constant(builder.i64, 0)
|
||||
chosen[pred_match] = val
|
||||
# Finally add incomings
|
||||
for pred_bid, val in chosen.items():
|
||||
pred_bb = builder.bb_map.get(pred_bid)
|
||||
if pred_bb is None:
|
||||
continue
|
||||
phi.add_incoming(val, pred_bb)
|
||||
wire_incomings(builder, int(block_id), int(dst_vid), incoming)
|
||||
|
||||
65
src/llvm_py/tests/test_phi_wiring.py
Normal file
65
src/llvm_py/tests/test_phi_wiring.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""
|
||||
Unit tests for phi_wiring helpers
|
||||
|
||||
These tests construct a minimal function with two blocks and a PHI in the
|
||||
second block. We verify that placeholders are created and incoming edges
|
||||
are wired from the correct predecessor, using end-of-block snapshots.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure 'src' is importable when running this test directly
|
||||
TEST_DIR = Path(__file__).resolve().parent
|
||||
PKG_DIR = TEST_DIR.parent # src/llvm_py
|
||||
ROOT = PKG_DIR.parent # src
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
if str(PKG_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(PKG_DIR))
|
||||
|
||||
import llvmlite.ir as ir # type: ignore
|
||||
|
||||
from phi_wiring import setup_phi_placeholders, finalize_phis # type: ignore
|
||||
import llvm_builder # type: ignore
|
||||
|
||||
|
||||
def _simple_mir_with_phi():
|
||||
"""
|
||||
Build a minimal MIR JSON that compiles to:
|
||||
bb0: const v1=42; jump bb1
|
||||
bb1: phi v2=[(bb0,v1)] ; ret v2
|
||||
"""
|
||||
return {
|
||||
"functions": [
|
||||
{
|
||||
"name": "main",
|
||||
"params": [],
|
||||
"blocks": [
|
||||
{"id": 0, "instructions": [
|
||||
{"op": "const", "dst": 1, "value": {"type": "int", "value": 42}},
|
||||
{"op": "jump", "target": 1}
|
||||
]},
|
||||
{"id": 1, "instructions": [
|
||||
{"op": "phi", "dst": 2, "incoming": [[1, 0]]},
|
||||
{"op": "ret", "value": 2}
|
||||
]}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_phi_placeholders_and_finalize_basic():
|
||||
mir = _simple_mir_with_phi()
|
||||
b = llvm_builder.NyashLLVMBuilder()
|
||||
# Build once to create function, blocks, preds; stop before finalize by calling internals like lower_function
|
||||
reader_functions = mir["functions"]
|
||||
assert reader_functions
|
||||
b.lower_function(reader_functions[0])
|
||||
# After lowering a function, finalize_phis is already called at the end of lower_function.
|
||||
# Verify via IR text that a PHI exists in bb1 with an incoming from bb0.
|
||||
ir_text = str(b.module)
|
||||
assert 'bb1' in ir_text
|
||||
assert 'phi i64' in ir_text
|
||||
assert '[0, %"bb0"]' in ir_text or '[ i64 0, %"bb0"]' in ir_text
|
||||
@ -105,7 +105,7 @@ impl MirOptimizer {
|
||||
|
||||
// Pass 7 (optional): Core-13 pure normalization
|
||||
if crate::config::env::mir_core13_pure() {
|
||||
stats.merge(self.normalize_pure_core13(module));
|
||||
stats.merge(crate::mir::optimizer_passes::normalize_core13_pure::normalize_pure_core13(self, module));
|
||||
}
|
||||
|
||||
if self.debug {
|
||||
@ -147,200 +147,7 @@ impl MirOptimizer {
|
||||
/// Neg x => BinOp(Sub, Const 0, x)
|
||||
/// Not x => Compare(Eq, x, Const false)
|
||||
/// BitNot x => BinOp(BitXor, x, Const(-1))
|
||||
fn normalize_pure_core13(&mut self, module: &mut MirModule) -> OptimizationStats {
|
||||
use super::instruction::ConstValue;
|
||||
use super::{BinaryOp, CompareOp, MirInstruction as I};
|
||||
let mut stats = OptimizationStats::new();
|
||||
for (_fname, function) in &mut module.functions {
|
||||
for (_bb, block) in &mut function.blocks {
|
||||
let mut out: Vec<I> = Vec::with_capacity(block.instructions.len() + 8);
|
||||
let old = std::mem::take(&mut block.instructions);
|
||||
for inst in old.into_iter() {
|
||||
match inst {
|
||||
I::Load { dst, ptr } => {
|
||||
out.push(I::ExternCall {
|
||||
dst: Some(dst),
|
||||
iface_name: "env.local".to_string(),
|
||||
method_name: "get".to_string(),
|
||||
args: vec![ptr],
|
||||
effects: super::EffectMask::READ,
|
||||
});
|
||||
stats.intrinsic_optimizations += 1;
|
||||
}
|
||||
I::Store { value, ptr } => {
|
||||
out.push(I::ExternCall {
|
||||
dst: None,
|
||||
iface_name: "env.local".to_string(),
|
||||
method_name: "set".to_string(),
|
||||
args: vec![ptr, value],
|
||||
effects: super::EffectMask::WRITE,
|
||||
});
|
||||
stats.intrinsic_optimizations += 1;
|
||||
}
|
||||
I::NewBox {
|
||||
dst,
|
||||
box_type,
|
||||
mut args,
|
||||
} => {
|
||||
// prepend type name as Const String
|
||||
let ty_id = super::ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
out.push(I::Const {
|
||||
dst: ty_id,
|
||||
value: ConstValue::String(box_type),
|
||||
});
|
||||
let mut call_args = Vec::with_capacity(1 + args.len());
|
||||
call_args.push(ty_id);
|
||||
call_args.append(&mut args);
|
||||
out.push(I::ExternCall {
|
||||
dst: Some(dst),
|
||||
iface_name: "env.box".to_string(),
|
||||
method_name: "new".to_string(),
|
||||
args: call_args,
|
||||
effects: super::EffectMask::PURE, // constructor is logically alloc; conservatively PURE here
|
||||
});
|
||||
stats.intrinsic_optimizations += 1;
|
||||
}
|
||||
I::UnaryOp { dst, op, operand } => {
|
||||
match op {
|
||||
super::UnaryOp::Neg => {
|
||||
let zero = super::ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
out.push(I::Const {
|
||||
dst: zero,
|
||||
value: ConstValue::Integer(0),
|
||||
});
|
||||
out.push(I::BinOp {
|
||||
dst,
|
||||
op: BinaryOp::Sub,
|
||||
lhs: zero,
|
||||
rhs: operand,
|
||||
});
|
||||
}
|
||||
super::UnaryOp::Not => {
|
||||
let f = super::ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
out.push(I::Const {
|
||||
dst: f,
|
||||
value: ConstValue::Bool(false),
|
||||
});
|
||||
out.push(I::Compare {
|
||||
dst,
|
||||
op: CompareOp::Eq,
|
||||
lhs: operand,
|
||||
rhs: f,
|
||||
});
|
||||
}
|
||||
super::UnaryOp::BitNot => {
|
||||
let all1 = super::ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
out.push(I::Const {
|
||||
dst: all1,
|
||||
value: ConstValue::Integer(-1),
|
||||
});
|
||||
out.push(I::BinOp {
|
||||
dst,
|
||||
op: BinaryOp::BitXor,
|
||||
lhs: operand,
|
||||
rhs: all1,
|
||||
});
|
||||
}
|
||||
}
|
||||
stats.intrinsic_optimizations += 1;
|
||||
}
|
||||
other => out.push(other),
|
||||
}
|
||||
}
|
||||
block.instructions = out;
|
||||
if let Some(term) = block.terminator.take() {
|
||||
block.terminator = Some(match term {
|
||||
I::Load { dst, ptr } => I::ExternCall {
|
||||
dst: Some(dst),
|
||||
iface_name: "env.local".to_string(),
|
||||
method_name: "get".to_string(),
|
||||
args: vec![ptr],
|
||||
effects: super::EffectMask::READ,
|
||||
},
|
||||
I::Store { value, ptr } => I::ExternCall {
|
||||
dst: None,
|
||||
iface_name: "env.local".to_string(),
|
||||
method_name: "set".to_string(),
|
||||
args: vec![ptr, value],
|
||||
effects: super::EffectMask::WRITE,
|
||||
},
|
||||
I::NewBox {
|
||||
dst,
|
||||
box_type,
|
||||
mut args,
|
||||
} => {
|
||||
let ty_id = super::ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
block.instructions.push(I::Const {
|
||||
dst: ty_id,
|
||||
value: ConstValue::String(box_type),
|
||||
});
|
||||
let mut call_args = Vec::with_capacity(1 + args.len());
|
||||
call_args.push(ty_id);
|
||||
call_args.append(&mut args);
|
||||
I::ExternCall {
|
||||
dst: Some(dst),
|
||||
iface_name: "env.box".to_string(),
|
||||
method_name: "new".to_string(),
|
||||
args: call_args,
|
||||
effects: super::EffectMask::PURE,
|
||||
}
|
||||
}
|
||||
I::UnaryOp { dst, op, operand } => match op {
|
||||
super::UnaryOp::Neg => {
|
||||
let zero = super::ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
block.instructions.push(I::Const {
|
||||
dst: zero,
|
||||
value: ConstValue::Integer(0),
|
||||
});
|
||||
I::BinOp {
|
||||
dst,
|
||||
op: BinaryOp::Sub,
|
||||
lhs: zero,
|
||||
rhs: operand,
|
||||
}
|
||||
}
|
||||
super::UnaryOp::Not => {
|
||||
let f = super::ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
block.instructions.push(I::Const {
|
||||
dst: f,
|
||||
value: ConstValue::Bool(false),
|
||||
});
|
||||
I::Compare {
|
||||
dst,
|
||||
op: CompareOp::Eq,
|
||||
lhs: operand,
|
||||
rhs: f,
|
||||
}
|
||||
}
|
||||
super::UnaryOp::BitNot => {
|
||||
let all1 = super::ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
block.instructions.push(I::Const {
|
||||
dst: all1,
|
||||
value: ConstValue::Integer(-1),
|
||||
});
|
||||
I::BinOp {
|
||||
dst,
|
||||
op: BinaryOp::BitXor,
|
||||
lhs: operand,
|
||||
rhs: all1,
|
||||
}
|
||||
}
|
||||
},
|
||||
other => other,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
stats
|
||||
}
|
||||
// normalize_pure_core13 moved to optimizer_passes::normalize_core13_pure
|
||||
|
||||
/// Eliminate dead code in a single function
|
||||
fn eliminate_dead_code_in_function(&mut self, function: &mut MirFunction) -> usize {
|
||||
|
||||
@ -3,3 +3,5 @@ pub mod diagnostics;
|
||||
pub mod intrinsics;
|
||||
pub mod normalize;
|
||||
pub mod reorder;
|
||||
pub mod normalize_core13_pure;
|
||||
pub mod normalize_legacy_all;
|
||||
|
||||
145
src/mir/optimizer_passes/normalize_core13_pure.rs
Normal file
145
src/mir/optimizer_passes/normalize_core13_pure.rs
Normal file
@ -0,0 +1,145 @@
|
||||
use crate::mir::optimizer::MirOptimizer;
|
||||
use crate::mir::optimizer_stats::OptimizationStats;
|
||||
use crate::mir::{BinaryOp, CompareOp, EffectMask, MirInstruction as I, MirModule, MirType, ValueId};
|
||||
|
||||
/// Core-13 "pure" normalization: rewrite a few non-13 ops to allowed forms.
|
||||
/// - Load(dst, ptr) => ExternCall(Some dst, env.local.get, [ptr])
|
||||
/// - Store(val, ptr) => ExternCall(None, env.local.set, [ptr, val])
|
||||
/// - NewBox(dst, T, args...) => ExternCall(Some dst, env.box.new, [Const String(T), args...])
|
||||
/// - UnaryOp:
|
||||
/// Neg x => BinOp(Sub, Const 0, x)
|
||||
/// Not x => Compare(Eq, x, Const false)
|
||||
/// BitNot x => BinOp(BitXor, x, Const(-1))
|
||||
pub fn normalize_pure_core13(_opt: &mut MirOptimizer, module: &mut MirModule) -> OptimizationStats {
|
||||
use crate::mir::instruction::ConstValue;
|
||||
let mut stats = OptimizationStats::new();
|
||||
for (_fname, function) in &mut module.functions {
|
||||
for (_bb, block) in &mut function.blocks {
|
||||
let mut out: Vec<I> = Vec::with_capacity(block.instructions.len() + 8);
|
||||
let old = std::mem::take(&mut block.instructions);
|
||||
for inst in old.into_iter() {
|
||||
match inst {
|
||||
I::Load { dst, ptr } => {
|
||||
out.push(I::ExternCall {
|
||||
dst: Some(dst),
|
||||
iface_name: "env.local".to_string(),
|
||||
method_name: "get".to_string(),
|
||||
args: vec![ptr],
|
||||
effects: EffectMask::READ,
|
||||
});
|
||||
stats.intrinsic_optimizations += 1;
|
||||
}
|
||||
I::Store { value, ptr } => {
|
||||
out.push(I::ExternCall {
|
||||
dst: None,
|
||||
iface_name: "env.local".to_string(),
|
||||
method_name: "set".to_string(),
|
||||
args: vec![ptr, value],
|
||||
effects: EffectMask::WRITE,
|
||||
});
|
||||
stats.intrinsic_optimizations += 1;
|
||||
}
|
||||
I::NewBox { dst, box_type, mut args } => {
|
||||
// prepend type name as Const String
|
||||
let ty_id = ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
out.push(I::Const { dst: ty_id, value: ConstValue::String(box_type) });
|
||||
let mut call_args = Vec::with_capacity(1 + args.len());
|
||||
call_args.push(ty_id);
|
||||
call_args.append(&mut args);
|
||||
out.push(I::ExternCall {
|
||||
dst: Some(dst),
|
||||
iface_name: "env.box".to_string(),
|
||||
method_name: "new".to_string(),
|
||||
args: call_args,
|
||||
effects: EffectMask::PURE, // constructor is logically alloc; conservatively PURE here
|
||||
});
|
||||
stats.intrinsic_optimizations += 1;
|
||||
}
|
||||
I::UnaryOp { dst, op, operand } => {
|
||||
match op {
|
||||
crate::mir::UnaryOp::Neg => {
|
||||
let zero = ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
out.push(I::Const { dst: zero, value: ConstValue::Integer(0) });
|
||||
out.push(I::BinOp { dst, op: BinaryOp::Sub, lhs: zero, rhs: operand });
|
||||
}
|
||||
crate::mir::UnaryOp::Not => {
|
||||
let f = ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
out.push(I::Const { dst: f, value: ConstValue::Bool(false) });
|
||||
out.push(I::Compare { dst, op: CompareOp::Eq, lhs: operand, rhs: f });
|
||||
}
|
||||
crate::mir::UnaryOp::BitNot => {
|
||||
let all1 = ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
out.push(I::Const { dst: all1, value: ConstValue::Integer(-1) });
|
||||
out.push(I::BinOp { dst, op: BinaryOp::BitXor, lhs: operand, rhs: all1 });
|
||||
}
|
||||
}
|
||||
stats.intrinsic_optimizations += 1;
|
||||
}
|
||||
other => out.push(other),
|
||||
}
|
||||
}
|
||||
block.instructions = out;
|
||||
|
||||
if let Some(term) = block.terminator.take() {
|
||||
block.terminator = Some(match term {
|
||||
I::Load { dst, ptr } => I::ExternCall {
|
||||
dst: Some(dst),
|
||||
iface_name: "env.local".to_string(),
|
||||
method_name: "get".to_string(),
|
||||
args: vec![ptr],
|
||||
effects: EffectMask::READ,
|
||||
},
|
||||
I::Store { value, ptr } => I::ExternCall {
|
||||
dst: None,
|
||||
iface_name: "env.local".to_string(),
|
||||
method_name: "set".to_string(),
|
||||
args: vec![ptr, value],
|
||||
effects: EffectMask::WRITE,
|
||||
},
|
||||
I::NewBox { dst, box_type, mut args } => {
|
||||
let ty_id = ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
block.instructions.push(I::Const { dst: ty_id, value: ConstValue::String(box_type) });
|
||||
let mut call_args = Vec::with_capacity(1 + args.len());
|
||||
call_args.push(ty_id);
|
||||
call_args.append(&mut args);
|
||||
I::ExternCall {
|
||||
dst: Some(dst),
|
||||
iface_name: "env.box".to_string(),
|
||||
method_name: "new".to_string(),
|
||||
args: call_args,
|
||||
effects: EffectMask::PURE,
|
||||
}
|
||||
}
|
||||
I::UnaryOp { dst, op, operand } => match op {
|
||||
crate::mir::UnaryOp::Neg => {
|
||||
let zero = ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
block.instructions.push(I::Const { dst: zero, value: ConstValue::Integer(0) });
|
||||
I::BinOp { dst, op: BinaryOp::Sub, lhs: zero, rhs: operand }
|
||||
}
|
||||
crate::mir::UnaryOp::Not => {
|
||||
let f = ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
block.instructions.push(I::Const { dst: f, value: ConstValue::Bool(false) });
|
||||
I::Compare { dst, op: CompareOp::Eq, lhs: operand, rhs: f }
|
||||
}
|
||||
crate::mir::UnaryOp::BitNot => {
|
||||
let all1 = ValueId::new(function.next_value_id);
|
||||
function.next_value_id += 1;
|
||||
block.instructions.push(I::Const { dst: all1, value: ConstValue::Integer(-1) });
|
||||
I::BinOp { dst, op: BinaryOp::BitXor, lhs: operand, rhs: all1 }
|
||||
}
|
||||
},
|
||||
other => other,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
stats
|
||||
}
|
||||
|
||||
8
src/mir/optimizer_passes/normalize_legacy_all.rs
Normal file
8
src/mir/optimizer_passes/normalize_legacy_all.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use crate::mir::optimizer::MirOptimizer;
|
||||
use crate::mir::optimizer_stats::OptimizationStats;
|
||||
|
||||
/// Delegate: legacy normalization (moved from optimizer.rs)
|
||||
pub fn normalize_legacy_instructions(opt: &mut MirOptimizer, module: &mut crate::mir::MirModule) -> OptimizationStats {
|
||||
crate::mir::optimizer_passes::normalize::normalize_legacy_instructions(opt, module)
|
||||
}
|
||||
|
||||
@ -143,6 +143,12 @@ impl NyashRunner {
|
||||
if self.config.cli_verbose {
|
||||
std::env::set_var("NYASH_CLI_VERBOSE", "1");
|
||||
}
|
||||
// GC mode forwarding: map CLI --gc to NYASH_GC_MODE for downstream runtimes
|
||||
if let Some(ref m) = self.config.gc_mode {
|
||||
if !m.trim().is_empty() {
|
||||
std::env::set_var("NYASH_GC_MODE", m);
|
||||
}
|
||||
}
|
||||
// Script-level env directives (special comments) — parse early
|
||||
// Supported:
|
||||
// // @env KEY=VALUE
|
||||
|
||||
@ -11,8 +11,6 @@ use std::thread::sleep;
|
||||
use crate::runner::pipeline::{suggest_in_base, resolve_using_target};
|
||||
use crate::runner::trace::cli_verbose;
|
||||
use crate::cli_v;
|
||||
use crate::runner::trace::cli_verbose;
|
||||
use crate::cli_v;
|
||||
|
||||
// (moved) suggest_in_base is now in runner/pipeline.rs
|
||||
|
||||
@ -131,97 +129,13 @@ impl NyashRunner {
|
||||
|
||||
/// Helper: run PyVM harness over a MIR module, returning the exit code
|
||||
fn run_pyvm_harness(&self, module: &nyash_rust::mir::MirModule, tag: &str) -> Result<i32, String> {
|
||||
let py3 = which::which("python3").map_err(|e| format!("python3 not found: {}", e))?;
|
||||
let runner = std::path::Path::new("tools/pyvm_runner.py");
|
||||
if !runner.exists() { return Err(format!("PyVM runner not found: {}", runner.display())); }
|
||||
let tmp_dir = std::path::Path::new("tmp");
|
||||
let _ = std::fs::create_dir_all(tmp_dir);
|
||||
let mir_json_path = tmp_dir.join("nyash_pyvm_mir.json");
|
||||
crate::runner::mir_json_emit::emit_mir_json_for_harness_bin(module, &mir_json_path)
|
||||
.map_err(|e| format!("PyVM MIR JSON emit error: {}", e))?;
|
||||
cli_v!("[ny-compiler] using PyVM ({} ) → {}", tag, mir_json_path.display());
|
||||
// Determine entry function hint (prefer Main.main if present)
|
||||
let entry = if module.functions.contains_key("Main.main") { "Main.main" }
|
||||
else if module.functions.contains_key("main") { "main" } else { "Main.main" };
|
||||
let status = std::process::Command::new(py3)
|
||||
.args([
|
||||
runner.to_string_lossy().as_ref(),
|
||||
"--in",
|
||||
&mir_json_path.display().to_string(),
|
||||
"--entry",
|
||||
entry,
|
||||
])
|
||||
.status()
|
||||
.map_err(|e| format!("spawn pyvm: {}", e))?;
|
||||
let code = status.code().unwrap_or(1);
|
||||
if !status.success() { cli_v!("❌ PyVM ({}) failed (status={})", tag, code); }
|
||||
Ok(code)
|
||||
super::common_util::pyvm::run_pyvm_harness(module, tag)
|
||||
}
|
||||
|
||||
/// Helper: try external selfhost compiler EXE to parse Ny -> JSON v0 and return MIR module
|
||||
/// Returns Some(module) on success, None on failure (timeout/invalid output/missing exe)
|
||||
fn exe_try_parse_json_v0(&self, filename: &str, timeout_ms: u64) -> Option<nyash_rust::mir::MirModule> {
|
||||
// Resolve parser EXE path
|
||||
let exe_path = if let Ok(p) = std::env::var("NYASH_NY_COMPILER_EXE_PATH") {
|
||||
std::path::PathBuf::from(p)
|
||||
} else {
|
||||
let mut p = std::path::PathBuf::from("dist/nyash_compiler");
|
||||
#[cfg(windows)]
|
||||
{ p.push("nyash_compiler.exe"); }
|
||||
#[cfg(not(windows))]
|
||||
{ p.push("nyash_compiler"); }
|
||||
if !p.exists() {
|
||||
if let Ok(w) = which::which("nyash_compiler") { w } else { p }
|
||||
} else { p }
|
||||
};
|
||||
if !exe_path.exists() { cli_v!("[ny-compiler] exe not found at {}", exe_path.display()); return None; }
|
||||
|
||||
// Build command
|
||||
let mut cmd = std::process::Command::new(&exe_path);
|
||||
cmd.arg(filename);
|
||||
if crate::config::env::ny_compiler_min_json() { cmd.arg("--min-json"); }
|
||||
if crate::config::env::selfhost_read_tmp() { cmd.arg("--read-tmp"); }
|
||||
if let Some(raw) = crate::config::env::ny_compiler_child_args() { for tok in raw.split_whitespace() { cmd.arg(tok); } }
|
||||
let mut cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
let mut child = match cmd.spawn() { Ok(c) => c, Err(e) => { eprintln!("[ny-compiler] exe spawn failed: {}", e); return None; } };
|
||||
let mut ch_stdout = child.stdout.take();
|
||||
let mut ch_stderr = child.stderr.take();
|
||||
let start = Instant::now();
|
||||
let mut timed_out = false;
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_)) => break,
|
||||
Ok(None) => {
|
||||
if start.elapsed() >= Duration::from_millis(timeout_ms) { let _ = child.kill(); let _ = child.wait(); timed_out = true; break; }
|
||||
sleep(Duration::from_millis(10));
|
||||
}
|
||||
Err(e) => { eprintln!("[ny-compiler] exe wait error: {}", e); return None; }
|
||||
}
|
||||
}
|
||||
let mut out_buf = Vec::new();
|
||||
let mut err_buf = Vec::new();
|
||||
if let Some(mut s) = ch_stdout { let _ = s.read_to_end(&mut out_buf); }
|
||||
if let Some(mut s) = ch_stderr { let _ = s.read_to_end(&mut err_buf); }
|
||||
if timed_out {
|
||||
let head = String::from_utf8_lossy(&out_buf).chars().take(200).collect::<String>();
|
||||
eprintln!("[ny-compiler] exe timeout after {} ms; stdout(head)='{}'", timeout_ms, head.replace('\n', "\\n"));
|
||||
return None;
|
||||
}
|
||||
let stdout = match String::from_utf8(out_buf) { Ok(s) => s, Err(_) => String::new() };
|
||||
let mut json_line = String::new();
|
||||
for line in stdout.lines() { let t = line.trim(); if t.starts_with('{') && t.contains("\"version\"") && t.contains("\"kind\"") { json_line = t.to_string(); break; } }
|
||||
if json_line.is_empty() {
|
||||
if cli_verbose() {
|
||||
let head: String = stdout.chars().take(200).collect();
|
||||
let errh: String = String::from_utf8_lossy(&err_buf).chars().take(200).collect();
|
||||
cli_v!("[ny-compiler] exe produced no JSON; stdout(head)='{}' stderr(head)='{}'", head.replace('\n', "\\n"), errh.replace('\n', "\\n"));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
match json_v0_bridge::parse_json_v0_to_module(&json_line) {
|
||||
Ok(module) => Some(module),
|
||||
Err(e) => { eprintln!("[ny-compiler] JSON parse failed (exe): {}", e); None }
|
||||
}
|
||||
super::common_util::selfhost_exe::exe_try_parse_json_v0(filename, timeout_ms)
|
||||
}
|
||||
|
||||
/// Phase-15.3: Attempt Ny compiler pipeline (Ny -> JSON v0 via Ny program), then execute MIR
|
||||
@ -324,38 +238,22 @@ impl NyashRunner {
|
||||
if crate::config::env::selfhost_read_tmp() { cmd.arg("--read-tmp"); }
|
||||
if let Some(raw) = crate::config::env::ny_compiler_child_args() { for tok in raw.split_whitespace() { cmd.arg(tok); } }
|
||||
let timeout_ms: u64 = crate::config::env::ny_compiler_timeout_ms();
|
||||
let mut cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
let mut child = match cmd.spawn() { Ok(c) => c, Err(e) => { eprintln!("[ny-compiler] exe spawn failed: {}", e); return false; } };
|
||||
let mut ch_stdout = child.stdout.take();
|
||||
let mut ch_stderr = child.stderr.take();
|
||||
let start = Instant::now();
|
||||
let mut timed_out = false;
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_status)) => { break; }
|
||||
Ok(None) => {
|
||||
if start.elapsed() >= Duration::from_millis(timeout_ms) { let _ = child.kill(); let _ = child.wait(); timed_out = true; break; }
|
||||
sleep(Duration::from_millis(10));
|
||||
}
|
||||
Err(e) => { eprintln!("[ny-compiler] exe wait error: {}", e); return false; }
|
||||
}
|
||||
}
|
||||
let mut out_buf = Vec::new();
|
||||
let mut err_buf = Vec::new();
|
||||
if let Some(mut s) = ch_stdout { let _ = s.read_to_end(&mut out_buf); }
|
||||
if let Some(mut s) = ch_stderr { let _ = s.read_to_end(&mut err_buf); }
|
||||
if timed_out {
|
||||
let head = String::from_utf8_lossy(&out_buf).chars().take(200).collect::<String>();
|
||||
let out = match super::common_util::io::spawn_with_timeout(cmd, timeout_ms) {
|
||||
Ok(o) => o,
|
||||
Err(e) => { eprintln!("[ny-compiler] exe spawn failed: {}", e); return false; }
|
||||
};
|
||||
if out.timed_out {
|
||||
let head = String::from_utf8_lossy(&out.stdout).chars().take(200).collect::<String>();
|
||||
eprintln!("[ny-compiler] exe timeout after {} ms; stdout(head)='{}'", timeout_ms, head.replace('\n', "\\n"));
|
||||
return false;
|
||||
}
|
||||
let stdout = match String::from_utf8(out_buf) { Ok(s) => s, Err(_) => String::new() };
|
||||
let stdout = match String::from_utf8(out.stdout) { Ok(s) => s, Err(_) => String::new() };
|
||||
let mut json_line = String::new();
|
||||
for line in stdout.lines() { let t = line.trim(); if t.starts_with('{') && t.contains("\"version\"") && t.contains("\"kind\"") { json_line = t.to_string(); break; } }
|
||||
if json_line.is_empty() {
|
||||
if crate::config::env::cli_verbose() {
|
||||
let head: String = stdout.chars().take(200).collect();
|
||||
let errh: String = String::from_utf8_lossy(&err_buf).chars().take(200).collect();
|
||||
let errh: String = String::from_utf8_lossy(&out.stderr).chars().take(200).collect();
|
||||
eprintln!("[ny-compiler] exe produced no JSON; stdout(head)='{}' stderr(head)='{}'", head.replace('\n', "\\n"), errh.replace('\n', "\\n"));
|
||||
}
|
||||
return false;
|
||||
@ -450,40 +348,15 @@ impl NyashRunner {
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.unwrap_or(2000);
|
||||
let mut cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
let mut child = match cmd.spawn() {
|
||||
Ok(c) => c,
|
||||
let out = match super::common_util::io::spawn_with_timeout(cmd, timeout_ms) {
|
||||
Ok(o) => o,
|
||||
Err(e) => { eprintln!("[ny-compiler] spawn failed: {}", e); return false; }
|
||||
};
|
||||
let mut ch_stdout = child.stdout.take();
|
||||
let mut ch_stderr = child.stderr.take();
|
||||
let start = Instant::now();
|
||||
let mut timed_out = false;
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_status)) => { break; }
|
||||
Ok(None) => {
|
||||
if start.elapsed() >= Duration::from_millis(timeout_ms) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
timed_out = true;
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(10));
|
||||
}
|
||||
Err(e) => { eprintln!("[ny-compiler] wait error: {}", e); return false; }
|
||||
}
|
||||
}
|
||||
// Collect any available output
|
||||
let mut out_buf = Vec::new();
|
||||
let mut err_buf = Vec::new();
|
||||
if let Some(mut s) = ch_stdout { let _ = s.read_to_end(&mut out_buf); }
|
||||
if let Some(mut s) = ch_stderr { let _ = s.read_to_end(&mut err_buf); }
|
||||
if timed_out {
|
||||
let head = String::from_utf8_lossy(&out_buf).chars().take(200).collect::<String>();
|
||||
if out.timed_out {
|
||||
let head = String::from_utf8_lossy(&out.stdout).chars().take(200).collect::<String>();
|
||||
eprintln!("[ny-compiler] child timeout after {} ms; stdout(head)='{}'", timeout_ms, head.replace('\n', "\\n"));
|
||||
}
|
||||
let stdout = match String::from_utf8(out_buf) { Ok(s) => s, Err(_) => String::new() };
|
||||
let stdout = match String::from_utf8(out.stdout.clone()) { Ok(s) => s, Err(_) => String::new() };
|
||||
if timed_out {
|
||||
// Fall back path will be taken below when json_line remains empty
|
||||
} else if let Ok(s) = String::from_utf8(err_buf.clone()) {
|
||||
|
||||
55
src/runner/modes/common_util/io.rs
Normal file
55
src/runner/modes/common_util/io.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use std::io::Read;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::thread::sleep;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub struct ChildOutput {
|
||||
pub stdout: Vec<u8>,
|
||||
pub stderr: Vec<u8>,
|
||||
pub status_ok: bool,
|
||||
pub exit_code: Option<i32>,
|
||||
pub timed_out: bool,
|
||||
}
|
||||
|
||||
/// Spawn command with timeout (ms), capture stdout/stderr, and return ChildOutput.
|
||||
pub fn spawn_with_timeout(mut cmd: Command, timeout_ms: u64) -> std::io::Result<ChildOutput> {
|
||||
let mut cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
let mut child = cmd.spawn()?;
|
||||
let mut ch_stdout = child.stdout.take();
|
||||
let mut ch_stderr = child.stderr.take();
|
||||
let start = Instant::now();
|
||||
let mut timed_out = false;
|
||||
let mut exit_status: Option<std::process::ExitStatus> = None;
|
||||
loop {
|
||||
match child.try_wait()? {
|
||||
Some(status) => { exit_status = Some(status); break },
|
||||
None => {
|
||||
if start.elapsed() >= Duration::from_millis(timeout_ms) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
timed_out = true;
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out_buf = Vec::new();
|
||||
let mut err_buf = Vec::new();
|
||||
if let Some(mut s) = ch_stdout {
|
||||
let _ = s.read_to_end(&mut out_buf);
|
||||
}
|
||||
if let Some(mut s) = ch_stderr {
|
||||
let _ = s.read_to_end(&mut err_buf);
|
||||
}
|
||||
let (status_ok, exit_code) = if let Some(st) = exit_status {
|
||||
(st.success(), st.code())
|
||||
} else { (false, None) };
|
||||
Ok(ChildOutput {
|
||||
stdout: out_buf,
|
||||
stderr: err_buf,
|
||||
status_ok,
|
||||
exit_code,
|
||||
timed_out,
|
||||
})
|
||||
}
|
||||
9
src/runner/modes/common_util/mod.rs
Normal file
9
src/runner/modes/common_util/mod.rs
Normal file
@ -0,0 +1,9 @@
|
||||
/*!
|
||||
* Shared helpers for runner/modes/common.rs
|
||||
*
|
||||
* Minimal extraction to reduce duplication and prepare for full split.
|
||||
*/
|
||||
|
||||
pub mod pyvm;
|
||||
pub mod selfhost_exe;
|
||||
pub mod io;
|
||||
39
src/runner/modes/common_util/pyvm.rs
Normal file
39
src/runner/modes/common_util/pyvm.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use std::process::Stdio;
|
||||
|
||||
/// Run PyVM harness over a MIR module, returning the exit code
|
||||
pub fn run_pyvm_harness(module: &crate::mir::MirModule, tag: &str) -> Result<i32, String> {
|
||||
let py3 = which::which("python3").map_err(|e| format!("python3 not found: {}", e))?;
|
||||
let runner = std::path::Path::new("tools/pyvm_runner.py");
|
||||
if !runner.exists() {
|
||||
return Err(format!("PyVM runner not found: {}", runner.display()));
|
||||
}
|
||||
let tmp_dir = std::path::Path::new("tmp");
|
||||
let _ = std::fs::create_dir_all(tmp_dir);
|
||||
let mir_json_path = tmp_dir.join("nyash_pyvm_mir.json");
|
||||
crate::runner::mir_json_emit::emit_mir_json_for_harness_bin(module, &mir_json_path)
|
||||
.map_err(|e| format!("PyVM MIR JSON emit error: {}", e))?;
|
||||
crate::cli_v!("[ny-compiler] using PyVM ({} ) → {}", tag, mir_json_path.display());
|
||||
// Determine entry function hint (prefer Main.main if present)
|
||||
let entry = if module.functions.contains_key("Main.main") {
|
||||
"Main.main"
|
||||
} else if module.functions.contains_key("main") {
|
||||
"main"
|
||||
} else {
|
||||
"Main.main"
|
||||
};
|
||||
let status = std::process::Command::new(py3)
|
||||
.args([
|
||||
runner.to_string_lossy().as_ref(),
|
||||
"--in",
|
||||
&mir_json_path.display().to_string(),
|
||||
"--entry",
|
||||
entry,
|
||||
])
|
||||
.status()
|
||||
.map_err(|e| format!("spawn pyvm: {}", e))?;
|
||||
let code = status.code().unwrap_or(1);
|
||||
if !status.success() {
|
||||
crate::cli_v!("❌ PyVM ({}) failed (status={})", tag, code);
|
||||
}
|
||||
Ok(code)
|
||||
}
|
||||
131
src/runner/modes/common_util/selfhost_exe.rs
Normal file
131
src/runner/modes/common_util/selfhost_exe.rs
Normal file
@ -0,0 +1,131 @@
|
||||
use std::io::Read;
|
||||
use std::process::Stdio;
|
||||
use std::thread::sleep;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Try external selfhost compiler EXE to parse Ny -> JSON v0 and return MIR module.
|
||||
/// Returns Some(module) on success, None on failure (timeout/invalid output/missing exe)
|
||||
pub fn exe_try_parse_json_v0(filename: &str, timeout_ms: u64) -> Option<crate::mir::MirModule> {
|
||||
// Resolve parser EXE path
|
||||
let exe_path = if let Ok(p) = std::env::var("NYASH_NY_COMPILER_EXE_PATH") {
|
||||
std::path::PathBuf::from(p)
|
||||
} else {
|
||||
let mut p = std::path::PathBuf::from("dist/nyash_compiler");
|
||||
#[cfg(windows)]
|
||||
{
|
||||
p.push("nyash_compiler.exe");
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
p.push("nyash_compiler");
|
||||
}
|
||||
if !p.exists() {
|
||||
if let Ok(w) = which::which("nyash_compiler") {
|
||||
w
|
||||
} else {
|
||||
p
|
||||
}
|
||||
} else {
|
||||
p
|
||||
}
|
||||
};
|
||||
if !exe_path.exists() {
|
||||
crate::cli_v!("[ny-compiler] exe not found at {}", exe_path.display());
|
||||
return None;
|
||||
}
|
||||
// Build command
|
||||
let mut cmd = std::process::Command::new(&exe_path);
|
||||
cmd.arg(filename);
|
||||
if crate::config::env::ny_compiler_min_json() {
|
||||
cmd.arg("--min-json");
|
||||
}
|
||||
if crate::config::env::selfhost_read_tmp() {
|
||||
cmd.arg("--read-tmp");
|
||||
}
|
||||
if let Some(raw) = crate::config::env::ny_compiler_child_args() {
|
||||
for tok in raw.split_whitespace() {
|
||||
cmd.arg(tok);
|
||||
}
|
||||
}
|
||||
let mut cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
let mut child = match cmd.spawn() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("[ny-compiler] exe spawn failed: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let mut ch_stdout = child.stdout.take();
|
||||
let mut ch_stderr = child.stderr.take();
|
||||
let start = Instant::now();
|
||||
let mut timed_out = false;
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_)) => break,
|
||||
Ok(None) => {
|
||||
if start.elapsed() >= Duration::from_millis(timeout_ms) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
timed_out = true;
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(10));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[ny-compiler] exe wait error: {}", e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out_buf = Vec::new();
|
||||
let mut err_buf = Vec::new();
|
||||
if let Some(mut s) = ch_stdout {
|
||||
let _ = s.read_to_end(&mut out_buf);
|
||||
}
|
||||
if let Some(mut s) = ch_stderr {
|
||||
let _ = s.read_to_end(&mut err_buf);
|
||||
}
|
||||
if timed_out {
|
||||
let head = String::from_utf8_lossy(&out_buf)
|
||||
.chars()
|
||||
.take(200)
|
||||
.collect::<String>();
|
||||
eprintln!(
|
||||
"[ny-compiler] exe timeout after {} ms; stdout(head)='{}'",
|
||||
timeout_ms,
|
||||
head.replace('\n', "\\n")
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let stdout = match String::from_utf8(out_buf) {
|
||||
Ok(s) => s,
|
||||
Err(_) => String::new(),
|
||||
};
|
||||
let mut json_line = String::new();
|
||||
for line in stdout.lines() {
|
||||
let t = line.trim();
|
||||
if t.starts_with('{') && t.contains("\"version\"") && t.contains("\"kind\"") {
|
||||
json_line = t.to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if json_line.is_empty() {
|
||||
if crate::config::env::cli_verbose() {
|
||||
let head: String = stdout.chars().take(200).collect();
|
||||
let errh: String = String::from_utf8_lossy(&err_buf).chars().take(200).collect();
|
||||
crate::cli_v!(
|
||||
"[ny-compiler] exe produced no JSON; stdout(head)='{}' stderr(head)='{}'",
|
||||
head.replace('\n', "\\n"),
|
||||
errh.replace('\n', "\\n")
|
||||
);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
match crate::runner::json_v0_bridge::parse_json_v0_to_module(&json_line) {
|
||||
Ok(module) => Some(module),
|
||||
Err(e) => {
|
||||
eprintln!("[ny-compiler] JSON parse failed: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -84,21 +84,22 @@ impl NyashRunner {
|
||||
);
|
||||
}
|
||||
// 2) Run harness with --in/--out(失敗時は即エラー)
|
||||
let status = std::process::Command::new(py3)
|
||||
.args([
|
||||
harness.to_string_lossy().as_ref(),
|
||||
"--in",
|
||||
&mir_json_path.display().to_string(),
|
||||
"--out",
|
||||
&_out_path,
|
||||
])
|
||||
.status()
|
||||
let mut cmd = std::process::Command::new(py3);
|
||||
cmd.args([
|
||||
harness.to_string_lossy().as_ref(),
|
||||
"--in",
|
||||
&mir_json_path.display().to_string(),
|
||||
"--out",
|
||||
&_out_path,
|
||||
]);
|
||||
let out = crate::runner::modes::common_util::io::spawn_with_timeout(cmd, 20_000)
|
||||
.map_err(|e| format!("spawn harness: {}", e))
|
||||
.unwrap();
|
||||
if !status.success() {
|
||||
if out.timed_out || !out.status_ok {
|
||||
eprintln!(
|
||||
"❌ llvmlite harness failed (status={})",
|
||||
status.code().unwrap_or(-1)
|
||||
"❌ llvmlite harness failed (timeout={} code={:?})",
|
||||
out.timed_out,
|
||||
out.exit_code
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
@ -8,5 +8,8 @@ pub mod mir;
|
||||
pub mod vm;
|
||||
pub mod pyvm;
|
||||
|
||||
// Shared helpers extracted from common.rs (in progress)
|
||||
pub mod common_util;
|
||||
|
||||
#[cfg(feature = "cranelift-jit")]
|
||||
pub mod aot;
|
||||
|
||||
@ -76,22 +76,20 @@ pub fn execute_pyvm_only(_runner: &NyashRunner, filename: &str) {
|
||||
mir_json_path.display()
|
||||
);
|
||||
}
|
||||
let status = std::process::Command::new(py3)
|
||||
.args([
|
||||
runner.to_string_lossy().as_ref(),
|
||||
"--in",
|
||||
&mir_json_path.display().to_string(),
|
||||
"--entry",
|
||||
entry,
|
||||
])
|
||||
.status()
|
||||
let mut cmd = std::process::Command::new(py3);
|
||||
cmd.args([
|
||||
runner.to_string_lossy().as_ref(),
|
||||
"--in",
|
||||
&mir_json_path.display().to_string(),
|
||||
"--entry",
|
||||
entry,
|
||||
]);
|
||||
let out = crate::runner::modes::common_util::io::spawn_with_timeout(cmd, 10_000)
|
||||
.map_err(|e| format!("spawn pyvm: {}", e))
|
||||
.unwrap();
|
||||
let code = status.code().unwrap_or(1);
|
||||
if !status.success() {
|
||||
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
|
||||
eprintln!("❌ PyVM failed (status={})", code);
|
||||
}
|
||||
let code = if out.timed_out { 1 } else { out.exit_code.unwrap_or(1) };
|
||||
if out.timed_out && std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
|
||||
eprintln!("❌ PyVM timeout");
|
||||
}
|
||||
process::exit(code);
|
||||
} else {
|
||||
@ -99,4 +97,3 @@ pub fn execute_pyvm_only(_runner: &NyashRunner, filename: &str) {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -207,23 +207,20 @@ impl NyashRunner {
|
||||
"Main.main"
|
||||
};
|
||||
// Spawn runner
|
||||
let status = std::process::Command::new(py3)
|
||||
.args([
|
||||
runner.to_string_lossy().as_ref(),
|
||||
"--in",
|
||||
&mir_json_path.display().to_string(),
|
||||
"--entry",
|
||||
entry,
|
||||
])
|
||||
.status()
|
||||
let mut cmd = std::process::Command::new(py3);
|
||||
cmd.args([
|
||||
runner.to_string_lossy().as_ref(),
|
||||
"--in",
|
||||
&mir_json_path.display().to_string(),
|
||||
"--entry",
|
||||
entry,
|
||||
]);
|
||||
let out = super::common_util::io::spawn_with_timeout(cmd, 10_000)
|
||||
.map_err(|e| format!("spawn pyvm: {}", e))
|
||||
.unwrap();
|
||||
// Always propagate PyVM exit code to match llvmlite semantics
|
||||
let code = status.code().unwrap_or(1);
|
||||
if !status.success() {
|
||||
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
|
||||
eprintln!("❌ PyVM failed (status={})", code);
|
||||
}
|
||||
let code = if out.timed_out { 1 } else { out.exit_code.unwrap_or(1) };
|
||||
if out.timed_out && std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
|
||||
eprintln!("❌ PyVM timeout");
|
||||
}
|
||||
process::exit(code);
|
||||
} else {
|
||||
|
||||
@ -248,14 +248,15 @@ impl NyashRunner {
|
||||
if py.exists() {
|
||||
let mut cmd = std::process::Command::new(&py3);
|
||||
cmd.arg(py).arg(&tmp_path);
|
||||
let out = match cmd.output() {
|
||||
let timeout_ms: u64 = std::env::var("NYASH_NY_COMPILER_TIMEOUT_MS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(2000);
|
||||
let out = match super::modes::common_util::io::spawn_with_timeout(cmd, timeout_ms) {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
eprintln!("[ny-compiler] python harness failed to spawn: {}", e);
|
||||
return false;
|
||||
}
|
||||
Err(e) => { eprintln!("[ny-compiler] python harness failed: {}", e); return false; }
|
||||
};
|
||||
if out.status.success() {
|
||||
if !out.timed_out {
|
||||
if let Ok(line) = String::from_utf8(out.stdout)
|
||||
.map(|s| s.lines().next().unwrap_or("").to_string())
|
||||
{
|
||||
@ -366,179 +367,48 @@ impl NyashRunner {
|
||||
}
|
||||
};
|
||||
if exe_path.exists() {
|
||||
let mut cmd = std::process::Command::new(&exe_path);
|
||||
// Prefer passing the original filename directly (parser EXE accepts positional path)
|
||||
cmd.arg(filename);
|
||||
// Gates
|
||||
if std::env::var("NYASH_NY_COMPILER_MIN_JSON").ok().as_deref() == Some("1") {
|
||||
cmd.arg("--min-json");
|
||||
}
|
||||
if std::env::var("NYASH_SELFHOST_READ_TMP").ok().as_deref() == Some("1") {
|
||||
cmd.arg("--read-tmp");
|
||||
}
|
||||
if std::env::var("NYASH_NY_COMPILER_STAGE3").ok().as_deref() == Some("1") {
|
||||
cmd.arg("--stage3");
|
||||
}
|
||||
if let Ok(raw) = std::env::var("NYASH_NY_COMPILER_CHILD_ARGS") {
|
||||
for tok in raw.split_whitespace() {
|
||||
cmd.arg(tok);
|
||||
}
|
||||
}
|
||||
let timeout_ms: u64 = std::env::var("NYASH_NY_COMPILER_TIMEOUT_MS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(2000);
|
||||
let mut cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
let mut child = match cmd.spawn() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("[ny-compiler] exe spawn failed: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let mut ch_stdout = child.stdout.take();
|
||||
let mut ch_stderr = child.stderr.take();
|
||||
let start = Instant::now();
|
||||
let mut timed_out = false;
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_status)) => {
|
||||
break;
|
||||
}
|
||||
Ok(None) => {
|
||||
if start.elapsed() >= Duration::from_millis(timeout_ms) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
timed_out = true;
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(10));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[ny-compiler] exe wait error: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out_buf = Vec::new();
|
||||
let mut err_buf = Vec::new();
|
||||
if let Some(mut s) = ch_stdout {
|
||||
let _ = s.read_to_end(&mut out_buf);
|
||||
}
|
||||
if let Some(mut s) = ch_stderr {
|
||||
let _ = s.read_to_end(&mut err_buf);
|
||||
}
|
||||
if timed_out {
|
||||
let head = String::from_utf8_lossy(&out_buf)
|
||||
.chars()
|
||||
.take(200)
|
||||
.collect::<String>();
|
||||
eprintln!(
|
||||
"[ny-compiler] exe timeout after {} ms; stdout(head)='{}'",
|
||||
timeout_ms,
|
||||
head.replace('\n', "\\n")
|
||||
);
|
||||
return false;
|
||||
}
|
||||
let stdout = match String::from_utf8(out_buf) {
|
||||
Ok(s) => s,
|
||||
Err(_) => String::new(),
|
||||
};
|
||||
let mut json_line = String::new();
|
||||
for line in stdout.lines() {
|
||||
let t = line.trim();
|
||||
if t.starts_with('{') && t.contains("\"version\"") && t.contains("\"kind\"") {
|
||||
json_line = t.to_string();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if json_line.is_empty() {
|
||||
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
|
||||
let head: String = stdout.chars().take(200).collect();
|
||||
let errh: String = String::from_utf8_lossy(&err_buf)
|
||||
.chars()
|
||||
.take(200)
|
||||
.collect();
|
||||
eprintln!("[ny-compiler] exe produced no JSON; stdout(head)='{}' stderr(head)='{}'", head.replace('\n', "\\n"), errh.replace('\n', "\\n"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Parse JSON v0 → MIR module
|
||||
match super::json_v0_bridge::parse_json_v0_to_module(&json_line) {
|
||||
Ok(module) => {
|
||||
println!("🚀 Ny compiler EXE path (ny→json_v0) ON");
|
||||
super::json_v0_bridge::maybe_dump_mir(&module);
|
||||
let emit_only = std::env::var("NYASH_NY_COMPILER_EMIT_ONLY")
|
||||
.unwrap_or_else(|_| "1".to_string())
|
||||
== "1";
|
||||
if emit_only {
|
||||
return false;
|
||||
} else {
|
||||
// Prefer PyVM when requested (reference semantics), regardless of BoxCall presence
|
||||
let prefer_pyvm =
|
||||
std::env::var("NYASH_VM_USE_PY").ok().as_deref() == Some("1");
|
||||
if prefer_pyvm {
|
||||
if let Ok(py3) = which::which("python3") {
|
||||
let runner = std::path::Path::new("tools/pyvm_runner.py");
|
||||
if runner.exists() {
|
||||
let tmp_dir = std::path::Path::new("tmp");
|
||||
let _ = std::fs::create_dir_all(tmp_dir);
|
||||
let mir_json_path = tmp_dir.join("nyash_pyvm_mir.json");
|
||||
if let Err(e) = crate::runner::mir_json_emit::emit_mir_json_for_harness_bin(&module, &mir_json_path) {
|
||||
eprintln!("❌ PyVM MIR JSON emit error: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref()
|
||||
== Some("1")
|
||||
{
|
||||
eprintln!(
|
||||
"[Bridge] using PyVM (selfhost) → {}",
|
||||
mir_json_path.display()
|
||||
);
|
||||
}
|
||||
let entry = if module.functions.contains_key("Main.main") {
|
||||
"Main.main"
|
||||
} else if module.functions.contains_key("main") {
|
||||
"main"
|
||||
} else {
|
||||
"Main.main"
|
||||
};
|
||||
let status = std::process::Command::new(py3)
|
||||
.args([
|
||||
"tools/pyvm_runner.py",
|
||||
"--in",
|
||||
&mir_json_path.display().to_string(),
|
||||
"--entry",
|
||||
entry,
|
||||
])
|
||||
.status()
|
||||
.map_err(|e| format!("spawn pyvm: {}", e))
|
||||
.unwrap();
|
||||
let code = status.code().unwrap_or(1);
|
||||
if !status.success() {
|
||||
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref()
|
||||
== Some("1")
|
||||
{
|
||||
eprintln!(
|
||||
"❌ PyVM (selfhost) failed (status={})",
|
||||
code
|
||||
);
|
||||
}
|
||||
}
|
||||
// Harmonize with interpreter path for smokes: print Result then exit code
|
||||
println!("Result: {}", code);
|
||||
std::process::exit(code);
|
||||
}
|
||||
if let Some(module) = super::modes::common_util::selfhost_exe::exe_try_parse_json_v0(filename, timeout_ms) {
|
||||
super::json_v0_bridge::maybe_dump_mir(&module);
|
||||
let emit_only = std::env::var("NYASH_NY_COMPILER_EMIT_ONLY")
|
||||
.unwrap_or_else(|_| "1".to_string())
|
||||
== "1";
|
||||
if emit_only { return false; }
|
||||
// Prefer PyVM when requested (reference semantics)
|
||||
if std::env::var("NYASH_VM_USE_PY").ok().as_deref() == Some("1") {
|
||||
if let Ok(py3) = which::which("python3") {
|
||||
let runner = std::path::Path::new("tools/pyvm_runner.py");
|
||||
if runner.exists() {
|
||||
let tmp_dir = std::path::Path::new("tmp");
|
||||
let _ = std::fs::create_dir_all(tmp_dir);
|
||||
let mir_json_path = tmp_dir.join("nyash_pyvm_mir.json");
|
||||
if let Err(e) = crate::runner::mir_json_emit::emit_mir_json_for_harness_bin(&module, &mir_json_path) {
|
||||
eprintln!("❌ PyVM MIR JSON emit error: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
|
||||
eprintln!("[Bridge] using PyVM (selfhost) → {}", mir_json_path.display());
|
||||
}
|
||||
let entry = if module.functions.contains_key("Main.main") { "Main.main" }
|
||||
else if module.functions.contains_key("main") { "main" } else { "Main.main" };
|
||||
let status = std::process::Command::new(py3)
|
||||
.args(["tools/pyvm_runner.py", "--in", &mir_json_path.display().to_string(), "--entry", entry])
|
||||
.status()
|
||||
.map_err(|e| format!("spawn pyvm: {}", e))
|
||||
.unwrap();
|
||||
let code = status.code().unwrap_or(1);
|
||||
println!("Result: {}", code);
|
||||
std::process::exit(code);
|
||||
}
|
||||
self.execute_mir_module(&module);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[ny-compiler] json parse error: {}", e);
|
||||
return false;
|
||||
}
|
||||
self.execute_mir_module(&module);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -578,52 +448,15 @@ impl NyashRunner {
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(2000);
|
||||
let mut cmd = cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
let mut child = match cmd.spawn() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("[ny-compiler] spawn inline vm failed: {}", e);
|
||||
return false;
|
||||
}
|
||||
let out = match super::modes::common_util::io::spawn_with_timeout(cmd, timeout_ms) {
|
||||
Ok(o) => o,
|
||||
Err(e) => { eprintln!("[ny-compiler] spawn inline vm failed: {}", e); return false; }
|
||||
};
|
||||
let mut ch_stdout = child.stdout.take();
|
||||
let mut ch_stderr = child.stderr.take();
|
||||
let start = Instant::now();
|
||||
let mut timed_out = false;
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(_)) => break,
|
||||
Ok(None) => {
|
||||
if start.elapsed() >= Duration::from_millis(timeout_ms) {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
timed_out = true;
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(10));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[ny-compiler] inline wait error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if out.timed_out {
|
||||
let head = String::from_utf8_lossy(&out.stdout).chars().take(200).collect::<String>();
|
||||
eprintln!("[ny-compiler] inline timeout after {} ms; stdout(head)='{}'", timeout_ms, head.replace('\n', "\\n"));
|
||||
}
|
||||
let mut out_buf = Vec::new();
|
||||
if let Some(mut s) = ch_stdout {
|
||||
let _ = s.read_to_end(&mut out_buf);
|
||||
}
|
||||
if timed_out {
|
||||
let head = String::from_utf8_lossy(&out_buf)
|
||||
.chars()
|
||||
.take(200)
|
||||
.collect::<String>();
|
||||
eprintln!(
|
||||
"[ny-compiler] inline timeout after {} ms; stdout(head)='{}'",
|
||||
timeout_ms,
|
||||
head.replace('\n', "\\n")
|
||||
);
|
||||
}
|
||||
raw = String::from_utf8_lossy(&out_buf).to_string();
|
||||
raw = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
}
|
||||
let mut json_line = String::new();
|
||||
for line in raw.lines() {
|
||||
|
||||
@ -11,11 +11,13 @@ pub enum BarrierKind {
|
||||
|
||||
/// GC hooks that execution engines may call at key points.
|
||||
/// Implementations must be Send + Sync for multi-thread preparation.
|
||||
pub trait GcHooks: Send + Sync {
|
||||
pub trait GcHooks: Send + Sync + std::any::Any {
|
||||
/// Safe point for cooperative GC (e.g., poll or yield).
|
||||
fn safepoint(&self) {}
|
||||
/// Memory barrier hint for loads/stores.
|
||||
fn barrier(&self, _kind: BarrierKind) {}
|
||||
/// Allocation accounting (bytes are best-effort; may be 0 when unknown)
|
||||
fn alloc(&self, _bytes: u64) {}
|
||||
/// Optional counters snapshot for diagnostics. Default: None.
|
||||
fn snapshot_counters(&self) -> Option<(u64, u64, u64)> {
|
||||
None
|
||||
@ -27,48 +29,35 @@ pub struct NullGc;
|
||||
|
||||
impl GcHooks for NullGc {}
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
/// Simple counting GC (PoC): counts safepoints and barriers.
|
||||
/// Useful to validate hook frequency without affecting semantics.
|
||||
/// CountingGc is now a thin wrapper around the unified GcController.
|
||||
pub struct CountingGc {
|
||||
pub safepoints: AtomicU64,
|
||||
pub barrier_reads: AtomicU64,
|
||||
pub barrier_writes: AtomicU64,
|
||||
inner: crate::runtime::gc_controller::GcController,
|
||||
}
|
||||
|
||||
impl CountingGc {
|
||||
pub fn new() -> Self {
|
||||
// Default to rc+cycle mode for development metrics
|
||||
let mode = crate::runtime::gc_mode::GcMode::RcCycle;
|
||||
Self {
|
||||
safepoints: AtomicU64::new(0),
|
||||
barrier_reads: AtomicU64::new(0),
|
||||
barrier_writes: AtomicU64::new(0),
|
||||
inner: crate::runtime::gc_controller::GcController::new(mode),
|
||||
}
|
||||
}
|
||||
pub fn snapshot(&self) -> (u64, u64, u64) {
|
||||
(
|
||||
self.safepoints.load(Ordering::Relaxed),
|
||||
self.barrier_reads.load(Ordering::Relaxed),
|
||||
self.barrier_writes.load(Ordering::Relaxed),
|
||||
)
|
||||
self.inner.snapshot()
|
||||
}
|
||||
}
|
||||
|
||||
impl GcHooks for CountingGc {
|
||||
fn safepoint(&self) {
|
||||
self.safepoints.fetch_add(1, Ordering::Relaxed);
|
||||
self.inner.safepoint();
|
||||
}
|
||||
fn barrier(&self, kind: BarrierKind) {
|
||||
match kind {
|
||||
BarrierKind::Read => {
|
||||
self.barrier_reads.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
BarrierKind::Write => {
|
||||
self.barrier_writes.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
self.inner.barrier(kind);
|
||||
}
|
||||
fn alloc(&self, bytes: u64) {
|
||||
self.inner.alloc(bytes);
|
||||
}
|
||||
fn snapshot_counters(&self) -> Option<(u64, u64, u64)> {
|
||||
Some(self.snapshot())
|
||||
Some(self.inner.snapshot())
|
||||
}
|
||||
}
|
||||
|
||||
207
src/runtime/gc_controller.rs
Normal file
207
src/runtime/gc_controller.rs
Normal file
@ -0,0 +1,207 @@
|
||||
//! Unified GC controller (skeleton)
|
||||
//! Implements GcHooks and centralizes mode selection and metrics.
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use super::gc::{BarrierKind, GcHooks};
|
||||
use super::gc_mode::GcMode;
|
||||
use crate::config::env;
|
||||
use crate::runtime::gc_trace;
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
|
||||
pub struct GcController {
|
||||
mode: GcMode,
|
||||
safepoints: AtomicU64,
|
||||
barrier_reads: AtomicU64,
|
||||
barrier_writes: AtomicU64,
|
||||
alloc_bytes: AtomicU64,
|
||||
alloc_count: AtomicU64,
|
||||
sp_since_last: AtomicU64,
|
||||
bytes_since_last: AtomicU64,
|
||||
collect_sp_interval: Option<u64>,
|
||||
collect_alloc_bytes: Option<u64>,
|
||||
// Diagnostics: last trial reachability counters
|
||||
trial_nodes_last: AtomicU64,
|
||||
trial_edges_last: AtomicU64,
|
||||
// Diagnostics: collection counters and last duration/flags
|
||||
collect_count_total: AtomicU64,
|
||||
collect_by_sp: AtomicU64,
|
||||
collect_by_alloc: AtomicU64,
|
||||
trial_duration_last_ms: AtomicU64,
|
||||
trial_reason_last: AtomicU64, // bitflags: 1=sp, 2=alloc
|
||||
}
|
||||
|
||||
impl GcController {
|
||||
pub fn new(mode: GcMode) -> Self {
|
||||
Self {
|
||||
mode,
|
||||
safepoints: AtomicU64::new(0),
|
||||
barrier_reads: AtomicU64::new(0),
|
||||
barrier_writes: AtomicU64::new(0),
|
||||
alloc_bytes: AtomicU64::new(0),
|
||||
alloc_count: AtomicU64::new(0),
|
||||
sp_since_last: AtomicU64::new(0),
|
||||
bytes_since_last: AtomicU64::new(0),
|
||||
collect_sp_interval: env::gc_collect_sp_interval(),
|
||||
collect_alloc_bytes: env::gc_collect_alloc_bytes(),
|
||||
trial_nodes_last: AtomicU64::new(0),
|
||||
trial_edges_last: AtomicU64::new(0),
|
||||
collect_count_total: AtomicU64::new(0),
|
||||
collect_by_sp: AtomicU64::new(0),
|
||||
collect_by_alloc: AtomicU64::new(0),
|
||||
trial_duration_last_ms: AtomicU64::new(0),
|
||||
trial_reason_last: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
pub fn mode(&self) -> GcMode {
|
||||
self.mode
|
||||
}
|
||||
pub fn snapshot(&self) -> (u64, u64, u64) {
|
||||
(
|
||||
self.safepoints.load(Ordering::Relaxed),
|
||||
self.barrier_reads.load(Ordering::Relaxed),
|
||||
self.barrier_writes.load(Ordering::Relaxed),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl GcHooks for GcController {
|
||||
fn safepoint(&self) {
|
||||
// Off mode: minimal overhead but still callable
|
||||
if self.mode != GcMode::Off {
|
||||
self.safepoints.fetch_add(1, Ordering::Relaxed);
|
||||
let sp = self.sp_since_last.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
// Opportunistic collection trigger
|
||||
let sp_hit = self
|
||||
.collect_sp_interval
|
||||
.map(|n| n > 0 && sp >= n)
|
||||
.unwrap_or(false);
|
||||
let bytes = self.bytes_since_last.load(Ordering::Relaxed);
|
||||
let bytes_hit = self
|
||||
.collect_alloc_bytes
|
||||
.map(|n| n > 0 && bytes >= n)
|
||||
.unwrap_or(false);
|
||||
if sp_hit || bytes_hit {
|
||||
// Record reason flags for diagnostics
|
||||
let mut flags: u64 = 0;
|
||||
if sp_hit { flags |= 1; self.collect_by_sp.fetch_add(1, Ordering::Relaxed); }
|
||||
if bytes_hit { flags |= 2; self.collect_by_alloc.fetch_add(1, Ordering::Relaxed); }
|
||||
self.trial_reason_last.store(flags, Ordering::Relaxed);
|
||||
self.run_trial_collection();
|
||||
}
|
||||
}
|
||||
// Future: per-mode collection/cooperation hooks
|
||||
}
|
||||
fn barrier(&self, kind: BarrierKind) {
|
||||
if self.mode == GcMode::Off {
|
||||
return;
|
||||
}
|
||||
match kind {
|
||||
BarrierKind::Read => {
|
||||
self.barrier_reads.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
BarrierKind::Write => {
|
||||
self.barrier_writes.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn snapshot_counters(&self) -> Option<(u64, u64, u64)> {
|
||||
Some(self.snapshot())
|
||||
}
|
||||
fn alloc(&self, bytes: u64) {
|
||||
if self.mode == GcMode::Off {
|
||||
return;
|
||||
}
|
||||
self.alloc_count.fetch_add(1, Ordering::Relaxed);
|
||||
self.alloc_bytes.fetch_add(bytes, Ordering::Relaxed);
|
||||
self.bytes_since_last
|
||||
.fetch_add(bytes, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl GcController {
|
||||
pub fn alloc_totals(&self) -> (u64, u64) {
|
||||
(
|
||||
self.alloc_count.load(Ordering::Relaxed),
|
||||
self.alloc_bytes.load(Ordering::Relaxed),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl GcController {
|
||||
fn run_trial_collection(&self) {
|
||||
// Reset windows
|
||||
self.sp_since_last.store(0, Ordering::Relaxed);
|
||||
self.bytes_since_last.store(0, Ordering::Relaxed);
|
||||
// PoC: no object graph; report current handles as leak candidates and return.
|
||||
if self.mode == GcMode::Off {
|
||||
return;
|
||||
}
|
||||
// Only run for rc/rc+cycle/stw; rc+cycle is default.
|
||||
match self.mode {
|
||||
GcMode::Rc | GcMode::RcCycle | GcMode::STW => {
|
||||
let started = std::time::Instant::now();
|
||||
// Roots: JIT/AOT handle registry snapshot
|
||||
let roots = crate::jit::rt::handles::snapshot_arcs();
|
||||
let mut visited: HashSet<u64> = HashSet::new();
|
||||
let mut q: VecDeque<std::sync::Arc<dyn crate::box_trait::NyashBox>> =
|
||||
VecDeque::new();
|
||||
for r in roots.into_iter() {
|
||||
let id = r.box_id();
|
||||
if visited.insert(id) {
|
||||
q.push_back(r);
|
||||
}
|
||||
}
|
||||
let mut nodes: u64 = visited.len() as u64;
|
||||
let mut edges: u64 = 0;
|
||||
while let Some(cur) = q.pop_front() {
|
||||
gc_trace::trace_children(&*cur, &mut |child| {
|
||||
edges += 1;
|
||||
let id = child.box_id();
|
||||
if visited.insert(id) {
|
||||
nodes += 1;
|
||||
q.push_back(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Store last diagnostics (available for JSON metrics)
|
||||
self.trial_nodes_last.store(nodes, Ordering::Relaxed);
|
||||
self.trial_edges_last.store(edges, Ordering::Relaxed);
|
||||
if (nodes + edges) > 0 && crate::config::env::gc_metrics() {
|
||||
eprintln!(
|
||||
"[GC] trial: reachable nodes={} edges={} (roots=jit_handles)",
|
||||
nodes, edges
|
||||
);
|
||||
}
|
||||
// Update counters
|
||||
let dur = started.elapsed();
|
||||
let ms = dur.as_millis() as u64;
|
||||
self.trial_duration_last_ms.store(ms, Ordering::Relaxed);
|
||||
self.collect_count_total.fetch_add(1, Ordering::Relaxed);
|
||||
// Reason flags derive from current env thresholds vs last windows reaching triggers
|
||||
// Note: we set flags in safepoint() where triggers were decided.
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GcController {
|
||||
pub fn trial_reachability_last(&self) -> (u64, u64) {
|
||||
(
|
||||
self.trial_nodes_last.load(Ordering::Relaxed),
|
||||
self.trial_edges_last.load(Ordering::Relaxed),
|
||||
)
|
||||
}
|
||||
pub fn collection_totals(&self) -> (u64, u64, u64) {
|
||||
(
|
||||
self.collect_count_total.load(Ordering::Relaxed),
|
||||
self.collect_by_sp.load(Ordering::Relaxed),
|
||||
self.collect_by_alloc.load(Ordering::Relaxed),
|
||||
)
|
||||
}
|
||||
pub fn trial_duration_last_ms(&self) -> u64 {
|
||||
self.trial_duration_last_ms.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn trial_reason_last_bits(&self) -> u64 { self.trial_reason_last.load(Ordering::Relaxed) }
|
||||
}
|
||||
34
src/runtime/gc_mode.rs
Normal file
34
src/runtime/gc_mode.rs
Normal file
@ -0,0 +1,34 @@
|
||||
//! GC mode selection (user-facing)
|
||||
use crate::config::env;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GcMode {
|
||||
RcCycle,
|
||||
Minorgen,
|
||||
STW,
|
||||
Rc,
|
||||
Off,
|
||||
}
|
||||
|
||||
impl GcMode {
|
||||
pub fn from_env() -> Self {
|
||||
match env::gc_mode().to_ascii_lowercase().as_str() {
|
||||
"auto" | "rc+cycle" => GcMode::RcCycle,
|
||||
"minorgen" => GcMode::Minorgen,
|
||||
"stw" => GcMode::STW,
|
||||
"rc" => GcMode::Rc,
|
||||
"off" => GcMode::Off,
|
||||
_ => GcMode::RcCycle,
|
||||
}
|
||||
}
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
GcMode::RcCycle => "rc+cycle",
|
||||
GcMode::Minorgen => "minorgen",
|
||||
GcMode::STW => "stw",
|
||||
GcMode::Rc => "rc",
|
||||
GcMode::Off => "off",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
src/runtime/gc_trace.rs
Normal file
35
src/runtime/gc_trace.rs
Normal file
@ -0,0 +1,35 @@
|
||||
//! Minimal GC tracing helpers (skeleton)
|
||||
//!
|
||||
//! Downcast-based child edge enumeration for builtin containers.
|
||||
//! This is a non-invasive helper to support diagnostics and future collectors.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::box_trait::NyashBox;
|
||||
|
||||
/// Visit child boxes of a given object and invoke `visit(child)` for each.
|
||||
/// This function recognizes builtin containers (ArrayBox/MapBox) and is a no-op otherwise.
|
||||
pub fn trace_children(obj: &dyn NyashBox, visit: &mut dyn FnMut(Arc<dyn NyashBox>)) {
|
||||
// ArrayBox
|
||||
if let Some(arr) = obj.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
|
||||
if let Ok(items) = arr.items.read() {
|
||||
for it in items.iter() {
|
||||
// Convert Box<dyn NyashBox> to Arc<dyn NyashBox>
|
||||
let arc: Arc<dyn NyashBox> = Arc::from(it.clone_box());
|
||||
visit(arc);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// MapBox
|
||||
if let Some(map) = obj.as_any().downcast_ref::<crate::boxes::map_box::MapBox>() {
|
||||
if let Ok(data) = map.get_data().read() {
|
||||
for (_k, v) in data.iter() {
|
||||
let arc: Arc<dyn NyashBox> = Arc::from(v.clone_box());
|
||||
visit(arc);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,97 +4,74 @@ use once_cell::sync::OnceCell;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use super::scheduler::CancellationToken;
|
||||
use super::{gc::GcHooks, scheduler::Scheduler};
|
||||
use super::{gc::BarrierKind, gc::GcHooks, scheduler::Scheduler};
|
||||
|
||||
static GLOBAL_GC: OnceCell<RwLock<Option<Arc<dyn GcHooks>>>> = OnceCell::new();
|
||||
static GLOBAL_SCHED: OnceCell<RwLock<Option<Arc<dyn Scheduler>>>> = OnceCell::new();
|
||||
// Phase 2 scaffold: current task group's cancellation token (no-op default)
|
||||
static GLOBAL_CUR_TOKEN: OnceCell<RwLock<Option<CancellationToken>>> = OnceCell::new();
|
||||
// Phase 2 scaffold: current group's child futures registry (best-effort)
|
||||
static GLOBAL_GROUP_FUTURES: OnceCell<RwLock<Vec<crate::boxes::future::FutureWeak>>> =
|
||||
OnceCell::new();
|
||||
// Strong ownership list for implicit group (pre-TaskGroup actualization)
|
||||
static GLOBAL_GROUP_STRONG: OnceCell<RwLock<Vec<crate::boxes::future::FutureBox>>> =
|
||||
OnceCell::new();
|
||||
// Simple scope depth counter for implicit group (join-at-scope-exit footing)
|
||||
static TASK_SCOPE_DEPTH: OnceCell<RwLock<usize>> = OnceCell::new();
|
||||
// TaskGroup scope stack (explicit group ownership per function scope)
|
||||
static TASK_GROUP_STACK: OnceCell<
|
||||
RwLock<Vec<std::sync::Arc<crate::boxes::task_group_box::TaskGroupInner>>>,
|
||||
> = OnceCell::new();
|
||||
// Unified global runtime hooks state (single lock for consistency)
|
||||
struct GlobalHooksState {
|
||||
gc: Option<Arc<dyn GcHooks>>,
|
||||
sched: Option<Arc<dyn Scheduler>>,
|
||||
cur_token: Option<CancellationToken>,
|
||||
futures: Vec<crate::boxes::future::FutureWeak>,
|
||||
strong: Vec<crate::boxes::future::FutureBox>,
|
||||
scope_depth: usize,
|
||||
group_stack: Vec<std::sync::Arc<crate::boxes::task_group_box::TaskGroupInner>>,
|
||||
}
|
||||
|
||||
fn gc_cell() -> &'static RwLock<Option<Arc<dyn GcHooks>>> {
|
||||
GLOBAL_GC.get_or_init(|| RwLock::new(None))
|
||||
impl GlobalHooksState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
gc: None,
|
||||
sched: None,
|
||||
cur_token: None,
|
||||
futures: Vec::new(),
|
||||
strong: Vec::new(),
|
||||
scope_depth: 0,
|
||||
group_stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn sched_cell() -> &'static RwLock<Option<Arc<dyn Scheduler>>> {
|
||||
GLOBAL_SCHED.get_or_init(|| RwLock::new(None))
|
||||
}
|
||||
fn token_cell() -> &'static RwLock<Option<CancellationToken>> {
|
||||
GLOBAL_CUR_TOKEN.get_or_init(|| RwLock::new(None))
|
||||
}
|
||||
fn futures_cell() -> &'static RwLock<Vec<crate::boxes::future::FutureWeak>> {
|
||||
GLOBAL_GROUP_FUTURES.get_or_init(|| RwLock::new(Vec::new()))
|
||||
}
|
||||
fn strong_cell() -> &'static RwLock<Vec<crate::boxes::future::FutureBox>> {
|
||||
GLOBAL_GROUP_STRONG.get_or_init(|| RwLock::new(Vec::new()))
|
||||
}
|
||||
fn scope_depth_cell() -> &'static RwLock<usize> {
|
||||
TASK_SCOPE_DEPTH.get_or_init(|| RwLock::new(0))
|
||||
}
|
||||
fn group_stack_cell(
|
||||
) -> &'static RwLock<Vec<std::sync::Arc<crate::boxes::task_group_box::TaskGroupInner>>> {
|
||||
TASK_GROUP_STACK.get_or_init(|| RwLock::new(Vec::new()))
|
||||
|
||||
static GLOBAL_STATE: OnceCell<RwLock<GlobalHooksState>> = OnceCell::new();
|
||||
|
||||
fn state() -> &'static RwLock<GlobalHooksState> {
|
||||
GLOBAL_STATE.get_or_init(|| RwLock::new(GlobalHooksState::new()))
|
||||
}
|
||||
|
||||
pub fn set_from_runtime(rt: &crate::runtime::nyash_runtime::NyashRuntime) {
|
||||
if let Ok(mut g) = gc_cell().write() {
|
||||
*g = Some(rt.gc.clone());
|
||||
}
|
||||
if let Ok(mut s) = sched_cell().write() {
|
||||
*s = rt.scheduler.as_ref().cloned();
|
||||
}
|
||||
// Optional: initialize a fresh token for the runtime's root group (Phase 2 wiring)
|
||||
if let Ok(mut t) = token_cell().write() {
|
||||
if t.is_none() {
|
||||
*t = Some(CancellationToken::new());
|
||||
if let Ok(mut st) = state().write() {
|
||||
st.gc = Some(rt.gc.clone());
|
||||
st.sched = rt.scheduler.as_ref().cloned();
|
||||
if st.cur_token.is_none() {
|
||||
st.cur_token = Some(CancellationToken::new());
|
||||
}
|
||||
}
|
||||
// Reset group futures registry on new runtime
|
||||
if let Ok(mut f) = futures_cell().write() {
|
||||
f.clear();
|
||||
}
|
||||
if let Ok(mut s) = strong_cell().write() {
|
||||
s.clear();
|
||||
}
|
||||
if let Ok(mut d) = scope_depth_cell().write() {
|
||||
*d = 0;
|
||||
}
|
||||
if let Ok(mut st) = group_stack_cell().write() {
|
||||
st.clear();
|
||||
st.futures.clear();
|
||||
st.strong.clear();
|
||||
st.scope_depth = 0;
|
||||
st.group_stack.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_gc(gc: Arc<dyn GcHooks>) {
|
||||
if let Ok(mut g) = gc_cell().write() {
|
||||
*g = Some(gc);
|
||||
if let Ok(mut st) = state().write() {
|
||||
st.gc = Some(gc);
|
||||
}
|
||||
}
|
||||
pub fn set_scheduler(s: Arc<dyn Scheduler>) {
|
||||
if let Ok(mut w) = sched_cell().write() {
|
||||
*w = Some(s);
|
||||
if let Ok(mut st) = state().write() {
|
||||
st.sched = Some(s);
|
||||
}
|
||||
}
|
||||
/// Set the current task group's cancellation token (scaffold).
|
||||
pub fn set_current_group_token(tok: CancellationToken) {
|
||||
if let Ok(mut w) = token_cell().write() {
|
||||
*w = Some(tok);
|
||||
if let Ok(mut st) = state().write() {
|
||||
st.cur_token = Some(tok);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current task group's cancellation token (no-op default).
|
||||
pub fn current_group_token() -> CancellationToken {
|
||||
if let Ok(r) = token_cell().read() {
|
||||
if let Some(t) = r.as_ref() {
|
||||
if let Ok(st) = state().read() {
|
||||
if let Some(t) = st.cur_token.as_ref() {
|
||||
return t.clone();
|
||||
}
|
||||
}
|
||||
@ -103,21 +80,17 @@ pub fn current_group_token() -> CancellationToken {
|
||||
|
||||
/// Register a Future into the current group's registry (best-effort; clones share state)
|
||||
pub fn register_future_to_current_group(fut: &crate::boxes::future::FutureBox) {
|
||||
// Prefer explicit current TaskGroup at top of stack
|
||||
if let Ok(st) = group_stack_cell().read() {
|
||||
if let Some(inner) = st.last() {
|
||||
if let Ok(mut st) = state().write() {
|
||||
// Prefer explicit current TaskGroup at top of stack
|
||||
if let Some(inner) = st.group_stack.last() {
|
||||
if let Ok(mut v) = inner.strong.lock() {
|
||||
v.push(fut.clone());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback to implicit global group
|
||||
if let Ok(mut list) = futures_cell().write() {
|
||||
list.push(fut.downgrade());
|
||||
}
|
||||
if let Ok(mut s) = strong_cell().write() {
|
||||
s.push(fut.clone());
|
||||
// Fallback to implicit global group
|
||||
st.futures.push(fut.downgrade());
|
||||
st.strong.push(fut.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,26 +100,15 @@ pub fn join_all_registered_futures(timeout_ms: u64) {
|
||||
let deadline = Instant::now() + Duration::from_millis(timeout_ms);
|
||||
loop {
|
||||
let mut all_ready = true;
|
||||
// purge list of dropped or completed futures opportunistically
|
||||
{
|
||||
// purge weak list: keep only upgradeable futures
|
||||
if let Ok(mut list) = futures_cell().write() {
|
||||
list.retain(|fw| fw.is_ready().is_some());
|
||||
}
|
||||
// purge strong list: remove completed futures to reduce retention
|
||||
if let Ok(mut s) = strong_cell().write() {
|
||||
s.retain(|f| !f.ready());
|
||||
}
|
||||
}
|
||||
// check readiness
|
||||
{
|
||||
if let Ok(list) = futures_cell().read() {
|
||||
for fw in list.iter() {
|
||||
if let Some(ready) = fw.is_ready() {
|
||||
if !ready {
|
||||
all_ready = false;
|
||||
break;
|
||||
}
|
||||
// purge + readiness check under single state lock (short critical sections)
|
||||
if let Ok(mut st) = state().write() {
|
||||
st.futures.retain(|fw| fw.is_ready().is_some());
|
||||
st.strong.retain(|f| !f.ready());
|
||||
for fw in st.futures.iter() {
|
||||
if let Some(ready) = fw.is_ready() {
|
||||
if !ready {
|
||||
all_ready = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -161,22 +123,18 @@ pub fn join_all_registered_futures(timeout_ms: u64) {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
// Final sweep
|
||||
if let Ok(mut s) = strong_cell().write() {
|
||||
s.retain(|f| !f.ready());
|
||||
}
|
||||
if let Ok(mut list) = futures_cell().write() {
|
||||
list.retain(|fw| matches!(fw.is_ready(), Some(false)));
|
||||
if let Ok(mut st) = state().write() {
|
||||
st.strong.retain(|f| !f.ready());
|
||||
st.futures.retain(|fw| matches!(fw.is_ready(), Some(false)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a task scope (footing). On pop of the outermost scope, perform a best-effort join.
|
||||
pub fn push_task_scope() {
|
||||
if let Ok(mut d) = scope_depth_cell().write() {
|
||||
*d += 1;
|
||||
}
|
||||
// Push a new explicit TaskGroup for this scope
|
||||
if let Ok(mut st) = group_stack_cell().write() {
|
||||
st.push(std::sync::Arc::new(
|
||||
if let Ok(mut st) = state().write() {
|
||||
st.scope_depth += 1;
|
||||
// Push a new explicit TaskGroup for this scope
|
||||
st.group_stack.push(std::sync::Arc::new(
|
||||
crate::boxes::task_group_box::TaskGroupInner {
|
||||
strong: std::sync::Mutex::new(Vec::new()),
|
||||
},
|
||||
@ -190,19 +148,13 @@ pub fn push_task_scope() {
|
||||
pub fn pop_task_scope() {
|
||||
let mut do_join = false;
|
||||
let mut popped: Option<std::sync::Arc<crate::boxes::task_group_box::TaskGroupInner>> = None;
|
||||
{
|
||||
if let Ok(mut d) = scope_depth_cell().write() {
|
||||
if *d > 0 {
|
||||
*d -= 1;
|
||||
}
|
||||
if *d == 0 {
|
||||
do_join = true;
|
||||
}
|
||||
if let Ok(mut st) = state().write() {
|
||||
if st.scope_depth > 0 {
|
||||
st.scope_depth -= 1;
|
||||
}
|
||||
}
|
||||
// Pop explicit group for this scope
|
||||
if let Ok(mut st) = group_stack_cell().write() {
|
||||
popped = st.pop();
|
||||
if st.scope_depth == 0 { do_join = true; }
|
||||
// Pop explicit group for this scope
|
||||
popped = st.group_stack.pop();
|
||||
}
|
||||
if do_join {
|
||||
let ms: u64 = std::env::var("NYASH_TASK_SCOPE_JOIN_MS")
|
||||
@ -240,13 +192,11 @@ pub fn pop_task_scope() {
|
||||
|
||||
/// Perform a runtime safepoint and poll the scheduler if available.
|
||||
pub fn safepoint_and_poll() {
|
||||
if let Ok(g) = gc_cell().read() {
|
||||
if let Some(gc) = g.as_ref() {
|
||||
if let Ok(st) = state().read() {
|
||||
if let Some(gc) = st.gc.as_ref() {
|
||||
gc.safepoint();
|
||||
}
|
||||
}
|
||||
if let Ok(s) = sched_cell().read() {
|
||||
if let Some(sched) = s.as_ref() {
|
||||
if let Some(sched) = st.sched.as_ref() {
|
||||
sched.poll();
|
||||
}
|
||||
}
|
||||
@ -255,8 +205,8 @@ pub fn safepoint_and_poll() {
|
||||
/// Try to schedule a task on the global scheduler. Returns true if scheduled.
|
||||
pub fn spawn_task(name: &str, f: Box<dyn FnOnce() + Send + 'static>) -> bool {
|
||||
// If a scheduler is registered, enqueue the task; otherwise run inline.
|
||||
if let Ok(s) = sched_cell().read() {
|
||||
if let Some(sched) = s.as_ref() {
|
||||
if let Ok(st) = state().read() {
|
||||
if let Some(sched) = st.sched.as_ref() {
|
||||
sched.spawn(name, f);
|
||||
return true;
|
||||
}
|
||||
@ -272,8 +222,8 @@ pub fn spawn_task_with_token(
|
||||
token: crate::runtime::scheduler::CancellationToken,
|
||||
f: Box<dyn FnOnce() + Send + 'static>,
|
||||
) -> bool {
|
||||
if let Ok(s) = sched_cell().read() {
|
||||
if let Some(sched) = s.as_ref() {
|
||||
if let Ok(st) = state().read() {
|
||||
if let Some(sched) = st.sched.as_ref() {
|
||||
sched.spawn_with_token(name, token, f);
|
||||
return true;
|
||||
}
|
||||
@ -284,8 +234,8 @@ pub fn spawn_task_with_token(
|
||||
|
||||
/// Spawn a delayed task via scheduler if available; returns true if scheduled.
|
||||
pub fn spawn_task_after(delay_ms: u64, name: &str, f: Box<dyn FnOnce() + Send + 'static>) -> bool {
|
||||
if let Ok(s) = sched_cell().read() {
|
||||
if let Some(sched) = s.as_ref() {
|
||||
if let Ok(st) = state().read() {
|
||||
if let Some(sched) = st.sched.as_ref() {
|
||||
sched.spawn_after(delay_ms, name, f);
|
||||
return true;
|
||||
}
|
||||
@ -297,3 +247,21 @@ pub fn spawn_task_after(delay_ms: u64, name: &str, f: Box<dyn FnOnce() + Send +
|
||||
});
|
||||
false
|
||||
}
|
||||
|
||||
/// Forward a GC barrier event to the currently registered GC hooks (if any).
|
||||
pub fn gc_barrier(kind: BarrierKind) {
|
||||
if let Ok(st) = state().read() {
|
||||
if let Some(gc) = st.gc.as_ref() {
|
||||
gc.barrier(kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Report an allocation to the current GC hooks (best-effort)
|
||||
pub fn gc_alloc(bytes: u64) {
|
||||
if let Ok(st) = state().read() {
|
||||
if let Some(gc) = st.gc.as_ref() {
|
||||
gc.alloc(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
|
||||
pub mod box_registry;
|
||||
pub mod gc;
|
||||
pub mod gc_controller;
|
||||
pub mod gc_mode;
|
||||
pub mod gc_trace;
|
||||
pub mod global_hooks;
|
||||
pub mod leak_tracker;
|
||||
pub mod nyash_runtime;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use super::types::{LoadedPluginV2, PluginBoxMetadata, PluginBoxV2, PluginHandleInner};
|
||||
use super::types::{LoadedPluginV2, NyashTypeBoxFfi, PluginBoxMetadata, PluginBoxV2, PluginHandleInner};
|
||||
use crate::bid::{BidError, BidResult};
|
||||
use crate::box_trait::NyashBox;
|
||||
use crate::config::nyash_toml_v2::{LibraryDefinition, NyashConfigV2};
|
||||
@ -12,11 +12,15 @@ fn dbg_on() -> bool {
|
||||
std::env::var("NYASH_DEBUG_PLUGIN").unwrap_or_default() == "1"
|
||||
}
|
||||
|
||||
type BoxInvokeFn = extern "C" fn(u32, u32, *const u8, usize, *mut u8, *mut usize) -> i32;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct LoadedBoxSpec {
|
||||
type_id: Option<u32>,
|
||||
methods: HashMap<String, MethodSpec>,
|
||||
fini_method_id: Option<u32>,
|
||||
// Optional Nyash ABI v2 per-box invoke entry (not yet used for calls)
|
||||
invoke_id: Option<BoxInvokeFn>,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct MethodSpec {
|
||||
@ -124,7 +128,7 @@ impl PluginLoaderV2 {
|
||||
let lib = unsafe { Library::new(&lib_path) }.map_err(|_| BidError::PluginError)?;
|
||||
let lib_arc = Arc::new(lib);
|
||||
|
||||
// Resolve required invoke symbol (TypeBox v2: nyash_plugin_invoke)
|
||||
// Resolve required invoke symbol (legacy library-level): nyash_plugin_invoke
|
||||
unsafe {
|
||||
let invoke_sym: Symbol<
|
||||
unsafe extern "C" fn(u32, u32, u32, *const u8, usize, *mut u8, *mut usize) -> i32,
|
||||
@ -152,6 +156,35 @@ impl PluginLoaderV2 {
|
||||
.insert(lib_name.to_string(), Arc::new(loaded));
|
||||
}
|
||||
|
||||
// Try to resolve Nyash ABI v2 per-box TypeBox symbols and record invoke_id
|
||||
// Symbol pattern: nyash_typebox_<BoxType>
|
||||
for box_type in &lib_def.boxes {
|
||||
let sym_name = format!("nyash_typebox_{}\0", box_type);
|
||||
unsafe {
|
||||
if let Ok(tb_sym) = lib_arc.get::<Symbol<&NyashTypeBoxFfi>>(sym_name.as_bytes()) {
|
||||
let st: &NyashTypeBoxFfi = &*tb_sym;
|
||||
// Validate ABI tag 'TYBX' (0x54594258) and basic invariants
|
||||
let abi_ok = st.abi_tag == 0x5459_4258
|
||||
&& st.struct_size as usize >= std::mem::size_of::<NyashTypeBoxFfi>();
|
||||
if !abi_ok {
|
||||
continue;
|
||||
}
|
||||
// Remember invoke_id in box_specs for (lib_name, box_type)
|
||||
if let Some(invoke_id) = st.invoke_id {
|
||||
let key = (lib_name.to_string(), box_type.to_string());
|
||||
let mut map = self.box_specs.write().map_err(|_| BidError::PluginError)?;
|
||||
let entry = map.entry(key).or_insert(LoadedBoxSpec {
|
||||
type_id: None,
|
||||
methods: HashMap::new(),
|
||||
fini_method_id: None,
|
||||
invoke_id: None,
|
||||
});
|
||||
entry.invoke_id = Some(invoke_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user