refactor(modules): split kernel exports and join_ir lowerers

This commit is contained in:
2025-12-28 02:38:51 +09:00
parent bf7b203586
commit 79c2953a1f
28 changed files with 3452 additions and 3254 deletions

View File

@ -0,0 +1,199 @@
// Process entry point for NyRT.
// ---- Process entry (driver) ----
#[cfg(not(test))]
#[no_mangle]
pub extern "C" fn main() -> i32 {
// Phase 88: AOT 実行器でも Ring0Context は必須PluginHost/ログなどが依存する)。
// EXE 直起動では host 側の init が存在しないため、ここで先に初期化する。
if nyash_rust::runtime::ring0::GLOBAL_RING0.get().is_none() {
nyash_rust::runtime::ring0::init_global_ring0(nyash_rust::runtime::ring0::default_ring0());
}
// Initialize plugin host: prefer nyash.toml next to the executable; fallback to CWD
let exe_dir = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.to_path_buf()));
// Windows: assist DLL/plugin discovery by extending PATH and normalizing PYTHONHOME
#[cfg(target_os = "windows")]
if let Some(dir) = &exe_dir {
use std::path::PathBuf;
// Extend PATH with exe_dir and exe_dir\plugins if not already present
let mut path_val = std::env::var("PATH").unwrap_or_default();
let add_path = |pv: &mut String, p: &PathBuf| {
let ps = p.display().to_string();
if !pv.split(';').any(|seg| seg.eq_ignore_ascii_case(&ps)) {
if !pv.is_empty() {
pv.push(';');
}
pv.push_str(&ps);
}
};
add_path(&mut path_val, dir);
let plug = dir.join("plugins");
if plug.is_dir() {
add_path(&mut path_val, &plug);
}
std::env::set_var("PATH", &path_val);
// Normalize PYTHONHOME: if unset, point to exe_dir\python when present.
match std::env::var("PYTHONHOME") {
Ok(v) => {
// If relative, make absolute under exe_dir
let pb = PathBuf::from(&v);
if pb.is_relative() {
let abs = dir.join(pb);
std::env::set_var("PYTHONHOME", abs.display().to_string());
}
}
Err(_) => {
let cand = dir.join("python");
if cand.is_dir() {
std::env::set_var("PYTHONHOME", cand.display().to_string());
}
}
}
}
// Initialize a minimal runtime to back global hooks (GC/scheduler) for safepoints
// Choose GC hooks based on env (default dev: Counting for observability unless explicitly off)
let mut rt_builder = nyash_rust::runtime::NyashRuntimeBuilder::new();
let gc_mode = nyash_rust::runtime::gc_mode::GcMode::from_env();
let controller = std::sync::Arc::new(nyash_rust::runtime::gc_controller::GcController::new(
gc_mode,
));
rt_builder = rt_builder.with_gc_hooks(controller);
let rt_hooks = rt_builder.build();
nyash_rust::runtime::global_hooks::set_from_runtime(&rt_hooks);
let mut inited = false;
if let Some(dir) = &exe_dir {
let candidate = dir.join("nyash.toml");
if candidate.exists() {
let _ =
nyash_rust::runtime::init_global_plugin_host(candidate.to_string_lossy().as_ref());
inited = true;
}
}
if !inited {
let _ = nyash_rust::runtime::init_global_plugin_host("nyash.toml");
}
// Optional verbosity
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
println!(
"🔌 nyrt: plugin host init attempted (exe_dir={}, cwd={})",
exe_dir
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "?".into()),
std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "?".into())
);
}
// Call exported Nyash entry if linked: `ny_main` (i64 -> return code normalized)
unsafe {
extern "C" {
fn ny_main() -> i64;
}
// SAFETY: if not linked, calling will be an unresolved symbol at link-time; we rely on link step to include ny_main.
let v = ny_main();
let exit_code: i64 = {
use nyash_rust::{box_trait::IntegerBox, runtime::host_handles as handles};
if v > 0 {
if let Some(obj) = handles::get(v as u64) {
if let Some(ib) = obj.as_any().downcast_ref::<IntegerBox>() {
ib.value as i64
} else {
// Avoid “raw integer vs handle id” collision:
// if the handle exists but isn't an IntegerBox, treat `v` as a raw i64.
v
}
} else {
v
}
} else {
v
}
};
// Print standardized result line for golden comparisons (can be silenced for tests)
let silent = std::env::var("NYASH_NYRT_SILENT_RESULT").ok().as_deref() == Some("1");
if !silent {
println!("Result: {}", exit_code);
}
// Optional GC metrics after program completes
let want_json = std::env::var("NYASH_GC_METRICS_JSON").ok().as_deref() == Some("1");
let want_text = std::env::var("NYASH_GC_METRICS").ok().as_deref() == Some("1");
if want_json || want_text {
let (sp, br, bw) = rt_hooks.gc.snapshot_counters().unwrap_or((0, 0, 0));
// ✂️ REMOVED: Legacy JIT handles::len() - part of 42% deletable functions
let handles = 0u64; // Placeholder: handles tracking removed with JIT archival
let gc_mode_s = gc_mode.as_str();
// Include allocation totals if controller is used
let any_gc: &dyn std::any::Any = &*rt_hooks.gc;
let (
alloc_count,
alloc_bytes,
trial_nodes,
trial_edges,
collect_total,
collect_sp,
collect_alloc,
last_ms,
last_reason,
) = if let Some(ctrl) =
any_gc.downcast_ref::<nyash_rust::runtime::gc_controller::GcController>()
{
let (ac, ab) = ctrl.alloc_totals();
let (tn, te) = ctrl.trial_reachability_last();
let (ct, csp, calloc) = ctrl.collection_totals();
let lms = ctrl.trial_duration_last_ms();
let lrf = ctrl.trial_reason_last_bits();
(ac, ab, tn, te, ct, csp, calloc, lms, lrf)
} else {
(0, 0, 0, 0, 0, 0, 0, 0, 0)
};
// Settings snapshot (env)
let sp_interval = std::env::var("NYASH_GC_COLLECT_SP")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
let alloc_thresh = std::env::var("NYASH_GC_COLLECT_ALLOC")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
let auto_sp = std::env::var("NYASH_LLVM_AUTO_SAFEPOINT")
.ok()
.map(|v| v == "1")
.unwrap_or(true);
if want_json {
// Minimal JSON assembly to avoid extra deps in nyrt
println!(
"{{\"kind\":\"gc_metrics\",\"safepoints\":{},\"barrier_reads\":{},\"barrier_writes\":{},\"jit_handles\":{},\"alloc_count\":{},\"alloc_bytes\":{},\"trial_nodes\":{},\"trial_edges\":{},\"collections\":{},\"collect_by_sp\":{},\"collect_by_alloc\":{},\"last_collect_ms\":{},\"last_reason_bits\":{},\"sp_interval\":{},\"alloc_threshold\":{},\"auto_safepoint\":{},\"gc_mode\":\"{}\"}}",
sp, br, bw, handles, alloc_count, alloc_bytes, trial_nodes, trial_edges, collect_total, collect_sp, collect_alloc, last_ms, last_reason, sp_interval, alloc_thresh, if auto_sp {1} else {0}, gc_mode_s
);
} else if want_text {
eprintln!(
"[GC] metrics: safepoints={} read_barriers={} write_barriers={} jit_handles={} allocs={} bytes={} collections={} (sp={} alloc={}) last_ms={} mode={}",
sp, br, bw, handles, alloc_count, alloc_bytes, collect_total, collect_sp, collect_alloc, last_ms, gc_mode_s
);
}
// Threshold warning
if let Ok(s) = std::env::var("NYASH_GC_ALLOC_THRESHOLD") {
if let Ok(th) = s.parse::<u64>() {
if alloc_bytes > th {
eprintln!(
"[GC][warn] allocation bytes {} exceeded threshold {}",
alloc_bytes, th
);
}
}
}
}
// ✂️ REMOVED: Legacy JIT leak diagnostics - part of 42% deletable functions
// Leak diagnostics functionality removed with JIT archival
// handles::type_tally() no longer available in Plugin-First architecture
exit_code as i32
}
}

View File

@ -0,0 +1,167 @@
// Any box helpers.
// Any.length_h(handle) -> i64 (Array/String/Map)
#[export_name = "nyash.any.length_h"]
pub extern "C" fn nyash_any_length_h_export(handle: i64) -> i64 {
use nyash_rust::runtime::host_handles as handles;
if std::env::var("NYASH_JIT_TRACE_LEN").ok().as_deref() == Some("1") {
let present = if handle > 0 {
handles::get(handle as u64).is_some()
} else {
false
};
eprintln!(
"[AOT-LEN_H] any.length_h handle={} present={}",
handle, present
);
}
if handle <= 0 {
return 0;
}
if let Some(obj) = handles::get(handle as u64) {
if let Some(arr) = obj
.as_any()
.downcast_ref::<nyash_rust::boxes::array::ArrayBox>()
{
if let Some(ib) = arr
.length()
.as_any()
.downcast_ref::<nyash_rust::box_trait::IntegerBox>()
{
return ib.value;
}
}
if let Some(sb) = obj
.as_any()
.downcast_ref::<nyash_rust::box_trait::StringBox>()
{
return sb.value.len() as i64;
}
if let Some(map) = obj
.as_any()
.downcast_ref::<nyash_rust::boxes::map_box::MapBox>()
{
if let Some(ib) = map
.size()
.as_any()
.downcast_ref::<nyash_rust::box_trait::IntegerBox>()
{
return ib.value;
}
}
}
0
}
// Any.toString_h(handle) -> handle (StringBox)
//
// Universal display conversion for LLVM/JIT paths where method dispatch may not
// have plugin slots for builtin boxes. This should match the VM's expectation
// that `toString()` is always available (universal slot #0).
#[export_name = "nyash.any.toString_h"]
pub extern "C" fn nyash_any_to_string_h_export(handle: i64) -> i64 {
use nyash_rust::{
box_trait::{NyashBox, StringBox},
runtime::host_handles as handles,
};
// Treat <=0 as the null/void handle in AOT paths.
if handle <= 0 {
let s = "null".to_string();
let arc: std::sync::Arc<dyn NyashBox> = std::sync::Arc::new(StringBox::new(s.clone()));
nyash_rust::runtime::global_hooks::gc_alloc(s.len() as u64);
return handles::to_handle_arc(arc) as i64;
}
let obj = match handles::get(handle as u64) {
Some(o) => o,
None => return 0,
};
let s = obj.to_string_box().value;
let arc: std::sync::Arc<dyn NyashBox> = std::sync::Arc::new(StringBox::new(s.clone()));
nyash_rust::runtime::global_hooks::gc_alloc(s.len() as u64);
handles::to_handle_arc(arc) as i64
}
// Any.is_empty_h(handle) -> i64 (0/1)
#[export_name = "nyash.any.is_empty_h"]
pub extern "C" fn nyash_any_is_empty_h_export(handle: i64) -> i64 {
use nyash_rust::runtime::host_handles as handles;
if handle <= 0 {
return 1;
}
if let Some(obj) = handles::get(handle as u64) {
if let Some(arr) = obj
.as_any()
.downcast_ref::<nyash_rust::boxes::array::ArrayBox>()
{
if let Ok(items) = arr.items.read() {
return if items.is_empty() { 1 } else { 0 };
}
}
if let Some(sb) = obj
.as_any()
.downcast_ref::<nyash_rust::box_trait::StringBox>()
{
return if sb.value.is_empty() { 1 } else { 0 };
}
if let Some(map) = obj
.as_any()
.downcast_ref::<nyash_rust::boxes::map_box::MapBox>()
{
if let Some(ib) = map
.size()
.as_any()
.downcast_ref::<nyash_rust::box_trait::IntegerBox>()
{
return if ib.value == 0 { 1 } else { 0 };
}
}
}
1
}
// ---- Type introspection (Phase 274 P2) ----
/// Runtime type check for TypeOp implementation
/// Returns 1 if handle's runtime type matches type_name, 0 otherwise
#[export_name = "nyash.any.is_type_h"]
pub extern "C" fn nyash_any_is_type_h(handle: i64, type_name_ptr: *const i8) -> i64 {
use nyash_rust::runtime::host_handles as handles;
// Validate handle
if handle <= 0 {
return 0;
}
// Parse type_name from C string
let type_name = unsafe {
if type_name_ptr.is_null() {
return 0;
}
match std::ffi::CStr::from_ptr(type_name_ptr).to_str() {
Ok(s) => s,
Err(_) => return 0,
}
};
// Get object from handle registry
let obj = match handles::get(handle as u64) {
Some(o) => o,
None => return 0,
};
// Compare type_name() with requested type
let actual_type = obj.type_name();
if actual_type == type_name {
return 1;
}
// For InstanceBox, also check class_name field
if let Some(inst) = obj.as_any().downcast_ref::<nyash_rust::instance_v2::InstanceBox>() {
if inst.class_name == type_name {
return 1;
}
}
// No match
0
}

View File

@ -0,0 +1,60 @@
// Birth helpers for core boxes.
#[export_name = "nyash.string.birth_h"]
pub extern "C" fn nyash_string_birth_h_export() -> i64 {
// Create a new StringBox via unified plugin host; return runtime handle as i64
if let Ok(host_g) = nyash_rust::runtime::get_global_plugin_host().read() {
if let Ok(b) = host_g.create_box("StringBox", &[]) {
let arc: std::sync::Arc<dyn nyash_rust::box_trait::NyashBox> = std::sync::Arc::from(b);
let h = nyash_rust::runtime::host_handles::to_handle_arc(arc) as u64;
nyash_rust::runtime::global_hooks::gc_alloc(0);
return h as i64;
}
}
0
}
#[export_name = "nyash.integer.birth_h"]
pub extern "C" fn nyash_integer_birth_h_export() -> i64 {
if let Ok(host_g) = nyash_rust::runtime::get_global_plugin_host().read() {
if let Ok(b) = host_g.create_box("IntegerBox", &[]) {
let arc: std::sync::Arc<dyn nyash_rust::box_trait::NyashBox> = std::sync::Arc::from(b);
let h = nyash_rust::runtime::host_handles::to_handle_arc(arc) as u64;
nyash_rust::runtime::global_hooks::gc_alloc(0);
return h as i64;
}
}
0
}
// ConsoleBox birth shim for AOT/JIT handle-based creation
#[export_name = "nyash.console.birth_h"]
pub extern "C" fn nyash_console_birth_h_export() -> i64 {
if let Ok(host_g) = nyash_rust::runtime::get_global_plugin_host().read() {
if let Ok(b) = host_g.create_box("ConsoleBox", &[]) {
let arc: std::sync::Arc<dyn nyash_rust::box_trait::NyashBox> = std::sync::Arc::from(b);
let h = nyash_rust::runtime::host_handles::to_handle_arc(arc) as u64;
nyash_rust::runtime::global_hooks::gc_alloc(0);
return h as i64;
}
}
0
}
// ArrayBox birth shim for AOT/JIT handle-based creation
#[export_name = "nyash.array.birth_h"]
pub extern "C" fn nyash_array_birth_h_export() -> i64 {
let arc: std::sync::Arc<dyn nyash_rust::box_trait::NyashBox> =
std::sync::Arc::new(nyash_rust::boxes::array::ArrayBox::new());
nyash_rust::runtime::global_hooks::gc_alloc(0);
nyash_rust::runtime::host_handles::to_handle_arc(arc) as i64
}
// MapBox birth shim for AOT/JIT handle-based creation
#[export_name = "nyash.map.birth_h"]
pub extern "C" fn nyash_map_birth_h_export() -> i64 {
let arc: std::sync::Arc<dyn nyash_rust::box_trait::NyashBox> =
std::sync::Arc::new(nyash_rust::boxes::map_box::MapBox::new());
nyash_rust::runtime::global_hooks::gc_alloc(0);
nyash_rust::runtime::host_handles::to_handle_arc(arc) as i64
}

