refactor(modules): split kernel exports and join_ir lowerers
This commit is contained in:
199
crates/nyash_kernel/src/entry.rs
Normal file
199
crates/nyash_kernel/src/entry.rs
Normal 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
|
||||
}
|
||||
}
|
||||
167
crates/nyash_kernel/src/exports/any.rs
Normal file
167
crates/nyash_kernel/src/exports/any.rs
Normal 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
|
||||
}
|
||||
60
crates/nyash_kernel/src/exports/birth.rs
Normal file
60
crates/nyash_kernel/src/exports/birth.rs
Normal 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
|
||||
}
|
||||
38
crates/nyash_kernel/src/exports/box_helpers.rs
Normal file
38
crates/nyash_kernel/src/exports/box_helpers.rs
Normal 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
|
||||
}
|
||||
20
crates/nyash_kernel/src/exports/cmp.rs
Normal file
20
crates/nyash_kernel/src/exports/cmp.rs
Normal 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
|
||||
}
|
||||
150
crates/nyash_kernel/src/exports/env.rs
Normal file
150
crates/nyash_kernel/src/exports/env.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
32
crates/nyash_kernel/src/exports/instance.rs
Normal file
32
crates/nyash_kernel/src/exports/instance.rs
Normal 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
|
||||
}
|
||||
25
crates/nyash_kernel/src/exports/mod.rs
Normal file
25
crates/nyash_kernel/src/exports/mod.rs
Normal 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::*;
|
||||
67
crates/nyash_kernel/src/exports/primitive.rs
Normal file
67
crates/nyash_kernel/src/exports/primitive.rs
Normal 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
|
||||
}
|
||||
40
crates/nyash_kernel/src/exports/runtime.rs
Normal file
40
crates/nyash_kernel/src/exports/runtime.rs
Normal 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();
|
||||
}
|
||||
236
crates/nyash_kernel/src/exports/string.rs
Normal file
236
crates/nyash_kernel/src/exports/string.rs
Normal 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
|
||||
}
|
||||
66
crates/nyash_kernel/src/exports/user_box.rs
Normal file
66
crates/nyash_kernel/src/exports/user_box.rs
Normal 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
87
crates/nyash_kernel/src/tests.rs
Normal file
87
crates/nyash_kernel/src/tests.rs
Normal 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");
|
||||
}
|
||||
50
crates/nyash_kernel/src/user_box_registry.rs
Normal file
50
crates/nyash_kernel/src/user_box_registry.rs
Normal 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
38
src/boxes/file/handle_box/core.rs
Normal file
38
src/boxes/file/handle_box/core.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
156
src/boxes/file/handle_box/io.rs
Normal file
156
src/boxes/file/handle_box/io.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
65
src/boxes/file/handle_box/metadata.rs
Normal file
65
src/boxes/file/handle_box/metadata.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
122
src/boxes/file/handle_box/mod.rs
Normal file
122
src/boxes/file/handle_box/mod.rs
Normal 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);
|
||||
}
|
||||
630
src/boxes/file/handle_box/tests.rs
Normal file
630
src/boxes/file/handle_box/tests.rs
Normal 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);
|
||||
}
|
||||
78
src/boxes/file/handle_box/traits.rs
Normal file
78
src/boxes/file/handle_box/traits.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
97
src/mir/join_ir/lowering/condition_lowerer/api.rs
Normal file
97
src/mir/join_ir/lowering/condition_lowerer/api.rs
Normal 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)
|
||||
}
|
||||
344
src/mir/join_ir/lowering/condition_lowerer/condition_ops.rs
Normal file
344
src/mir/join_ir/lowering/condition_lowerer/condition_ops.rs
Normal 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)
|
||||
}
|
||||
22
src/mir/join_ir/lowering/condition_lowerer/mod.rs
Normal file
22
src/mir/join_ir/lowering/condition_lowerer/mod.rs
Normal 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;
|
||||
465
src/mir/join_ir/lowering/condition_lowerer/tests.rs
Normal file
465
src/mir/join_ir/lowering/condition_lowerer/tests.rs
Normal 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
|
||||
);
|
||||
}
|
||||
191
src/mir/join_ir/lowering/condition_lowerer/value_expr.rs
Normal file
191
src/mir/join_ir/lowering/condition_lowerer/value_expr.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user