diff --git a/crates/jam-sema/src/astgen.rs b/crates/jam-sema/src/astgen.rs index 158259c..c0b7d34 100644 --- a/crates/jam-sema/src/astgen.rs +++ b/crates/jam-sema/src/astgen.rs @@ -1247,6 +1247,13 @@ fn astgen_expr(gctx: &mut AstGenCtx, node: NodeIdx, expected: TypeIdx) -> Result AstTag::Call => astgen_call(gctx, &n, NO_JIR_REF), AstTag::TypeMethodCall => astgen_type_method_call(gctx, &n, NO_JIR_REF), AstTag::AtCall => astgen_at_call(gctx, &n), + // Reached only when `args...` appears somewhere other than the + // supported call-argument tails (@callC / direct calls). + AstTag::Spread => Ok(recover_here( + gctx, + "`...` pack spread is only valid as the last argument of a call".into(), + TypeIdx::NONE, + )), AstTag::StructLit => astgen_struct_lit(gctx, &n, expected), AstTag::ArrayLit => astgen_array_lit(gctx, &n, expected), AstTag::ArrayRepeat => astgen_array_repeat(gctx, &n, expected), @@ -3020,32 +3027,72 @@ fn comp_value_spelling(v: &ComptimeValue, gctx: &AstGenCtx) -> String { } } -/// A call to a fn with `comp` value params (`fn scale(comp k: u32, x: u32)`): -/// fold each comp arg to a value, bake it into a per-instantiation symbol -/// (`scale__u2`), and dispatch to that monomorphization passing only the -/// runtime args. The instantiated body is emitted lazily by the backend and is -/// not part of the JIR dump, so only the call site is lowered here. +/// A call to a fn/method with comptime signature axes — `comp` value params +/// (`fn scale(comp k: u32, x: u32)`), `T: type` params on a value-returning +/// fn, and/or a trailing variadic pack (`cfn send(self: S, args: ...)`): fold +/// each comptime arg, bake the shape into a per-instantiation symbol +/// (`scale__u2`, `send__va2__p9__p12`), and dispatch to that monomorphization +/// passing only the runtime args. Type params substitute through the clone's +/// signature and body (a `current_subst` frame); pack args materialize as +/// trailing `__va` params typed by each call-site arg's static type. +/// `recv` is the pre-lowered receiver arg for method calls (the `self` param, +/// `args[0]`, is then skipped when mapping call args to params). fn astgen_comp_instantiated_call( gctx: &mut AstGenCtx, - n: &AstNode, fn_ast: &FunctionAST, + recv: Option, + args_extra: u32, + arg_count: u32, dest_ptr: JirRef, ) -> Result { - let args_extra = n.rhs; - let arg_count = gctx.ctx.node_store.get_extra(ExtraIdx::new(args_extra)); + let param_offset = usize::from(recv.is_some()); + if recv.is_some() && fn_ast.args.is_empty() { + return Err(format!( + "astgen: method `{}` takes no `self` but was called on an instance", + fn_ast.name + )); + } + // Arity up front: without a pack the call must bind every param exactly; + // with one, everything past the fixed params is the pack tail. + let fixed = fn_ast.args.len() - param_offset; + let count = arg_count as usize; + if (fn_ast.var_args_pack.is_none() && count != fixed) + || (fn_ast.var_args_pack.is_some() && count < fixed) + { + let least = if fn_ast.var_args_pack.is_some() { + "at least " + } else { + "" + }; + return Err(format!( + "astgen: `{}` expects {least}{fixed} arg(s), got {count}", + fn_ast.name + )); + } let mut suffix = String::new(); - let mut runtime_idx: Vec = Vec::new(); let mut runtime_params: Vec = Vec::new(); + let mut arg_refs: Vec = Vec::new(); + if let Some(r) = recv { + runtime_params.push(fn_ast.args[0].clone()); + arg_refs.push(r); + } // The comp-param substitution baked into the clone's body: a body // reference to `k` folds to this call-site value. let mut comp_subst: HashMap = HashMap::new(); + // The type-param substitution: body/signature refs to `T` rewrite to the + // call site's concrete type. + let mut type_subst: HashMap = HashMap::new(); + let mut pack_len = 0u32; + if fn_ast.var_args_pack.is_some() { + suffix.push_str("__va"); + } for i in 0..arg_count { let arg_idx = NodeIdx::new( gctx.ctx .node_store .get_extra(ExtraIdx::new(args_extra + 1 + i)), ); - match fn_ast.args.get(i as usize) { + match fn_ast.args.get(param_offset + i as usize) { Some(p) if p.is_comp => { let v = gctx.ctx.fold_comptime_expr_in(arg_idx, &gctx.comp_scope); if matches!(v, ComptimeValue::None) { @@ -3062,25 +3109,88 @@ fn astgen_comp_instantiated_call( suffix.push_str(&comp_value_spelling(&v, gctx)); comp_subst.insert(p.name.clone(), v); } + Some(p) if p.ty == builtin::TYPE => { + let t = type_arg_from_expr(gctx, arg_idx, &p.name)?; + suffix.push_str("__"); + suffix.push_str(&comp_value_spelling(&ComptimeValue::Type(t), gctx)); + type_subst.insert(p.name.clone(), t); + } Some(p) => { - runtime_idx.push(arg_idx); - runtime_params.push(p.clone()); + // Fixed runtime param — lower against the (substituted) + // declared type. Type params conventionally precede runtime + // params, so the subst gathered so far applies. + let mut cp = p.clone(); + cp.ty = crate::generics::substitute_type( + cp.ty, + &type_subst, + &gctx.ctx.type_pool, + &gctx.ctx.string_pool, + ); + arg_refs.push(lower_arg(gctx, arg_idx, &cp)?); + runtime_params.push(cp); + } + None if fn_ast.var_args_pack.is_some() => { + // Pack tail: the arg's static type becomes a materialized + // trailing param (`__va`), read-only by-value mode. + if gctx.ctx.node_store.get(arg_idx).tag == AstTag::Spread { + return Err( + "astgen: cannot re-spread a pack into a variadic cfn's pack \ + (forward it to @callC or a concrete call instead)" + .into(), + ); + } + let r = astgen_expr(gctx, arg_idx, TypeIdx::NONE)?; + let ty = gctx.jfn.get_inst(r).ty; + suffix.push_str(&format!("__p{}", ty.raw())); + runtime_params.push(jam_syntax::ast::Param::new(format!("__va{pack_len}"), ty)); + pack_len += 1; + arg_refs.push(r); + } + None => { + // Unreachable: the up-front arity check bounds pack-less + // calls to exactly `fixed` args. + return Err(format!( + "astgen: internal: extra argument to `{}` past the arity check", + fn_ast.name + )); } - None => runtime_idx.push(arg_idx), } } - let base = if fn_ast.module_path.is_empty() { - fn_ast.name.clone() - } else { - format!("{}.{}", fn_ast.module_path, fn_ast.name) - }; - // A clone whose signature drops the comp params; module_path empty so the - // mangler passes the instantiation name through unchanged. + // Instantiation base: module + owning struct + name — two same-named + // methods on different structs (`Class.msgSend` / `Object.msgSend`) must + // not share clones. + let mut base = String::new(); + if !fn_ast.module_path.is_empty() { + base.push_str(&fn_ast.module_path); + base.push('.'); + } + if !fn_ast.parent_struct.is_empty() { + base.push_str(&fn_ast.parent_struct); + base.push('.'); + } + base.push_str(&fn_ast.name); + // A clone whose signature drops the comptime params and materializes the + // pack; module_path empty so the mangler passes the instantiation name + // through unchanged. let mut clone = fn_ast.clone(); clone.name = format!("{base}{suffix}"); clone.module_path = String::new(); clone.parent_struct = String::new(); clone.args = runtime_params; + clone.return_type = crate::generics::substitute_type( + fn_ast.return_type, + &type_subst, + &gctx.ctx.type_pool, + &gctx.ctx.string_pool, + ); + // A type param bound to `void` (`obj.msgSend(void, ...)`) makes the clone + // an ordinary void fn — the pipeline spells that TypeIdx::NONE, never + // builtin::VOID. + if clone.return_type == builtin::VOID { + clone.return_type = TypeIdx::NONE; + } + clone.var_args_pack = None; + clone.pack_len = pack_len; // EAGERLY declare + define the clone the first time this instantiation is // seen: the clone body is never @@ -3090,28 +3200,82 @@ fn astgen_comp_instantiated_call( if gctx.ctx.get_function_ast(&clone.name).is_none() { gctx.ctx .register_function_ast(clone.name.clone(), clone.clone()); - // Metadata + LLVM prototype, then the body — with the comp subst - // active so body refs to comp params fold to the baked constants, and - // the defining module pushed so bare type/name refs resolve there. + // Metadata + LLVM prototype, then the body — with the comp/type + // substs and pack frame active so body refs to comp params fold to + // the baked constants, `T` refs rewrite to the concrete types, and + // `args...` expands to the materialized params; the defining module + // pushed so bare type/name refs resolve there. let mut jfn = astgen_metadata(&clone, gctx.ctx); jfn.name = clone.name.clone(); let _ = jir_declare_prototype(&jfn, gctx.ctx); gctx.ctx.push_body_module(fn_ast.module_path.clone()); gctx.ctx.set_current_comp_subst(comp_subst); + let has_ts = !type_subst.is_empty(); + if has_ts { + gctx.ctx.push_subst(type_subst.clone()); + } + gctx.ctx.push_pack( + fn_ast + .var_args_pack + .as_ref() + .map(|name| (name.clone(), pack_len)), + ); let body_res = astgen_body_into(&mut jfn, &clone, gctx.ctx); + gctx.ctx.pop_pack(); + if has_ts { + gctx.ctx.pop_subst(); + } gctx.ctx.clear_current_comp_subst(); + // LLVM lowering still inside the defining module's scope so the + // body's bare type references (`ObjcSuper`) requalify there. + let def_res = body_res.and_then(|_| jir_define_body(&jfn, gctx.ctx)); gctx.ctx.pop_body_module(); - body_res?; - jir_define_body(&jfn, gctx.ctx)?; + def_res?; } - let mut arg_refs: Vec = Vec::with_capacity(runtime_idx.len()); - for (j, arg_idx) in runtime_idx.iter().enumerate() { - arg_refs.push(lower_arg(gctx, *arg_idx, &clone.args[j])?); - } emit_call(gctx, &clone, &arg_refs, dest_ptr) } +/// Resolve a call argument bound to a `T: type` param to a concrete TypeIdx. +/// The parser encodes a type name in expression position as a `Variable` +/// (`f(u32, x)` / `f(NSRect, x)`); resolve builtin spellings directly, other +/// names as (subst-applied, requalified) Named types. +fn type_arg_from_expr( + gctx: &mut AstGenCtx, + arg_idx: NodeIdx, + param_name: &str, +) -> Result { + let n = gctx.ctx.node_store.get(arg_idx); + if n.tag != AstTag::Variable { + return Err(format!( + "astgen: argument for type param `{param_name}` must be a type name" + )); + } + let name = str_at(gctx, n.lhs); + let t = match name.as_str() { + "void" => builtin::VOID, + "bool" | "u1" => builtin::BOOL, + "u8" => builtin::U8, + "i8" => builtin::I8, + "u16" => builtin::U16, + "i16" => builtin::I16, + "u32" => builtin::U32, + "i32" => builtin::I32, + "u64" => builtin::U64, + "i64" => builtin::I64, + "f32" => builtin::F32, + "f64" => builtin::F64, + _ => { + let sid = gctx.ctx.string_pool.intern(name.as_bytes()); + let named = gctx.ctx.type_pool.intern_named(sid); + let named = gctx.ctx.apply_current_subst(named); + let bm = gctx.ctx.current_body_module(); + gctx.ctx.requalify_type(named, &bm) + } + }; + Ok(t) +} + /// Emit a `Call` to `fn_ast` with already-lowered `arg_refs`. sret-returning /// callees get their result slot as a leading arg (the caller's `dest_ptr` /// when supplied, else a fresh alloca). The arg list is packed @@ -3397,8 +3561,8 @@ fn astgen_call(gctx: &mut AstGenCtx, n: &AstNode, dest_ptr: JirRef) -> Result = Vec::with_capacity(arg_count as usize); @@ -3408,6 +3572,17 @@ fn astgen_call(gctx: &mut AstGenCtx, n: &AstNode, dest_ptr: JirRef) -> Result Result { + // A comptime cfn's args must fold to constants — a pack of runtime values + // is meaningless here. Packs live on struct cfn methods + // (CFN_VARIADIC_PLAN.md). + if fn_ast.var_args_pack.is_some() { + return Ok(recover_here( + gctx, + format!( + "variadic packs are not supported on comptime cfns; declare `{}` \ + as a struct method", + fn_ast.name + ), + TypeIdx::NONE, + )); + } if arg_count as usize != fn_ast.args.len() { // cfn doesn't support varargs. return Ok(recover_here( @@ -4730,6 +4919,19 @@ fn astgen_dotted_call( }, ) }; + // A method with comptime signature axes (type params, comp params, a + // variadic pack — e.g. `cfn msgSend(self: Object, R: type, args: ...)`) + // instantiates a per-shape clone instead of calling a fixed symbol. + if method.needs_instantiation() { + return Ok(Some(astgen_comp_instantiated_call( + gctx, + &method, + Some(recv_arg), + args_extra, + arg_count, + dest_ptr, + )?)); + } let mut arg_refs = vec![recv_arg]; arg_refs.extend(lower_method_args(gctx, &method, args_extra, arg_count)?); Ok(Some(emit_call(gctx, &method, &arg_refs, dest_ptr)?)) @@ -4941,6 +5143,211 @@ fn build_indirect_call( )) } +/// `@callC(R, addr, args...)` — indirect C call through a raw address with a +/// fixed signature synthesized from the args' static types (CALLC_PLAN.md). +/// extra = [retTypeIdx, argCount, addr, arg0, ...]. The address is punned to +/// the synthesized `fn` type via IntToPtr, then flows through the same +/// CallIndirect the union-pun pattern produces. +fn astgen_call_c(gctx: &mut AstGenCtx, n: &AstNode) -> Result { + let extra = n.rhs; + let mut ret_ty = TypeIdx::new(gctx.ctx.node_store.get_extra(ExtraIdx::new(extra))); + ret_ty = gctx.ctx.apply_current_subst(ret_ty); + if gctx.ctx.type_pool.get(ret_ty).kind == TypeKind::ArrayExpr { + ret_ty = gctx.ctx.resolve_array_expr_instantiate(ret_ty)?; + } + ret_ty = gctx.ctx.resolve_generic_call_instantiate(ret_ty)?; + if ret_ty != builtin::VOID { + callc_check_abi(gctx, ret_ty, "return")?; + } + + let count = gctx.ctx.node_store.get_extra(ExtraIdx::new(extra + 1)); + debug_assert!(count >= 1, "parser guarantees an address operand"); + let addr_idx = NodeIdx::new(gctx.ctx.node_store.get_extra(ExtraIdx::new(extra + 2))); + let addr_ref = astgen_expr(gctx, addr_idx, builtin::U64)?; + let ak = gctx.ctx.type_pool.get(gctx.jfn.get_inst(addr_ref).ty); + if ak.kind != TypeKind::Int || ak.a != 64 || ak.b != 0 { + return Err("astgen: @callC address must be a u64".into()); + } + + let mut arg_refs: Vec = Vec::with_capacity(count as usize - 1); + let mut param_tys: Vec = Vec::with_capacity(count as usize - 1); + for i in 1..count { + let arg_idx = NodeIdx::new( + gctx.ctx + .node_store + .get_extra(ExtraIdx::new(extra + 2 + i)), + ); + // `pack...` — forward the enclosing variadic-cfn clone's pack; each + // materialized param contributes its own static type to the + // synthesized signature. + if gctx.ctx.node_store.get(arg_idx).tag == AstTag::Spread { + if i + 1 != count { + return Err("astgen: a pack spread must be the last argument".into()); + } + let spread_name = str_at(gctx, gctx.ctx.node_store.get(arg_idx).lhs); + for r in expand_pack_spread(gctx, &spread_name)? { + let ty = gctx.jfn.get_inst(r).ty; + callc_check_abi(gctx, ty, "argument")?; + arg_refs.push(r); + param_tys.push(ty); + } + continue; + } + let arg_ref = astgen_expr(gctx, arg_idx, TypeIdx::NONE)?; + let arg_ty = gctx.jfn.get_inst(arg_ref).ty; + callc_check_abi(gctx, arg_ty, "argument")?; + arg_refs.push(arg_ref); + param_tys.push(arg_ty); + } + + let fn_ty = gctx.ctx.type_pool.intern_fn(ret_ty, param_tys); + let callee = emit( + gctx, + JirInst { + tag: JirTag::IntToPtr, + a: addr_ref, + ty: fn_ty, + ..Default::default() + }, + ); + let mut packed: Vec = Vec::with_capacity(1 + arg_refs.len()); + packed.push(arg_refs.len() as u32); + packed.extend_from_slice(&arg_refs); + let args = gctx.jfn.push_extra(&packed); + Ok(emit( + gctx, + JirInst { + tag: JirTag::CallIndirect, + a: callee, + b: args, + ty: ret_ty, + ..Default::default() + }, + )) +} + +/// Expand `name...` into loads of the enclosing variadic-cfn clone's +/// materialized pack params (`__va0..__va`), in declaration order. +/// Errors outside a variadic-cfn clone body or on a pack-name mismatch. +fn expand_pack_spread(gctx: &mut AstGenCtx, name: &str) -> Result, String> { + let Some((pack_name, len)) = gctx.ctx.current_pack() else { + return Err(format!( + "astgen: `{name}...` is only valid inside a variadic cfn" + )); + }; + if name != pack_name { + return Err(format!( + "astgen: unknown pack `{name}` (the enclosing cfn's pack is `{pack_name}`)" + )); + } + let mut refs = Vec::with_capacity(len as usize); + for i in 0..len { + let pname = format!("__va{i}"); + let (Some(&slot), Some(&ty)) = (gctx.locals.get(&pname), gctx.local_types.get(&pname)) + else { + return Err(format!("astgen: internal: pack param `{pname}` missing")); + }; + refs.push(emit( + gctx, + JirInst { + tag: JirTag::Load, + a: slot, + ty, + ..Default::default() + }, + )); + } + Ok(refs) +} + +/// Reject `@callC` types the naive CallIndirect lowering can't pass per the +/// AArch64 C ABI (CALLC_PLAN.md §4): comptime-only types always; aggregates +/// unless they are a homogeneous float aggregate of <= 4 same-width fields +/// (v0..v3) or <= 2 full 64-bit words (x-register pair). LLVM splits a +/// first-class aggregate one member per register, which matches the C ABI's +/// packing only for those two shapes — anything else (sub-word fields like +/// `{i32,i32}`, mixed float/int, > 16 bytes) needs real byval/sret/coercion +/// support (plan phase 4). +fn callc_check_abi(gctx: &AstGenCtx, ty: TypeIdx, what: &str) -> Result<(), String> { + match gctx.ctx.type_pool.get(ty).kind { + TypeKind::Invalid | TypeKind::Void | TypeKind::NoReturn | TypeKind::Type + | TypeKind::Module => { + return Err(format!("astgen: @callC {what} has no runtime value")); + } + _ => {} + } + if !is_by_ref(ty, gctx.ctx) { + return Ok(()); + } + if let Some((_bits, leaves)) = callc_float_leaves(gctx, ty) { + if (1..=4).contains(&leaves) { + return Ok(()); + } + } + if let Some(words) = callc_word_leaves(gctx, ty) { + if (1..=2).contains(&words) { + return Ok(()); + } + } + Err(format!( + "astgen: aggregate {what} type unsupported by @callC (must be a homogeneous \ + float aggregate of <= 4 floats or <= 2 64-bit words; anything else needs \ + byval/sret ABI support)" + )) +} + +/// Walk an aggregate's leaf fields; `Some((bits, count))` when every leaf is +/// a float of one width — the homogeneous-float-aggregate shape AAPCS64 +/// passes in FP registers. `None` for any other composition. +fn callc_float_leaves(gctx: &AstGenCtx, ty: TypeIdx) -> Option<(u32, u32)> { + let k = gctx.ctx.type_pool.get(ty); + match k.kind { + TypeKind::Float => Some((k.a, 1)), + TypeKind::Array => { + let (bits, n) = callc_float_leaves(gctx, TypeIdx::new(k.a))?; + Some((bits, n * k.b)) + } + TypeKind::Struct | TypeKind::Named => { + let fields = gctx.ctx.struct_fields(ty)?; + let mut bits = 0u32; + let mut count = 0u32; + for (_, fty) in fields { + let (fbits, fcount) = callc_float_leaves(gctx, fty)?; + if bits == 0 { + bits = fbits; + } else if bits != fbits { + return None; // mixed widths — not an HFA + } + count += fcount; + } + if count == 0 { None } else { Some((bits, count)) } + } + _ => None, + } +} + +/// Count an aggregate's leaves when every one is a full 64-bit word (64-bit +/// int or a pointer) — the GP-register shape whose naive LLVM split matches +/// the C ABI's packing. `None` for any sub-word, float, or unknown leaf. +/// Unions are rejected (no dominant member to classify by). +fn callc_word_leaves(gctx: &AstGenCtx, ty: TypeIdx) -> Option { + let k = gctx.ctx.type_pool.get(ty); + match k.kind { + TypeKind::Int if k.a == 64 => Some(1), + TypeKind::PtrSingle | TypeKind::PtrMany | TypeKind::Fn => Some(1), + TypeKind::Array => Some(callc_word_leaves(gctx, TypeIdx::new(k.a))? * k.b), + TypeKind::Struct | TypeKind::Named => { + let fields = gctx.ctx.struct_fields(ty)?; + let mut count = 0u32; + for (_, fty) in fields { + count += callc_word_leaves(gctx, fty)?; + } + if count == 0 { None } else { Some(count) } + } + _ => None, + } +} + /// Call through a Fn-typed local (`f(args)`) or a Fn-typed struct field /// (`recv.field(args)`). Returns `None` when the callee isn't one of those. fn astgen_indirect_fn_call( @@ -5026,6 +5433,10 @@ fn astgen_indirect_fn_call( /// integer constant (an `[expr]T` argument resolves its comptime length first). fn astgen_at_call(gctx: &mut AstGenCtx, n: &AstNode) -> Result { let name = str_at(gctx, n.lhs); + // `@callC(R, addr, args...)` (flags&2: extra carries a leading TypeIdx). + if n.flags & 2 != 0 && name == "callC" { + return astgen_call_c(gctx, n); + } // `@dropInPlace(ptr)` (flags&1: n.rhs is an args ExtraIdx, not a type arg): // synthesize T's drop sequence at the pointee. Rust's `drop_in_place::`. if n.flags & 1 != 0 && name == "dropInPlace" { @@ -7271,6 +7682,27 @@ fn astgen_return(gctx: &mut AstGenCtx, n: &AstNode) -> Result<(), String> { } } val_ref = astgen_expr(gctx, val_idx, ret_ty)?; + // `return voidExpr;` (e.g. `return @callC(void, ...)` in a variadic + // cfn instantiated at R = void): evaluate, then return nothing — + // only legal when the function itself returns nothing. + if gctx.jfn.get_inst(val_ref).ty == builtin::VOID { + if !ret_ty.is_none() && ret_ty != builtin::VOID { + return Err(format!( + "astgen: cannot return a void expression from `{}`, which declares \ + a return type", + gctx.jfn.name + )); + } + emit_return_drops(gctx, moved_var.as_deref()); + emit( + gctx, + JirInst { + tag: JirTag::Ret, + ..Default::default() + }, + ); + return Ok(()); + } // Reconcile the returned value's type with the declared return type // (resolve-then-compare, like the var-decl declared-vs-init check). // A LOSSLESS narrower int widens (ZExt/SExt by the value's own @@ -8334,6 +8766,437 @@ mod tests { astgen_function(&module.functions[0], &mut cg).map(|_| ()) } + /// Astgen the first function and hand the JIR + context to `check` — for + /// asserting on the emitted instructions (no LLVM lowering). + fn with_first_fn_jir(src: &str, check: impl FnOnce(&JirFunction, &CodegenContext)) { + let owner = Context::new(); + let mut cg = CodegenContext::new(&owner, "m"); + let module = { + let mut lexer = Lexer::new(src.as_bytes().to_vec()); + lexer.scan_tokens().expect("lex"); + let tokens = lexer.tokens().to_vec(); + let mut diags = jam_core::diag::Diagnostics::new(); + let mut parser = Parser::new( + tokens, + src.as_bytes().to_vec(), + &mut cg.type_pool, + &mut cg.string_pool, + &mut cg.node_store, + &mut diags, + "test.jam", + ); + let m = parser.parse().expect("parse"); + assert!(!diags.has_errors(), "parse errors"); + m + }; + let jfn = astgen_function(&module.functions[0], &mut cg).expect("astgen"); + check(&jfn, &cg); + } + + #[test] + fn callc_scalar_end_to_end() { + lower_first_fn("fn f(a: u64) u64 { return @callC(u64, a, 1 as u64, 2 as u64); }"); + } + + #[test] + fn callc_zero_call_args_end_to_end() { + lower_first_fn("fn f(a: u64) u64 { return @callC(u64, a); }"); + } + + #[test] + fn callc_f64_end_to_end() { + lower_first_fn("fn f(a: u64, x: f64) f64 { return @callC(f64, a, x); }"); + } + + #[test] + fn callc_void_return_statement() { + lower_first_fn("fn f(a: u64) { @callC(void, a); }"); + } + + #[test] + fn callc_synthesizes_fn_type_from_static_arg_types() { + // Each arg's static type becomes a param of the synthesized Fn + // type; the address is punned to it via IntToPtr. + with_first_fn_jir( + "fn f(a: u64, x: f64) f64 { return @callC(f64, a, x); }", + |jfn, cg| { + let call = jfn + .insts + .iter() + .find(|i| i.tag == JirTag::CallIndirect) + .expect("no CallIndirect emitted"); + assert_eq!(call.ty, builtin::F64); + assert_eq!(jfn.get_extra(call.b), 1, "one call arg"); + let callee = jfn.get_inst(call.a); + assert_eq!(callee.tag, JirTag::IntToPtr); + let k = cg.type_pool.get(callee.ty); + assert_eq!(k.kind, TypeKind::Fn); + assert_eq!(TypeIdx::new(k.a), builtin::F64); + assert_eq!(cg.type_pool.fn_params_at(k.b), vec![builtin::F64]); + }, + ); + } + + #[test] + fn callc_hfa_struct_arg_admitted() { + // NSRect-shaped homogeneous float aggregate (4 x f64) passes the + // aggregate guard and becomes a by-value struct param. + let owner = Context::new(); + let mut cg = CodegenContext::new(&owner, "m"); + let src = "const R = struct { x: f64, y: f64, w: f64, h: f64 };\n\ + fn f(a: u64, r: R) u64 { return @callC(u64, a, r); }"; + let module = { + let mut lexer = Lexer::new(src.as_bytes().to_vec()); + lexer.scan_tokens().expect("lex"); + let tokens = lexer.tokens().to_vec(); + let mut diags = jam_core::diag::Diagnostics::new(); + let mut parser = Parser::new( + tokens, + src.as_bytes().to_vec(), + &mut cg.type_pool, + &mut cg.string_pool, + &mut cg.node_store, + &mut diags, + "test.jam", + ); + let m = parser.parse().expect("parse"); + assert!(!diags.has_errors(), "parse errors"); + m + }; + let st = &module.structs[0]; + let named = cg.context().named_struct(&st.name); + cg.register_struct(st.name.clone(), named, st.fields.clone()); + let f64t = cg.context().f64_type(); + named.set_body(&[f64t, f64t, f64t, f64t], false); + let jfn = astgen_function(&module.functions[0], &mut cg).expect("astgen"); + let call = jfn + .insts + .iter() + .find(|i| i.tag == JirTag::CallIndirect) + .expect("no CallIndirect emitted"); + let k = cg.type_pool.get(jfn.get_inst(call.a).ty); + assert_eq!(cg.type_pool.fn_params_at(k.b).len(), 1); + } + + #[test] + fn callc_rejects_large_non_hfa_aggregate() { + // {u64,u64,u64} = 24 bytes, not an HFA — the naive indirect lowering + // would miscompile it (needs byval/sret), so it must be a compile + // error until CALLC_PLAN.md phase 4. + let owner = Context::new(); + let mut cg = CodegenContext::new(&owner, "m"); + let src = "const S = struct { a: u64, b: u64, c: u64 };\n\ + fn f(p: u64, s: S) u64 { return @callC(u64, p, s); }"; + let module = { + let mut lexer = Lexer::new(src.as_bytes().to_vec()); + lexer.scan_tokens().expect("lex"); + let tokens = lexer.tokens().to_vec(); + let mut diags = jam_core::diag::Diagnostics::new(); + let mut parser = Parser::new( + tokens, + src.as_bytes().to_vec(), + &mut cg.type_pool, + &mut cg.string_pool, + &mut cg.node_store, + &mut diags, + "test.jam", + ); + let m = parser.parse().expect("parse"); + assert!(!diags.has_errors(), "parse errors"); + m + }; + let st = &module.structs[0]; + let named = cg.context().named_struct(&st.name); + cg.register_struct(st.name.clone(), named, st.fields.clone()); + let i64t = cg.context().i64_type(); + named.set_body(&[i64t, i64t, i64t], false); + let err = astgen_function(&module.functions[0], &mut cg).unwrap_err(); + assert!( + err.contains("unsupported by @callC"), + "expected aggregate guard error, got: {err}" + ); + } + + #[test] + fn callc_rejects_subword_field_aggregate() { + // {i32,i32} is 8 bytes but NOT admissible: LLVM splits it into two + // w-registers while the C ABI packs it into one x-register (the + // div_t shape). Must be rejected until phase-4 coercion exists. + let owner = Context::new(); + let mut cg = CodegenContext::new(&owner, "m"); + let src = "const D = struct { quot: i32, rem: i32 };\n\ + fn f(p: u64, d: D) u64 { return @callC(u64, p, d); }"; + let module = { + let mut lexer = Lexer::new(src.as_bytes().to_vec()); + lexer.scan_tokens().expect("lex"); + let tokens = lexer.tokens().to_vec(); + let mut diags = jam_core::diag::Diagnostics::new(); + let mut parser = Parser::new( + tokens, + src.as_bytes().to_vec(), + &mut cg.type_pool, + &mut cg.string_pool, + &mut cg.node_store, + &mut diags, + "test.jam", + ); + let m = parser.parse().expect("parse"); + assert!(!diags.has_errors(), "parse errors"); + m + }; + let st = &module.structs[0]; + let named = cg.context().named_struct(&st.name); + cg.register_struct(st.name.clone(), named, st.fields.clone()); + let i32t = cg.context().i32_type(); + named.set_body(&[i32t, i32t], false); + let err = astgen_function(&module.functions[0], &mut cg).unwrap_err(); + assert!( + err.contains("unsupported by @callC"), + "expected aggregate guard error, got: {err}" + ); + } + + /// Parse a module with structs (+ cfn methods) and caller fns; register + /// each struct and its methods like the emit pipeline does, astgen the + /// `fn_index`-th function, and hand the result + context to `check`. + fn lower_struct_module( + src: &str, + fn_index: usize, + check: impl FnOnce(Result, &CodegenContext), + ) { + let owner = Context::new(); + let mut cg = CodegenContext::new(&owner, "m"); + let module = { + let mut lexer = Lexer::new(src.as_bytes().to_vec()); + lexer.scan_tokens().expect("lex"); + let tokens = lexer.tokens().to_vec(); + let mut diags = jam_core::diag::Diagnostics::new(); + let mut parser = Parser::new( + tokens, + src.as_bytes().to_vec(), + &mut cg.type_pool, + &mut cg.string_pool, + &mut cg.node_store, + &mut diags, + "test.jam", + ); + let m = parser.parse().expect("parse"); + assert!(!diags.has_errors(), "parse errors"); + m + }; + for st in &module.structs { + let named = cg.context().named_struct(&st.name); + cg.register_struct(st.name.clone(), named, st.fields.clone()); + // Test structs here carry only word-sized fields. + let i64t = cg.context().i64_type(); + named.set_body(&vec![i64t; st.fields.len()], false); + for meth in &st.methods { + cg.register_function_ast(format!("{}.{}", st.name, meth.name), meth.clone()); + } + } + let r = astgen_function(&module.functions[fn_index], &mut cg); + check(r, &cg); + } + + const VARIADIC_CALLER: &str = "const Caller = struct {\n\ + addr: u64,\n\ + cfn callThrough(self: Caller, R: type, args: ...) R {\n\ + return @callC(R, self.addr, args...);\n\ + }\n\ + };\n"; + + /// Callee names of every direct Call in the function, in emit order. + fn call_names(jfn: &JirFunction, cg: &CodegenContext) -> Vec { + jfn.insts + .iter() + .filter(|i| i.tag == JirTag::Call) + .map(|i| { + String::from_utf8_lossy(&cg.string_pool.get(StringIdx::new(i.a))).into_owned() + }) + .collect() + } + + #[test] + fn variadic_cfn_pack_forwards_into_callc() { + // The core contract: the call site's tail types shape the clone, and + // the clone's @callC synthesizes its signature from the pack. + let src = format!( + "{VARIADIC_CALLER}\ + fn f(a: u64, x: f64) f64 {{\n\ + var c: Caller = Caller {{ addr: a }};\n\ + return c.callThrough(f64, x);\n\ + }}\n" + ); + lower_struct_module(&src, 0, |r, cg| { + let jfn = r.expect("astgen"); + let names = call_names(&jfn, cg); + assert!( + names.iter().any(|n| n.contains("callThrough__va")), + "expected a call to a pack clone, got: {names:?}" + ); + // The clone was registered with a materialized f64 tail param. + let clone_name = names + .iter() + .find(|n| n.contains("callThrough__va")) + .unwrap(); + let clone = cg + .get_function_ast(clone_name) + .expect("clone registered"); + assert_eq!(clone.pack_len, 1); + let last = clone.args.last().unwrap(); + assert_eq!(last.name, "__va0"); + assert_eq!(last.ty, builtin::F64); + assert_eq!(clone.return_type, builtin::F64); + }); + } + + #[test] + fn variadic_cfn_clones_memoize_per_shape() { + // Same tail shape twice -> one clone name; a different shape -> a + // different clone name. + let src = format!( + "{VARIADIC_CALLER}\ + fn f(a: u64, x: f64) f64 {{\n\ + var c: Caller = Caller {{ addr: a }};\n\ + const r1: f64 = c.callThrough(f64, x);\n\ + const r2: f64 = c.callThrough(f64, r1);\n\ + const r3: u64 = c.callThrough(u64, a, a);\n\ + return r2;\n\ + }}\n" + ); + lower_struct_module(&src, 0, |r, cg| { + let jfn = r.expect("astgen"); + let names: Vec = call_names(&jfn, cg) + .into_iter() + .filter(|n| n.contains("callThrough__va")) + .collect(); + assert_eq!(names.len(), 3, "three instantiated calls: {names:?}"); + assert_eq!(names[0], names[1], "same shape must reuse the clone"); + assert_ne!(names[1], names[2], "different shape must get its own clone"); + }); + } + + #[test] + fn variadic_cfn_empty_pack_ok() { + let src = format!( + "{VARIADIC_CALLER}\ + fn f(a: u64) u64 {{\n\ + var c: Caller = Caller {{ addr: a }};\n\ + return c.callThrough(u64);\n\ + }}\n" + ); + lower_struct_module(&src, 0, |r, cg| { + let jfn = r.expect("astgen"); + let names = call_names(&jfn, cg); + let clone_name = names + .iter() + .find(|n| n.contains("callThrough__va")) + .expect("clone call"); + let clone = cg.get_function_ast(clone_name).unwrap(); + assert_eq!(clone.pack_len, 0); + }); + } + + #[test] + fn void_expr_return_in_valued_fn_rejected() { + // Used to compile and crash at runtime — the bare-Ret path must + // only apply when the function itself returns nothing. + let err = astgen_first_fn("fn f(a: u64) u64 { return @callC(void, a); }").unwrap_err(); + assert!( + err.contains("cannot return a void expression"), + "got: {err}" + ); + } + + /// Parse a module, register every function, then astgen functions[0]. + fn astgen_first_fn_registered(src: &str) -> Result<(), String> { + let owner = Context::new(); + let mut cg = CodegenContext::new(&owner, "m"); + let module = { + let mut lexer = Lexer::new(src.as_bytes().to_vec()); + lexer.scan_tokens().expect("lex"); + let tokens = lexer.tokens().to_vec(); + let mut diags = jam_core::diag::Diagnostics::new(); + let mut parser = Parser::new( + tokens, + src.as_bytes().to_vec(), + &mut cg.type_pool, + &mut cg.string_pool, + &mut cg.node_store, + &mut diags, + "test.jam", + ); + let m = parser.parse().expect("parse"); + assert!(!diags.has_errors(), "parse errors"); + m + }; + for f in &module.functions { + cg.register_function_ast(f.name.clone(), f.clone()); + } + astgen_function(&module.functions[0], &mut cg).map(|_| ()) + } + + #[test] + fn instantiated_call_arity_checked() { + // Extra args used to be appended silently (an invalid call), and + // missing args surfaced as "unknown variable" from inside the clone. + let err = astgen_first_fn_registered( + "fn f() u64 { return pick(u32, 1 as u64, 2 as u64); }\n\ + fn pick(T: type, x: u64) u64 { return x; }", + ) + .unwrap_err(); + assert!( + err.contains("expects 2 arg(s), got 3"), + "extra args must be an arity error, got: {err}" + ); + let err = astgen_first_fn_registered( + "fn f() u64 { return pick(u32); }\n\ + fn pick(T: type, x: u64) u64 { return x; }", + ) + .unwrap_err(); + assert!( + err.contains("expects 2 arg(s), got 1"), + "missing args must be an arity error, got: {err}" + ); + } + + #[test] + fn spread_outside_variadic_cfn_rejected() { + let err = + astgen_first_fn("fn f(a: u64) u64 { return @callC(u64, a, args...); }").unwrap_err(); + assert!( + err.contains("only valid inside a variadic cfn"), + "got: {err}" + ); + } + + #[test] + fn spread_of_wrong_pack_name_rejected() { + let src = "const S = struct {\n\ + addr: u64,\n\ + cfn send(self: S, args: ...) u64 {\n\ + return @callC(u64, self.addr, argz...);\n\ + }\n\ + };\n\ + fn f(a: u64) u64 {\n\ + var s: S = S { addr: a };\n\ + return s.send(a);\n\ + }\n"; + lower_struct_module(src, 0, |r, _| { + let err = r.unwrap_err(); + assert!(err.contains("unknown pack `argz`"), "got: {err}"); + }); + } + + #[test] + fn callc_rejects_non_u64_address() { + let err = astgen_first_fn("fn f(b: bool) u64 { return @callC(u64, b); }").unwrap_err(); + assert!( + err.contains("@callC address must be a u64"), + "got: {err}" + ); + } + /// Recoverable diagnostics accumulate: one pass over a body reports every /// independent error, not just the first. #[test] diff --git a/crates/jam-sema/src/codegen_context.rs b/crates/jam-sema/src/codegen_context.rs index d197bbb..dd6b68c 100644 --- a/crates/jam-sema/src/codegen_context.rs +++ b/crates/jam-sema/src/codegen_context.rs @@ -172,6 +172,13 @@ pub struct CodegenContext<'ctx> { // reference to a comp param (`k`) folds to the call-site constant. current_comp_subst: RefCell>, + // Variadic-pack context stack, parallel to instantiated-clone body + // lowering: `Some((pack_name, len))` while a variadic cfn clone's body is + // lowered (its pack materialized as `__va0..__va` params), `None` + // frames for non-pack instantiations so an inner body never sees an outer + // pack. `args...` expansion reads the top frame. + current_pack: RefCell>>, + // module_path -> the module's anonymous struct bodies (the `return struct // {...}` of a generic factory), so generic instantiation can reach the body // a `StructExpr` node references (its `lhs` indexes this per-module list). @@ -278,6 +285,7 @@ impl<'ctx> CodegenContext<'ctx> { requalify_map: RefCell::new(HashMap::new()), current_subst: RefCell::new(Vec::new()), current_comp_subst: RefCell::new(HashMap::new()), + current_pack: RefCell::new(Vec::new()), anon_structs: RefCell::new(HashMap::new()), anon_enums: RefCell::new(HashMap::new()), type_aliases: RefCell::new(HashMap::new()), @@ -1033,6 +1041,21 @@ impl<'ctx> CodegenContext<'ctx> { !self.current_subst.borrow().is_empty() } + /// Push the variadic-pack frame for an instantiated clone body about to be + /// lowered (`None` for a pack-less instantiation, masking any outer pack). + pub fn push_pack(&self, frame: Option<(String, u32)>) { + self.current_pack.borrow_mut().push(frame); + } + /// Pop the innermost pack frame. + pub fn pop_pack(&self) { + self.current_pack.borrow_mut().pop(); + } + /// The active pack (name, materialized length), if the body being lowered + /// belongs to a variadic cfn clone. + pub fn current_pack(&self) -> Option<(String, u32)> { + self.current_pack.borrow().last().cloned().flatten() + } + /// Install the comp-value substitution map active while a comp-instantiated /// fn clone's body is lowered. pub fn set_current_comp_subst(&self, s: HashMap) { diff --git a/crates/jam-sema/src/init_analysis.rs b/crates/jam-sema/src/init_analysis.rs index 84c2095..279b300 100644 --- a/crates/jam-sema/src/init_analysis.rs +++ b/crates/jam-sema/src/init_analysis.rs @@ -264,6 +264,12 @@ impl<'a, 'ctx> Analyzer<'a, 'ctx> { } } AstTag::Call => self.analyze_call(idx, state), + // `args...` spread: the pack params are materialized (always + // initialized) clone params — nothing to track. + AstTag::Spread => StmtResult { + state, + terminated: false, + }, AstTag::TypeMethodCall => self.analyze_type_method_call(idx, &n, state), AstTag::BinaryOp => { let r = self.analyze_node(NodeIdx::new(n.lhs), state); diff --git a/crates/jam-syntax/src/ast.rs b/crates/jam-syntax/src/ast.rs index c2885d3..497c3e7 100644 --- a/crates/jam-syntax/src/ast.rs +++ b/crates/jam-syntax/src/ast.rs @@ -61,6 +61,14 @@ pub struct FunctionAST { pub parent_struct: String, /// Owning module path (empty for the entry module / qualified clones). pub module_path: String, + /// Trailing variadic pack (`args: ...`) on a `cfn` — an instantiation + /// axis: the template is cloned per call-site tail shape, never lowered + /// as-declared. The pack name is spendable only as `args...` inside a + /// call-argument list (CFN_VARIADIC_PLAN.md). + pub var_args_pack: Option, + /// On an instantiated clone: how many materialized pack params + /// (`__va0..__va`) the signature tail carries. 0 elsewhere. + pub pack_len: u32, } impl FunctionAST { @@ -84,12 +92,15 @@ impl FunctionAST { is_comp_time_fn: false, parent_struct: String::new(), module_path: String::new(), + var_args_pack: None, + pack_len: 0, } } /// A function is generic iff its return type is the meta-type `type`, it is - /// a compile-time fn, or any parameter is `type`-typed or `comp`. Generic - /// functions are instantiated per call site, not lowered at decl time. + /// a compile-time fn, or any parameter is `type`-typed or `comp`, or it + /// declares a variadic pack. Generic functions are instantiated per call + /// site, not lowered at decl time. pub fn is_generic(&self) -> bool { if self.return_type == builtin::TYPE { return true; @@ -97,8 +108,23 @@ impl FunctionAST { if self.is_comp_time_fn { return true; } + if self.var_args_pack.is_some() { + return true; + } self.args.iter().any(|p| p.ty == builtin::TYPE || p.is_comp) } + + /// Callable only through per-callsite clone instantiation (comp params, + /// `type` params on a value-returning fn, or a variadic pack) — as opposed + /// to type-constructor generics (`return_type == type`) and comptime fns, + /// which have their own paths. + pub fn needs_instantiation(&self) -> bool { + if self.is_comp_time_fn || self.return_type == builtin::TYPE { + return false; + } + self.var_args_pack.is_some() + || self.args.iter().any(|p| p.is_comp || p.ty == builtin::TYPE) + } } /// Top-level struct: `const Vec3 = struct { x: f32, y: f32 };`. Methods declared diff --git a/crates/jam-syntax/src/ast_flat.rs b/crates/jam-syntax/src/ast_flat.rs index 783845f..e48984a 100644 --- a/crates/jam-syntax/src/ast_flat.rs +++ b/crates/jam-syntax/src/ast_flat.rs @@ -69,7 +69,9 @@ pub enum AstTag { EnumExpr, // lhs = u32 (index into ModuleAST::AnonEnums) MatchNode, // lhs = NodeIdx (scrutinee); rhs = ExtraIdx -> [armCount, ...] AsCast, // lhs = NodeIdx (operand), rhs = TypeIdx (target) - AtCall, // lhs = StringIdx (name); rhs = TypeIdx | ExtraIdx (flags bit0) + AtCall, // lhs = StringIdx (name); rhs = TypeIdx | ExtraIdx (flags bit0); + // flags bit1: extra has a leading TypeIdx header + // ([retTypeIdx, argCount, arg0, ...] — @callC) TypeMethodCall, // lhs = TypeIdx (receiver); rhs = ExtraIdx -> [methodNameId, argCount, ...] // Pattern atoms (internal to MatchNode arms) @@ -79,6 +81,8 @@ pub enum AstTag { PatOr, // lhs = ExtraIdx -> [count, sub0, ...] PatEnumVariant, // lhs = StringIdx (enum), rhs = StringIdx (variant) + Spread, // lhs = StringIdx (pack name) — `args...` in a call-arg list + Count, // sentinel for table sizing } diff --git a/crates/jam-syntax/src/parser.rs b/crates/jam-syntax/src/parser.rs index 9359800..1964dab 100644 --- a/crates/jam-syntax/src/parser.rs +++ b/crates/jam-syntax/src/parser.rs @@ -1271,6 +1271,72 @@ impl<'a> Parser<'a> { // (flags=1, extra=[argCount=0]). let is_emit_family = name.starts_with(b"emit"); let is_expr_intrinsic = is_emit_family || name.as_slice() == b"dropInPlace"; + // `@callC(R, addr, args...)` is a hybrid: a leading type arg, + // then expression args. flags=3 (bit0 expr args, bit1 leading + // TypeIdx header); rhs = ExtraIdx -> [retTypeIdx, argCount, + // arg0, ...] so astgen's [count, args...] readers work from + // extra+1. + if name.as_slice() == b"callC" { + // `void` isn't a jam type keyword (return slots are omitted + // instead), but a C signature needs to spell "no return" — + // accept it here only. + let ret_ty = if self.check(TokenType::Identifier) + && self.tokens[self.current].text(&self.source) == b"void" + { + self.advance(); + builtin::VOID + } else { + self.parse_type()? + }; + let mut args = Vec::new(); + while self.match_(TokenType::Comma) { + let a = self.parse_logical_or()?; + // `args...` pack spread, same as in plain call args. + if self.check(TokenType::Ellipsis) { + self.advance(); + let an = self.nodes.get(a); + if an.tag != AstTag::Variable { + return Err(self.parse_error("`...` must follow a pack name")); + } + let name_id = an.lhs; + args.push(self.emit(AstNode { + tag: AstTag::Spread, + op: 0, + flags: 0, + main_token: 0, + lhs: name_id, + rhs: 0, + })); + continue; + } + args.push(a); + } + self.consume( + TokenType::CloseParen, + "Expected ')' after '@' intrinsic arguments", + )?; + if args.is_empty() { + return Err(self.parse_error( + "@callC expects a function address after the return type", + )); + } + let e = self.nodes.reserve_extra(2 + args.len()); + self.nodes.set_extra(e, ret_ty.raw()); + self.nodes + .set_extra(ExtraIdx::new(e.raw() + 1), args.len() as u32); + for (i, a) in args.iter().enumerate() { + self.nodes + .set_extra(ExtraIdx::new(e.raw() + 2 + i as u32), a.raw()); + } + return Ok(self.emit(AstNode { + tag: AstTag::AtCall, + op: 0, + flags: 3, + main_token: 0, + lhs: name_id.raw(), + rhs: e.raw(), + })); + } if is_expr_intrinsic { let mut args = Vec::new(); if !self.check(TokenType::CloseParen) { @@ -1600,9 +1666,9 @@ impl<'a> Parser<'a> { self.advance(); // consume '(' let mut args = Vec::new(); if !self.check(TokenType::CloseParen) { - args.push(self.parse_comparison()?); + args.push(self.parse_call_arg()?); while self.match_(TokenType::Comma) { - args.push(self.parse_comparison()?); + args.push(self.parse_call_arg()?); } } self.consume( @@ -1712,6 +1778,30 @@ impl<'a> Parser<'a> { })) } + /// One call argument: an expression, optionally wrapped by a trailing + /// `...` into a pack-spread node (`args...` — forwards a variadic cfn's + /// pack, CFN_VARIADIC_PLAN.md). + fn parse_call_arg(&mut self) -> PResult { + let e = self.parse_comparison()?; + if self.check(TokenType::Ellipsis) { + self.advance(); + let n = self.nodes.get(e); + if n.tag != AstTag::Variable { + return Err(self.parse_error("`...` must follow a pack name")); + } + let name_id = n.lhs; + return Ok(self.emit(AstNode { + tag: AstTag::Spread, + op: 0, + flags: 0, + main_token: 0, + lhs: name_id, + rhs: 0, + })); + } + Ok(e) + } + /// Emit a `Call` node, choosing the direct (StringIdx callee) vs indirect /// (NodeIdx receiver, flags bit 0) encoding. fn emit_call(&mut self, expr: NodeIdx, bare_name: &[u8], args_extra: ExtraIdx) -> NodeIdx { @@ -1997,6 +2087,7 @@ impl<'a> Parser<'a> { let mut args: Vec = Vec::new(); let mut is_var_args = false; + let mut var_args_pack: Option = None; if !self.check(TokenType::CloseParen) { loop { if self.check(TokenType::Ellipsis) { @@ -2013,6 +2104,26 @@ impl<'a> Parser<'a> { self.consume(TokenType::Identifier, "Expected parameter name")?; let pname = String::from_utf8_lossy(self.prev_text()).into_owned(); self.consume(TokenType::Colon, "Expected ':' after parameter name")?; + // Named variadic pack `name: ...` — cfn-only instantiation + // axis (CFN_VARIADIC_PLAN.md), must be the last parameter. + if self.check(TokenType::Ellipsis) { + self.advance(); + if !is_cfn { + return Err( + self.parse_error("argument packs (`name: ...`) require `cfn`") + ); + } + if is_comp { + return Err(self.parse_error("a variadic pack cannot be `comp`")); + } + if self.match_(TokenType::Comma) { + return Err( + self.parse_error("the variadic pack must be the last parameter") + ); + } + var_args_pack = Some(pname); + break; + } let mode = if self.match_(TokenType::Mut) { jam_core::ParamMode::Mut } else if self.match_(TokenType::Move) { @@ -2060,6 +2171,7 @@ impl<'a> Parser<'a> { f.is_test = false; // extern fns are never tests f.is_var_args = is_var_args; f.is_cfn = is_cfn; + f.var_args_pack = var_args_pack; return Ok(f); } self.consume(TokenType::OpenBrace, "Expected '{' before function body")?; @@ -2074,6 +2186,7 @@ impl<'a> Parser<'a> { f.is_test = is_test; f.is_var_args = false; // var-args only applies to extern decls f.is_cfn = is_cfn; + f.var_args_pack = var_args_pack; Ok(f) } @@ -2582,6 +2695,166 @@ mod tests { (ns, root) } + /// Like `parse_expr` but the source must fail; returns the diagnostics. + fn parse_expr_err(src: &str) -> Vec { + let mut lx = Lexer::new(src.as_bytes().to_vec()); + lx.scan_tokens().unwrap(); + let tokens = lx.tokens().to_vec(); + let source = lx.source().to_vec(); + let mut tp = TypePool::new(); + let mut sp = StringPool::new(); + let mut ns = NodeStore::new(); + let mut diags = Diagnostics::new(); + { + let mut p = Parser::new( + tokens, source, &mut tp, &mut sp, &mut ns, &mut diags, "t.jam", + ); + p.parse_logical_or().expect_err("expected parse failure"); + } + diags.all().iter().map(|d| d.message.clone()).collect() + } + + #[test] + fn callc_parses_type_then_args() { + // @callC(f64, a, x, y) -> AtCall flags=3, + // extra = [F64, 3, addr, arg, arg]. + let (ns, root) = parse_expr("@callC(f64, a, x, y)"); + let n = ns.get(root); + assert_eq!(n.tag, AstTag::AtCall); + assert_eq!(n.flags, 3); + let e = ExtraIdx::new(n.rhs); + assert_eq!(ns.get_extra(e), builtin::F64.raw()); + assert_eq!(ns.get_extra(ExtraIdx::new(n.rhs + 1)), 3); + for i in 0..3 { + let arg = NodeIdx::new(ns.get_extra(ExtraIdx::new(n.rhs + 2 + i))); + assert_eq!(ns.get(arg).tag, AstTag::Variable); + } + } + + #[test] + fn callc_zero_call_args_is_addr_only() { + let (ns, root) = parse_expr("@callC(u64, a)"); + let n = ns.get(root); + assert_eq!(n.tag, AstTag::AtCall); + assert_eq!(n.flags, 3); + assert_eq!(ns.get_extra(ExtraIdx::new(n.rhs)), builtin::U64.raw()); + assert_eq!(ns.get_extra(ExtraIdx::new(n.rhs + 1)), 1); + } + + #[test] + fn callc_named_return_type() { + // A user type as R parses through parse_type (Named, not builtin). + let (ns, root) = parse_expr("@callC(NSRect, a, r)"); + let n = ns.get(root); + assert_eq!(n.tag, AstTag::AtCall); + assert_eq!(n.flags, 3); + let ret_ty = ns.get_extra(ExtraIdx::new(n.rhs)); + assert!(ret_ty > builtin::NORETURN.raw()); + assert_eq!(ns.get_extra(ExtraIdx::new(n.rhs + 1)), 2); + } + + #[test] + fn callc_void_return_type() { + // `void` is not a type keyword anywhere else in jam — the @callC + // branch accepts it as "no return" for C signatures. + let (ns, root) = parse_expr("@callC(void, a)"); + let n = ns.get(root); + assert_eq!(n.tag, AstTag::AtCall); + assert_eq!(n.flags, 3); + assert_eq!(ns.get_extra(ExtraIdx::new(n.rhs)), builtin::VOID.raw()); + } + + #[test] + fn callc_missing_addr_errors() { + let msgs = parse_expr_err("@callC(u64)"); + assert!( + msgs.iter().any(|m| m.contains("function address")), + "got: {msgs:?}" + ); + } + + /// Like `parse_module` (defined below) but the source must fail; returns + /// the diagnostics. + fn parse_module_err(src: &str) -> Vec { + let mut lx = Lexer::new(src.as_bytes().to_vec()); + lx.scan_tokens().unwrap(); + let tokens = lx.tokens().to_vec(); + let source = lx.source().to_vec(); + let mut tp = TypePool::new(); + let mut sp = StringPool::new(); + let mut ns = NodeStore::new(); + let mut diags = Diagnostics::new(); + { + let mut p = Parser::new( + tokens, source, &mut tp, &mut sp, &mut ns, &mut diags, "t.jam", + ); + p.parse().expect_err("expected parse failure"); + } + diags.all().iter().map(|d| d.message.clone()).collect() + } + + #[test] + fn cfn_pack_param_parses() { + let m = parse_module( + "const S = struct { v: u64, cfn f(self: S, args: ...) u64 { return 0; } };", + ); + let meth = &m.structs[0].methods[0]; + assert_eq!(meth.var_args_pack.as_deref(), Some("args")); + assert!(!meth.is_var_args, "pack must not set the extern varargs flag"); + assert!(meth.is_generic(), "pack-bearing cfn is an instantiation template"); + assert_eq!(meth.args.len(), 1, "pack is not a Param"); + } + + #[test] + fn pack_on_fn_rejected() { + let msgs = parse_module_err("fn f(args: ...) u64 { return 0; }"); + assert!( + msgs.iter().any(|m| m.contains("require `cfn`")), + "got: {msgs:?}" + ); + } + + #[test] + fn pack_not_last_rejected() { + let msgs = parse_module_err( + "const S = struct { v: u64, cfn f(self: S, args: ..., x: u64) {} };", + ); + assert!( + msgs.iter().any(|m| m.contains("last parameter")), + "got: {msgs:?}" + ); + } + + #[test] + fn spread_in_call_args_parses() { + let (ns, root) = parse_expr("g(x, args...)"); + let n = ns.get(root); + assert_eq!(n.tag, AstTag::Call); + let e = n.rhs; + assert_eq!(ns.get_extra(ExtraIdx::new(e)), 2); + let a1 = ns.get(NodeIdx::new(ns.get_extra(ExtraIdx::new(e + 2)))); + assert_eq!(a1.tag, AstTag::Spread); + } + + #[test] + fn spread_in_callc_args_parses() { + let (ns, root) = parse_expr("@callC(u64, a, args...)"); + let n = ns.get(root); + assert_eq!(n.tag, AstTag::AtCall); + assert_eq!(ns.get_extra(ExtraIdx::new(n.rhs + 1)), 2); + let a1 = ns.get(NodeIdx::new(ns.get_extra(ExtraIdx::new(n.rhs + 3)))); + assert_eq!(a1.tag, AstTag::Spread); + } + + #[test] + fn spread_after_non_name_rejected() { + let msgs = parse_expr_err("g(1 + 2 ...)"); + assert!( + msgs.iter().any(|m| m.contains("must follow a pack name")), + "got: {msgs:?}" + ); + } + #[test] fn precedence_mul_binds_tighter_than_add() { // 1 + 2 * 3 => Add(1, Mul(2,3)) diff --git a/crates/jam/src/cli/emit.rs b/crates/jam/src/cli/emit.rs index 95502a0..5439a25 100644 --- a/crates/jam/src/cli/emit.rs +++ b/crates/jam/src/cli/emit.rs @@ -275,6 +275,11 @@ fn dump_node( o.extend_from_slice(&sp.get(StringIdx::new(n.lhs))); o.push(b'\n'); } + AstTag::Spread => { + o.extend_from_slice(b"Spread "); + o.extend_from_slice(&sp.get(StringIdx::new(n.lhs))); + o.extend_from_slice(b"...\n"); + } AstTag::MemberAccess => { o.extend_from_slice(b"MemberAccess ."); o.extend_from_slice(&sp.get(StringIdx::new(n.rhs))); @@ -1972,6 +1977,9 @@ pub fn emit_jir(path: &str, mode: EmitMode, opt: OptLevel, lto: Lto, strip: Stri if !meth.is_pub && !meth.is_cfn { continue; } + if meth.is_generic() { + continue; + } let mut jfn = astgen_metadata(meth, &cg); jfn.name = mangled_function_name(meth, &cg.type_pool, &cg.string_pool); let _ = jir_declare_prototype(&jfn, &cg); @@ -1981,6 +1989,9 @@ pub fn emit_jir(path: &str, mode: EmitMode, opt: OptLevel, lto: Lto, strip: Stri } for s in &module.structs { for meth in &s.methods { + if meth.is_generic() { + continue; + } let mut jfn = astgen_metadata(meth, &cg); jfn.name = mangled_function_name(meth, &cg.type_pool, &cg.string_pool); let _ = jir_declare_prototype(&jfn, &cg); @@ -2025,6 +2036,9 @@ pub fn emit_jir(path: &str, mode: EmitMode, opt: OptLevel, lto: Lto, strip: Stri if !meth.is_pub && !meth.is_cfn { continue; } + if meth.is_generic() { + continue; + } if is_jir { lower(cg, meth, jir, errs); } else { @@ -2071,6 +2085,9 @@ pub fn emit_jir(path: &str, mode: EmitMode, opt: OptLevel, lto: Lto, strip: Stri } for s in &module.structs { for m in &s.methods { + if m.is_generic() { + continue; + } if is_jir { lower(cg, m, jir, errs); } else { @@ -2177,6 +2194,9 @@ pub fn emit_jir(path: &str, mode: EmitMode, opt: OptLevel, lto: Lto, strip: Stri } for s in &module.structs { for m in &s.methods { + if m.is_generic() { + continue; + } run_analysis_in(&cg, m, path); } } @@ -2193,6 +2213,9 @@ pub fn emit_jir(path: &str, mode: EmitMode, opt: OptLevel, lto: Lto, strip: Stri } for s in &pair.1.structs { for m in &s.methods { + if m.is_generic() { + continue; + } run_analysis_in(&cg, m, &imported_file); } } diff --git a/tests/reject/reject_callc_bad_addr_type.jam b/tests/reject/reject_callc_bad_addr_type.jam new file mode 100644 index 0000000..f2889cb --- /dev/null +++ b/tests/reject/reject_callc_bad_addr_type.jam @@ -0,0 +1,4 @@ +// expect-error: @callC address must be a u64 +fn f(b: bool) u64 { + return @callC(u64, b); +} diff --git a/tests/reject/reject_callc_large_aggregate.jam b/tests/reject/reject_callc_large_aggregate.jam new file mode 100644 index 0000000..5e38669 --- /dev/null +++ b/tests/reject/reject_callc_large_aggregate.jam @@ -0,0 +1,9 @@ +// expect-error: unsupported by @callC +// 24-byte non-HFA aggregate: the naive indirect lowering would +// miscompile it (needs byval/sret — CALLC_PLAN.md phase 4), so it must +// be rejected at compile time. +const S = struct { a: u64, b: u64, c: u64 }; + +fn f(p: u64, s: S) u64 { + return @callC(u64, p, s); +} diff --git a/tests/reject/reject_callc_missing_addr.jam b/tests/reject/reject_callc_missing_addr.jam new file mode 100644 index 0000000..55f91dd --- /dev/null +++ b/tests/reject/reject_callc_missing_addr.jam @@ -0,0 +1,4 @@ +// expect-error: @callC expects a function address after the return type +fn f() u64 { + return @callC(u64); +} diff --git a/tests/reject/reject_callc_subword_aggregate.jam b/tests/reject/reject_callc_subword_aggregate.jam new file mode 100644 index 0000000..0b07cdc --- /dev/null +++ b/tests/reject/reject_callc_subword_aggregate.jam @@ -0,0 +1,9 @@ +// expect-error: unsupported by @callC +// div_t's shape: 8 bytes, but sub-word fields — LLVM would split it +// into two w-registers where the C ABI packs it into one x-register. +// Rejected until phase-4 coercion (CALLC_PLAN.md). +const DivT = struct { quot: i32, rem: i32 }; + +fn f(p: u64, d: DivT) u64 { + return @callC(u64, p, d); +} diff --git a/tests/reject/reject_callc_void_return_value.jam b/tests/reject/reject_callc_void_return_value.jam new file mode 100644 index 0000000..1d02e20 --- /dev/null +++ b/tests/reject/reject_callc_void_return_value.jam @@ -0,0 +1,8 @@ +// expect-error: cannot return a void expression +// Used to compile and crash at runtime before the void-expr return +// was gated on the function's own return type. +extern fn free(ptr: *mut[] u8); + +fn f(a: u64) u64 { + return @callC(void, a); +} diff --git a/tests/reject/reject_pack_not_last.jam b/tests/reject/reject_pack_not_last.jam new file mode 100644 index 0000000..11a43a0 --- /dev/null +++ b/tests/reject/reject_pack_not_last.jam @@ -0,0 +1,5 @@ +// expect-error: must be the last parameter +const S = struct { + v: u64, + cfn f(self: S, args: ..., x: u64) u64 { return x; } +}; diff --git a/tests/reject/reject_pack_on_comptime_cfn.jam b/tests/reject/reject_pack_on_comptime_cfn.jam new file mode 100644 index 0000000..51f8a04 --- /dev/null +++ b/tests/reject/reject_pack_on_comptime_cfn.jam @@ -0,0 +1,9 @@ +// expect-error: variadic packs are not supported on comptime cfns +// A top-level cfn (first param not `self`) is a comptime fn — its args +// must fold to constants, so a pack of runtime values is meaningless. +cfn log(tag: u64, args: ...) { +} + +fn main() { + log(1, 2); +} diff --git a/tests/reject/reject_pack_on_fn.jam b/tests/reject/reject_pack_on_fn.jam new file mode 100644 index 0000000..7be13e1 --- /dev/null +++ b/tests/reject/reject_pack_on_fn.jam @@ -0,0 +1,4 @@ +// expect-error: require `cfn` +fn f(args: ...) u64 { + return 0; +} diff --git a/tests/reject/reject_spread_outside_pack.jam b/tests/reject/reject_spread_outside_pack.jam new file mode 100644 index 0000000..a7f2175 --- /dev/null +++ b/tests/reject/reject_spread_outside_pack.jam @@ -0,0 +1,4 @@ +// expect-error: only valid inside a variadic cfn +fn f(a: u64) u64 { + return @callC(u64, a, args...); +} diff --git a/tests/unit/test_callc.jam b/tests/unit/test_callc.jam new file mode 100644 index 0000000..9a3db5a --- /dev/null +++ b/tests/unit/test_callc.jam @@ -0,0 +1,65 @@ +// `@callC(R, addr, args...)` — indirect C call through a raw function +// address, signature synthesized from the args' static types +// (CALLC_PLAN.md). Exercises libc through raw addresses: int and float +// args/returns (FP-register placement both directions), a mixed +// int+float shape, a zero-arg tail, a void return, and a small +// aggregate return. +const { assert } = import("test"); + +extern fn abs(x: i32) i32; +extern fn fabs(x: f64) f64; +extern fn fmax(a: f64, b: f64) f64; +extern fn ldexp(x: f64, n: i32) f64; +extern fn getpid() i32; +extern fn malloc(size: u64) *mut[] u8; +extern fn free(ptr: *mut[] u8); +extern fn lldiv(numer: i64, denom: i64) i64; + +// lldiv(3) really returns lldiv_t { quot, rem } — two 64-bit words in +// the x0/x1 return pair, the aggregate shape the guard admits. (div_t's +// {i32,i32} is deliberately rejected: LLVM would split it across two +// registers where C packs it into one.) +const LldivT = struct { quot: i64, rem: i64 }; + +tfn callcIntArgAndReturn() { + const addr: u64 = abs as u64; + assert(@callC(i32, addr, -7 as i32), 7); +} + +tfn callcFloatArgAndReturn() { + // f64 through FP registers both directions (the SigF1 shape). + assert(@callC(f64, fabs as u64, -2.5) as i32, 2); +} + +tfn callcTwoFloatArgs() { + // Both args must land in v0/v1 (the SigD2 shape). + assert(@callC(f64, fmax as u64, 1.5, 2.5) as i32, 2); +} + +tfn callcMixedFloatIntArgs() { + // f64 in v0, i32 in w1 — the interleaving the C-varargs ABI would + // get wrong, and the whole reason @callC synthesizes a fixed + // signature. ldexp(1.5, 3) = 12. + assert(@callC(f64, ldexp as u64, 1.5, 3 as i32) as i32, 12); +} + +tfn callcZeroArgs() { + const pid: i32 = @callC(i32, getpid as u64); + var ok: i32 = 0; + if (pid > 0) { ok = 1; } + assert(ok, 1); +} + +tfn callcVoidReturn() { + var p: *mut[] u8 = malloc(16); + p[0] = 42; + assert(p[0] as i32, 42); + @callC(void, free as u64, p as u64); +} + +tfn callcSmallStructReturn() { + // 16-byte two-word aggregate return rides the x0/x1 pair. + const d: LldivT = @callC(LldivT, lldiv as u64, 7 as i64, 2 as i64); + assert(d.quot as i32, 3); + assert(d.rem as i32, 1); +} diff --git a/tests/unit/test_cfn_variadic.jam b/tests/unit/test_cfn_variadic.jam new file mode 100644 index 0000000..4adf9e9 --- /dev/null +++ b/tests/unit/test_cfn_variadic.jam @@ -0,0 +1,58 @@ +// Variadic cfn (`args: ...` — CFN_VARIADIC_PLAN.md): a struct cfn method +// with a trailing pack is instantiated per call-site tail shape; the pack +// forwards with `args...` into @callC (synthesizing the C signature from +// the tail's static types) or into an extern C-variadic call. +const { assert } = import("test"); + +extern fn abs(x: i32) i32; +extern fn fabs(x: f64) f64; +extern fn fmax(a: f64, b: f64) f64; +extern fn snprintf(buf: *mut[] u8, size: u64, fmt: *const[] u8, ...) i32; + +const Caller = struct { + addr: u64, + cfn callThrough(self: Caller, R: type, args: ...) R { + return @callC(R, self.addr, args...); + } +}; + +tfn packIntShape() { + const c: Caller = Caller { addr: abs as u64 }; + assert(c.callThrough(i32, -7 as i32), 7); +} + +tfn packFloatShape() { + // A different tail shape through the SAME cfn — its own clone. + const c: Caller = Caller { addr: fabs as u64 }; + assert(c.callThrough(f64, -2.5) as i32, 2); +} + +tfn packTwoFloatArgs() { + const c: Caller = Caller { addr: fmax as u64 }; + assert(c.callThrough(f64, 1.5, 2.5) as i32, 2); +} + +tfn packSameShapeMemoizes() { + // Two calls at one shape reuse one clone; results stay independent. + const c: Caller = Caller { addr: abs as u64 }; + assert(c.callThrough(i32, -3 as i32), 3); + assert(c.callThrough(i32, 11 as i32), 11); +} + +const Fmt = struct { + pad: u64, + cfn format(self: Fmt, buf: *mut[] u8, size: u64, fmt: *const[] u8, args: ...) i32 { + // Pack forwarding into an extern C-variadic (the varargs-tail path). + return snprintf(buf, size, fmt, args...); + } +}; + +tfn packForwardsToCVarargs() { + var b: [64]u8 = [0; 64]; + const f: Fmt = Fmt { pad: 0 }; + const fmtStr: str = "%d-%d"; + const n: i32 = f.format(b.asMutPtr(), 64, fmtStr.ptr, 7 as i32, 42 as i32); + assert(n, 4); // "7-42" + assert(b[0] as i32, 55); // '7' + assert(b[1] as i32, 45); // '-' +}