View File

@ -0,0 +1,38 @@
// Box helper exports.
// box.from_i8_string(ptr) -> handle
// Helper: build a StringBox from i8* and return a handle for AOT marshalling
#[export_name = "nyash.box.from_i8_string"]
pub extern "C" fn nyash_box_from_i8_string(ptr: *const i8) -> i64 {
use nyash_rust::{
box_trait::{NyashBox, StringBox},
runtime::host_handles as handles,
};
use std::ffi::CStr;
if ptr.is_null() {
return 0;
}
let c = unsafe { CStr::from_ptr(ptr) };
let s = match c.to_str() {
Ok(v) => v.to_string(),
Err(_) => return 0,
};
let arc: std::sync::Arc<dyn NyashBox> = std::sync::Arc::new(StringBox::new(s.clone()));
nyash_rust::runtime::global_hooks::gc_alloc(s.len() as u64);
let h = handles::to_handle_arc(arc) as i64;
h
}
// box.from_i64(val) -> handle
// Helper: build an IntegerBox and return a handle
#[export_name = "nyash.box.from_i64"]
pub extern "C" fn nyash_box_from_i64(val: i64) -> i64 {
use nyash_rust::{
box_trait::{IntegerBox, NyashBox},
runtime::host_handles as handles,
};
let arc: std::sync::Arc<dyn NyashBox> = std::sync::Arc::new(IntegerBox::new(val));
nyash_rust::runtime::global_hooks::gc_alloc(8);
let h = handles::to_handle_arc(arc) as i64;
h
}

View File

@ -0,0 +1,20 @@
// Comparison helpers for LLVM harness.
// Phase 275 B2: Precise Int↔Float equality helper for LLVM harness
// Returns 1 if equal (precise), 0 otherwise
// Takes f64 directly (LLVM harness emits Float constants as double, not handle)
#[export_name = "nyash.cmp.int_float_eq"]
pub extern "C" fn nyash_cmp_int_float_eq(int_val: i64, float_val: f64) -> i64 {
// Precise Int↔Float equality (Phase 275 B2)
if float_val.is_nan() {
return 0; // NaN != anything
}
if float_val.is_finite() && float_val.fract() == 0.0 {
// Float is integral - check exact representability
let f_int = float_val as i64;
if (f_int as f64) == float_val && f_int == int_val {
return 1; // Exact match
}
}
0 // Non-integral or inexact
}

View File

@ -0,0 +1,150 @@
// Environment and box creation exports.
use crate::user_box_registry::get_user_box_fields;
// Build ArrayBox from process argv (excluding program name)
// Exported as: nyash.env.argv_get() -> i64 (ArrayBox handle)
#[export_name = "nyash.env.argv_get"]
pub extern "C" fn nyash_env_argv_get() -> i64 {
use nyash_rust::{
box_trait::{NyashBox, StringBox},
boxes::array::ArrayBox,
runtime::host_handles as handles,
};
let arr = ArrayBox::new();
// Skip argv[0] (program name), collect the rest
for (i, a) in std::env::args().enumerate() {
if i == 0 {
continue;
}
let sb: Box<dyn NyashBox> = Box::new(StringBox::new(a));
let _ = arr.push(sb);
}
let arc: std::sync::Arc<dyn NyashBox> = std::sync::Arc::new(arr);
handles::to_handle_arc(arc) as i64
}
// env.box.new(type_name: *const i8) -> handle (i64)
// Minimal shim for Core-13 pure AOT: constructs Box via registry by name (no args)
#[export_name = "nyash.env.box.new"]
pub extern "C" fn nyash_env_box_new(type_name: *const i8) -> i64 {
use nyash_rust::{
box_trait::NyashBox,
runtime::{box_registry::get_global_registry, host_handles as handles},
};
use std::ffi::CStr;
if type_name.is_null() {
return 0;
}
let cstr = unsafe { CStr::from_ptr(type_name) };
let ty = match cstr.to_str() {
Ok(s) => s,
Err(_) => return 0,
};
// Core-first special cases: construct built-in boxes directly
if ty == "MapBox" {
use nyash_rust::boxes::map_box::MapBox;
let arc: std::sync::Arc<dyn NyashBox> = std::sync::Arc::new(MapBox::new());
return handles::to_handle_arc(arc) as i64;
}
if ty == "ArrayBox" {
use nyash_rust::boxes::array::ArrayBox;
let arc: std::sync::Arc<dyn NyashBox> = std::sync::Arc::new(ArrayBox::new());
let h = handles::to_handle_arc(arc) as i64;
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
eprintln!("nyrt: env.box.new ArrayBox -> handle={}", h);
}
return h;
}
let reg = get_global_registry();
match reg.create_box(ty, &[]) {
Ok(b) => {
let arc: std::sync::Arc<dyn NyashBox> = b.into();
handles::to_handle_arc(arc) as i64
}
Err(_) => 0,
}
}
// env.box.new_i64x(type_name: *const i8, argc: i64, a1: i64, a2: i64, a3: i64, a4: i64) -> handle (i64)
// Minimal shim: construct args from handles or wrap i64 as IntegerBox
#[export_name = "nyash.env.box.new_i64x"]
pub extern "C" fn nyash_env_box_new_i64x(
type_name: *const i8,
argc: i64,
a1: i64,
a2: i64,
a3: i64,
a4: i64,
) -> i64 {
use nyash_rust::{
box_trait::{IntegerBox, NyashBox},
runtime::{box_registry::get_global_registry, host_handles as handles},
};
use std::ffi::CStr;
if type_name.is_null() {
return 0;
}
let cstr = unsafe { CStr::from_ptr(type_name) };
let ty = match cstr.to_str() {
Ok(s) => s,
Err(_) => return 0,
};
// Build args vec from provided i64 words
let mut argv: Vec<Box<dyn NyashBox>> = Vec::new();
let push_val = |dst: &mut Vec<Box<dyn NyashBox>>, v: i64| {
if v > 0 {
if let Some(obj) = handles::get(v as u64) {
dst.push(obj.share_box());
return;
}
}
dst.push(Box::new(IntegerBox::new(v)));
};
if argc >= 1 {
push_val(&mut argv, a1);
}
if argc >= 2 {
push_val(&mut argv, a2);
}
if argc >= 3 {
push_val(&mut argv, a3);
}
if argc >= 4 {
push_val(&mut argv, a4);
}
// Phase 285LLVM-1.1: Check if this is a user-defined box in the field registry
if let Some(fields) = get_user_box_fields(ty) {
// Create InstanceBox with the registered fields
use nyash_rust::instance_v2::InstanceBox;
use std::collections::HashMap as StdHashMap;
use std::sync::Arc;
eprintln!("[DEBUG] Creating user box '{}' with fields: {:?}", ty, fields);
let instance = InstanceBox::from_declaration(
ty.to_string(),
fields.clone(),
StdHashMap::new(),
);
let boxed: Box<dyn NyashBox> = Box::new(instance);
let arc: Arc<dyn NyashBox> = Arc::from(boxed);
let handle = handles::to_handle_arc(arc) as i64;
return handle;
}
let reg = get_global_registry();
match reg.create_box(ty, &argv) {
Ok(b) => {
let arc: std::sync::Arc<dyn NyashBox> = b.into();
handles::to_handle_arc(arc) as i64
}
Err(e) => {
// Phase 285LLVM-1.1: Improved error message
eprintln!("[nyrt_error] Failed to create box '{}': {}", ty, e);
eprintln!("[nyrt_hint] User-defined boxes must be registered via nyrt_register_user_box_decl()");
eprintln!("[nyrt_hint] Check MIR JSON user_box_decls or box declaration metadata");
0
}
}
}

View File

@ -0,0 +1,32 @@
// Instance creation exports.
// Instance birth by name (packed u64x2 + len) -> handle
// export: nyash.instance.birth_name_u64x2(lo, hi, len) -> i64
#[export_name = "nyash.instance.birth_name_u64x2"]
pub extern "C" fn nyash_instance_birth_name_u64x2_export(lo: i64, hi: i64, len: i64) -> i64 {
use nyash_rust::runtime::get_global_plugin_host;
let mut bytes = Vec::with_capacity(len.max(0) as usize);
let lo_u = lo as u64;
let hi_u = hi as u64;
let l = len.max(0) as usize;
let take = core::cmp::min(16, l);
for i in 0..take.min(8) {
bytes.push(((lo_u >> (8 * i)) & 0xff) as u8);
}
for i in 0..take.saturating_sub(8) {
bytes.push(((hi_u >> (8 * i)) & 0xff) as u8);
}
// If len > 16, remaining bytes are not represented in (lo,hi); assume names <=16 bytes for now.
if bytes.len() != l {
bytes.resize(l, 0);
}
let name = String::from_utf8_lossy(&bytes).to_string();
if let Ok(host_g) = get_global_plugin_host().read() {
if let Ok(b) = host_g.create_box(&name, &[]) {
let arc: std::sync::Arc<dyn nyash_rust::box_trait::NyashBox> = std::sync::Arc::from(b);
let h = nyash_rust::runtime::host_handles::to_handle_arc(arc) as u64;
return h as i64;
}
}
0
}

View File

@ -0,0 +1,25 @@
//! C ABI exports for NyRT (AOT/JIT helpers).
//!
//! This module keeps export symbols grouped by responsibility.
pub(crate) mod any;
pub(crate) mod birth;
pub(crate) mod box_helpers;
pub(crate) mod cmp;
pub(crate) mod env;
pub(crate) mod instance;
pub(crate) mod primitive;
pub(crate) mod runtime;
pub(crate) mod string;
pub(crate) mod user_box;
pub use any::*;
pub use birth::*;
pub use box_helpers::*;
pub use cmp::*;
pub use env::*;
pub use instance::*;
pub use primitive::*;
pub use runtime::*;
pub use string::*;
pub use user_box::*;

View File

@ -0,0 +1,67 @@
// Primitive box helpers.
// integer.get_h(handle) -> i64
// Extract IntegerBox value from a handle. Returns 0 if handle is invalid or not an IntegerBox.
#[export_name = "nyash.integer.get_h"]
pub extern "C" fn nyash_integer_get_h_export(h: i64) -> i64 {
use nyash_rust::{box_trait::IntegerBox, runtime::host_handles as handles};
if h <= 0 {
return 0;
}
if let Some(obj) = handles::get(h as u64) {
if let Some(ib) = obj.as_any().downcast_ref::<IntegerBox>() {
return ib.value;
}
}
0
}
// bool.get_h(handle) -> i64 (0/1)
#[export_name = "nyash.bool.get_h"]
pub extern "C" fn nyash_bool_get_h_export(h: i64) -> i64 {
use nyash_rust::{box_trait::BoolBox, runtime::host_handles as handles};
if h <= 0 {
return 0;
}
if let Some(obj) = handles::get(h as u64) {
if let Some(bb) = obj.as_any().downcast_ref::<BoolBox>() {
return if bb.value { 1 } else { 0 };
}
}
0
}
// float.get_bits_h(handle) -> i64 (f64 bits)
#[export_name = "nyash.float.get_bits_h"]
pub extern "C" fn nyash_float_get_bits_h_export(h: i64) -> i64 {
use nyash_rust::{boxes::FloatBox, runtime::host_handles as handles};
if h <= 0 {
return 0;
}
if let Some(obj) = handles::get(h as u64) {
if let Some(fb) = obj.as_any().downcast_ref::<FloatBox>() {
return fb.value.to_bits() as i64;
}
}
0
}
// Phase 275 C2: Float unbox helper for LLVM harness (Int+Float addition)
// Returns f64 value from Float handle
#[export_name = "nyash.float.unbox_to_f64"]
pub extern "C" fn nyash_float_unbox_to_f64(float_handle: i64) -> f64 {
use nyash_rust::runtime::host_handles as handles;
// Get the FloatBox from handle
if float_handle <= 0 {
return 0.0; // Invalid handle
}
if let Some(obj) = handles::get(float_handle as u64) {
if let Some(fb) = obj.as_any().downcast_ref::<nyash_rust::FloatBox>() {
return fb.value;
}
}
0.0 // Not a FloatBox or handle invalid
}

View File

@ -0,0 +1,40 @@
// Runtime/GC exports.
// Exported as: nyash.rt.checkpoint
#[export_name = "nyash.rt.checkpoint"]
pub extern "C" fn nyash_rt_checkpoint_export() -> i64 {
if std::env::var("NYASH_RUNTIME_CHECKPOINT_TRACE")
.ok()
.as_deref()
== Some("1")
{
eprintln!("[nyrt] nyash.rt.checkpoint reached");
}
0
}
// Exported as: nyash.gc.barrier_write
#[export_name = "nyash.gc.barrier_write"]
pub extern "C" fn nyash_gc_barrier_write_export(handle_or_ptr: i64) -> i64 {
let _ = handle_or_ptr;
if std::env::var("NYASH_GC_BARRIER_TRACE").ok().as_deref() == Some("1") {
eprintln!("[nyrt] nyash.gc.barrier_write h=0x{:x}", handle_or_ptr);
}
// Forward to runtime GC hooks when available (Write barrier)
nyash_rust::runtime::global_hooks::gc_barrier(nyash_rust::runtime::BarrierKind::Write);
0
}
// LLVM safepoint exports (llvmlite harness)
// export: ny_safepoint(live_count: i64, live_values: i64*) -> void
#[no_mangle]
pub extern "C" fn ny_safepoint(_live_count: i64, _live_values: *const i64) {
// For now we ignore live-values; runtime uses cooperative safepoint + poll
nyash_rust::runtime::global_hooks::safepoint_and_poll();
}
// export: ny_check_safepoint() -> void
#[no_mangle]
pub extern "C" fn ny_check_safepoint() {
nyash_rust::runtime::global_hooks::safepoint_and_poll();
}

View File

