Phase 11-12: LLVM backend initial, semantics layer, plugin unification

Major changes:
- LLVM backend initial implementation (compiler.rs, llvm mode)
- Semantics layer integration in interpreter (operators.rs)
- Phase 12 plugin architecture revision (3-layer system)
- Builtin box removal preparation
- MIR instruction set documentation (26→Core-15 migration)
- Cross-backend testing infrastructure
- Await/nowait syntax support

New features:
- LLVM AOT compilation support (--backend llvm)
- Semantics layer for interpreter→VM flow
- Tri-backend smoke tests
- Plugin-only registry mode

Bug fixes:
- Interpreter plugin box arithmetic operations
- Branch test returns incorrect values

Documentation:
- Phase 12 README.md updated with new plugin architecture
- Removed obsolete NYIR proposals
- Added LLVM test programs documentation

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Moe Charm
2025-09-01 23:44:34 +09:00
parent fff9749f47
commit 11506cee3b
196 changed files with 10955 additions and 380 deletions

View File

@ -189,6 +189,9 @@ impl LLVMCompiler {
mir_module: &MirModule,
output_path: &str,
) -> Result<(), String> {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
eprintln!("[LLVM] compile_module start: functions={}, out={}", mir_module.functions.len(), output_path);
}
let context = Context::create();
let codegen = CodegenContext::new(&context, "nyash_module")?;
// Lower only Main.main for now
@ -257,9 +260,45 @@ impl LLVMCompiler {
// SSA value map
let mut vmap: StdHashMap<ValueId, BasicValueEnum> = StdHashMap::new();
// Helper ops
// Helper ops (centralized conversions and comparisons)
fn as_int<'ctx>(v: BasicValueEnum<'ctx>) -> Option<IntValue<'ctx>> { if let BasicValueEnum::IntValue(iv) = v { Some(iv) } else { None } }
fn as_float<'ctx>(v: BasicValueEnum<'ctx>) -> Option<FloatValue<'ctx>> { if let BasicValueEnum::FloatValue(fv) = v { Some(fv) } else { None } }
fn to_i64_any<'ctx>(ctx: &'ctx Context, builder: &inkwell::builder::Builder<'ctx>, v: BasicValueEnum<'ctx>) -> Result<IntValue<'ctx>, String> {
let i64t = ctx.i64_type();
Ok(match v {
BasicValueEnum::IntValue(iv) => {
if iv.get_type().get_bit_width() == 64 { iv }
else if iv.get_type().get_bit_width() < 64 { builder.build_int_z_extend(iv, i64t, "zext_i64").map_err(|e| e.to_string())? }
else { builder.build_int_truncate(iv, i64t, "trunc_i64").map_err(|e| e.to_string())? }
}
BasicValueEnum::PointerValue(pv) => builder.build_ptr_to_int(pv, i64t, "p2i64").map_err(|e| e.to_string())?,
BasicValueEnum::FloatValue(fv) => {
// Bitcast f64 -> i64 via stack slot
let slot = builder.get_insert_block().and_then(|bb| bb.get_parent()).and_then(|f| f.get_first_basic_block()).map(|entry| {
let eb = ctx.create_builder(); eb.position_at_end(entry); eb
}).unwrap_or_else(|| ctx.create_builder());
let i64p = i64t.ptr_type(AddressSpace::from(0));
let tmp = slot.build_alloca(i64t, "f2i_tmp").map_err(|e| e.to_string())?;
let fptr_ty = ctx.f64_type().ptr_type(AddressSpace::from(0));
let castp = builder.build_pointer_cast(tmp, fptr_ty, "i64p_to_f64p").map_err(|e| e.to_string())?;
builder.build_store(castp, fv).map_err(|e| e.to_string())?;
builder.build_load(i64t, tmp, "ld_f2i").map_err(|e| e.to_string())?.into_int_value()
}
_ => return Err("unsupported value for i64 conversion".to_string()),
})
}
fn i64_to_ptr<'ctx>(ctx: &'ctx Context, builder: &inkwell::builder::Builder<'ctx>, iv: IntValue<'ctx>) -> Result<PointerValue<'ctx>, String> {
let pty = ctx.i8_type().ptr_type(AddressSpace::from(0));
builder.build_int_to_ptr(iv, pty, "i64_to_ptr").map_err(|e| e.to_string())
}
fn classify_tag<'ctx>(v: BasicValueEnum<'ctx>) -> i64 {
match v {
BasicValueEnum::FloatValue(_) => 5, // float
BasicValueEnum::PointerValue(_) => 8, // handle/ptr
BasicValueEnum::IntValue(_) => 3, // integer/bool
_ => 3,
}
}
fn to_bool<'ctx>(ctx: &'ctx Context, b: BasicValueEnum<'ctx>, builder: &inkwell::builder::Builder<'ctx>) -> Result<IntValue<'ctx>, String> {
if let Some(bb) = as_int(b) {
// If not i1, compare != 0
@ -276,6 +315,26 @@ impl LLVMCompiler {
Err("Unsupported value for boolean conversion".to_string())
}
}
fn cmp_eq_ne_any<'ctx>(ctx: &'ctx Context, builder: &inkwell::builder::Builder<'ctx>, op: &crate::mir::CompareOp, lv: BasicValueEnum<'ctx>, rv: BasicValueEnum<'ctx>) -> Result<BasicValueEnum<'ctx>, String> {
use crate::mir::CompareOp as C;
match (lv, rv) {
(BasicValueEnum::IntValue(li), BasicValueEnum::IntValue(ri)) => {
let pred = if matches!(op, C::Eq) { inkwell::IntPredicate::EQ } else { inkwell::IntPredicate::NE };
Ok(builder.build_int_compare(pred, li, ri, "icmp").map_err(|e| e.to_string())?.into())
}
(BasicValueEnum::FloatValue(lf), BasicValueEnum::FloatValue(rf)) => {
let pred = if matches!(op, C::Eq) { inkwell::FloatPredicate::OEQ } else { inkwell::FloatPredicate::ONE };
Ok(builder.build_float_compare(pred, lf, rf, "fcmp").map_err(|e| e.to_string())?.into())
}
(BasicValueEnum::PointerValue(_), _) | (_, BasicValueEnum::PointerValue(_)) => {
let li = to_i64_any(ctx, builder, lv)?;
let ri = to_i64_any(ctx, builder, rv)?;
let pred = if matches!(op, C::Eq) { inkwell::IntPredicate::EQ } else { inkwell::IntPredicate::NE };
Ok(builder.build_int_compare(pred, li, ri, "pcmp_any").map_err(|e| e.to_string())?.into())
}
_ => Err("compare type mismatch".to_string()),
}
}
// Pre-create allocas for locals on demand (entry-only builder)
let mut allocas: StdHashMap<ValueId, PointerValue> = StdHashMap::new();
@ -422,7 +481,16 @@ impl LLVMCompiler {
let i64t = codegen.context.i64_type();
// Receiver handle (i64)
let recv_v = *vmap.get(box_val).ok_or("box receiver missing")?;
let recv_p = if let BasicValueEnum::PointerValue(pv) = recv_v { pv } else { return Err("box receiver must be pointer".to_string()); };
// Accept either an opaque pointer (i8*) or an i64 handle for the receiver
let recv_p = match recv_v {
BasicValueEnum::PointerValue(pv) => pv,
BasicValueEnum::IntValue(iv) => {
// Treat as Nyash handle and convert to opaque pointer
let pty = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
codegen.builder.build_int_to_ptr(iv, pty, "recv_i2p").map_err(|e| e.to_string())?
}
_ => return Err("box receiver must be pointer or i64 handle".to_string()),
};
let recv_h = codegen.builder.build_ptr_to_int(recv_p, i64t, "recv_p2i").map_err(|e| e.to_string())?;
// Resolve type_id from metadata (Box("Type")) via nyash.toml
let type_id: i64 = if let Some(crate::mir::MirType::Box(bname)) = func.metadata.value_types.get(box_val) {
@ -431,9 +499,9 @@ impl LLVMCompiler {
*box_type_ids.get("StringBox").unwrap_or(&0)
} else { 0 };
// Special-case ArrayBox get/set until general by-id is widely annotated
// Special-case ArrayBox get/set/push/length until general by-id is widely annotated
if let Some(crate::mir::MirType::Box(bname)) = func.metadata.value_types.get(box_val) {
if bname == "ArrayBox" && (method == "get" || method == "set") {
if bname == "ArrayBox" && (method == "get" || method == "set" || method == "push" || method == "length") {
match method.as_str() {
"get" => {
if args.len() != 1 { return Err("ArrayBox.get expects 1 arg".to_string()); }
@ -443,7 +511,6 @@ impl LLVMCompiler {
let callee = codegen.module.get_function("nyash_array_get_h").unwrap_or_else(|| codegen.module.add_function("nyash_array_get_h", fnty, None));
let call = codegen.builder.build_call(callee, &[recv_h.into(), idx_i.into()], "aget").map_err(|e| e.to_string())?;
if let Some(d) = dst { let rv = call.try_as_basic_value().left().ok_or("array_get_h returned void".to_string())?; vmap.insert(*d, rv); }
return Ok(());
}
"set" => {
if args.len() != 2 { return Err("ArrayBox.set expects 2 arg".to_string()); }
@ -454,72 +521,275 @@ impl LLVMCompiler {
let fnty = i64t.fn_type(&[i64t.into(), i64t.into(), i64t.into()], false);
let callee = codegen.module.get_function("nyash_array_set_h").unwrap_or_else(|| codegen.module.add_function("nyash_array_set_h", fnty, None));
let _ = codegen.builder.build_call(callee, &[recv_h.into(), idx_i.into(), val_i.into()], "aset").map_err(|e| e.to_string())?;
return Ok(());
}
"push" => {
if args.len() != 1 { return Err("ArrayBox.push expects 1 arg".to_string()); }
let val_v = *vmap.get(&args[0]).ok_or("array.push value missing")?;
let val_i = match val_v {
BasicValueEnum::IntValue(iv) => iv,
BasicValueEnum::PointerValue(pv) => codegen.builder.build_ptr_to_int(pv, i64t, "val_p2i").map_err(|e| e.to_string())?,
_ => return Err("array.push value must be int or handle ptr".to_string()),
};
let fnty = i64t.fn_type(&[i64t.into(), i64t.into()], false);
let callee = codegen.module.get_function("nyash_array_push_h").unwrap_or_else(|| codegen.module.add_function("nyash_array_push_h", fnty, None));
let _ = codegen.builder.build_call(callee, &[recv_h.into(), val_i.into()], "apush").map_err(|e| e.to_string())?;
}
"length" => {
if !args.is_empty() { return Err("ArrayBox.length expects 0 arg".to_string()); }
let fnty = i64t.fn_type(&[i64t.into()], false);
let callee = codegen.module.get_function("nyash_array_length_h").unwrap_or_else(|| codegen.module.add_function("nyash_array_length_h", fnty, None));
let call = codegen.builder.build_call(callee, &[recv_h.into()], "alen").map_err(|e| e.to_string())?;
if let Some(d) = dst { let rv = call.try_as_basic_value().left().ok_or("array_length_h returned void".to_string())?; vmap.insert(*d, rv); }
}
_ => {}
}
}
}
// Instance field helpers: getField/setField (safe path)
if method == "getField" {
if args.len() != 1 { return Err("getField expects 1 arg (name)".to_string()); }
let name_v = *vmap.get(&args[0]).ok_or("getField name missing")?;
let name_p = if let BasicValueEnum::PointerValue(pv) = name_v { pv } else { return Err("getField name must be pointer".to_string()); };
let i8p = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
let fnty = i64t.fn_type(&[i64t.into(), i8p.into()], false);
let callee = codegen.module.get_function("nyash.instance.get_field_h").unwrap_or_else(|| codegen.module.add_function("nyash.instance.get_field_h", fnty, None));
let call = codegen.builder.build_call(callee, &[recv_h.into(), name_p.into()], "getField").map_err(|e| e.to_string())?;
if let Some(d) = dst {
let rv = call.try_as_basic_value().left().ok_or("get_field returned void".to_string())?;
// rv is i64 handle; convert to i8*
let h = if let BasicValueEnum::IntValue(iv) = rv { iv } else { return Err("get_field ret expected i64".to_string()); };
let pty = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
let ptr = codegen.builder.build_int_to_ptr(h, pty, "gf_handle_to_ptr").map_err(|e| e.to_string())?;
vmap.insert(*d, ptr.into());
}
// no early return; continue lowering
}
if method == "setField" {
if args.len() != 2 { return Err("setField expects 2 args (name, value)".to_string()); }
let name_v = *vmap.get(&args[0]).ok_or("setField name missing")?;
let val_v = *vmap.get(&args[1]).ok_or("setField value missing")?;
let name_p = if let BasicValueEnum::PointerValue(pv) = name_v { pv } else { return Err("setField name must be pointer".to_string()); };
let val_h = match val_v {
BasicValueEnum::PointerValue(pv) => codegen.builder.build_ptr_to_int(pv, i64t, "val_p2i").map_err(|e| e.to_string())?,
BasicValueEnum::IntValue(iv) => iv,
_ => return Err("setField value must be handle/ptr or i64".to_string()),
};
let i8p = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
let fnty = i64t.fn_type(&[i64t.into(), i8p.into(), i64t.into()], false);
let callee = codegen.module.get_function("nyash.instance.set_field_h").unwrap_or_else(|| codegen.module.add_function("nyash.instance.set_field_h", fnty, None));
let _ = codegen.builder.build_call(callee, &[recv_h.into(), name_p.into(), val_h.into()], "setField").map_err(|e| e.to_string())?;
// no early return; continue lowering
}
// General by-id invoke when method_id is available
if let Some(mid) = method_id {
// Prepare up to 2 args for now (extend later)
// Prepare up to 4 args (i64 or f64 bits or handle)
let argc_val = i64t.const_int(args.len() as u64, false);
let mut a1 = i64t.const_zero();
let mut a2 = i64t.const_zero();
let mut a3 = i64t.const_zero();
let mut a4 = i64t.const_zero();
let mut get_i64 = |vid: ValueId| -> Result<inkwell::values::IntValue, String> {
let v = *vmap.get(&vid).ok_or("arg missing")?;
Ok(match v {
BasicValueEnum::IntValue(iv) => iv,
BasicValueEnum::PointerValue(pv) => codegen.builder.build_ptr_to_int(pv, i64t, "p2i").map_err(|e| e.to_string())?,
_ => return Err("unsupported arg value (expect int or handle ptr)".to_string()),
})
to_i64_any(codegen.context, &codegen.builder, v)
};
if args.len() >= 1 { a1 = get_i64(args[0])?; }
if args.len() >= 2 { a2 = get_i64(args[1])?; }
// declare i64 @nyash_plugin_invoke3_i64(i64 type_id, i64 method_id, i64 argc, i64 a0, i64 a1, i64 a2)
let fnty = i64t.fn_type(&[i64t.into(), i64t.into(), i64t.into(), i64t.into(), i64t.into(), i64t.into()], false);
let callee = codegen.module.get_function("nyash_plugin_invoke3_i64").unwrap_or_else(|| codegen.module.add_function("nyash_plugin_invoke3_i64", fnty, None));
let tid = i64t.const_int(type_id as u64, true);
let midv = i64t.const_int((*mid) as u64, false);
let call = codegen.builder.build_call(callee, &[tid.into(), midv.into(), argc_val.into(), recv_h.into(), a1.into(), a2.into()], "pinvoke").map_err(|e| e.to_string())?;
if let Some(d) = dst {
let rv = call.try_as_basic_value().left().ok_or("invoke3_i64 returned void".to_string())?;
// Decide return lowering by dst annotated type
if let Some(mt) = func.metadata.value_types.get(d) {
match mt {
crate::mir::MirType::Integer | crate::mir::MirType::Bool => { vmap.insert(*d, rv); }
crate::mir::MirType::Box(_) | crate::mir::MirType::String | crate::mir::MirType::Array(_) | crate::mir::MirType::Future(_) | crate::mir::MirType::Unknown => {
// rv is i64 handle; convert to i8*
let h = if let BasicValueEnum::IntValue(iv) = rv { iv } else { return Err("invoke ret expected i64".to_string()); };
let pty = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
let ptr = codegen.builder.build_int_to_ptr(h, pty, "ret_handle_to_ptr").map_err(|e| e.to_string())?;
vmap.insert(*d, ptr.into());
}
_ => { vmap.insert(*d, rv); }
}
} else {
if args.len() >= 3 { a3 = get_i64(args[2])?; }
if args.len() >= 4 { a4 = get_i64(args[3])?; }
// Choose return ABI by dst annotated type
let dst_ty = dst.as_ref().and_then(|d| func.metadata.value_types.get(d));
let use_f64_ret = matches!(dst_ty, Some(crate::mir::MirType::Float));
if use_f64_ret {
// declare double @nyash_plugin_invoke3_f64(i64,i64,i64,i64,i64,i64)
let fnty = codegen.context.f64_type().fn_type(&[i64t.into(), i64t.into(), i64t.into(), i64t.into(), i64t.into(), i64t.into()], false);
let callee = codegen.module.get_function("nyash_plugin_invoke3_f64").unwrap_or_else(|| codegen.module.add_function("nyash_plugin_invoke3_f64", fnty, None));
let tid = i64t.const_int(type_id as u64, true);
let midv = i64t.const_int((*mid) as u64, false);
let call = codegen.builder.build_call(callee, &[tid.into(), midv.into(), argc_val.into(), recv_h.into(), a1.into(), a2.into()], "pinvoke_f64").map_err(|e| e.to_string())?;
if let Some(d) = dst {
let rv = call.try_as_basic_value().left().ok_or("invoke3_f64 returned void".to_string())?;
vmap.insert(*d, rv);
}
return Ok(());
}
// For argument typing, use tagged variant to allow f64/handle
// Prepare tags for a1..a4: 5=float, 8=handle(ptr), 3=int
let mut tag1 = i64t.const_int(3, false);
let mut tag2 = i64t.const_int(3, false);
let mut tag3 = i64t.const_int(3, false);
let mut tag4 = i64t.const_int(3, false);
let classify = |vid: ValueId| -> Option<i64> { vmap.get(&vid).map(|v| classify_tag(*v)) };
if args.len() >= 1 { if let Some(t) = classify(args[0]) { tag1 = i64t.const_int(t as u64, false); } }
if args.len() >= 2 { if let Some(t) = classify(args[1]) { tag2 = i64t.const_int(t as u64, false); } }
if args.len() >= 3 { if let Some(t) = classify(args[2]) { tag3 = i64t.const_int(t as u64, false); } }
if args.len() >= 4 { if let Some(t) = classify(args[3]) { tag4 = i64t.const_int(t as u64, false); } }
if args.len() <= 4 {
// Call fixed-arity tagged shim (up to 4 args)
let fnty = i64t.fn_type(&[
i64t.into(), i64t.into(), i64t.into(), i64t.into(),
i64t.into(), i64t.into(), i64t.into(), i64t.into(),
i64t.into(), i64t.into(), i64t.into(), i64t.into()
], false);
let callee = codegen.module.get_function("nyash_plugin_invoke3_tagged_i64").unwrap_or_else(|| codegen.module.add_function("nyash_plugin_invoke3_tagged_i64", fnty, None));
let tid = i64t.const_int(type_id as u64, true);
let midv = i64t.const_int((*mid) as u64, false);
let call = codegen.builder.build_call(
callee,
&[tid.into(), midv.into(), argc_val.into(), recv_h.into(),
a1.into(), tag1.into(), a2.into(), tag2.into(),
a3.into(), tag3.into(), a4.into(), tag4.into()],
"pinvoke_tagged"
).map_err(|e| e.to_string())?;
if let Some(d) = dst {
let rv = call.try_as_basic_value().left().ok_or("invoke3_i64 returned void".to_string())?;
// Decide return lowering by dst annotated type
if let Some(mt) = func.metadata.value_types.get(d) {
match mt {
crate::mir::MirType::Integer | crate::mir::MirType::Bool => { vmap.insert(*d, rv); }
crate::mir::MirType::Box(_) | crate::mir::MirType::String | crate::mir::MirType::Array(_) | crate::mir::MirType::Future(_) | crate::mir::MirType::Unknown => {
let h = if let BasicValueEnum::IntValue(iv) = rv { iv } else { return Err("invoke ret expected i64".to_string()); };
let pty = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
let ptr = codegen.builder.build_int_to_ptr(h, pty, "ret_handle_to_ptr").map_err(|e| e.to_string())?;
vmap.insert(*d, ptr.into());
}
_ => { vmap.insert(*d, rv); }
}
} else {
vmap.insert(*d, rv);
}
}
} else {
// Variable-length path: build arrays of values/tags and call vector shim
let n = args.len() as u32;
// alloca [N x i64] for vals and tags
let arr_ty = i64t.array_type(n);
let vals_arr = entry_builder.build_alloca(arr_ty, "vals_arr").map_err(|e| e.to_string())?;
let tags_arr = entry_builder.build_alloca(arr_ty, "tags_arr").map_err(|e| e.to_string())?;
for (i, vid) in args.iter().enumerate() {
let idx = [codegen.context.i32_type().const_zero(), codegen.context.i32_type().const_int(i as u64, false)];
let gep_v = unsafe { codegen.builder.build_in_bounds_gep(arr_ty, vals_arr, &idx, &format!("v_gep_{}", i)).map_err(|e| e.to_string())? };
let gep_t = unsafe { codegen.builder.build_in_bounds_gep(arr_ty, tags_arr, &idx, &format!("t_gep_{}", i)).map_err(|e| e.to_string())? };
let vi = get_i64(*vid)?;
let tag = classify(*vid).unwrap_or(3);
let tagv = i64t.const_int(tag as u64, false);
codegen.builder.build_store(gep_v, vi).map_err(|e| e.to_string())?;
codegen.builder.build_store(gep_t, tagv).map_err(|e| e.to_string())?;
}
// cast to i64* pointers
let i64p = i64t.ptr_type(AddressSpace::from(0));
let vals_ptr = codegen.builder.build_pointer_cast(vals_arr, i64p, "vals_ptr").map_err(|e| e.to_string())?;
let tags_ptr = codegen.builder.build_pointer_cast(tags_arr, i64p, "tags_ptr").map_err(|e| e.to_string())?;
// declare i64 @nyash.plugin.invoke_tagged_v_i64(i64,i64,i64,i64,i64*,i64*)
let fnty = i64t.fn_type(&[i64t.into(), i64t.into(), i64t.into(), i64t.into(), i64p.into(), i64p.into()], false);
let callee = codegen.module.get_function("nyash.plugin.invoke_tagged_v_i64").unwrap_or_else(|| codegen.module.add_function("nyash.plugin.invoke_tagged_v_i64", fnty, None));
let tid = i64t.const_int(type_id as u64, true);
let midv = i64t.const_int((*mid) as u64, false);
let call = codegen.builder.build_call(callee, &[tid.into(), midv.into(), argc_val.into(), recv_h.into(), vals_ptr.into(), tags_ptr.into()], "pinvoke_tagged_v").map_err(|e| e.to_string())?;
if let Some(d) = dst {
let rv = call.try_as_basic_value().left().ok_or("invoke_v returned void".to_string())?;
if let Some(mt) = func.metadata.value_types.get(d) {
match mt {
crate::mir::MirType::Integer | crate::mir::MirType::Bool => { vmap.insert(*d, rv); }
crate::mir::MirType::Box(_) | crate::mir::MirType::String | crate::mir::MirType::Array(_) | crate::mir::MirType::Future(_) | crate::mir::MirType::Unknown => {
let h = if let BasicValueEnum::IntValue(iv) = rv { iv } else { return Err("invoke ret expected i64".to_string()); };
let pty = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
let ptr = codegen.builder.build_int_to_ptr(h, pty, "ret_handle_to_ptr").map_err(|e| e.to_string())?;
vmap.insert(*d, ptr.into());
}
_ => { vmap.insert(*d, rv); }
}
} else {
vmap.insert(*d, rv);
}
}
}
// handled above per-branch
} else {
return Err("BoxCall without method_id not supported in by-id path (enable by-name wrapper if needed)".to_string());
// Optional by-name fallback (debug): use NYASH_LLVM_ALLOW_BY_NAME=1
if std::env::var("NYASH_LLVM_ALLOW_BY_NAME").ok().as_deref() == Some("1") {
// Build global string for method name
let gsp = codegen.builder.build_global_string_ptr(method, "method_name").map_err(|e| e.to_string())?;
let mptr = gsp.as_pointer_value();
let argc_val = i64t.const_int(args.len() as u64, false);
let mut a1 = i64t.const_zero();
let mut a2 = i64t.const_zero();
let mut get_i64 = |vid: ValueId| -> Result<inkwell::values::IntValue, String> {
let v = *vmap.get(&vid).ok_or("arg missing")?;
Ok(match v {
BasicValueEnum::IntValue(iv) => iv,
BasicValueEnum::FloatValue(fv) => {
let slot = entry_builder.build_alloca(i64t, "f2i_slot").map_err(|e| e.to_string())?;
let fptr_ty = codegen.context.f64_type().ptr_type(AddressSpace::from(0));
let castp = codegen.builder.build_pointer_cast(slot, fptr_ty, "i64p_to_f64p").map_err(|e| e.to_string())?;
let _ = codegen.builder.build_store(castp, fv).map_err(|e| e.to_string())?;
codegen.builder.build_load(i64t, slot, "ld_f2i").map_err(|e| e.to_string())?.into_int_value()
},
BasicValueEnum::PointerValue(pv) => codegen.builder.build_ptr_to_int(pv, i64t, "p2i").map_err(|e| e.to_string())?,
_ => return Err("unsupported arg value (expect int or handle ptr)".to_string()),
})
};
if args.len() >= 1 { a1 = get_i64(args[0])?; }
if args.len() >= 2 { a2 = get_i64(args[1])?; }
// declare i64 @nyash.plugin.invoke_by_name_i64(i64 recv_h, i8* name, i64 argc, i64 a1, i64 a2)
let i8p = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
let fnty = i64t.fn_type(&[i64t.into(), i8p.into(), i64t.into(), i64t.into(), i64t.into()], false);
let callee = codegen.module.get_function("nyash.plugin.invoke_by_name_i64").unwrap_or_else(|| codegen.module.add_function("nyash.plugin.invoke_by_name_i64", fnty, None));
let call = codegen.builder.build_call(callee, &[recv_h.into(), mptr.into(), argc_val.into(), a1.into(), a2.into()], "pinvoke_byname").map_err(|e| e.to_string())?;
if let Some(d) = dst {
let rv = call.try_as_basic_value().left().ok_or("invoke_by_name returned void".to_string())?;
// Treat like i64 path
if let Some(mt) = func.metadata.value_types.get(d) {
match mt {
crate::mir::MirType::Integer | crate::mir::MirType::Bool => { vmap.insert(*d, rv); }
crate::mir::MirType::Box(_) | crate::mir::MirType::String | crate::mir::MirType::Array(_) | crate::mir::MirType::Future(_) | crate::mir::MirType::Unknown => {
let h = if let BasicValueEnum::IntValue(iv) = rv { iv } else { return Err("invoke ret expected i64".to_string()); };
let pty = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
let ptr = codegen.builder.build_int_to_ptr(h, pty, "ret_handle_to_ptr").map_err(|e| e.to_string())?;
vmap.insert(*d, ptr.into());
}
_ => { vmap.insert(*d, rv); }
}
} else { vmap.insert(*d, rv); }
}
} else {
return Err(format!("BoxCall requires method_id (by-id). Enable NYASH_LLVM_ALLOW_BY_NAME=1 to use by-name fallback for method '{}'", method));
}
}
}
MirInstruction::ExternCall { dst, iface_name, method_name, args, effects: _ } => {
// Minimal: map console.log / debug.trace to libc puts
if (iface_name == "env.console" && method_name == "log") || (iface_name == "env.debug" && method_name == "trace") {
if args.len() != 1 { return Err("console.log/debug.trace expect 1 arg (string)".to_string()); }
// Route console.log/warn/error/readLine and debug.trace to NyRT shims
if (iface_name == "env.console" && (method_name == "log" || method_name == "warn" || method_name == "error"))
|| (iface_name == "env.debug" && method_name == "trace") {
if args.len() != 1 { return Err(format!("{}.{} expects 1 arg (string)", iface_name, method_name)); }
let av = *vmap.get(&args[0]).ok_or("extern arg missing")?;
let sp = if let BasicValueEnum::PointerValue(pv) = av { pv } else { return Err("extern arg must be string pointer".to_string()); };
// declare i32 @puts(i8*)
let i8p = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
let puts_ty = codegen.context.i32_type().fn_type(&[i8p.into()], false);
let puts = codegen.module.get_function("puts").unwrap_or_else(|| codegen.module.add_function("puts", puts_ty, None));
let _ = codegen.builder.build_call(puts, &[sp.into()], "puts").map_err(|e| e.to_string())?;
let sp = match av {
BasicValueEnum::PointerValue(pv) => pv,
BasicValueEnum::IntValue(iv) => codegen.builder.build_int_to_ptr(iv, i8p, "arg_i2p").map_err(|e| e.to_string())?,
_ => return Err("extern arg must be string pointer or i64 handle".to_string()),
};
let i8p = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
let fnty = codegen.context.i64_type().fn_type(&[i8p.into()], false);
let fname = if iface_name == "env.console" {
match method_name.as_str() { "log" => "nyash.console.log", "warn" => "nyash.console.warn", _ => "nyash.console.error" }
} else { "nyash.debug.trace" };
let callee = codegen.module.get_function(fname).unwrap_or_else(|| codegen.module.add_function(fname, fnty, None));
let _ = codegen.builder.build_call(callee, &[sp.into()], "extern_rt").map_err(|e| e.to_string())?;
if let Some(d) = dst { vmap.insert(*d, codegen.context.i64_type().const_zero().into()); }
} else if iface_name == "env.console" && method_name == "readLine" {
if !args.is_empty() { return Err("console.readLine expects 0 args".to_string()); }
let i8p = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
let fnty = i8p.fn_type(&[], false);
let callee = codegen.module.get_function("nyash.console.readline").unwrap_or_else(|| codegen.module.add_function("nyash.console.readline", fnty, None));
let call = codegen.builder.build_call(callee, &[], "readline").map_err(|e| e.to_string())?;
if let Some(d) = dst {
let rv = call.try_as_basic_value().left().ok_or("readline returned void".to_string())?;
vmap.insert(*d, rv);
}
} else {
return Err("ExternCall lowering supports only env.console.log/env.debug.trace in 11.2 minimal".to_string());
return Err(format!("ExternCall lowering unsupported: {}.{} (enable NYASH_LLVM_ALLOW_BY_NAME=1 to try by-name, or add a NyRT shim)", iface_name, method_name));
}
}
MirInstruction::UnaryOp { dst, op, operand } => {
@ -540,43 +810,82 @@ impl LLVMCompiler {
MirInstruction::BinOp { dst, op, lhs, rhs } => {
let lv = *vmap.get(lhs).ok_or("lhs missing")?;
let rv = *vmap.get(rhs).ok_or("rhs missing")?;
let out = if let (Some(li), Some(ri)) = (as_int(lv), as_int(rv)) {
use crate::mir::BinaryOp as B;
match op {
B::Add => codegen.builder.build_int_add(li, ri, "iadd").map_err(|e| e.to_string())?.into(),
B::Sub => codegen.builder.build_int_sub(li, ri, "isub").map_err(|e| e.to_string())?.into(),
B::Mul => codegen.builder.build_int_mul(li, ri, "imul").map_err(|e| e.to_string())?.into(),
B::Div => codegen.builder.build_int_signed_div(li, ri, "idiv").map_err(|e| e.to_string())?.into(),
B::Mod => codegen.builder.build_int_signed_rem(li, ri, "imod").map_err(|e| e.to_string())?.into(),
B::BitAnd => codegen.builder.build_and(li, ri, "iand").map_err(|e| e.to_string())?.into(),
B::BitOr => codegen.builder.build_or(li, ri, "ior").map_err(|e| e.to_string())?.into(),
B::BitXor => codegen.builder.build_xor(li, ri, "ixor").map_err(|e| e.to_string())?.into(),
B::Shl => codegen.builder.build_left_shift(li, ri, "ishl").map_err(|e| e.to_string())?.into(),
B::Shr => codegen.builder.build_right_shift(li, ri, false, "ishr").map_err(|e| e.to_string())?.into(),
B::And | B::Or => {
// Treat as logical on integers: convert to i1 and and/or
let lb = to_bool(codegen.context, li.into(), &codegen.builder)?;
let rb = to_bool(codegen.context, ri.into(), &codegen.builder)?;
match op {
B::And => codegen.builder.build_and(lb, rb, "land").map_err(|e| e.to_string())?.into(),
_ => codegen.builder.build_or(lb, rb, "lor").map_err(|e| e.to_string())?.into(),
let mut handled_concat = false;
// String-like concat handling: if either side is a pointer (i8*),
// and op is Add, route to NyRT concat helpers
if let crate::mir::BinaryOp::Add = op {
let i8p = codegen.context.i8_type().ptr_type(AddressSpace::from(0));
match (lv, rv) {
(BasicValueEnum::PointerValue(lp), BasicValueEnum::PointerValue(rp)) => {
let fnty = i8p.fn_type(&[i8p.into(), i8p.into()], false);
let callee = codegen.module.get_function("nyash.string.concat_ss").unwrap_or_else(|| codegen.module.add_function("nyash.string.concat_ss", fnty, None));
let call = codegen.builder.build_call(callee, &[lp.into(), rp.into()], "concat_ss").map_err(|e| e.to_string())?;
let rv = call.try_as_basic_value().left().ok_or("concat_ss returned void".to_string())?;
vmap.insert(*dst, rv);
handled_concat = true;
}
(BasicValueEnum::PointerValue(lp), BasicValueEnum::IntValue(ri)) => {
let i64t = codegen.context.i64_type();
let fnty = i8p.fn_type(&[i8p.into(), i64t.into()], false);
let callee = codegen.module.get_function("nyash.string.concat_si").unwrap_or_else(|| codegen.module.add_function("nyash.string.concat_si", fnty, None));
let call = codegen.builder.build_call(callee, &[lp.into(), ri.into()], "concat_si").map_err(|e| e.to_string())?;
let rv = call.try_as_basic_value().left().ok_or("concat_si returned void".to_string())?;
vmap.insert(*dst, rv);
handled_concat = true;
}
(BasicValueEnum::IntValue(li), BasicValueEnum::PointerValue(rp)) => {
let i64t = codegen.context.i64_type();
let fnty = i8p.fn_type(&[i64t.into(), i8p.into()], false);
let callee = codegen.module.get_function("nyash.string.concat_is").unwrap_or_else(|| codegen.module.add_function("nyash.string.concat_is", fnty, None));
let call = codegen.builder.build_call(callee, &[li.into(), rp.into()], "concat_is").map_err(|e| e.to_string())?;
let rv = call.try_as_basic_value().left().ok_or("concat_is returned void".to_string())?;
vmap.insert(*dst, rv);
handled_concat = true;
}
_ => {}
}
}
if handled_concat {
// Concat already lowered and dst set
} else {
let out = if let (Some(li), Some(ri)) = (as_int(lv), as_int(rv)) {
use crate::mir::BinaryOp as B;
match op {
B::Add => codegen.builder.build_int_add(li, ri, "iadd").map_err(|e| e.to_string())?.into(),
B::Sub => codegen.builder.build_int_sub(li, ri, "isub").map_err(|e| e.to_string())?.into(),
B::Mul => codegen.builder.build_int_mul(li, ri, "imul").map_err(|e| e.to_string())?.into(),
B::Div => codegen.builder.build_int_signed_div(li, ri, "idiv").map_err(|e| e.to_string())?.into(),
B::Mod => codegen.builder.build_int_signed_rem(li, ri, "imod").map_err(|e| e.to_string())?.into(),
B::BitAnd => codegen.builder.build_and(li, ri, "iand").map_err(|e| e.to_string())?.into(),
B::BitOr => codegen.builder.build_or(li, ri, "ior").map_err(|e| e.to_string())?.into(),
B::BitXor => codegen.builder.build_xor(li, ri, "ixor").map_err(|e| e.to_string())?.into(),
B::Shl => codegen.builder.build_left_shift(li, ri, "ishl").map_err(|e| e.to_string())?.into(),
B::Shr => codegen.builder.build_right_shift(li, ri, false, "ishr").map_err(|e| e.to_string())?.into(),
B::And | B::Or => {
// Treat as logical on integers: convert to i1 and and/or
let lb = to_bool(codegen.context, li.into(), &codegen.builder)?;
let rb = to_bool(codegen.context, ri.into(), &codegen.builder)?;
match op {
B::And => codegen.builder.build_and(lb, rb, "land").map_err(|e| e.to_string())?.into(),
_ => codegen.builder.build_or(lb, rb, "lor").map_err(|e| e.to_string())?.into(),
}
}
}
}
} else if let (Some(lf), Some(rf)) = (as_float(lv), as_float(rv)) {
use crate::mir::BinaryOp as B;
match op {
B::Add => codegen.builder.build_float_add(lf, rf, "fadd").map_err(|e| e.to_string())?.into(),
B::Sub => codegen.builder.build_float_sub(lf, rf, "fsub").map_err(|e| e.to_string())?.into(),
B::Mul => codegen.builder.build_float_mul(lf, rf, "fmul").map_err(|e| e.to_string())?.into(),
B::Div => codegen.builder.build_float_div(lf, rf, "fdiv").map_err(|e| e.to_string())?.into(),
B::Mod => return Err("fmod not supported yet".to_string()),
_ => return Err("bit/logic ops on float".to_string()),
}
} else {
return Err("binop type mismatch".to_string());
};
vmap.insert(*dst, out);
} else if let (Some(lf), Some(rf)) = (as_float(lv), as_float(rv)) {
use crate::mir::BinaryOp as B;
match op {
B::Add => codegen.builder.build_float_add(lf, rf, "fadd").map_err(|e| e.to_string())?.into(),
B::Sub => codegen.builder.build_float_sub(lf, rf, "fsub").map_err(|e| e.to_string())?.into(),
B::Mul => codegen.builder.build_float_mul(lf, rf, "fmul").map_err(|e| e.to_string())?.into(),
B::Div => codegen.builder.build_float_div(lf, rf, "fdiv").map_err(|e| e.to_string())?.into(),
B::Mod => return Err("fmod not supported yet".to_string()),
_ => return Err("bit/logic ops on float".to_string()),
}
} else {
return Err("binop type mismatch".to_string());
};
vmap.insert(*dst, out);
}
}
MirInstruction::Compare { dst, op, lhs, rhs } => {
let lv = *vmap.get(lhs).ok_or("lhs missing")?;
@ -589,7 +898,47 @@ impl LLVMCompiler {
use crate::mir::CompareOp as C;
let pred = match op { C::Eq=>inkwell::FloatPredicate::OEQ, C::Ne=>inkwell::FloatPredicate::ONE, C::Lt=>inkwell::FloatPredicate::OLT, C::Le=>inkwell::FloatPredicate::OLE, C::Gt=>inkwell::FloatPredicate::OGT, C::Ge=>inkwell::FloatPredicate::OGE };
codegen.builder.build_float_compare(pred, lf, rf, "fcmp").map_err(|e| e.to_string())?.into()
} else if let (BasicValueEnum::PointerValue(lp), BasicValueEnum::PointerValue(rp)) = (lv, rv) {
// Support pointer equality/inequality comparisons
use crate::mir::CompareOp as C;
match op {
C::Eq | C::Ne => {
let i64t = codegen.context.i64_type();
let li = codegen.builder.build_ptr_to_int(lp, i64t, "pi_l").map_err(|e| e.to_string())?;
let ri = codegen.builder.build_ptr_to_int(rp, i64t, "pi_r").map_err(|e| e.to_string())?;
let pred = if matches!(op, C::Eq) { inkwell::IntPredicate::EQ } else { inkwell::IntPredicate::NE };
codegen.builder.build_int_compare(pred, li, ri, "pcmp").map_err(|e| e.to_string())?.into()
}
_ => return Err("unsupported pointer comparison (only Eq/Ne)".to_string()),
}
} else if let (BasicValueEnum::PointerValue(lp), BasicValueEnum::IntValue(ri)) = (lv, rv) {
use crate::mir::CompareOp as C;
match op {
C::Eq | C::Ne => {
let i64t = codegen.context.i64_type();
let li = codegen.builder.build_ptr_to_int(lp, i64t, "pi_l").map_err(|e| e.to_string())?;
let pred = if matches!(op, C::Eq) { inkwell::IntPredicate::EQ } else { inkwell::IntPredicate::NE };
codegen.builder.build_int_compare(pred, li, ri, "pcmpi").map_err(|e| e.to_string())?.into()
}
_ => return Err("unsupported pointer-int comparison (only Eq/Ne)".to_string()),
}
} else if let (BasicValueEnum::IntValue(li), BasicValueEnum::PointerValue(rp)) = (lv, rv) {
use crate::mir::CompareOp as C;
match op {
C::Eq | C::Ne => {
let i64t = codegen.context.i64_type();
let ri = codegen.builder.build_ptr_to_int(rp, i64t, "pi_r").map_err(|e| e.to_string())?;
let pred = if matches!(op, C::Eq) { inkwell::IntPredicate::EQ } else { inkwell::IntPredicate::NE };
codegen.builder.build_int_compare(pred, li, ri, "ipcmi").map_err(|e| e.to_string())?.into()
}
_ => return Err("unsupported int-pointer comparison (only Eq/Ne)".to_string()),
}
} else {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
let lk = match lv { BasicValueEnum::IntValue(_) => "int", BasicValueEnum::FloatValue(_) => "float", BasicValueEnum::PointerValue(_) => "ptr", _ => "other" };
let rk = match rv { BasicValueEnum::IntValue(_) => "int", BasicValueEnum::FloatValue(_) => "float", BasicValueEnum::PointerValue(_) => "ptr", _ => "other" };
eprintln!("[LLVM] compare type mismatch: lhs={}, rhs={} (op={:?})", lk, rk, op);
}
return Err("compare type mismatch".to_string());
};
vmap.insert(*dst, out);
@ -774,8 +1123,36 @@ impl LLVMCompiler {
if !llvm_func.verify(true) {
return Err("Function verification failed".to_string());
}
codegen.target_machine.write_to_file(&codegen.module, inkwell::targets::FileType::Object, output_path.as_ref()).map_err(|e| format!("Failed to write object file: {}", e))?;
Ok(())
// Try writing via file API first; if it succeeds but file is missing due to env/FS quirks,
// also write via memory buffer as a fallback to ensure presence.
let verbose = std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1");
if verbose { eprintln!("[LLVM] emitting object to {} (begin)", output_path); }
match codegen.target_machine.write_to_file(&codegen.module, inkwell::targets::FileType::Object, std::path::Path::new(output_path)) {
Ok(_) => {
// Verify; if missing, fallback to memory buffer write
if std::fs::metadata(output_path).is_err() {
let buf = codegen.target_machine
.write_to_memory_buffer(&codegen.module, inkwell::targets::FileType::Object)
.map_err(|e| format!("Failed to get object buffer: {}", e))?;
std::fs::write(output_path, buf.as_slice()).map_err(|e| format!("Failed to write object to '{}': {}", output_path, e))?;
if verbose { eprintln!("[LLVM] wrote object via memory buffer fallback: {} ({} bytes)", output_path, buf.get_size()); }
} else if verbose {
if let Ok(meta) = std::fs::metadata(output_path) { eprintln!("[LLVM] wrote object via file API: {} ({} bytes)", output_path, meta.len()); }
}
if verbose { eprintln!("[LLVM] emit complete (Ok branch) for {}", output_path); }
Ok(())
}
Err(e) => {
// Fallback: memory buffer
let buf = codegen.target_machine
.write_to_memory_buffer(&codegen.module, inkwell::targets::FileType::Object)
.map_err(|ee| format!("Failed to write object ({}); and memory buffer failed: {}", e, ee))?;
std::fs::write(output_path, buf.as_slice()).map_err(|ee| format!("Failed to write object to '{}': {} (original error: {})", output_path, ee, e))?;
if verbose { eprintln!("[LLVM] wrote object via error fallback: {} ({} bytes)", output_path, buf.get_size()); }
if verbose { eprintln!("[LLVM] emit complete (Err branch handled) for {}", output_path); }
Ok(())
}
}
}
pub fn compile_and_execute(