@ -0,0 +1,236 @@
// String-related C ABI exports.
// String.len_h(handle) -> i64
#[export_name = "nyash.string.len_h"]
pub extern "C" fn nyash_string_len_h(handle: i64) -> i64 {
use nyash_rust::runtime::host_handles as handles;
if std::env::var("NYASH_JIT_TRACE_LEN").ok().as_deref() == Some("1") {
let present = if handle > 0 {
handles::get(handle as u64).is_some()
} else {
false
};
eprintln!(
"[AOT-LEN_H] string.len_h handle={} present={}",
handle, present
);
}
if handle <= 0 {
return 0;
}
if let Some(obj) = handles::get(handle as u64) {
if let Some(sb) = obj
.as_any()
.downcast_ref::<nyash_rust::box_trait::StringBox>()
{
return sb.value.len() as i64;
}
}
0
}
// FAST-path helper: compute string length from raw pointer (i8*) with mode (reserved)
// Exported as both legacy name (nyash.string.length_si) and neutral name (nyrt_string_length)
#[export_name = "nyrt_string_length"]
pub extern "C" fn nyrt_string_length(ptr: *const i8, _mode: i64) -> i64 {
use std::ffi::CStr;
if ptr.is_null() {
return 0;
}
// Safety: pointer is expected to point to a null-terminated UTF-8 byte string
let c = unsafe { CStr::from_ptr(ptr) };
match c.to_bytes().len() {
n => n as i64,
}
}
// String.charCodeAt_h(handle, idx) -> i64 (byte-based; -1 if OOB)
#[export_name = "nyash.string.charCodeAt_h"]
pub extern "C" fn nyash_string_charcode_at_h_export(handle: i64, idx: i64) -> i64 {
use nyash_rust::runtime::host_handles as handles;
if idx < 0 {
return -1;
}
if handle <= 0 {
return -1;
}
if let Some(obj) = handles::get(handle as u64) {
if let Some(sb) = obj
.as_any()
.downcast_ref::<nyash_rust::box_trait::StringBox>()
{
let s = &sb.value;
let i = idx as usize;
if i < s.len() {
return s.as_bytes()[i] as i64;
}
return -1;
}
}
-1
}
// String.concat_hh(lhs_h, rhs_h) -> handle
#[export_name = "nyash.string.concat_hh"]
pub extern "C" fn nyash_string_concat_hh_export(a_h: i64, b_h: i64) -> i64 {
use nyash_rust::{
box_trait::{NyashBox, StringBox},
runtime::host_handles as handles,
};
let to_s = |h: i64| -> String {
if h > 0 {
if let Some(o) = handles::get(h as u64) {
return o.to_string_box().value;
}
}
String::new()
};
let s = format!("{}{}", to_s(a_h), to_s(b_h));
nyash_rust::runtime::global_hooks::gc_alloc(s.len() as u64);
let arc: std::sync::Arc<dyn NyashBox> = std::sync::Arc::new(StringBox::new(s));
let h = handles::to_handle_arc(arc) as i64;
h
}
// String.eq_hh(lhs_h, rhs_h) -> i64 (0/1)
#[export_name = "nyash.string.eq_hh"]
pub extern "C" fn nyash_string_eq_hh_export(a_h: i64, b_h: i64) -> i64 {
use nyash_rust::runtime::host_handles as handles;
let to_s = |h: i64| -> String {
if h > 0 {
if let Some(o) = handles::get(h as u64) {
return o.to_string_box().value;
}
}
String::new()
};
if to_s(a_h) == to_s(b_h) {
1
} else {
0
}
}
// String.substring_hii(handle, start, end) -> handle
#[export_name = "nyash.string.substring_hii"]
pub extern "C" fn nyash_string_substring_hii_export(h: i64, start: i64, end: i64) -> i64 {
use nyash_rust::{box_trait::NyashBox, box_trait::StringBox, runtime::host_handles as handles};
if h <= 0 {
return 0;
}
let s = if let Some(obj) = handles::get(h as u64) {
if let Some(sb) = obj.as_any().downcast_ref::<StringBox>() {
sb.value.clone()
} else {
String::new()
}
} else {
String::new()
};
let n = s.len() as i64;
let mut st = if start < 0 { 0 } else { start };
let mut en = if end < 0 { 0 } else { end };
if st > n {
st = n;
}
if en > n {
en = n;
}
if en < st {
std::mem::swap(&mut st, &mut en);
}
let (st_u, en_u) = (st as usize, en as usize);
let sub = s.get(st_u.min(s.len())..en_u.min(s.len())).unwrap_or("");
let arc: std::sync::Arc<dyn NyashBox> = std::sync::Arc::new(StringBox::new(sub.to_string()));
nyash_rust::runtime::global_hooks::gc_alloc(sub.len() as u64);
let nh = handles::to_handle_arc(arc) as i64;
eprintln!("[TRACE] substring_hii -> {}", nh);
nh
}
// String.lastIndexOf_hh(haystack_h, needle_h) -> i64
#[export_name = "nyash.string.lastIndexOf_hh"]
pub extern "C" fn nyash_string_lastindexof_hh_export(h: i64, n: i64) -> i64 {
use nyash_rust::{box_trait::StringBox, runtime::host_handles as handles};
let hay = if h > 0 {
if let Some(o) = handles::get(h as u64) {
if let Some(sb) = o.as_any().downcast_ref::<StringBox>() {
sb.value.clone()
} else {
String::new()
}
} else {
String::new()
}
} else {
String::new()
};
let nee = if n > 0 {
if let Some(o) = handles::get(n as u64) {
if let Some(sb) = o.as_any().downcast_ref::<StringBox>() {
sb.value.clone()
} else {
String::new()
}
} else {
String::new()
}
} else {
String::new()
};
if nee.is_empty() {
return hay.len() as i64;
}
if let Some(pos) = hay.rfind(&nee) {
pos as i64
} else {
-1
}
}
// String.lt_hh(lhs_h, rhs_h) -> i64 (0/1)
#[export_name = "nyash.string.lt_hh"]
pub extern "C" fn nyash_string_lt_hh_export(a_h: i64, b_h: i64) -> i64 {
use nyash_rust::runtime::host_handles as handles;
let to_s = |h: i64| -> String {
if h > 0 {
if let Some(o) = handles::get(h as u64) {
return o.to_string_box().value;
}
}
String::new()
};
if to_s(a_h) < to_s(b_h) {
1
} else {
0
}
}
// Construct StringBox from two u64 words (little-endian) + length (<=16) and return handle
// export: nyash.string.from_u64x2(lo, hi, len) -> i64
#[export_name = "nyash.string.from_u64x2"]
pub extern "C" fn nyash_string_from_u64x2_export(lo: i64, hi: i64, len: i64) -> i64 {
use nyash_rust::{
box_trait::{NyashBox, StringBox},
runtime::host_handles as handles,
};
let l = if len < 0 {
0
} else {
core::cmp::min(len as usize, 16)
};
let mut bytes: Vec<u8> = Vec::with_capacity(l);
let lo_u = lo as u64;
let hi_u = hi as u64;
for i in 0..l.min(8) {
bytes.push(((lo_u >> (8 * i)) & 0xff) as u8);
}
for i in 0..l.saturating_sub(8) {
bytes.push(((hi_u >> (8 * i)) & 0xff) as u8);
}
let s = String::from_utf8_lossy(&bytes).to_string();
let arc: std::sync::Arc<dyn NyashBox> = std::sync::Arc::new(StringBox::new(s.clone()));
nyash_rust::runtime::global_hooks::gc_alloc(s.len() as u64);
handles::to_handle_arc(arc) as i64
}

View File

@ -0,0 +1,66 @@
// User box registry exports.
use crate::user_box_registry::register_user_box_fields;
/// Register user-defined box declaration (LLVM harness support)
/// Phase 285LLVM-1.1: Enable user box instantiation with fields in LLVM harness mode
///
/// # Arguments
/// * `type_name` - Box type name (e.g., "SomeBox")
/// * `fields_json` - JSON array of field names (e.g., "[\"x\",\"y\"]")
///
/// # Returns
/// * `0` - Success
/// * `-1` - Error: null pointer
/// * `-2` - Error: invalid UTF-8
/// * `-3` - Error: invalid JSON
#[export_name = "nyrt_register_user_box_decl"]
pub extern "C" fn nyrt_register_user_box_decl(
type_name: *const i8,
fields_json: *const i8,
) -> i32 {
use std::ffi::CStr;
if type_name.is_null() || fields_json.is_null() {
eprintln!("[nyrt_register_user_box_decl] Error: null pointer");
return -1;
}
let ty = match unsafe { CStr::from_ptr(type_name) }.to_str() {
Ok(s) => s.to_string(),
Err(e) => {
eprintln!(
"[nyrt_register_user_box_decl] Error: invalid UTF-8 in type_name: {:?}",
e
);
return -2;
}
};
let fields_str = match unsafe { CStr::from_ptr(fields_json) }.to_str() {
Ok(s) => s,
Err(e) => {
eprintln!(
"[nyrt_register_user_box_decl] Error: invalid UTF-8 in fields_json: {:?}",
e
);
return -2;
}
};
// Parse JSON array of field names
let fields: Vec<String> = match serde_json::from_str(fields_str) {
Ok(f) => f,
Err(e) => {
eprintln!("[nyrt_register_user_box_decl] Error: invalid JSON in fields: {:?}", e);
return -3;
}
};
// Store fields in global registry
// The actual box creation will be handled in nyash_env_box_new_i64x
register_user_box_fields(ty.clone(), fields.clone());
eprintln!("[DEBUG] Registered user box '{}' with fields: {:?}", ty, fields);
0 // Success
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,87 @@
use super::*;
use nyash_rust::{
box_trait::{NyashBox, StringBox},
runtime::{host_handles as handles, plugin_loader_v2::make_plugin_box_v2},
};
use std::sync::Arc;
unsafe extern "C" fn fake_i32(
_t: u32,
_m: u32,
_i: u32,
_a: *const u8,
_al: usize,
res: *mut u8,
len: *mut usize,
) -> i32 {
let mut buf = Vec::new();
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.push(2);
buf.push(0);
buf.extend_from_slice(&4u16.to_le_bytes());
buf.extend_from_slice(&123i32.to_le_bytes());
if res.is_null() || len.is_null() || unsafe { *len } < buf.len() {
unsafe {
if !len.is_null() {
*len = buf.len();
}
}
return -1;
}
unsafe {
std::ptr::copy_nonoverlapping(buf.as_ptr(), res, buf.len());
*len = buf.len();
}
0
}
unsafe extern "C" fn fake_str(
_t: u32,
_m: u32,
_i: u32,
_a: *const u8,
_al: usize,
res: *mut u8,
len: *mut usize,
) -> i32 {
let s = b"hi";
let mut buf = Vec::new();
buf.extend_from_slice(&1u16.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes());
buf.push(7);
buf.push(0);
buf.extend_from_slice(&(s.len() as u16).to_le_bytes());
buf.extend_from_slice(s);
if res.is_null() || len.is_null() || unsafe { *len } < buf.len() {
unsafe {
if !len.is_null() {
*len = buf.len();
}
}
return -1;
}
unsafe {
std::ptr::copy_nonoverlapping(buf.as_ptr(), res, buf.len());
*len = buf.len();
}
0
}
#[test]
fn decode_i32_and_string_returns() {
let pb = make_plugin_box_v2("Dummy".into(), 1, 1, fake_i32);
let arc: Arc<dyn NyashBox> = Arc::new(pb);
let handle = handles::to_handle_arc(arc) as i64;
let val = nyash_plugin_invoke3_tagged_i64(1, 0, 0, handle, 0, 0, 0, 0, 0, 0, 0, 0);
assert_eq!(val, 123);
let pb = make_plugin_box_v2("Dummy".into(), 1, 2, fake_str);
let arc: Arc<dyn NyashBox> = Arc::new(pb);
let handle = handles::to_handle_arc(arc) as i64;
let h = nyash_plugin_invoke3_tagged_i64(1, 0, 0, handle, 0, 0, 0, 0, 0, 0, 0, 0);
assert!(h > 0);
let obj = handles::get(h as u64).unwrap();
let sb = obj.as_any().downcast_ref::<StringBox>().unwrap();
assert_eq!(sb.value, "hi");
}

View File

@ -0,0 +1,50 @@
// Phase 285LLVM-1.1: Global registry for user box field declarations.
use std::collections::HashMap;
use std::sync::RwLock;
static USER_BOX_FIELDS: RwLock<Option<HashMap<String, Vec<String>>>> = RwLock::new(None);
pub(crate) fn get_user_box_fields(box_name: &str) -> Option<Vec<String>> {
if let Ok(guard) = USER_BOX_FIELDS.read() {
if let Some(ref map) = *guard {
return map.get(box_name).cloned();
}
}
None
}
pub(crate) fn register_user_box_fields(box_name: String, fields: Vec<String>) {
if let Ok(mut guard) = USER_BOX_FIELDS.write() {
if guard.is_none() {
*guard = Some(HashMap::new());
}
if let Some(ref mut map) = *guard {
map.insert(box_name, fields);
}
}
}
// Phase 285LLVM-1.1: Factory function for user-defined boxes.
#[allow(dead_code)]
pub(crate) fn create_user_box_from_registry(
box_name: &str,
_args: &[Box<dyn nyash_rust::box_trait::NyashBox>],
) -> Result<Box<dyn nyash_rust::box_trait::NyashBox>, String> {
use nyash_rust::{box_trait::NyashBox, instance_v2::InstanceBox};
use std::collections::HashMap as StdHashMap;
if let Some(fields) = get_user_box_fields(box_name) {
let instance = InstanceBox::from_declaration(
box_name.to_string(),
fields,
StdHashMap::new(),
);
Ok(Box::new(instance) as Box<dyn NyashBox>)
} else {
Err(format!(
"User box '{}' not registered in field registry",
box_name
))
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
use super::FileHandleBox;
use crate::box_trait::BoxBase;
impl std::fmt::Debug for FileHandleBox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FileHandleBox")
.field("path", &self.path)
.field("mode", &self.mode)
.field("is_open", &self.is_open())
.finish()
}
}
impl Clone for FileHandleBox {
fn clone(&self) -> Self {
// Clone creates a new independent handle (not open)
// Design decision: Cloning an open handle creates a closed handle
// Rationale: Prevents accidental file handle leaks
FileHandleBox {
base: BoxBase::new(),
path: String::new(),
mode: String::new(),
io: None,
}
}
}
impl FileHandleBox {
/// Create new FileHandleBox (file not yet opened)
pub fn new() -> Self {
FileHandleBox {
base: BoxBase::new(),
path: String::new(),
mode: String::new(),
io: None,
}
}
}

View File

@ -0,0 +1,156 @@
use super::FileHandleBox;
use crate::boxes::file::errors::*;
use crate::boxes::file::provider::FileIo;
use crate::runtime::provider_lock;
use std::sync::Arc;
impl FileHandleBox {
/// Open a file
///
/// # Arguments
///
/// - path: File path to open
/// - mode: "r" (read), "w" (write/truncate), or "a" (append)
///
/// # Errors
///
/// - Already open: "FileHandleBox is already open. Call close() first."
/// - Unsupported mode: "Unsupported mode: X. Use 'r', 'w', or 'a'"
/// - NoFs profile: "File I/O disabled in no-fs profile. FileHandleBox is not available."
/// - File not found (mode="r"): "File not found: PATH"
///
/// # Design Notes
///
/// - Phase 111: Supports "r", "w", and "a" modes
/// - Each FileHandleBox instance gets its own FileIo (independent)
pub fn open(&mut self, path: &str, mode: &str) -> Result<(), String> {
// Fail-Fast: Check for double open
if self.io.is_some() {
return Err(already_open());
}
// Validate mode (Phase 111: "a" added)
if mode != "r" && mode != "w" && mode != "a" {
return Err(unsupported_mode(mode));
}
// Get FileIo provider to check capabilities
let provider =
provider_lock::get_filebox_provider().ok_or_else(provider_not_initialized)?;
// NoFs profile check (Fail-Fast)
let caps = provider.caps();
if !caps.read && !caps.write {
return Err(provider_disabled_in_nofs_profile());
}
// Mode-specific capability check (using helper)
caps.check_mode(mode)?;
// Create NEW independent Ring0FsFileIo instance for this handle
// IMPORTANT: We must create a new instance, not clone the Arc
// because Ring0FsFileIo has internal state (path field)
use crate::providers::ring1::file::ring0_fs_fileio::Ring0FsFileIo;
use crate::runtime::get_global_ring0;
let ring0 = get_global_ring0();
// For write/append mode, create the file if it doesn't exist
// Ring0FsFileIo expects the file to exist for open(), so we create it first
if mode == "w" || mode == "a" {
use std::path::Path;
let path_obj = Path::new(path);
if !ring0.fs.exists(path_obj) {
// Create empty file for write/append mode
ring0
.fs
.write_all(path_obj, &[])
.map_err(|e| format!("Failed to create file: {}", e))?;
}
}
let file_io = Ring0FsFileIo::new(ring0.clone());
// Set mode BEFORE opening (Phase 111)
file_io.set_mode(mode.to_string());
let io: Arc<dyn FileIo> = Arc::new(file_io);
// Now open the file with the new instance
io.open(path)
.map_err(|e| format!("Failed to open file: {}", e))?;
// Store state
self.path = path.to_string();
self.mode = mode.to_string();
self.io = Some(io);
Ok(())
}
/// Read file contents to string
///
/// # Errors
///
/// - Not open: "FileHandleBox is not open"
/// - Read failed: "Read failed: ERROR"
pub fn read_to_string(&self) -> Result<String, String> {
self.io
.as_ref()
.ok_or_else(not_open)?
.read()
.map_err(|e| format!("Read failed: {}", e))
}
/// Write content to file
///
/// # Errors
///
/// - Not open: "FileHandleBox is not open"
/// - Wrong mode: "FileHandleBox opened in read mode"
/// - Write failed: "Write failed: ERROR"
///
/// # Phase 111
///
/// Supports both "w" (truncate) and "a" (append) modes.
/// Mode "r" returns error.
pub fn write_all(&self, content: &str) -> Result<(), String> {
// Fail-Fast: Check mode (Phase 111: allow "w" and "a")
if self.mode == "r" {
return Err("FileHandleBox is opened in read-only mode".to_string());
}
self.io
.as_ref()
.ok_or_else(not_open)?
.write(content)
.map_err(|e| format!("Write failed: {}", e))
}
/// Close the file
///
/// # Errors
///
/// - Not open: "FileHandleBox is not open"
///
/// # Post-condition
///
/// After close(), is_open() returns false and read/write will fail.
pub fn close(&mut self) -> Result<(), String> {
if self.io.is_none() {
return Err(not_open());
}
// Drop the FileIo instance
self.io.take();
self.path.clear();
self.mode.clear();
Ok(())
}
/// Check if file is currently open
pub fn is_open(&self) -> bool {
self.io.is_some()
}
}

View File

@ -0,0 +1,65 @@
use super::FileHandleBox;
impl FileHandleBox {
// ===== Phase 111/114: Metadata methods (internal Rust API) =====
/// Phase 114: Internal helper using FileIo::stat()
///
/// Unified metadata access through FileIo trait.
fn metadata_internal(&self) -> Result<crate::boxes::file::provider::FileStat, String> {
let io = self
.io
.as_ref()
.ok_or_else(|| "FileHandleBox is not open".to_string())?;
io.stat().map_err(|e| format!("Metadata failed: {}", e))
}
/// Get file size in bytes
///
/// # Errors
///
/// - Not open: "FileHandleBox is not open"
/// - Metadata failed: "Metadata failed: ERROR"
pub fn size(&self) -> Result<u64, String> {
self.metadata_internal().map(|meta| meta.size)
}
/// Check if file exists
///
/// # Errors
///
/// - Not open: "FileHandleBox is not open"
///
/// # Note
///
/// Uses FileIo::exists() for direct check.
pub fn exists(&self) -> Result<bool, String> {
let io = self
.io
.as_ref()
.ok_or_else(|| "FileHandleBox is not open".to_string())?;
Ok(io.exists())
}
/// Check if path is a file
///
/// # Errors
///
/// - Not open: "FileHandleBox is not open"
/// - Metadata failed: "Metadata failed: ERROR"
pub fn is_file(&self) -> Result<bool, String> {
self.metadata_internal().map(|meta| meta.is_file)
}
/// Check if path is a directory
///
/// # Errors
///
/// - Not open: "FileHandleBox is not open"
/// - Metadata failed: "Metadata failed: ERROR"
pub fn is_dir(&self) -> Result<bool, String> {
self.metadata_internal().map(|meta| meta.is_dir)
}
}

View File

@ -0,0 +1,122 @@
//! Phase 110: FileHandleBox - Handle-based file I/O
//!
//! Provides multiple-access file I/O pattern:
//! - open(path, mode) → read/write (multiple times) → close()
//!
//! Complements FileBox (one-shot I/O) by allowing multiple reads/writes
//! to the same file handle.
use crate::box_trait::{BoolBox, BoxBase, StringBox};
use crate::boxes::file::provider::FileIo;
use std::sync::Arc;
mod core;
mod io;
mod metadata;
mod traits;
#[cfg(test)]
mod tests;
// ===== Phase 115: Helper macros for Nyash wrapper methods =====
macro_rules! ny_wrap_void {
($name:ident, $inner:ident, $display_name:expr, $($arg_name:ident: $arg_ty:ty),*) => {
#[allow(unused_mut)]
pub fn $name(&mut self, $($arg_name: $arg_ty),*) {
self.$inner($($arg_name),*).unwrap_or_else(|e| panic!("FileHandleBox.{}() failed: {}", $display_name, e));
}
};
}
macro_rules! ny_wrap_string {
($name:ident, $inner:ident) => {
pub fn $name(&self) -> StringBox {
match self.$inner() {
Ok(result) => StringBox::new(result),
Err(e) => panic!(
"FileHandleBox.{}() failed: {}",
stringify!($name).trim_start_matches("ny_"),
e
),
}
}
};
}
macro_rules! ny_wrap_bool {
($name:ident, $inner:ident) => {
pub fn $name(&self) -> BoolBox {
match self.$inner() {
Ok(result) => BoolBox::new(result),
Err(e) => panic!(
"FileHandleBox.{}() failed: {}",
stringify!($name).trim_start_matches("ny_"),
e
),
}
}
};
}
macro_rules! ny_wrap_integer {
($name:ident, $inner:ident) => {
pub fn $name(&self) -> crate::box_trait::IntegerBox {
match self.$inner() {
Ok(result) => crate::box_trait::IntegerBox::new(result as i64),
Err(e) => panic!(
"FileHandleBox.{}() failed: {}",
stringify!($name).trim_start_matches("ny_"),
e
),
}
}
};
}
/// Phase 110: FileHandleBox
///
/// Handle-based file I/O for multiple-access patterns.
///
/// # Lifecycle
///
/// 1. new() - Create handle (file not yet opened)
/// 2. open(path, mode) - Open file (stores FileIo instance)
/// 3. read_to_string() / write_all() - Multiple accesses allowed
/// 4. close() - Close file (resets FileIo)
///
/// # Design Principles
///
/// - **Fail-Fast**: Double open() returns Err
/// - **Independent instances**: Each FileHandleBox has its own FileIo
/// - **Profile-aware**: NoFs profile → open() returns Err
/// - **Ring0 reuse**: Uses Ring0FsFileIo internally
///
/// # Code Organization
///
/// Phase 115: モジュール化・箱化実装
/// - Nyash メソッド (ny_*) はマクロで統一化(重複削減)
/// - テストヘルパーは tests/common/file_box_helpers.rs に外出し
/// - NyashBox trait impl は最小化(ボイラープレート削減)
pub struct FileHandleBox {
pub(super) base: BoxBase,
/// Current file path (empty if not open)
pub(super) path: String,
/// Current file mode ("r" or "w", empty if not open)
pub(super) mode: String,
/// FileIo instance (None if not open)
pub(super) io: Option<Arc<dyn FileIo>>,
}
impl FileHandleBox {
// ===== Phase 113: Nyash-visible public API methods =====
// Phase 115: Using macros for wrapper methods (defined at module level)
ny_wrap_void!(ny_open, open, "open", path: &str, mode: &str);
ny_wrap_string!(ny_read, read_to_string);
ny_wrap_void!(ny_write, write_all, "write", text: &str);
ny_wrap_void!(ny_close, close, "close",);
ny_wrap_bool!(ny_exists, exists);
ny_wrap_integer!(ny_size, size);
ny_wrap_bool!(ny_is_file, is_file);
ny_wrap_bool!(ny_is_dir, is_dir);
}

View File

@ -0,0 +1,630 @@
use super::*;
use std::fs;
// Import test helpers from tests/common/file_box_helpers.rs
// Note: These helpers are defined in the tests crate, so we can't import them here
// Instead, we'll keep local helpers for now and document the external helpers
// TODO: Consider moving these tests to integration tests to use shared helpers
fn setup_test_file(path: &str, content: &str) {
use std::io::Write;
let mut file = fs::File::create(path).unwrap();
file.write_all(content.as_bytes()).unwrap();
}
fn cleanup_test_file(path: &str) {
let _ = fs::remove_file(path);
}
/// Helper: Initialize FileBox provider for tests
fn init_test_provider() {
use crate::providers::ring1::file::ring0_fs_fileio::Ring0FsFileIo;
use crate::runtime::ring0::{default_ring0, init_global_ring0};
use std::panic;
use std::sync::Arc;
// Try to initialize Ring0 (ignore if already initialized)
let _ = panic::catch_unwind(|| {
let ring0 = default_ring0();
init_global_ring0(ring0);
});
// Set provider if not already set (ignore errors from re-initialization)
let ring0_arc = Arc::new(default_ring0());
let provider = Arc::new(Ring0FsFileIo::new(ring0_arc));
let _ = crate::runtime::provider_lock::set_filebox_provider(provider);
}
#[test]
fn test_filehandlebox_basic_write_read() {
init_test_provider();
let tmp_path = "/tmp/phase110_test_write_read.txt";
// Write to file
let mut h = FileHandleBox::new();
assert!(!h.is_open());
h.open(tmp_path, "w").expect("open for write failed");
assert!(h.is_open());
h.write_all("hello world").expect("write failed");
h.close().expect("close failed");
assert!(!h.is_open());
// Read from file (separate instance)
let mut h2 = FileHandleBox::new();
h2.open(tmp_path, "r").expect("open for read failed");
let content = h2.read_to_string().expect("read failed");
assert_eq!(content, "hello world");
h2.close().expect("close failed");
cleanup_test_file(tmp_path);
}
#[test]
fn test_filehandlebox_double_open_error() {
init_test_provider();
let tmp_path = "/tmp/phase110_test_double_open.txt";
setup_test_file(tmp_path, "test");
let mut h = FileHandleBox::new();
h.open(tmp_path, "r").expect("first open");
// Second open should fail
let result = h.open(tmp_path, "r");
assert!(result.is_err());
assert!(result.unwrap_err().contains("already open"));
h.close().expect("close");
cleanup_test_file(tmp_path);
}
#[test]
fn test_filehandlebox_closed_access_error() {
init_test_provider();
let tmp_path = "/tmp/phase110_test_closed_access.txt";
setup_test_file(tmp_path, "test");
let mut h = FileHandleBox::new();
h.open(tmp_path, "r").expect("open");
h.close().expect("close");
// Read after close should fail
let result = h.read_to_string();
assert!(result.is_err());
assert!(result.unwrap_err().contains("not open"));
cleanup_test_file(tmp_path);
}
#[test]
fn test_filehandlebox_write_wrong_mode() {
init_test_provider();
let tmp_path = "/tmp/phase110_test_write_wrong_mode.txt";
setup_test_file(tmp_path, "test");
let mut h = FileHandleBox::new();
h.open(tmp_path, "r").expect("open for read");
// Write in read mode should fail
let result = h.write_all("data");
assert!(result.is_err());
assert!(result.unwrap_err().contains("read-only"));
h.close().expect("close");
cleanup_test_file(tmp_path);
}
#[test]
fn test_filehandlebox_multiple_writes() {
init_test_provider();
// Use a dedicated path to avoid races with other write tests sharing the same file.
let tmp_path = "/tmp/phase110_test_multiple_writes_truncate.txt";
let mut h = FileHandleBox::new();
h.open(tmp_path, "w").expect("open");
// Multiple writes (note: current design is truncate mode)
h.write_all("first write").expect("first write");
// Second write will overwrite (truncate mode)
h.write_all("second write").expect("second write");
h.close().expect("close");
// Verify final content
let content = fs::read_to_string(tmp_path).unwrap();
assert_eq!(content, "second write");
cleanup_test_file(tmp_path);
}
#[test]
fn test_filehandlebox_unsupported_mode() {
init_test_provider();
let mut h = FileHandleBox::new();
// Phase 111: "a" is now supported, test with "x" instead
let result = h.open("/tmp/test.txt", "x");
assert!(result.is_err());
assert!(result.unwrap_err().contains("Unsupported mode"));
}
#[test]
fn test_filehandlebox_independent_instances() {
init_test_provider();
let tmp_path1 = "/tmp/phase110_test_independent1.txt";
let tmp_path2 = "/tmp/phase110_test_independent2.txt";
setup_test_file(tmp_path1, "file1");
setup_test_file(tmp_path2, "file2");
// Two independent handles
let mut h1 = FileHandleBox::new();
let mut h2 = FileHandleBox::new();
h1.open(tmp_path1, "r").expect("h1 open");
h2.open(tmp_path2, "r").expect("h2 open");
let content1 = h1.read_to_string().expect("h1 read");
let content2 = h2.read_to_string().expect("h2 read");
assert_eq!(content1, "file1");
assert_eq!(content2, "file2");
h1.close().expect("h1 close");
h2.close().expect("h2 close");
cleanup_test_file(tmp_path1);
cleanup_test_file(tmp_path2);
}
// ==================== Priority 4: Integration Tests ====================
// Integration Test 1: 複数 FileHandleBox インスタンスが同一ファイルを読む
#[test]
fn test_multiple_filehandle_concurrent_read() {
init_test_provider();
let tmp_path = "/tmp/phase110_test_concurrent_read.txt";
setup_test_file(tmp_path, "shared content");
let mut h1 = FileHandleBox::new();
let mut h2 = FileHandleBox::new();
h1.open(tmp_path, "r").expect("h1 open");
h2.open(tmp_path, "r").expect("h2 open");
let content1 = h1.read_to_string().expect("h1 read");
let content2 = h2.read_to_string().expect("h2 read");
assert_eq!(content1, content2);
assert_eq!(content1, "shared content");
h1.close().expect("h1 close");
h2.close().expect("h2 close");
cleanup_test_file(tmp_path);
}
// Integration Test 2: 複数 FileHandleBox インスタンスが異なるファイルを操作
#[test]
fn test_multiple_filehandles_different_files() {
init_test_provider();
let tmp1 = "/tmp/phase110_test_handle1.txt";
let tmp2 = "/tmp/phase110_test_handle2.txt";
let mut h1 = FileHandleBox::new();
let mut h2 = FileHandleBox::new();
h1.open(tmp1, "w").expect("h1 open");
h2.open(tmp2, "w").expect("h2 open");
h1.write_all("file1 content").expect("h1 write");
h2.write_all("file2 content").expect("h2 write");
h1.close().expect("h1 close");
h2.close().expect("h2 close");
// Verify
let content1 = fs::read_to_string(tmp1).expect("read tmp1");
let content2 = fs::read_to_string(tmp2).expect("read tmp2");
assert_eq!(content1, "file1 content");
assert_eq!(content2, "file2 content");
cleanup_test_file(tmp1);
cleanup_test_file(tmp2);
}
// Integration Test 3: FileHandleBox sequential reads
#[test]
fn test_filehandle_sequential_reads() {
init_test_provider();
let tmp_path = "/tmp/phase110_test_sequential_reads.txt";
setup_test_file(tmp_path, "test data");
let mut h = FileHandleBox::new();
h.open(tmp_path, "r").expect("open");
// Read multiple times (each read returns the full content)
let content1 = h.read_to_string().expect("read 1");
let content2 = h.read_to_string().expect("read 2");
let content3 = h.read_to_string().expect("read 3");
assert_eq!(content1, "test data");
assert_eq!(content2, "test data");
assert_eq!(content3, "test data");
h.close().expect("close");
cleanup_test_file(tmp_path);
}
// Integration Test 4: FileHandleBox write multiple times (truncate behavior)
#[test]
fn test_filehandle_multiple_writes_truncate() {
init_test_provider();
let tmp_path = "/tmp/phase110_test_multiple_writes.txt";
let mut h = FileHandleBox::new();
h.open(tmp_path, "w").expect("open");
h.write_all("first").expect("write 1");
h.write_all("second").expect("write 2");
h.write_all("third").expect("write 3");
h.close().expect("close");
// Verify final content (truncate mode)
let content = fs::read_to_string(tmp_path).expect("read file");
assert_eq!(content, "third");
cleanup_test_file(tmp_path);
}
// ===== Phase 111: Append mode + metadata tests =====
#[test]
fn test_filehandlebox_append_mode() {
init_test_provider();
let path = "/tmp/phase111_append_test.txt";
let _ = fs::remove_file(path); // cleanup
// First write (truncate)
let mut handle = FileHandleBox::new();
handle.open(path, "w").unwrap();
handle.write_all("hello\n").unwrap();
handle.close().unwrap();
// Append
let mut handle = FileHandleBox::new();
handle.open(path, "a").unwrap();
handle.write_all("world\n").unwrap();
handle.close().unwrap();
// Verify
let content = fs::read_to_string(path).unwrap();
assert_eq!(content, "hello\nworld\n");
let _ = fs::remove_file(path);
}
#[test]
fn test_filehandlebox_metadata_size() {
init_test_provider();
let path = "/tmp/phase111_metadata_test.txt";
let _ = fs::remove_file(path);
// Write test file
let mut handle = FileHandleBox::new();
handle.open(path, "w").unwrap();
handle.write_all("hello").unwrap(); // 5 bytes
handle.close().unwrap();
// Check size
let mut handle = FileHandleBox::new();
handle.open(path, "r").unwrap();
let size = handle.size().unwrap();
assert_eq!(size, 5);
handle.close().unwrap();
let _ = fs::remove_file(path);
}
#[test]
fn test_filehandlebox_metadata_is_file() {
init_test_provider();
let path = "/tmp/phase111_file_test.txt";
let _ = fs::remove_file(path);
// Create file
let mut handle = FileHandleBox::new();
handle.open(path, "w").unwrap();
handle.close().unwrap();
// Check is_file
let mut handle = FileHandleBox::new();
handle.open(path, "r").unwrap();
let is_file = handle.is_file().unwrap();
assert!(is_file);
let is_dir = handle.is_dir().unwrap();
assert!(!is_dir);
handle.close().unwrap();
let _ = fs::remove_file(path);
}
#[test]
fn test_filehandlebox_write_readonly_error() {
init_test_provider();
let path = "/tmp/phase111_readonly_test.txt";
let _ = fs::remove_file(path);
// Create file
fs::write(path, "content").unwrap();
// Open in read mode
let mut handle = FileHandleBox::new();
handle.open(path, "r").unwrap();
// Try to write → Error
let result = handle.write_all("new");
assert!(result.is_err());
assert!(result.unwrap_err().contains("read-only"));
handle.close().unwrap();
let _ = fs::remove_file(path);
}
#[test]
#[ignore] // This test requires NYASH_RUNTIME_PROFILE=no-fs environment variable
fn test_filehandlebox_nofs_profile_error() {
// Note: This test should be run with NYASH_RUNTIME_PROFILE=no-fs
// For now, we test that open() fails with disabled message when provider is None
// Cannot directly test NoFs profile in unit tests without setting env var
// and reinitializing runtime. This test is marked as ignored and should be
// run separately with proper profile configuration.
// The test would look like:
// let mut handle = FileHandleBox::new();
// let result = handle.open("/tmp/test.txt", "w");
// assert!(result.is_err());
// assert!(result.unwrap_err().contains("disabled"));
}
// ===== Phase 113: Nyash-visible API tests =====
#[test]
fn test_phase113_ny_open_read_write_close() {
init_test_provider();
let path = "/tmp/phase113_ny_test.txt";
let _ = fs::remove_file(path);
let mut handle = FileHandleBox::new();
// Test ny_open + ny_write + ny_close
handle.ny_open(path, "w");
handle.ny_write("test content\n");
handle.ny_close();
// Test ny_open + ny_read + ny_close
handle.ny_open(path, "r");
let content = handle.ny_read();
assert_eq!(content.value, "test content\n");
handle.ny_close();
let _ = fs::remove_file(path);
}
#[test]
fn test_phase113_ny_append_mode() {
init_test_provider();
let path = "/tmp/phase113_ny_append.txt";
let _ = fs::remove_file(path);
let mut handle = FileHandleBox::new();
// First write
handle.ny_open(path, "w");
handle.ny_write("first\n");
handle.ny_close();
// Append
handle.ny_open(path, "a");
handle.ny_write("second\n");
handle.ny_close();
// Read and verify
handle.ny_open(path, "r");
let content = handle.ny_read();
assert_eq!(content.value, "first\nsecond\n");
handle.ny_close();
let _ = fs::remove_file(path);
}
#[test]
fn test_phase113_ny_metadata_methods() {
init_test_provider();
let path = "/tmp/phase113_ny_metadata.txt";
let _ = fs::remove_file(path);
let mut handle = FileHandleBox::new();
// Create file
handle.ny_open(path, "w");
handle.ny_write("hello");
handle.ny_close();
// Test metadata methods
handle.ny_open(path, "r");
let exists = handle.ny_exists();
assert!(exists.value);
let size = handle.ny_size();
assert_eq!(size.value, 5);
let is_file = handle.ny_is_file();
assert!(is_file.value);
let is_dir = handle.ny_is_dir();
assert!(!is_dir.value);
handle.ny_close();
let _ = fs::remove_file(path);
}
#[test]
#[should_panic(expected = "FileHandleBox.open() failed")]
fn test_phase113_ny_open_panic_on_error() {
init_test_provider();
let mut handle = FileHandleBox::new();
// Double open should panic
handle.ny_open("/tmp/test.txt", "w");
handle.ny_open("/tmp/test.txt", "w"); // This should panic
}
#[test]
#[should_panic(expected = "FileHandleBox.read() failed")]
fn test_phase113_ny_read_panic_when_not_open() {
init_test_provider();
let handle = FileHandleBox::new();
let _ = handle.ny_read(); // This should panic
}
#[test]
#[should_panic(expected = "FileHandleBox.write() failed")]
fn test_phase113_ny_write_panic_in_read_mode() {
init_test_provider();
let path = "/tmp/phase113_ny_write_panic.txt";
fs::write(path, "content").unwrap();
let mut handle = FileHandleBox::new();
handle.ny_open(path, "r");
handle.ny_write("data"); // This should panic (read-only mode)
}
// ===== Phase 114: metadata_internal() unification tests =====
#[test]
fn test_filehandlebox_metadata_internal_default() {
init_test_provider();
let path = "/tmp/phase114_metadata_internal.txt";
let _ = fs::remove_file(path);
// Create test file with known content
let mut handle = FileHandleBox::new();
handle.open(path, "w").unwrap();
handle.write_all("12345").unwrap(); // 5 bytes
handle.close().unwrap();
// Test metadata_internal() via stat()
handle.open(path, "r").unwrap();
let stat = handle
.metadata_internal()
.expect("metadata_internal should succeed");
assert!(stat.is_file);
assert!(!stat.is_dir);
assert_eq!(stat.size, 5);
handle.close().unwrap();
let _ = fs::remove_file(path);
}
#[test]
fn test_filehandlebox_metadata_internal_not_open() {
init_test_provider();
let handle = FileHandleBox::new();
let result = handle.metadata_internal();
assert!(result.is_err());
assert!(result.unwrap_err().contains("not open"));
}
#[test]
fn test_filehandlebox_ny_size_uses_stat() {
init_test_provider();
let path = "/tmp/phase114_ny_size_stat.txt";
let _ = fs::remove_file(path);
let mut handle = FileHandleBox::new();
handle.ny_open(path, "w");
handle.ny_write("test123"); // 7 bytes
handle.ny_close();
// Verify ny_size() uses stat() internally
handle.ny_open(path, "r");
let size = handle.ny_size();
assert_eq!(size.value, 7);
handle.ny_close();
let _ = fs::remove_file(path);
}
#[test]
fn test_filehandlebox_exists_uses_fileio() {
init_test_provider();
let path = "/tmp/phase114_exists_fileio.txt";
let _ = fs::remove_file(path);
let mut handle = FileHandleBox::new();
handle.ny_open(path, "w");
handle.ny_close();
// exists() should use FileIo::exists()
handle.ny_open(path, "r");
let exists = handle.ny_exists();
assert!(exists.value);
handle.ny_close();
let _ = fs::remove_file(path);
}
#[test]
fn test_filehandlebox_is_file_is_dir_via_stat() {
init_test_provider();
let path = "/tmp/phase114_is_file_dir.txt";
let _ = fs::remove_file(path);
let mut handle = FileHandleBox::new();
handle.ny_open(path, "w");
handle.ny_close();
// Test is_file/is_dir use metadata_internal() → stat()
handle.ny_open(path, "r");
let is_file = handle.is_file().expect("is_file should succeed");
assert!(is_file);
let is_dir = handle.is_dir().expect("is_dir should succeed");
assert!(!is_dir);
handle.ny_close();
let _ = fs::remove_file(path);
}

View File

@ -0,0 +1,78 @@
use super::FileHandleBox;
use crate::box_trait::{BoolBox, BoxCore, NyashBox, StringBox};
use std::any::Any;
impl BoxCore for FileHandleBox {
fn box_id(&self) -> u64 {
self.base.id
}
fn parent_type_id(&self) -> Option<std::any::TypeId> {
self.base.parent_type_id
}
fn fmt_box(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"FileHandleBox(path={}, mode={}, open={})",
if self.path.is_empty() {
"<none>"
} else {
&self.path
},
if self.mode.is_empty() {
"<none>"
} else {
&self.mode
},
self.is_open()
)
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl NyashBox for FileHandleBox {
fn clone_box(&self) -> Box<dyn NyashBox> {
Box::new(self.clone())
}
fn share_box(&self) -> Box<dyn NyashBox> {
self.clone_box()
}
fn to_string_box(&self) -> StringBox {
StringBox::new(format!(
"FileHandleBox(path={}, mode={}, open={})",
self.path,
self.mode,
self.is_open()
))
}
fn type_name(&self) -> &'static str {
"FileHandleBox"
}
fn equals(&self, other: &dyn NyashBox) -> BoolBox {
if let Some(other_handle) = other.as_any().downcast_ref::<FileHandleBox>() {
// Equality: Same path and mode
// Note: Two independent handles to same file are equal if path/mode match
BoolBox::new(self.path == other_handle.path && self.mode == other_handle.mode)
} else {
BoolBox::new(false)
}
}
}
impl std::fmt::Display for FileHandleBox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.fmt_box(f)
}
}

View File

@ -1,981 +0,0 @@
//! Condition Expression Lowerer
//!
//! This module provides the core logic for lowering AST condition expressions
//! to JoinIR instructions. It handles comparisons, logical operators, and
//! arithmetic expressions.
//!
//! ## Design Philosophy
//!
//! **Single Responsibility**: This module ONLY performs AST → JoinIR lowering.
//! It does NOT:
//! - Manage variable environments (that's condition_env.rs)
//! - Extract variables from AST (that's condition_var_extractor.rs)
//! - Manage HOST ↔ JoinIR bindings (that's inline_boundary.rs)
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator};
use crate::mir::join_ir::{BinOpKind, CompareOp, ConstValue, JoinInst, MirLikeInst, UnaryOp};
use crate::mir::ValueId;
use super::condition_env::ConditionEnv;
use super::loop_body_local_env::LoopBodyLocalEnv; // Phase 92 P2-2: Body-local support
use super::method_call_lowerer::MethodCallLowerer;
/// Lower an AST condition to JoinIR instructions
///
/// # Arguments
///
/// * `cond_ast` - AST node representing the boolean condition
/// * `alloc_value` - ValueId allocator function
/// * `env` - ConditionEnv for variable resolution (JoinIR-local ValueIds)
/// * `body_local_env` - Phase 92 P2-2: Optional body-local variable environment
///
/// # Returns
///
/// * `Ok((ValueId, Vec<JoinInst>))` - Condition result ValueId and evaluation instructions
/// * `Err(String)` - Lowering error message
///
/// # Supported Patterns
///
/// - Comparisons: `i < n`, `x == y`, `a != b`, `x <= y`, `x >= y`, `x > y`
/// - Logical: `a && b`, `a || b`, `!cond`
/// - Variables and literals
///
/// # Phase 92 P2-2: Body-Local Variable Support
///
/// When lowering conditions that reference body-local variables (e.g., `ch == '\\'`
/// in escape patterns), the `body_local_env` parameter provides name → ValueId
/// mappings for variables defined in the loop body.
///
/// Variable resolution priority:
/// 1. ConditionEnv (loop parameters, captured variables)
/// 2. LoopBodyLocalEnv (body-local variables like `ch`)
///
/// # Phase 252: This-Method Support
///
/// When lowering conditions in static box methods (e.g., `StringUtils.trim_end/1`),
/// the `current_static_box_name` parameter enables `this.method(...)` calls to be
/// resolved to the appropriate static box method.
///
/// # Example
///
/// ```ignore
/// let mut env = ConditionEnv::new();
/// env.insert("i".to_string(), ValueId(0));
/// env.insert("end".to_string(), ValueId(1));
///
/// let mut body_env = LoopBodyLocalEnv::new();
/// body_env.insert("ch".to_string(), ValueId(5)); // Phase 92 P2-2
///
/// let mut value_counter = 2u32;
/// let mut alloc_value = || {
/// let id = ValueId(value_counter);
/// value_counter += 1;
/// id
/// };
///
/// // Lower condition: ch == '\\'
/// let (cond_value, cond_insts) = lower_condition_to_joinir(
/// condition_ast,
/// &mut alloc_value,
/// &env,
/// Some(&body_env), // Phase 92 P2-2: Body-local support
/// Some("StringUtils"), // Phase 252: Static box name for this.method
/// )?;
/// ```
pub fn lower_condition_to_joinir(
cond_ast: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
) -> Result<(ValueId, Vec<JoinInst>), String> {
let mut instructions = Vec::new();
let result_value = lower_condition_recursive(
cond_ast,
alloc_value,
env,
body_local_env,
current_static_box_name,
&mut instructions,
)?;
Ok((result_value, instructions))
}
/// Convenience wrapper: lower a condition without body-local or static box support.
pub fn lower_condition_to_joinir_no_body_locals(
cond_ast: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
) -> Result<(ValueId, Vec<JoinInst>), String> {
lower_condition_to_joinir(cond_ast, alloc_value, env, None, None)
}
/// Recursive helper for condition lowering
///
/// Handles all supported AST node types and emits appropriate JoinIR instructions.
///
/// # Phase 92 P2-2
///
/// Added `body_local_env` parameter to support body-local variable resolution.
///
/// # Phase 252
///
/// Added `current_static_box_name` parameter to support `this.method(...)` calls
/// in static box method conditions.
fn lower_condition_recursive(
cond_ast: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
match cond_ast {
// Comparison operations: <, ==, !=, <=, >=, >
ASTNode::BinaryOp {
operator,
left,
right,
..
} => match operator {
BinaryOperator::Less
| BinaryOperator::Equal
| BinaryOperator::NotEqual
| BinaryOperator::LessEqual
| BinaryOperator::GreaterEqual
| BinaryOperator::Greater => {
lower_comparison(operator, left, right, alloc_value, env, body_local_env, current_static_box_name, instructions)
}
BinaryOperator::And => lower_logical_and(left, right, alloc_value, env, body_local_env, current_static_box_name, instructions),
BinaryOperator::Or => lower_logical_or(left, right, alloc_value, env, body_local_env, current_static_box_name, instructions),
_ => Err(format!(
"Unsupported binary operator in condition: {:?}",
operator
)),
},
// Unary NOT operator
ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand,
..
} => lower_not_operator(operand, alloc_value, env, body_local_env, current_static_box_name, instructions),
// Phase 92 P2-2: Variables - resolve from ConditionEnv or LoopBodyLocalEnv
ASTNode::Variable { name, .. } => {
// Priority 1: ConditionEnv (loop parameters, captured variables)
if let Some(value_id) = env.get(name) {
return Ok(value_id);
}
// Priority 2: LoopBodyLocalEnv (body-local variables like `ch`)
if let Some(body_env) = body_local_env {
if let Some(value_id) = body_env.get(name) {
return Ok(value_id);
}
}
Err(format!("Variable '{}' not found in ConditionEnv or LoopBodyLocalEnv", name))
}
// Literals - emit as constants
ASTNode::Literal { value, .. } => lower_literal(value, alloc_value, instructions),
// Phase 252: MethodCall support (this.method or builtin methods)
ASTNode::MethodCall {
object,
method,
arguments,
..
} => {
// Check if this is a me/this.method(...) call
match object.as_ref() {
ASTNode::Me { .. } | ASTNode::This { .. } => {
// me/this.method(...) - requires current_static_box_name
let box_name = current_static_box_name.ok_or_else(|| {
format!(
"this.{}(...) requires current_static_box_name (not in static box context)",
method
)
})?;
// Check if method is allowed in condition context via UserMethodPolicy
if !super::user_method_policy::UserMethodPolicy::allowed_in_condition(box_name, method) {
return Err(format!(
"User-defined method not allowed in loop condition: {}.{}() (not whitelisted)",
box_name, method
));
}
// Lower arguments using lower_for_init whitelist
// (Arguments are value expressions, not conditions, so we use init whitelist)
let mut arg_vals = Vec::new();
for arg_ast in arguments {
let arg_val = lower_value_expression(
arg_ast,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
)?;
arg_vals.push(arg_val);
}
// Emit BoxCall instruction
let dst = alloc_value();
instructions.push(JoinInst::Compute(MirLikeInst::BoxCall {
dst: Some(dst),
box_name: box_name.to_string(),
method: method.clone(),
args: arg_vals,
}));
Ok(dst)
}
_ => {
// Not this.method - treat as value expression (builtin methods via CoreMethodId)
lower_value_expression(object, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
Err(format!(
"MethodCall on non-this object not yet supported in condition: {:?}",
cond_ast
))
}
}
}
_ => Err(format!("Unsupported AST node in condition: {:?}", cond_ast)),
}
}
/// Lower a comparison operation (e.g., `i < end`)
fn lower_comparison(
operator: &BinaryOperator,
left: &ASTNode,
right: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
// Lower left and right sides
let lhs = lower_value_expression(left, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
let rhs = lower_value_expression(right, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
let dst = alloc_value();
let cmp_op = match operator {
BinaryOperator::Less => CompareOp::Lt,
BinaryOperator::Equal => CompareOp::Eq,
BinaryOperator::NotEqual => CompareOp::Ne,
BinaryOperator::LessEqual => CompareOp::Le,
BinaryOperator::GreaterEqual => CompareOp::Ge,
BinaryOperator::Greater => CompareOp::Gt,
_ => unreachable!(),
};
// Emit Compare instruction
instructions.push(JoinInst::Compute(MirLikeInst::Compare {
dst,
op: cmp_op,
lhs,
rhs,
}));
Ok(dst)
}
/// Lower logical AND operation (e.g., `a && b`)
fn lower_logical_and(
left: &ASTNode,
right: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
// Logical AND: evaluate both sides and combine
let lhs = lower_condition_recursive(left, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
let rhs = lower_condition_recursive(right, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
let dst = alloc_value();
// Emit BinOp And instruction
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
dst,
op: BinOpKind::And,
lhs,
rhs,
}));
Ok(dst)
}
/// Lower logical OR operation (e.g., `a || b`)
fn lower_logical_or(
left: &ASTNode,
right: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
// Logical OR: evaluate both sides and combine
let lhs = lower_condition_recursive(left, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
let rhs = lower_condition_recursive(right, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
let dst = alloc_value();
// Emit BinOp Or instruction
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
dst,
op: BinOpKind::Or,
lhs,
rhs,
}));
Ok(dst)
}
/// Lower NOT operator (e.g., `!cond`)
fn lower_not_operator(
operand: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
let operand_val = lower_condition_recursive(operand, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
let dst = alloc_value();
// Emit UnaryOp Not instruction
instructions.push(JoinInst::Compute(MirLikeInst::UnaryOp {
dst,
op: UnaryOp::Not,
operand: operand_val,
}));
Ok(dst)
}
/// Lower a literal value (e.g., `10`, `true`, `"text"`)
fn lower_literal(
value: &LiteralValue,
alloc_value: &mut dyn FnMut() -> ValueId,
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
let dst = alloc_value();
let const_value = match value {
LiteralValue::Integer(n) => ConstValue::Integer(*n),
LiteralValue::String(s) => ConstValue::String(s.clone()),
LiteralValue::Bool(b) => ConstValue::Bool(*b),
LiteralValue::Float(_) => {
return Err("Float literals not supported in JoinIR conditions yet".to_string());
}
_ => {
return Err(format!(
"Unsupported literal type in condition: {:?}",
value
));
}
};
instructions.push(JoinInst::Compute(MirLikeInst::Const {
dst,
value: const_value,
}));
Ok(dst)
}
/// Lower a value expression (for comparison operands, etc.)
///
/// This handles the common case where we need to evaluate a simple value
/// (variable or literal) as part of a comparison.
///
/// # Phase 92 P2-2
///
/// Added `body_local_env` parameter to support body-local variable resolution
/// (e.g., `ch` in `ch == '\\'`).
///
/// # Phase 252
///
/// Added `current_static_box_name` parameter to support `this.method(...)` calls
/// in argument expressions.
pub fn lower_value_expression(
expr: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
match expr {
// Phase 92 P2-2: Variables - resolve from ConditionEnv or LoopBodyLocalEnv
ASTNode::Variable { name, .. } => {
// Priority 1: ConditionEnv (loop parameters, captured variables)
if let Some(value_id) = env.get(name) {
return Ok(value_id);
}
// Priority 2: LoopBodyLocalEnv (body-local variables like `ch`)
if let Some(body_env) = body_local_env {
if let Some(value_id) = body_env.get(name) {
return Ok(value_id);
}
}
Err(format!("Variable '{}' not found in ConditionEnv or LoopBodyLocalEnv", name))
}
// Literals - emit as constants
ASTNode::Literal { value, .. } => lower_literal(value, alloc_value, instructions),
// Binary operations (for arithmetic in conditions like i + 1 < n)
ASTNode::BinaryOp {
operator,
left,
right,
..
} => lower_arithmetic_binop(operator, left, right, alloc_value, env, body_local_env, current_static_box_name, instructions),
// Phase 224-C: MethodCall support with arguments (e.g., s.length(), s.indexOf(ch))
ASTNode::MethodCall {
object,
method,
arguments,
..
} => {
// 1. Lower receiver (object) to ValueId
let recv_val = lower_value_expression(object, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
// 2. Lower method call using MethodCallLowerer
// Phase 256.7: Use lower_for_init (more permissive whitelist) for value expressions
// Value expressions like s.substring(i, i+1) should be allowed even in condition arguments
let empty_body_local = super::loop_body_local_env::LoopBodyLocalEnv::new();
let body_env = body_local_env.unwrap_or(&empty_body_local);
MethodCallLowerer::lower_for_init(
recv_val,
method,
arguments,
alloc_value,
env,
body_env,
instructions,
)
}
_ => Err(format!(
"Unsupported expression in value context: {:?}",
expr
)),
}
}
/// Lower an arithmetic binary operation (e.g., `i + 1`)
fn lower_arithmetic_binop(
operator: &BinaryOperator,
left: &ASTNode,
right: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
let lhs = lower_value_expression(left, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
let rhs = lower_value_expression(right, alloc_value, env, body_local_env, current_static_box_name, instructions)?;
let dst = alloc_value();
let bin_op = match operator {
BinaryOperator::Add => BinOpKind::Add,
BinaryOperator::Subtract => BinOpKind::Sub,
BinaryOperator::Multiply => BinOpKind::Mul,
BinaryOperator::Divide => BinOpKind::Div,
BinaryOperator::Modulo => BinOpKind::Mod,
_ => {
return Err(format!(
"Unsupported binary operator in expression: {:?}",
operator
));
}
};
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
dst,
op: bin_op,
lhs,
rhs,
}));
Ok(dst)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
/// Helper to create a test ConditionEnv with variables
fn create_test_env() -> ConditionEnv {
let mut env = ConditionEnv::new();
// Register test variables (using JoinIR-local ValueIds)
env.insert("i".to_string(), ValueId(0));
env.insert("end".to_string(), ValueId(1));
env
}
#[test]
fn test_simple_comparison() {
let env = create_test_env();
let mut value_counter = 2u32; // Start after i=0, end=1
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: i < end
let ast = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "end".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
assert!(result.is_ok(), "Simple comparison should succeed");
let (_cond_value, instructions) = result.unwrap();
assert_eq!(
instructions.len(),
1,
"Should generate 1 Compare instruction"
);
}
#[test]
fn test_comparison_with_literal() {
let env = create_test_env();
let mut value_counter = 2u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: i < 10
let ast = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
assert!(result.is_ok(), "Comparison with literal should succeed");
let (_cond_value, instructions) = result.unwrap();
// Should have: Const(10), Compare
assert_eq!(instructions.len(), 2, "Should generate Const + Compare");
}
#[test]
fn test_logical_or() {
let mut env = ConditionEnv::new();
env.insert("a".to_string(), ValueId(2));
env.insert("b".to_string(), ValueId(3));
let mut value_counter = 4u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: a < 5 || b < 5
let ast = ASTNode::BinaryOp {
operator: BinaryOperator::Or,
left: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "a".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(5),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
right: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "b".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(5),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
assert!(result.is_ok(), "OR expression should succeed");
let (_cond_value, instructions) = result.unwrap();
// Should have: Const(5), Compare(a<5), Const(5), Compare(b<5), BinOp(Or)
assert_eq!(instructions.len(), 5, "Should generate proper OR chain");
}
#[test]
fn test_not_operator() {
let env = create_test_env();
let mut value_counter = 2u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: !(i < end)
let ast = ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "end".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
assert!(result.is_ok(), "NOT operator should succeed");
let (_cond_value, instructions) = result.unwrap();
// Should have: Compare, UnaryOp(Not)
assert_eq!(instructions.len(), 2, "Should generate Compare + Not");
}
/// Phase 92 P4 Level 2: Test body-local variable resolution
///
/// This test verifies that conditions can reference body-local variables
/// (e.g., `ch == '\\'` in escape sequence patterns).
///
/// Variable resolution priority:
/// 1. ConditionEnv (loop parameters, captured variables)
/// 2. LoopBodyLocalEnv (body-local variables like `ch`)
#[test]
fn test_body_local_variable_resolution() {
// Setup ConditionEnv with loop variable
let mut env = ConditionEnv::new();
env.insert("i".to_string(), ValueId(100));
// Setup LoopBodyLocalEnv with body-local variable
let mut body_local_env = LoopBodyLocalEnv::new();
body_local_env.insert("ch".to_string(), ValueId(200));
let mut value_counter = 300u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: ch == "\\"
let ast = ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(ASTNode::Variable {
name: "ch".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::String("\\".to_string()),
span: Span::unknown(),
}),
span: Span::unknown(),
};
// Phase 92 P2-2: Use lower_condition_to_joinir with body_local_env
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env), None);
assert!(
result.is_ok(),
"Body-local variable resolution should succeed"
);
let (cond_value, instructions) = result.unwrap();
// Should have: Const("\\"), Compare(ch == "\\")
assert_eq!(
instructions.len(),
2,
"Should generate Const + Compare for body-local variable"
);
// Verify the comparison uses the body-local variable's ValueId(200)
if let Some(JoinInst::Compute(MirLikeInst::Compare { lhs, .. })) = instructions.get(1) {
assert_eq!(
*lhs,
ValueId(200),
"Compare should use body-local variable ValueId(200)"
);
} else {
panic!("Expected Compare instruction at position 1");
}
assert!(cond_value.0 >= 300, "Result should use newly allocated ValueId");
}
/// Phase 92 P4 Level 2: Test variable resolution priority (ConditionEnv takes precedence)
///
/// When a variable exists in both ConditionEnv and LoopBodyLocalEnv,
/// ConditionEnv should take priority.
#[test]
fn test_variable_resolution_priority() {
// Setup both environments with overlapping variable "x"
let mut env = ConditionEnv::new();
env.insert("x".to_string(), ValueId(100)); // ConditionEnv priority
let mut body_local_env = LoopBodyLocalEnv::new();
body_local_env.insert("x".to_string(), ValueId(200)); // Should be shadowed
let mut value_counter = 300u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: x == 42
let ast = ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(42),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env), None);
assert!(result.is_ok(), "Variable resolution should succeed");
let (_cond_value, instructions) = result.unwrap();
// Verify the comparison uses ConditionEnv's ValueId(100), not LoopBodyLocalEnv's ValueId(200)
if let Some(JoinInst::Compute(MirLikeInst::Compare { lhs, .. })) = instructions.get(1) {
assert_eq!(
*lhs,
ValueId(100),
"ConditionEnv should take priority over LoopBodyLocalEnv"
);
} else {
panic!("Expected Compare instruction at position 1");
}
}
/// Phase 92 P4 Level 2: Test error handling for undefined variables
///
/// Variables not found in either environment should produce clear error messages.
#[test]
fn test_undefined_variable_error() {
let env = ConditionEnv::new();
let body_local_env = LoopBodyLocalEnv::new();
let mut value_counter = 300u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: undefined_var == 42
let ast = ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(ASTNode::Variable {
name: "undefined_var".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(42),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env), None);
assert!(result.is_err(), "Undefined variable should fail");
let err = result.unwrap_err();
assert!(
err.contains("undefined_var"),
"Error message should mention the undefined variable name"
);
assert!(
err.contains("not found"),
"Error message should indicate variable was not found"
);
}
/// Phase 252 P1: Test this.methodcall(...) in conditions
///
/// Verifies that user-defined static box method calls work in conditions
#[test]
fn test_this_methodcall_in_condition() {
let env = create_test_env();
let mut value_counter = 2u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: not this.is_whitespace(ch)
// Simulates StringUtils.trim_end break condition
let method_call = ASTNode::MethodCall {
object: Box::new(ASTNode::Me {
span: Span::unknown(),
}),
method: "is_whitespace".to_string(),
arguments: vec![ASTNode::Variable {
name: "ch".to_string(),
span: Span::unknown(),
}],
span: Span::unknown(),
};
let ast = ASTNode::UnaryOp {
operator: crate::ast::UnaryOperator::Not,
operand: Box::new(method_call),
span: Span::unknown(),
};
// Register 'ch' variable for the test
let mut env = env;
env.insert("ch".to_string(), ValueId(100));
let result = lower_condition_to_joinir(
&ast,
&mut alloc_value,
&env,
None,
Some("StringUtils"), // Phase 252: static box context
);
assert!(result.is_ok(), "this.methodcall should succeed: {:?}", result);
let (_cond_value, instructions) = result.unwrap();
// Should have: BoxCall for is_whitespace, UnaryOp(Not)
assert!(
instructions.len() >= 2,
"Should generate BoxCall + Not instructions"
);
// Verify BoxCall instruction exists
let has_box_call = instructions.iter().any(|inst| matches!(
inst,
JoinInst::Compute(MirLikeInst::BoxCall { method, .. }) if method == "is_whitespace"
));
assert!(
has_box_call,
"Should generate BoxCall for is_whitespace"
);
}
/// Phase 252 P1: Test this.methodcall fails without static box context
#[test]
fn test_this_methodcall_requires_context() {
let env = create_test_env();
let mut value_counter = 2u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: this.is_whitespace(ch)
let ast = ASTNode::MethodCall {
object: Box::new(ASTNode::Me {
span: Span::unknown(),
}),
method: "is_whitespace".to_string(),
arguments: vec![],
span: Span::unknown(),
};
let result = lower_condition_to_joinir(
&ast,
&mut alloc_value,
&env,
None,
None, // No static box context
);
assert!(result.is_err(), "this.methodcall should fail without context");
let err = result.unwrap_err();
assert!(
err.contains("current_static_box_name"),
"Error should mention missing static box context"
);
}
/// Phase 252 P1: Test disallowed method fails
#[test]
fn test_this_methodcall_disallowed_method() {
let env = create_test_env();
let mut value_counter = 2u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: this.trim("test") - trim is NOT allowed in conditions
let ast = ASTNode::MethodCall {
object: Box::new(ASTNode::Me {
span: Span::unknown(),
}),
method: "trim".to_string(),
arguments: vec![],
span: Span::unknown(),
};
let result = lower_condition_to_joinir(
&ast,
&mut alloc_value,
&env,
None,
Some("StringUtils"),
);
assert!(result.is_err(), "Disallowed method should fail");
let err = result.unwrap_err();
assert!(
err.contains("not allowed") || err.contains("not whitelisted"),
"Error should indicate method is not allowed: {}",
err
);
}
}

View File

@ -0,0 +1,97 @@
use crate::ast::ASTNode;
use crate::mir::join_ir::JoinInst;
use crate::mir::ValueId;
use super::condition_ops::lower_condition_recursive;
use super::super::condition_env::ConditionEnv;
use super::super::loop_body_local_env::LoopBodyLocalEnv; // Phase 92 P2-2: Body-local support
/// Lower an AST condition to JoinIR instructions
///
/// # Arguments
///
/// * `cond_ast` - AST node representing the boolean condition
/// * `alloc_value` - ValueId allocator function
/// * `env` - ConditionEnv for variable resolution (JoinIR-local ValueIds)
/// * `body_local_env` - Phase 92 P2-2: Optional body-local variable environment
///
/// # Returns
///
/// * `Ok((ValueId, Vec<JoinInst>))` - Condition result ValueId and evaluation instructions
/// * `Err(String)` - Lowering error message
///
/// # Supported Patterns
///
/// - Comparisons: `i < n`, `x == y`, `a != b`, `x <= y`, `x >= y`, `x > y`
/// - Logical: `a && b`, `a || b`, `!cond`
/// - Variables and literals
///
/// # Phase 92 P2-2: Body-Local Variable Support
///
/// When lowering conditions that reference body-local variables (e.g., `ch == '\\'`
/// in escape patterns), the `body_local_env` parameter provides name → ValueId
/// mappings for variables defined in the loop body.
///
/// Variable resolution priority:
/// 1. ConditionEnv (loop parameters, captured variables)
/// 2. LoopBodyLocalEnv (body-local variables like `ch`)
///
/// # Phase 252: This-Method Support
///
/// When lowering conditions in static box methods (e.g., `StringUtils.trim_end/1`),
/// the `current_static_box_name` parameter enables `this.method(...)` calls to be
/// resolved to the appropriate static box method.
///
/// # Example
///
/// ```ignore
/// let mut env = ConditionEnv::new();
/// env.insert("i".to_string(), ValueId(0));
/// env.insert("end".to_string(), ValueId(1));
///
/// let mut body_env = LoopBodyLocalEnv::new();
/// body_env.insert("ch".to_string(), ValueId(5)); // Phase 92 P2-2
///
/// let mut value_counter = 2u32;
/// let mut alloc_value = || {
/// let id = ValueId(value_counter);
/// value_counter += 1;
/// id
/// };
///
/// // Lower condition: ch == '\\'
/// let (cond_value, cond_insts) = lower_condition_to_joinir(
/// condition_ast,
/// &mut alloc_value,
/// &env,
/// Some(&body_env), // Phase 92 P2-2: Body-local support
/// Some("StringUtils"), // Phase 252: Static box name for this.method
/// )?;
/// ```
pub fn lower_condition_to_joinir(
cond_ast: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
) -> Result<(ValueId, Vec<JoinInst>), String> {
let mut instructions = Vec::new();
let result_value = lower_condition_recursive(
cond_ast,
alloc_value,
env,
body_local_env,
current_static_box_name,
&mut instructions,
)?;
Ok((result_value, instructions))
}
/// Convenience wrapper: lower a condition without body-local or static box support.
pub fn lower_condition_to_joinir_no_body_locals(
cond_ast: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
) -> Result<(ValueId, Vec<JoinInst>), String> {
lower_condition_to_joinir(cond_ast, alloc_value, env, None, None)
}

View File

@ -0,0 +1,344 @@
use crate::ast::{ASTNode, BinaryOperator, UnaryOperator};
use crate::mir::join_ir::{BinOpKind, CompareOp, JoinInst, MirLikeInst, UnaryOp};
use crate::mir::ValueId;
use super::super::condition_env::ConditionEnv;
use super::super::loop_body_local_env::LoopBodyLocalEnv; // Phase 92 P2-2: Body-local support
use super::super::user_method_policy::UserMethodPolicy;
use super::value_expr::{lower_literal, lower_value_expression};
/// Recursive helper for condition lowering
///
/// Handles all supported AST node types and emits appropriate JoinIR instructions.
///
/// # Phase 92 P2-2
///
/// Added `body_local_env` parameter to support body-local variable resolution.
///
/// # Phase 252
///
/// Added `current_static_box_name` parameter to support `this.method(...)` calls
/// in static box method conditions.
pub(super) fn lower_condition_recursive(
cond_ast: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
match cond_ast {
// Comparison operations: <, ==, !=, <=, >=, >
ASTNode::BinaryOp {
operator,
left,
right,
..
} => match operator {
BinaryOperator::Less
| BinaryOperator::Equal
| BinaryOperator::NotEqual
| BinaryOperator::LessEqual
| BinaryOperator::GreaterEqual
| BinaryOperator::Greater => lower_comparison(
operator,
left,
right,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
),
BinaryOperator::And => lower_logical_and(
left,
right,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
),
BinaryOperator::Or => lower_logical_or(
left,
right,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
),
_ => Err(format!(
"Unsupported binary operator in condition: {:?}",
operator
)),
},
// Unary NOT operator
ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand,
..
} => lower_not_operator(
operand,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
),
// Phase 92 P2-2: Variables - resolve from ConditionEnv or LoopBodyLocalEnv
ASTNode::Variable { name, .. } => {
// Priority 1: ConditionEnv (loop parameters, captured variables)
if let Some(value_id) = env.get(name) {
return Ok(value_id);
}
// Priority 2: LoopBodyLocalEnv (body-local variables like `ch`)
if let Some(body_env) = body_local_env {
if let Some(value_id) = body_env.get(name) {
return Ok(value_id);
}
}
Err(format!(
"Variable '{}' not found in ConditionEnv or LoopBodyLocalEnv",
name
))
}
// Literals - emit as constants
ASTNode::Literal { value, .. } => lower_literal(value, alloc_value, instructions),
// Phase 252: MethodCall support (this.method or builtin methods)
ASTNode::MethodCall {
object,
method,
arguments,
..
} => {
// Check if this is a me/this.method(...) call
match object.as_ref() {
ASTNode::Me { .. } | ASTNode::This { .. } => {
// me/this.method(...) - requires current_static_box_name
let box_name = current_static_box_name.ok_or_else(|| {
format!(
"this.{}(...) requires current_static_box_name (not in static box context)",
method
)
})?;
// Check if method is allowed in condition context via UserMethodPolicy
if !UserMethodPolicy::allowed_in_condition(box_name, method) {
return Err(format!(
"User-defined method not allowed in loop condition: {}.{}() (not whitelisted)",
box_name, method
));
}
// Lower arguments using lower_for_init whitelist
// (Arguments are value expressions, not conditions, so we use init whitelist)
let mut arg_vals = Vec::new();
for arg_ast in arguments {
let arg_val = lower_value_expression(
arg_ast,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
)?;
arg_vals.push(arg_val);
}
// Emit BoxCall instruction
let dst = alloc_value();
instructions.push(JoinInst::Compute(MirLikeInst::BoxCall {
dst: Some(dst),
box_name: box_name.to_string(),
method: method.clone(),
args: arg_vals,
}));
Ok(dst)
}
_ => {
// Not this.method - treat as value expression (builtin methods via CoreMethodId)
lower_value_expression(
object,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
)?;
Err(format!(
"MethodCall on non-this object not yet supported in condition: {:?}",
cond_ast
))
}
}
}
_ => Err(format!("Unsupported AST node in condition: {:?}", cond_ast)),
}
}
/// Lower a comparison operation (e.g., `i < end`)
fn lower_comparison(
operator: &BinaryOperator,
left: &ASTNode,
right: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
// Lower left and right sides
let lhs = lower_value_expression(
left,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
)?;
let rhs = lower_value_expression(
right,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
)?;
let dst = alloc_value();
let cmp_op = match operator {
BinaryOperator::Less => CompareOp::Lt,
BinaryOperator::Equal => CompareOp::Eq,
BinaryOperator::NotEqual => CompareOp::Ne,
BinaryOperator::LessEqual => CompareOp::Le,
BinaryOperator::GreaterEqual => CompareOp::Ge,
BinaryOperator::Greater => CompareOp::Gt,
_ => unreachable!(),
};
// Emit Compare instruction
instructions.push(JoinInst::Compute(MirLikeInst::Compare {
dst,
op: cmp_op,
lhs,
rhs,
}));
Ok(dst)
}
/// Lower logical AND operation (e.g., `a && b`)
fn lower_logical_and(
left: &ASTNode,
right: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
// Logical AND: evaluate both sides and combine
let lhs = lower_condition_recursive(
left,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
)?;
let rhs = lower_condition_recursive(
right,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
)?;
let dst = alloc_value();
// Emit BinOp And instruction
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
dst,
op: BinOpKind::And,
lhs,
rhs,
}));
Ok(dst)
}
/// Lower logical OR operation (e.g., `a || b`)
fn lower_logical_or(
left: &ASTNode,
right: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
// Logical OR: evaluate both sides and combine
let lhs = lower_condition_recursive(
left,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
)?;
let rhs = lower_condition_recursive(
right,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
)?;
let dst = alloc_value();
// Emit BinOp Or instruction
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
dst,
op: BinOpKind::Or,
lhs,
rhs,
}));
Ok(dst)
}
/// Lower NOT operator (e.g., `!cond`)
fn lower_not_operator(
operand: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
let operand_val = lower_condition_recursive(
operand,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
)?;
let dst = alloc_value();
// Emit UnaryOp Not instruction
instructions.push(JoinInst::Compute(MirLikeInst::UnaryOp {
dst,
op: UnaryOp::Not,
operand: operand_val,
}));
Ok(dst)
}

View File

@ -0,0 +1,22 @@
//! Condition Expression Lowerer
//!
//! This module provides the core logic for lowering AST condition expressions
//! to JoinIR instructions. It handles comparisons, logical operators, and
//! arithmetic expressions.
//!
//! ## Design Philosophy
//!
//! **Single Responsibility**: This module ONLY performs AST → JoinIR lowering.
//! It does NOT:
//! - Manage variable environments (that's condition_env.rs)
//! - Extract variables from AST (that's condition_var_extractor.rs)
//! - Manage HOST ↔ JoinIR bindings (that's inline_boundary.rs)
mod api;
mod condition_ops;
mod value_expr;
#[cfg(test)]
mod tests;
pub use api::{lower_condition_to_joinir, lower_condition_to_joinir_no_body_locals};
pub use value_expr::lower_value_expression;

View File

@ -0,0 +1,465 @@
use super::{lower_condition_to_joinir, lower_condition_to_joinir_no_body_locals};
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span, UnaryOperator};
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
use crate::mir::join_ir::{JoinInst, MirLikeInst};
use crate::mir::ValueId;
/// Helper to create a test ConditionEnv with variables
fn create_test_env() -> ConditionEnv {
let mut env = ConditionEnv::new();
// Register test variables (using JoinIR-local ValueIds)
env.insert("i".to_string(), ValueId(0));
env.insert("end".to_string(), ValueId(1));
env
}
#[test]
fn test_simple_comparison() {
let env = create_test_env();
let mut value_counter = 2u32; // Start after i=0, end=1
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: i < end
let ast = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "end".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
assert!(result.is_ok(), "Simple comparison should succeed");
let (_cond_value, instructions) = result.unwrap();
assert_eq!(instructions.len(), 1, "Should generate 1 Compare instruction");
}
#[test]
fn test_comparison_with_literal() {
let env = create_test_env();
let mut value_counter = 2u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: i < 10
let ast = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
assert!(result.is_ok(), "Comparison with literal should succeed");
let (_cond_value, instructions) = result.unwrap();
// Should have: Const(10), Compare
assert_eq!(instructions.len(), 2, "Should generate Const + Compare");
}
#[test]
fn test_logical_or() {
let mut env = ConditionEnv::new();
env.insert("a".to_string(), ValueId(2));
env.insert("b".to_string(), ValueId(3));
let mut value_counter = 4u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: a < 5 || b < 5
let ast = ASTNode::BinaryOp {
operator: BinaryOperator::Or,
left: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "a".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(5),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
right: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "b".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(5),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
assert!(result.is_ok(), "OR expression should succeed");
let (_cond_value, instructions) = result.unwrap();
// Should have: Const(5), Compare(a<5), Const(5), Compare(b<5), BinOp(Or)
assert_eq!(instructions.len(), 5, "Should generate proper OR chain");
}
#[test]
fn test_not_operator() {
let env = create_test_env();
let mut value_counter = 2u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: !(i < end)
let ast = ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "end".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env);
assert!(result.is_ok(), "NOT operator should succeed");
let (_cond_value, instructions) = result.unwrap();
// Should have: Compare, UnaryOp(Not)
assert_eq!(instructions.len(), 2, "Should generate Compare + Not");
}
/// Phase 92 P4 Level 2: Test body-local variable resolution
///
/// This test verifies that conditions can reference body-local variables
/// (e.g., `ch == '\\'` in escape sequence patterns).
///
/// Variable resolution priority:
/// 1. ConditionEnv (loop parameters, captured variables)
/// 2. LoopBodyLocalEnv (body-local variables like `ch`)
#[test]
fn test_body_local_variable_resolution() {
// Setup ConditionEnv with loop variable
let mut env = ConditionEnv::new();
env.insert("i".to_string(), ValueId(100));
// Setup LoopBodyLocalEnv with body-local variable
let mut body_local_env = LoopBodyLocalEnv::new();
body_local_env.insert("ch".to_string(), ValueId(200));
let mut value_counter = 300u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: ch == "\\"
let ast = ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(ASTNode::Variable {
name: "ch".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::String("\\".to_string()),
span: Span::unknown(),
}),
span: Span::unknown(),
};
// Phase 92 P2-2: Use lower_condition_to_joinir with body_local_env
let result = lower_condition_to_joinir(
&ast,
&mut alloc_value,
&env,
Some(&body_local_env),
None,
);
assert!(
result.is_ok(),
"Body-local variable resolution should succeed"
);
let (cond_value, instructions) = result.unwrap();
// Should have: Const("\\"), Compare(ch == "\\")
assert_eq!(
instructions.len(),
2,
"Should generate Const + Compare for body-local variable"
);
// Verify the comparison uses the body-local variable's ValueId(200)
if let Some(JoinInst::Compute(MirLikeInst::Compare { lhs, .. })) = instructions.get(1) {
assert_eq!(
*lhs,
ValueId(200),
"Compare should use body-local variable ValueId(200)"
);
} else {
panic!("Expected Compare instruction at position 1");
}
assert!(cond_value.0 >= 300, "Result should use newly allocated ValueId");
}
/// Phase 92 P4 Level 2: Test variable resolution priority (ConditionEnv takes precedence)
///
/// When a variable exists in both ConditionEnv and LoopBodyLocalEnv,
/// ConditionEnv should take priority.
#[test]
fn test_variable_resolution_priority() {
// Setup both environments with overlapping variable "x"
let mut env = ConditionEnv::new();
env.insert("x".to_string(), ValueId(100)); // ConditionEnv priority
let mut body_local_env = LoopBodyLocalEnv::new();
body_local_env.insert("x".to_string(), ValueId(200)); // Should be shadowed
let mut value_counter = 300u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: x == 42
let ast = ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(ASTNode::Variable {
name: "x".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(42),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env), None);
assert!(result.is_ok(), "Variable resolution should succeed");
let (_cond_value, instructions) = result.unwrap();
// Verify the comparison uses ConditionEnv's ValueId(100), not LoopBodyLocalEnv's ValueId(200)
if let Some(JoinInst::Compute(MirLikeInst::Compare { lhs, .. })) = instructions.get(1) {
assert_eq!(
*lhs,
ValueId(100),
"ConditionEnv should take priority over LoopBodyLocalEnv"
);
} else {
panic!("Expected Compare instruction at position 1");
}
}
/// Phase 92 P4 Level 2: Test error handling for undefined variables
///
/// Variables not found in either environment should produce clear error messages.
#[test]
fn test_undefined_variable_error() {
let env = ConditionEnv::new();
let body_local_env = LoopBodyLocalEnv::new();
let mut value_counter = 300u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: undefined_var == 42
let ast = ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(ASTNode::Variable {
name: "undefined_var".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(42),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env), None);
assert!(result.is_err(), "Undefined variable should fail");
let err = result.unwrap_err();
assert!(
err.contains("undefined_var"),
"Error message should mention the undefined variable name"
);
assert!(
err.contains("not found"),
"Error message should indicate variable was not found"
);
}
/// Phase 252 P1: Test this.methodcall(...) in conditions
///
/// Verifies that user-defined static box method calls work in conditions
#[test]
fn test_this_methodcall_in_condition() {
let env = create_test_env();
let mut value_counter = 2u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: not this.is_whitespace(ch)
// Simulates StringUtils.trim_end break condition
let method_call = ASTNode::MethodCall {
object: Box::new(ASTNode::Me {
span: Span::unknown(),
}),
method: "is_whitespace".to_string(),
arguments: vec![ASTNode::Variable {
name: "ch".to_string(),
span: Span::unknown(),
}],
span: Span::unknown(),
};
let ast = ASTNode::UnaryOp {
operator: crate::ast::UnaryOperator::Not,
operand: Box::new(method_call),
span: Span::unknown(),
};
// Register 'ch' variable for the test
let mut env = env;
env.insert("ch".to_string(), ValueId(100));
let result = lower_condition_to_joinir(
&ast,
&mut alloc_value,
&env,
None,
Some("StringUtils"), // Phase 252: static box context
);
assert!(result.is_ok(), "this.methodcall should succeed: {:?}", result);
let (_cond_value, instructions) = result.unwrap();
// Should have: BoxCall for is_whitespace, UnaryOp(Not)
assert!(
instructions.len() >= 2,
"Should generate BoxCall + Not instructions"
);
// Verify BoxCall instruction exists
let has_box_call = instructions.iter().any(|inst| matches!(
inst,
JoinInst::Compute(MirLikeInst::BoxCall { method, .. }) if method == "is_whitespace"
));
assert!(has_box_call, "Should generate BoxCall for is_whitespace");
}
/// Phase 252 P1: Test this.methodcall fails without static box context
#[test]
fn test_this_methodcall_requires_context() {
let env = create_test_env();
let mut value_counter = 2u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: this.is_whitespace(ch)
let ast = ASTNode::MethodCall {
object: Box::new(ASTNode::Me {
span: Span::unknown(),
}),
method: "is_whitespace".to_string(),
arguments: vec![],
span: Span::unknown(),
};
let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, None, None);
assert!(result.is_err(), "this.methodcall should fail without context");
let err = result.unwrap_err();
assert!(
err.contains("current_static_box_name"),
"Error should mention missing static box context"
);
}
/// Phase 252 P1: Test disallowed method fails
#[test]
fn test_this_methodcall_disallowed_method() {
let env = create_test_env();
let mut value_counter = 2u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
// AST: this.trim("test") - trim is NOT allowed in conditions
let ast = ASTNode::MethodCall {
object: Box::new(ASTNode::Me {
span: Span::unknown(),
}),
method: "trim".to_string(),
arguments: vec![],
span: Span::unknown(),
};
let result = lower_condition_to_joinir(
&ast,
&mut alloc_value,
&env,
None,
Some("StringUtils"),
);
assert!(result.is_err(), "Disallowed method should fail");
let err = result.unwrap_err();
assert!(
err.contains("not allowed") || err.contains("not whitelisted"),
"Error should indicate method is not allowed: {}",
err
);
}

View File

@ -0,0 +1,191 @@
use crate::ast::{ASTNode, BinaryOperator, LiteralValue};
use crate::mir::join_ir::{BinOpKind, ConstValue, JoinInst, MirLikeInst};
use crate::mir::ValueId;
use super::super::condition_env::ConditionEnv;
use super::super::loop_body_local_env::LoopBodyLocalEnv; // Phase 92 P2-2: Body-local support
use super::super::method_call_lowerer::MethodCallLowerer;
/// Lower a literal value (e.g., `10`, `true`, `"text"`)
pub(super) fn lower_literal(
value: &LiteralValue,
alloc_value: &mut dyn FnMut() -> ValueId,
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
let dst = alloc_value();
let const_value = match value {
LiteralValue::Integer(n) => ConstValue::Integer(*n),
LiteralValue::String(s) => ConstValue::String(s.clone()),
LiteralValue::Bool(b) => ConstValue::Bool(*b),
LiteralValue::Float(_) => {
return Err("Float literals not supported in JoinIR conditions yet".to_string());
}
_ => {
return Err(format!(
"Unsupported literal type in condition: {:?}",
value
));
}
};
instructions.push(JoinInst::Compute(MirLikeInst::Const {
dst,
value: const_value,
}));
Ok(dst)
}
/// Lower a value expression (for comparison operands, etc.)
///
/// This handles the common case where we need to evaluate a simple value
/// (variable or literal) as part of a comparison.
///
/// # Phase 92 P2-2
///
/// Added `body_local_env` parameter to support body-local variable resolution
/// (e.g., `ch` in `ch == '\\'`).
///
/// # Phase 252
///
/// Added `current_static_box_name` parameter to support `this.method(...)` calls
/// in argument expressions.
pub fn lower_value_expression(
expr: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
match expr {
// Phase 92 P2-2: Variables - resolve from ConditionEnv or LoopBodyLocalEnv
ASTNode::Variable { name, .. } => {
// Priority 1: ConditionEnv (loop parameters, captured variables)
if let Some(value_id) = env.get(name) {
return Ok(value_id);
}
// Priority 2: LoopBodyLocalEnv (body-local variables like `ch`)
if let Some(body_env) = body_local_env {
if let Some(value_id) = body_env.get(name) {
return Ok(value_id);
}
}
Err(format!(
"Variable '{}' not found in ConditionEnv or LoopBodyLocalEnv",
name
))
}
// Literals - emit as constants
ASTNode::Literal { value, .. } => lower_literal(value, alloc_value, instructions),
// Binary operations (for arithmetic in conditions like i + 1 < n)
ASTNode::BinaryOp {
operator,
left,
right,
..
} => lower_arithmetic_binop(
operator,
left,
right,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
),
// Phase 224-C: MethodCall support with arguments (e.g., s.length(), s.indexOf(ch))
ASTNode::MethodCall {
object,
method,
arguments,
..
} => {
// 1. Lower receiver (object) to ValueId
let recv_val = lower_value_expression(
object,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
)?;
// 2. Lower method call using MethodCallLowerer
// Phase 256.7: Use lower_for_init (more permissive whitelist) for value expressions
// Value expressions like s.substring(i, i+1) should be allowed even in condition arguments
let empty_body_local = LoopBodyLocalEnv::new();
let body_env = body_local_env.unwrap_or(&empty_body_local);
MethodCallLowerer::lower_for_init(
recv_val,
method,
arguments,
alloc_value,
env,
body_env,
instructions,
)
}
_ => Err(format!(
"Unsupported expression in value context: {:?}",
expr
)),
}
}
/// Lower an arithmetic binary operation (e.g., `i + 1`)
fn lower_arithmetic_binop(
operator: &BinaryOperator,
left: &ASTNode,
right: &ASTNode,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &ConditionEnv,
body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2
current_static_box_name: Option<&str>, // Phase 252
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
let lhs = lower_value_expression(
left,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
)?;
let rhs = lower_value_expression(
right,
alloc_value,
env,
body_local_env,
current_static_box_name,
instructions,
)?;
let dst = alloc_value();
let bin_op = match operator {
BinaryOperator::Add => BinOpKind::Add,
BinaryOperator::Subtract => BinOpKind::Sub,
BinaryOperator::Multiply => BinOpKind::Mul,
BinaryOperator::Divide => BinOpKind::Div,
BinaryOperator::Modulo => BinOpKind::Mod,
_ => {
return Err(format!(
"Unsupported binary operator in expression: {:?}",
operator
));
}
};
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
dst,
op: bin_op,
lhs,
rhs,
}));
Ok(dst)
}