From debe349499dab46e2a9b351549ac1976180b20d0 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 9 Sep 2026 00:44:34 +0300 Subject: [PATCH 1/3] jit: an intrinsic emitter keys off the declared name, and a function value casts through its { ptr } aggregate Ten intrinsic handlers keyed on `expr.name`, the call-site spelling, while the dispatch tables key on the declaration. A module-qualified call kept its qualifier: as an intrinsic-name fragment that built `llvm.math::sqrt.f32`, which LLVM does not know and the C API dereferenced anyway; as a branch key it picked the other arm, so `math::min` emitted max. They read `expr.func.name` now, and the wrapper refuses intrinsic id 0. A missing intrinsic is no state to return: `LLVMLookupIntrinsicID` answers 0 only for a name that is no intrinsic, and LLVM's table is target-independent, so a foreign target's names resolve on any host built with it. It means the emitter built the name wrongly, and no caller can recover. The twenty-two lookups of a name the emitter builds go through `intrinsic_id`, which panics naming the string; the nineteen target-specific literals keep the raw lookup and their own fallback, since a trimmed LLVM can genuinely lack those. Three handlers also stop using the null the intrinsic builder returns for an operand type it does not serve, which is a different and reachable path. `visitExprCast` had no arm for `Type.tFunction`, so a function-address reinterpret emitted a bitcast between the `{ ptr }` aggregate and a scalar and the program failed to simulate. Two arms go through element 0 instead. Fixes #3968 Fixes #3969 --- modules/dasLLVM/REVIEW.md | 18 ++- modules/dasLLVM/daslib/llvm_boost.das | 11 +- modules/dasLLVM/daslib/llvm_jit.das | 20 ++- modules/dasLLVM/daslib/llvm_jit_intrin.das | 98 +++++++-------- modules/dasLLVM/daslib/llvm_jit_run.das | 4 +- tests/math/test_qualified_math_calls.das | 135 +++++++++++++++++++++ 6 files changed, 224 insertions(+), 62 deletions(-) create mode 100644 tests/math/test_qualified_math_calls.das diff --git a/modules/dasLLVM/REVIEW.md b/modules/dasLLVM/REVIEW.md index 81531e8f6a..63ebe78c3b 100644 --- a/modules/dasLLVM/REVIEW.md +++ b/modules/dasLLVM/REVIEW.md @@ -112,18 +112,24 @@ every box, so every perm that requires it silently declines to its fallback and no error names the cause. +- **An emitter under `daslib/` that uses a call's name as a KEY - an intrinsic-name fragment, a + lookup-table key, a branch on one spelling - reads `expr.func.name`, never `expr.name`; + `expr.name` stays only in a diagnostic or an LLVM value name, where it is what the user + wrote.** The dispatch tables key off the declaration, so a call site can still spell itself + module-qualified. + - **A diff that adds or changes a `build_vector_*` emitter (`daslib/llvm_jit_intrin.das`) emits each Horner step unfused, through `vmath_poly_step`, and calls `vmath_fma` only for the steps vecmath itself writes fused** (`ARCHITECTURE.md#vector-poly-fusion`). One fused step in a sign-alternating chain moves the last few bits of the result, and the interpreter and AOT answers do not move with it. -- **A diff that adds or changes an intrinsic emitter whose daslang body is the reference - implementation - a `build_vector_*` emitter, an `idot` lowering - also adds a cell comparing - the emitted result with the interpreted result over the operand range the emitter serves - (every vector width for `build_vector_*`, the full int8 lattice for a dot), and for a float - emitter one asserting both answer NaN in the same lanes; a lowering only a cross target runs - states in the PR body the artifact that compared them.** A clamp or a conversion written with +- **A diff that changes what an emitter whose daslang body is the reference implementation + produces - the emitter itself, or which of its arms a call selects - also adds a cell + comparing the emitted result with the interpreted result over the operand range that emitter + serves (every vector width for a vector emitter, the full int8 lattice for a dot), and for a + float emitter one asserting both answer NaN in the same lanes; a lowering only a cross target + runs states in the PR body the artifact that compared them.** A clamp or a conversion written with ordered compares turns a NaN lane into a number, and an accuracy bound reads that as success; an IR-shape test names the instruction and never a number. diff --git a/modules/dasLLVM/daslib/llvm_boost.das b/modules/dasLLVM/daslib/llvm_boost.das index f865318f94..c5f02b7444 100644 --- a/modules/dasLLVM/daslib/llvm_boost.das +++ b/modules/dasLLVM/daslib/llvm_boost.das @@ -425,7 +425,16 @@ def LLVMLookupIntrinsicID(name : string) { return LLVMLookupIntrinsicID(name, uint64(long_length(name))) } -def LLVMGetIntrinsicDeclaration(mod : LLVMOpaqueModule?; id : uint; var argTypes : array) { +//! Looks up an intrinsic name the emitter BUILT, and panics naming it when LLVM has none. A built name that does not resolve is an emitter defect - a table row, a stem, or a type suffix - and no caller can recover from it. A target-specific literal a trimmed LLVM may genuinely lack keeps the raw lookup and its own fallback. +def public intrinsic_id(name : string) : uint { + let id = LLVMLookupIntrinsicID(name, uint64(long_length(name))) + panic("no LLVM intrinsic named '{name}'") if (id == 0u) + return id +} + +//! Panics on intrinsic id 0. LLVMLookupIntrinsicID answers 0 only for a name that is no intrinsic - an emitter that built the name wrongly, never a target LLVM lacks - and the C API dereferences the id. +def LLVMGetIntrinsicDeclaration(mod : LLVMOpaqueModule?; id : uint; var argTypes : array) : LLVMOpaqueValue? { + panic("LLVM intrinsic id is 0 - the emitter asked for a name LLVM has no intrinsic for") if (id == 0u) return LLVMGetIntrinsicDeclaration(mod, id, array_data_ptr(argTypes), uint64(long_length(argTypes))) } diff --git a/modules/dasLLVM/daslib/llvm_jit.das b/modules/dasLLVM/daslib/llvm_jit.das index 038f49935d..567c7d98df 100644 --- a/modules/dasLLVM/daslib/llvm_jit.das +++ b/modules/dasLLVM/daslib/llvm_jit.das @@ -262,6 +262,10 @@ def public handle_each_is_jitable(src : ExpressionPtr) : bool { return get_jit_each(srcT.annotation) != null } +def private is_int64_or_uint64(t : Type) : bool { + return t == Type.tInt64 || t == Type.tUInt64 +} + //! The jit passes a handled index in its own C ABI, so the index has to be a primitive the //! emitter can put in a register. Anything else (a typed id, a struct key) gets no address and //! keeps the interpreter, whatever the annotation offers. Returns -1 when not passable. @@ -2042,7 +2046,7 @@ class public LlvmJitVisitor : AstVisitor { // (N == TABLE_MAX_LINEAR_CAPACITY == 8, so the mask is exactly i8 and cttz.i8 fits.) def first_set_lane_or_neg1(maskI8 : LLVMOpaqueValue?) : LLVMOpaqueValue? { var argTypes <- [ types.t_int8 ] - let id = LLVMLookupIntrinsicID("llvm.cttz.i8") + let id = intrinsic_id("llvm.cttz.i8") var decl = LLVMGetIntrinsicDeclaration(g_mod, id, argTypes) var cttzTy = LLVMFunctionType(types.t_int8, [ types.t_int8, types.t_int1 ]) var cttzArgs <- [ maskI8, LLVMConstInt(types.t_int1, 0ul, 0) ] @@ -3371,7 +3375,7 @@ class public LlvmJitVisitor : AstVisitor { } var args <- [ left, left, right] var argTypes <- [ base_type_to_llvm_type(shiftType.baseType)] - let id = LLVMLookupIntrinsicID(fshr_name) + let id = intrinsic_id(fshr_name) var decl = LLVMGetIntrinsicDeclaration(g_mod, id, argTypes) if (decl == null) { failed_E(expr, "failed to get intrinsic {fshr_name}") @@ -3390,7 +3394,7 @@ class public LlvmJitVisitor : AstVisitor { } var args <- [ r2v_left, r2v_left, right] var argTypes <- [ base_type_to_llvm_type(shiftType.baseType)] - let id = LLVMLookupIntrinsicID(fshr_name) + let id = intrinsic_id(fshr_name) var decl = LLVMGetIntrinsicDeclaration(g_mod, id, argTypes) if (decl == null) { failed_E(expr, "failed to get intrinsic {fshr_name}") @@ -5718,8 +5722,16 @@ class public LlvmJitVisitor : AstVisitor { res = LLVMBuildPointerCast(g_builder, subE, type_to_llvm_type(ect), "cast_p") } elif (ect.baseType == Type.tFixedArray && subT.isPointer) { res = LLVMBuildPointerCast(g_builder, subE, LLVMPointerType(type_to_llvm_type(ect), 0u), "") - } elif (ect.isInteger && (ect.baseType == Type.tInt64 || ect.baseType == Type.tUInt64) && subT.isPointer) { + } elif (is_int64_or_uint64(ect.baseType) && subT.isPointer) { res = LLVMBuildPtrToInt(g_builder, subE, type_to_llvm_type(ect), "cast_p_i") + } elif (subT.baseType == Type.tFunction && (ect.isPointer || is_int64_or_uint64(ect.baseType))) { + // reinterpret(f) + let fn_ptr = LLVMBuildExtractValue(g_builder, subE, 0u, "fn_ptr") + res = ect.isPointer ? fn_ptr : LLVMBuildPtrToInt(g_builder, fn_ptr, type_to_llvm_type(ect), "cast_fn_i") + } elif (ect.baseType == Type.tFunction && (subT.isPointer || is_int64_or_uint64(subT.baseType))) { + // reinterpret(h) + let fn_ptr = subT.isPointer ? subE : LLVMBuildIntToPtr(g_builder, subE, LLVMPointerType(g_t_simFunction, 0u), "fn_ptr") + res = LLVMBuildInsertValue(g_builder, LLVMGetUndef(g_t_function), fn_ptr, 0u, "cast_fn") } elif ((ect.isPointer || ect.isString) && subT.isInteger) { res = LLVMBuildIntToPtr(g_builder, subE, type_to_llvm_type(ect), "cast_i_p") } elif (!ect.isRefType && !subT.isRefType) { diff --git a/modules/dasLLVM/daslib/llvm_jit_intrin.das b/modules/dasLLVM/daslib/llvm_jit_intrin.das index 41c3d7b754..5c06b73d40 100644 --- a/modules/dasLLVM/daslib/llvm_jit_intrin.das +++ b/modules/dasLLVM/daslib/llvm_jit_intrin.das @@ -813,7 +813,7 @@ def build_const_int_splat(elemT : LLVMOpaqueType?; lanes : int; value : int) : L def build_int_minmax_call(var ctx : JitCtx; iname : string; a, b : LLVMOpaqueValue?; vecT : LLVMOpaqueType?) : LLVMOpaqueValue? { var argTypes <- [vecT] - let id = LLVMLookupIntrinsicID(iname) + let id = intrinsic_id(iname) var decl = LLVMGetIntrinsicDeclaration(g_mod, id, argTypes) var typ = LLVMFunctionType(vecT, [vecT, vecT]) var args <- [a, b] @@ -848,7 +848,7 @@ def intrinsic_builtin_float16(var ctx : JitCtx; expr : ExprCallFunc?; arguments } def intrinsic_half8_lo_hi(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { - let base = expr.name == "half8_lo" ? 0 : 4 + let base = expr.func.name == "half8_lo" ? 0 : 4 let mask <- [for (i in range(4)); base + i] var quad = LLVMBuildShuffleVector(ctx.builder, ctx.types, arguments[0], arguments[0], mask, string(expr.name)) return LLVMBuildFPExt(ctx.builder, quad, LLVMVectorType(ctx.types.t_float, 4u), "") @@ -903,7 +903,7 @@ def build_op_name(op_name : string; opType : TypeDeclPtr) : string { def intrinsic_bit_nzp_op1(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { assume opType = expr.arguments[0]._type var op_name : string - bit_op1_name |> get(string(expr.name)) $(pname) { + bit_op1_name |> get(string(expr.func.name)) $(pname) { op_name = pname } if (op_name == "") { @@ -917,7 +917,7 @@ def intrinsic_bit_nzp_op1(var ctx : JitCtx; expr : ExprCallFunc?; arguments : ar } var args <- [ arguments[0], LLVMConstInt(ctx.types.t_int1, 0ul, 0)] var argTypes <- [ type_to_llvm_abi_type(expr.arguments[0]._type)] - let id = LLVMLookupIntrinsicID(sqrt_name) + let id = intrinsic_id(sqrt_name) var decl = LLVMGetIntrinsicDeclaration(g_mod, id, argTypes) if (decl == null) { failed_E(expr, "missing intrinsic {sqrt_name}") @@ -930,7 +930,7 @@ def intrinsic_bit_nzp_op1(var ctx : JitCtx; expr : ExprCallFunc?; arguments : ar def intrinsic_bit_op1(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { assume opType = expr.arguments[0]._type var op_name : string - bit_op1_name |> get(string(expr.name)) $(pname) { + bit_op1_name |> get(string(expr.func.name)) $(pname) { op_name = pname } if (op_name == "") { @@ -944,7 +944,7 @@ def intrinsic_bit_op1(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array< } var args <- [ arguments[0]] var argTypes <- [ type_to_llvm_abi_type(expr.arguments[0]._type)] - let id = LLVMLookupIntrinsicID(sqrt_name) + let id = intrinsic_id(sqrt_name) var decl = LLVMGetIntrinsicDeclaration(g_mod, id, argTypes) if (decl == null) { failed_E(expr, "missing intrinsic {sqrt_name}") @@ -963,7 +963,7 @@ let private op2_name <- { def intrinsic_op2(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { assume opType = expr.arguments[0]._type var op_name : string - op2_name |> get(string(expr.name)) $(pname) { + op2_name |> get(string(expr.func.name)) $(pname) { op_name = pname } if (op_name == "") { @@ -977,7 +977,7 @@ def intrinsic_op2(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { - return build_math_minmax(ctx.builder, string(expr.name), expr, arguments[0], arguments[1]) + return build_math_minmax(ctx.builder, string(expr.func.name), expr, arguments[0], arguments[1]) } def intrinsic_math_clamp(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { @@ -1048,6 +1048,7 @@ def intrinsic_math_rsqrt(var ctx : JitCtx; expr : ExprCallFunc?; arguments : arr if (argType.baseType == Type.tFloat || (argType.isVectorType && argType.vectorBaseType == Type.tFloat)) { var one = build_broadcast_vector(ctx.builder, argType, LLVMConstReal(ctx.types.t_float, 1.0lf)) var sq = intrinsic_math_any_float_op1("sqrt", ctx.builder, expr, arguments) + return null if (sq == null) return LLVMBuildFDiv(ctx.builder, one, sq, "rsqrt") } else { failed_E(expr, "{expr.name}({describe(argType)}) is not supported (yet?)") @@ -1055,44 +1056,46 @@ def intrinsic_math_rsqrt(var ctx : JitCtx; expr : ExprCallFunc?; arguments : arr } } -def intrinsic_math_any_float_op1(expr_name : string; g_builder : LLVMOpaqueBuilder?; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { - var sqrt_name = "llvm.{expr_name}.f32" +def intrinsic_math_any_float_op1(math_op : string; g_builder : LLVMOpaqueBuilder?; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { + var sqrt_name = "llvm.{math_op}.f32" assume opType = expr.arguments[0]._type if (opType.isFloatOrDouble || (opType.isVectorType && opType.vectorBaseType == Type.tFloat)) { if (opType.baseType == Type.tDouble) { - sqrt_name = "llvm.{expr_name}.f64" + sqrt_name = "llvm.{math_op}.f64" } } else { - failed_E(expr, "{expr_name}({describe(opType)}) is not supported (yet?)") + failed_E(expr, "{math_op}({describe(opType)}) is not supported (yet?)") return null } var args <- [ arguments[0]] var argTypes <- [ type_to_llvm_abi_type(expr.arguments[0]._type)] - let id = LLVMLookupIntrinsicID(sqrt_name) + let id = intrinsic_id(sqrt_name) var decl = LLVMGetIntrinsicDeclaration(g_mod, id, argTypes) if (decl == null) { failed_E(expr, "missing intrinsic {sqrt_name}") return null } var typ = LLVMFunctionType(argTypes[0], argTypes) // type : a -> a - return LLVMBuildCall2(g_builder, typ, decl, args, expr_name) + return LLVMBuildCall2(g_builder, typ, decl, args, math_op) } def intrinsic_math_float_op1(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { - return intrinsic_math_any_float_op1(string(expr.name), ctx.builder, expr, arguments) + return intrinsic_math_any_float_op1(string(expr.func.name), ctx.builder, expr, arguments) } def intrinsic_math_float_op1_to_int(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { var op_name = "" - peek(expr.name) $(s) { + peek(expr.func.name) $(s) { op_name = slice(s, 0, -1) } var res = intrinsic_math_any_float_op1(op_name, ctx.builder, expr, arguments) + return null if (res == null) return LLVMBuildFPToSI(ctx.builder, res, type_to_llvm_abi_type(expr._type), "") } def intrinsic_math_fract(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { var fl = intrinsic_math_any_float_op1("floor", ctx.builder, expr, arguments) + return null if (fl == null) return LLVMBuildFSub(ctx.builder, arguments[0], fl, "fract") } @@ -1208,7 +1211,7 @@ def private vmath_poly_gate(opType : TypeDeclPtr) : bool { } def private vmath_fma_decl(opType : TypeDeclPtr; lt : LLVMOpaqueType?) : LLVMOpaqueValue? { - let fid = LLVMLookupIntrinsicID(build_op_name("fmuladd", opType)) + let fid = intrinsic_id(build_op_name("fmuladd", opType)) return fid != 0u ? LLVMGetIntrinsicDeclaration(g_mod, fid, [ lt]) : null } @@ -1220,7 +1223,7 @@ def private vmath_poly_step(bld : LLVMOpaqueBuilder?; a, b, c : LLVMOpaqueValue? def private vmath_call1(ctx : JitCtx; opType : TypeDeclPtr; op_name : string; x : LLVMOpaqueValue?) : LLVMOpaqueValue? { let lt = type_to_llvm_type(opType) let intrin_name = build_op_name(op_name, opType) - let id = LLVMLookupIntrinsicID(intrin_name) + let id = intrinsic_id(intrin_name) let decl = id != 0u ? LLVMGetIntrinsicDeclaration(g_mod, id, [ lt]) : null if (decl == null) { failed("missing intrinsic {intrin_name}") @@ -1231,7 +1234,7 @@ def private vmath_call1(ctx : JitCtx; opType : TypeDeclPtr; op_name : string; x // overloaded on result AND operand type: the declaration takes both, result first (llvm.fptosi.sat.v4i32.v4f32) def private vmath_call1_2t(ctx : JitCtx; intrin_name : string; resTy, argTy : LLVMOpaqueType?; x : LLVMOpaqueValue?) : LLVMOpaqueValue? { - let id = LLVMLookupIntrinsicID(intrin_name) + let id = intrinsic_id(intrin_name) var declTypes <- [ resTy, argTy] let decl = id != 0u ? LLVMGetIntrinsicDeclaration(g_mod, id, declTypes) : null if (decl == null) { @@ -1348,11 +1351,11 @@ def private build_vector_sincos(ctx : JitCtx; opType : TypeDeclPtr; x : LLVMOpaq def intrinsic_math_sincos(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { assume opType = expr.arguments[0]._type - let name = string(expr.name) + let func_name = string(expr.func.name) if (vmath_poly_gate(opType)) { - return build_vector_sincos(ctx, opType, arguments[0], name == "cos") + return build_vector_sincos(ctx, opType, arguments[0], func_name == "cos") } - return intrinsic_math_any_float_op1(name, ctx.builder, expr, arguments) + return intrinsic_math_any_float_op1(func_name, ctx.builder, expr, arguments) } // Vectorized tanf as inline IR - vecmath v_tan (dag_vecMath_trig.h), NOT sin/cos then divide: @@ -1529,11 +1532,11 @@ def private build_vector_hyper(ctx : JitCtx; opType : TypeDeclPtr; x : LLVMOpaqu def intrinsic_math_sinh_cosh_tanh(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { assume opType = expr.arguments[0]._type - let name = string(expr.name) + let func_name = string(expr.func.name) if (vmath_poly_gate(opType)) { - return build_vector_hyper(ctx, opType, arguments[0], name) + return build_vector_hyper(ctx, opType, arguments[0], func_name) } - return intrinsic_math_any_float_op1(name, ctx.builder, expr, arguments) + return intrinsic_math_any_float_op1(func_name, ctx.builder, expr, arguments) } // ===== the idot family: exact integer dots on the 8-bit lattice ===== @@ -1601,7 +1604,7 @@ def intrinsic_lattice_idot(var ctx : JitCtx; expr : ExprCallFunc?; arguments : a let v4i32 = ctx.types.LLVMInt4Type() var s = idot_wasm_simd128(ctx, null, arguments[0], arguments[1]) if (s != null) { - let rid = LLVMLookupIntrinsicID("llvm.vector.reduce.add") + let rid = intrinsic_id("llvm.vector.reduce.add") var rtys <- [v4i32] var rdecl = rid != 0u ? LLVMGetIntrinsicDeclaration(g_mod, rid, rtys) : null if (rdecl != null) { @@ -1615,7 +1618,7 @@ def intrinsic_lattice_idot(var ctx : JitCtx; expr : ExprCallFunc?; arguments : a let v4i32 = ctx.types.LLVMInt4Type() var s = idot_sdot_aarch64(ctx, LLVMConstNull(v4i32), arguments[0], arguments[1]) if (s != null) { - let rid = LLVMLookupIntrinsicID("llvm.vector.reduce.add") + let rid = intrinsic_id("llvm.vector.reduce.add") var rtys <- [v4i32] var rdecl = rid != 0u ? LLVMGetIntrinsicDeclaration(g_mod, rid, rtys) : null if (rdecl != null) { @@ -1627,7 +1630,7 @@ def intrinsic_lattice_idot(var ctx : JitCtx; expr : ExprCallFunc?; arguments : a } var prod = idot_products(ctx, expr, arguments[0], arguments[1], 0) let v16i32 = LLVMVectorType(ctx.types.t_int32, 16u) - let id = LLVMLookupIntrinsicID("llvm.vector.reduce.add") + let id = intrinsic_id("llvm.vector.reduce.add") var tys <- [v16i32] var decl = id != 0u ? LLVMGetIntrinsicDeclaration(g_mod, id, tys) : null if (decl == null) { @@ -1708,7 +1711,7 @@ def intrinsic_math_mad_op3(var ctx : JitCtx; expr : ExprCallFunc?; arguments : a if (bt == Type.tFloat || bt == Type.tDouble || bt == Type.tFloat2 || bt == Type.tFloat3 || bt == Type.tFloat4) { // a*b+c as @llvm.fmuladd — the backend fuses to one fmla (mad is a MAC by contract) let lt = type_to_llvm_abi_type(expr._type) - let id = LLVMLookupIntrinsicID(build_op_name("fmuladd", expr._type)) + let id = intrinsic_id(build_op_name("fmuladd", expr._type)) var fatys <- [ lt] var decl = id != 0u ? LLVMGetIntrinsicDeclaration(g_mod, id, fatys) : null if (decl != null) { @@ -1751,7 +1754,7 @@ def intrinsic_math_abs_float(int_name : string; g_builder : LLVMOpaqueBuilder?; assume opType = expr.arguments[0]._type var args <- [ arguments[0]] var argTypes <- [ type_to_llvm_abi_type(opType)] - let id = LLVMLookupIntrinsicID(int_name) + let id = intrinsic_id(int_name) var decl = LLVMGetIntrinsicDeclaration(g_mod, id, argTypes) if (decl == null) { failed_E(expr, "missing intrinsic {int_name}") @@ -1765,7 +1768,7 @@ def intrinsic_math_abs_int(abs_name : string; ctx : JitCtx; expr : ExprCallFunc? assume opType = expr.arguments[0]._type var args <- [ arguments[0], LLVMConstInt(ctx.types.t_int1, 0ul, 0)] var argTypes <- [ type_to_llvm_abi_type(opType)] - let id = LLVMLookupIntrinsicID(abs_name) + let id = intrinsic_id(abs_name) var decl = LLVMGetIntrinsicDeclaration(g_mod, id, argTypes) if (decl == null) { failed_E(expr, "missing intrinsic {abs_name}") @@ -1799,7 +1802,7 @@ def build_fadd(var ctx : JitCtx; opType : TypeDeclPtr; v2 : LLVMOpaqueValue?; na let neg_0 = LLVMConstReal(ctx.types.t_float, -double(0.)) var args_fadd <- [ neg_0, v2] var argTypes_fadd <- [ type_to_llvm_abi_type(opType)] - let id = LLVMLookupIntrinsicID(fadd_name) + let id = intrinsic_id(fadd_name) var decl_fadd = LLVMGetIntrinsicDeclaration(g_mod, id, argTypes_fadd) if (decl_fadd == null) { failed("missing intrinsic {fadd_name}") @@ -1813,7 +1816,7 @@ def build_fsqrt(var ctx : JitCtx; v2 : LLVMOpaqueValue?; name : string) : LLVMOp let fsqrt_name = "llvm.sqrt.f32" var args_fsqrt <- [ v2] var argTypes_fsqrt <- [ ctx.types.t_float] - let id = LLVMLookupIntrinsicID(fsqrt_name) + let id = intrinsic_id(fsqrt_name) var decl_fsqrt = LLVMGetIntrinsicDeclaration(g_mod, id, argTypes_fsqrt) if (decl_fsqrt == null) { failed("missing intrinsic {fsqrt_name}") @@ -1855,10 +1858,10 @@ def intrinsic_math_dot(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array def intrinsic_math_hminmax(var ctx : JitCtx; expr : ExprCallFunc?; arguments : array) : LLVMOpaqueValue? { assume opType = expr.arguments[0]._type assert(opType.isVectorType) - let op = expr.name == "hmin" ? "fmin" : "fmax" + let op = expr.func.name == "hmin" ? "fmin" : "fmax" let iname = "llvm.vector.reduce.{op}.v{opType.vectorDim}f32" var argTypes <- [ type_to_llvm_abi_type(opType)] - let id = LLVMLookupIntrinsicID(iname) + let id = intrinsic_id(iname) var decl = LLVMGetIntrinsicDeclaration(g_mod, id, argTypes) if (decl == null) { failed("missing intrinsic {iname}") @@ -2409,9 +2412,8 @@ def intrinsic_dot4_x64(var ctx : JitCtx; _expr : ExprCallFunc?; arguments : arra var xvec = LLVMBuildLoad2(ctx.builder, v16i8, xptr, "") LLVMSetAlignment(xvec, 1u) // uw = |w| — generic @llvm.abs.v16i8 (lowers to PABSB); is_int_min_poison = false keeps -128 defined - let abs_id = LLVMLookupIntrinsicID("llvm.abs") var absTypes <- [v16i8] - var abs_decl = LLVMGetIntrinsicDeclaration(g_mod, abs_id, absTypes) + var abs_decl = LLVMGetIntrinsicDeclaration(g_mod, intrinsic_id("llvm.abs"), absTypes) var absArgTypes <- [v16i8, ctx.types.t_int1] var absTy = LLVMFunctionType(v16i8, absArgTypes) var absArgs <- [wvec, ctx.types.ConstI1(false)] @@ -2645,9 +2647,8 @@ def emit_dot32_quads_v(var ctx : JitCtx; _expr : ExprCallFunc?; wvec, xvec : LLV let v16i16 = LLVMVectorType(ctx.types.t_int16, 16u) let v8i32 = LLVMVectorType(ctx.types.t_int32, 8u) // uw = |w| — generic @llvm.abs.v32i8 (lowers to VPABSB ymm); is_int_min_poison = false keeps -128 defined - let abs_id = LLVMLookupIntrinsicID("llvm.abs") var absTypes <- [v32i8] - var abs_decl = LLVMGetIntrinsicDeclaration(g_mod, abs_id, absTypes) + var abs_decl = LLVMGetIntrinsicDeclaration(g_mod, intrinsic_id("llvm.abs"), absTypes) var absArgTypes <- [v32i8, ctx.types.t_int1] var absTy = LLVMFunctionType(v32i8, absArgTypes) var absArgs <- [wvec, ctx.types.ConstI1(false)] @@ -2730,9 +2731,8 @@ def emit_dot64_acc16(var ctx : JitCtx; _expr : ExprCallFunc?; arguments : array< var xvec = LLVMBuildLoad2(ctx.builder, v64i8, xptr, "") LLVMSetAlignment(xvec, 1u) // uw = |w| — generic @llvm.abs.v64i8 (lowers to VPABSB zmm); is_int_min_poison = false keeps -128 defined - let abs_id = LLVMLookupIntrinsicID("llvm.abs") var absTypes <- [v64i8] - var abs_decl = LLVMGetIntrinsicDeclaration(g_mod, abs_id, absTypes) + var abs_decl = LLVMGetIntrinsicDeclaration(g_mod, intrinsic_id("llvm.abs"), absTypes) var absArgTypes <- [v64i8, ctx.types.t_int1] var absTy = LLVMFunctionType(v64i8, absArgTypes) var absArgs <- [wvec, ctx.types.ConstI1(false)] diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index 6f472f2547..3b20e78f9b 100644 --- a/modules/dasLLVM/daslib/llvm_jit_run.das +++ b/modules/dasLLVM/daslib/llvm_jit_run.das @@ -38,11 +38,11 @@ var LINK_WHOLE_LIB = false // when true, standalone exe links against the whole // invalidates cached DLLs (e.g. edits to llvm_jit.das, llvm_macro.das, llvm_jit_common.das, // runtime helper ABI, default target triple). Cache filenames fold this in, so a bump // makes every previously written DLL miss the cache on the next run and get GC'd. -let LLVM_JIT_CODEGEN_VERSION : uint64 = 0x78ul // 0x78: a standalone exe adopts its emitter-sealed function and global lookups right after the context is created and registers each global with its index, name and shared flag (0x77: a vector of a handled element type registers into the element's module, so the externs a DLL binds by mangled name moved out of `$` (0x76: a statement after a terminator in the same block list lands in its own dead block instead of after the ret (0x75: the global-offset lookup is memory(none) and emitted at its use site - LLVM dedups and hoists it, an untaken branch never pays it; a solid-context global resolves once per function at entry (0x74: a runtime-only exe emits no register_native_path rows, and a whole-lib exe emits them once (0x73: computed goto lowers to one switch with the trap as its default, not an icmp chain (0x72: policies.fast_math defaults to the host's float flags, so a fast-math host now JITs fast-math (0x71: a CPU class row's cpu is the arch's bare baseline, so a DAS_JIT_BASELINE build enables the row's set and nothing a level implies (0x70: the wasm feature string drops +relaxed-simd and the idot family keeps only the exact extmul + extadd_pairwise lowering on wasm SIMD128 (0x6f: the first wasm idot lowering; 0x6e: the aarch64 SDOT / SMMLA tables gate on DotProd / i8mm, not the arch alone, and the force env reaches the generic exe machine (0x6d: the inline polynomial rail carries NaN: tanh selects the operand back over its ordered clamp, and the sincos quadrant / tan octant convert through llvm.fptosi.sat instead of poisoning on NaN and out-of-range (0x6c: aarch64 vector tan/exp2/log2/log/pow join the inline polynomial rail bit-exactly with the interpreter, sinh/cosh/tanh ride the exp one; 0x6b: aarch64 vector sin/cos ride the inline polynomial; 0x6a: srem/urem for 32-bit %; 0x69: every string argument of an extern is substituted, not just the ones which asked))) +let LLVM_JIT_CODEGEN_VERSION : uint64 = 0x79ul // an intrinsic emitter keys off the declared name, so a module-qualified call (math::sqrt) builds llvm.sqrt.f32; a function <-> int64/uint64 reinterpret extracts and inserts the { ptr } aggregate instead of bitcasting itd flag (0x77: a vector of a handled element type registers into the element's module, so the externs a DLL binds by mangled name moved out of `$` (0x76: a statement after a terminator in the same block list lands in its own dead block instead of after the ret (0x75: the global-offset lookup is memory(none) and emitted at its use site - LLVM dedups and hoists it, an untaken branch never pays it; a solid-context global resolves once per function at entry (0x74: a runtime-only exe emits no register_native_path rows, and a whole-lib exe emits them once (0x73: computed goto lowers to one switch with the trap as its default, not an icmp chain (0x72: policies.fast_math defaults to the host's float flags, so a fast-math host now JITs fast-math (0x71: a CPU class row's cpu is the arch's bare baseline, so a DAS_JIT_BASELINE build enables the row's set and nothing a level implies (0x70: the wasm feature string drops +relaxed-simd and the idot family keeps only the exact extmul + extadd_pairwise lowering on wasm SIMD128 (0x6f: the first wasm idot lowering; 0x6e: the aarch64 SDOT / SMMLA tables gate on DotProd / i8mm, not the arch alone, and the force env reaches the generic exe machine (0x6d: the inline polynomial rail carries NaN: tanh selects the operand back over its ordered clamp, and the sincos quadrant / tan octant convert through llvm.fptosi.sat instead of poisoning on NaN and out-of-range (0x6c: aarch64 vector tan/exp2/log2/log/pow join the inline polynomial rail bit-exactly with the interpreter, sinh/cosh/tanh ride the exp one; 0x6b: aarch64 vector sin/cos ride the inline polynomial; 0x6a: srem/urem for 32-bit %; 0x69: every string argument of an extern is substituted, not just the ones which asked))) // Read by tests-cpp/small/test_jit_emitter_pin.cpp: FNV-1a64 of the emitter sources // (normalized to LF; file list in the test) -let LLVM_JIT_EMITTER_HASH : uint64 = 0xa85abd6c7514b352ul +let LLVM_JIT_EMITTER_HASH : uint64 = 0xe8f690d286b2775bul let JIT_FNV_PRIME : uint64 = 1099511628211ul diff --git a/tests/math/test_qualified_math_calls.das b/tests/math/test_qualified_math_calls.das new file mode 100644 index 0000000000..598b0b4a87 --- /dev/null +++ b/tests/math/test_qualified_math_calls.das @@ -0,0 +1,135 @@ +options gen2 +options indenting = 4 + +require math +require dastest/testing_boost public + +// A module-qualified builtin call answers exactly what the unqualified spelling answers, in +// every tier - interpreter, JIT and AOT. This file pins that, one call per assert, qualified +// against unqualified, over every builtin family a JIT intrinsic emitter keys by name, and +// pins the arm identity of the calls whose vector form rides an inline polynomial rail, where +// agreeing spellings are not enough. + +struct Vec2 { + x : float + y : float +} + +//! the file's own sqrt overload - it is what makes the math:: qualifier necessary on the builtin calls +def sqrt(v : Vec2) : Vec2 { + return Vec2(x = math::sqrt(v.x), y = math::sqrt(v.y)) +} + +// the operands stay mutable module globals - a literal or a `let` const-folds the call +// away before any backend sees it +var g_nine = 9.0f +var g_frac = 2.5f +var g_small = 0.5f +var g_lo = 3.0f +var g_hi = 7.0f +var g_two = 2.0f +var g_v4 = float4(4.0f, 1.0f, 9.0f, 2.0f) +var g_zero4 = float4(0.0f, 0.0f, 0.0f, 0.0f) +var g_one4 = float4(1.0f, 1.0f, 1.0f, 1.0f) +var g_zero = 0.0f +var g_bits = 0x7fffffffu + +[test] +def test_qualified_unary_float(t : T?) { + t |> equal(math::sqrt(g_nine), sqrt(g_nine), "math::sqrt") + t |> equal(math::floor(g_frac), floor(g_frac), "math::floor") + t |> equal(math::ceil(g_frac), ceil(g_frac), "math::ceil") + t |> equal(math::round(g_frac), round(g_frac), "math::round") + t |> equal(math::sin(g_small), sin(g_small), "math::sin") + t |> equal(math::cos(g_small), cos(g_small), "math::cos") + t |> equal(math::log(g_nine), log(g_nine), "math::log") + t |> equal(math::exp2(g_small), exp2(g_small), "math::exp2") + t |> equal(math::log2(g_nine), log2(g_nine), "math::log2") + t |> equal(math::sinh(g_small), sinh(g_small), "math::sinh") + t |> equal(math::cosh(g_small), cosh(g_small), "math::cosh") + t |> equal(math::tanh(g_small), tanh(g_small), "math::tanh") +} + +[test] +def test_qualified_float_to_int(t : T?) { + t |> equal(math::floori(g_frac), floori(g_frac), "math::floori") + t |> equal(math::ceili(g_frac), ceili(g_frac), "math::ceili") +} + +[test] +def test_qualified_two_operand(t : T?) { + t |> equal(math::pow(g_lo, g_two), pow(g_lo, g_two), "math::pow") + t |> equal(math::fmod(g_hi, g_lo), fmod(g_hi, g_lo), "math::fmod") +} + +// clz/ctz/popcnt live in the builtin module, whose qualified spelling is `_::` +[test] +def test_qualified_builtin_bits(t : T?) { + t |> equal(_::popcnt(g_bits), popcnt(g_bits), "_::popcnt") + t |> equal(_::clz(g_bits), clz(g_bits), "_::clz") + t |> equal(_::ctz(g_bits), ctz(g_bits), "_::ctz") +} + +// the half8 lane accessors are keyed by name the same way, and the qualified spelling used to +// answer the hi lanes for a lo call +[test] +def test_qualified_half8_lanes(t : T?) { + let p = half8(g_v4, g_v4 + g_one4) + t |> equal(_::half8_lo(p), half8_lo(p), "_::half8_lo") + t |> equal(_::half8_hi(p), half8_hi(p), "_::half8_hi") + t |> equal(_::half8_lo(p), g_v4, "_::half8_lo answers the lo lanes") +} + +[test] +def test_qualified_minmax(t : T?) { + t |> equal(math::min(g_lo, g_hi), min(g_lo, g_hi), "math::min") + t |> equal(math::max(g_lo, g_hi), max(g_lo, g_hi), "math::max") + t |> equal(math::hmin(g_v4), hmin(g_v4), "math::hmin") + t |> equal(math::hmax(g_v4), hmax(g_v4), "math::hmax") +} + +[test] +def test_qualified_on_vectors(t : T?) { + t |> equal(math::sqrt(g_v4), sqrt(g_v4), "math::sqrt(float4)") + t |> equal(math::sin(g_v4), sin(g_v4), "math::sin(float4)") + t |> equal(math::cos(g_v4), cos(g_v4), "math::cos(float4)") + t |> equal(math::tanh(g_v4), tanh(g_v4), "math::tanh(float4)") +} + +[test] +def test_qualified_call_inside_own_overload(t : T?) { + let s = sqrt(Vec2(x = g_nine, y = g_frac * g_frac)) + t |> equal(s.x, math::sqrt(g_nine), "own sqrt(Vec2).x") + t |> equal(s.y, g_frac, "own sqrt(Vec2).y") +} + +[test] +def test_qualified_vector_arm_identity(t : T?) { + // cos/cosh/tanh reach an inline polynomial rail whose arm is picked by the call's name, + // and that rail only runs on aarch64. Comparing spellings there proves nothing if both + // pick the same wrong arm, so these compare against the value the named function has: + // the neighbouring arm answers a different number at zero. + t |> equal(math::cos(g_zero4), g_one4, "math::cos(0) is 1, not sin(0)") + t |> equal(math::sin(g_zero4), g_zero4, "math::sin(0) is 0") + t |> equal(math::cosh(g_zero4), g_one4, "math::cosh(0) is 1, not sinh(0)") + t |> equal(math::sinh(g_zero4), g_zero4, "math::sinh(0) is 0") + t |> equal(math::tanh(g_zero4), g_zero4, "math::tanh(0) is 0") + let th = math::tanh(g_v4) + t |> success(abs(th.x) <= 1.0f && abs(th.y) <= 1.0f && abs(th.z) <= 1.0f && abs(th.w) <= 1.0f, + "math::tanh is bounded by 1 - sinh is not") + t |> equal(th, tanh(g_v4), "math::tanh matches the unqualified spelling") +} + +[test] +def test_qualified_nan_lanes(t : T?) { + // both spellings answer NaN in the same lanes; is_nan reads IEEE on a host that folds + // nan compares, so this cell holds wherever it runs + let nan = g_zero / g_zero // nolint:LINT007 - 0/0 is how this cell makes its nan + let nan4 = float4(nan, g_zero, nan, g_zero) + let q = math::cos(nan4) + let u = cos(nan4) + t |> equal(is_nan(q.x), is_nan(u.x), "lane 0") + t |> equal(is_nan(q.y), is_nan(u.y), "lane 1") + t |> equal(is_nan(q.z), is_nan(u.z), "lane 2") + t |> equal(is_nan(q.w), is_nan(u.w), "lane 3") +} From 3825401cf29ad27e636d7024126a1147e27ff5b2 Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 9 Sep 2026 00:44:34 +0300 Subject: [PATCH 2/3] simulate_nodes: a function address answers its typed eval slots, not a silent 0 The two `@@fn` const nodes left every typed slot as `DAS_ASSERT(0); return 0;`, and `DAS_NO_ASSERTIONS` is on in Release - so `reinterpret(@@fn)` read a silent 0 in every store position, while the same cast through a parameter, and the JIT and AOT tiers, answered the address. `evalPtr`, `evalInt64` and `evalUInt64` compute it; the narrower slots keep the assert. `tests/language/func_addr.das` covers the store positions and the handle round-trip, and `func_addr_solid.das` covers the solid-context node, which no test in the tree reached. This folder's checklist gets three fixes its own audit of this change found. Fixes #3970 --- include/daScript/simulate/ARCHITECTURE.md | 11 +++++ include/daScript/simulate/REVIEW.md | 20 +++++---- include/daScript/simulate/simulate_nodes.h | 30 ++++++++++++- tests/language/func_addr.das | 52 ++++++++++++++++++++++ tests/language/func_addr_solid.das | 32 +++++++++++++ 5 files changed, 134 insertions(+), 11 deletions(-) create mode 100644 tests/language/func_addr_solid.das diff --git a/include/daScript/simulate/ARCHITECTURE.md b/include/daScript/simulate/ARCHITECTURE.md index d950a8aeb7..47b7f764f6 100644 --- a/include/daScript/simulate/ARCHITECTURE.md +++ b/include/daScript/simulate/ARCHITECTURE.md @@ -137,3 +137,14 @@ correctness required it, and the alternative that was rejected. matching slot; only uint-index reads and value reinterpret land here). Rejected alternative: keeping the per-function NTTP matrix everywhere - master's shape, ~24KB of object code and ~11ms of compile per bind, mostly duplicated typed-eval stubs. + +- **Typed evals on the function-address const nodes** (`simulate_nodes.h`: + `SimNode_FuncConstValue`, `SimNode_FuncConstValueMnh`) - `evalPtr`, `evalInt64` and + `evalUInt64` route through the base as `cast::to(eval(context))`, a second virtual + dispatch plus a vec4f round-trip. Correctness requires them because without them those + three slots assert and return 0, which in a build with `DAS_NO_ASSERTIONS` is a silent null: + `reinterpret(@@fn)` written on the address-of expression is typed uint64, so every + store of it - a local, a field, an array element, an arithmetic operand, a return - reads the + uint64 slot, while the same cast through a parameter, an argument, JIT and AOT all answer the + address. Rejected alternative: rejecting the cast during inference, which would break the + reverse spelling and the two tiers that already answer correctly. diff --git a/include/daScript/simulate/REVIEW.md b/include/daScript/simulate/REVIEW.md index 6768857734..fb59b65ea4 100644 --- a/include/daScript/simulate/REVIEW.md +++ b/include/daScript/simulate/REVIEW.md @@ -3,11 +3,11 @@ **Read `REVIEW_COMMON.md` (repo root) first - its contract binds this checklist.** Architecture doc: `ARCHITECTURE.md`. A diff that changes a `debug_info.h` struct layout, or removes, renames or retypes a public member of a struct or class under this folder, applies -`skills/internal/abi_break_sweep.md` too. A diff that changes or removes a name under this -folder that a `daslib/*.das` file spells out - a struct or member the AOT C++ emitter writes -into generated code, a flag or field a daslib predicate reads - applies `daslib/REVIEW.md` -too; checklist discovery walks changed paths only, so the C++ half never opens the daslib -checklist on its own. +`skills/internal/abi_break_sweep.md` too. A diff that changes what a name under this folder +resolves to for a `daslib/*.das` caller - a rename, a removal, or a new overload of a struct or +member the AOT C++ emitter writes into generated code, or of a flag or field a daslib predicate +reads - applies `daslib/REVIEW.md` too; checklist discovery walks changed paths only, so the +C++ half never opens the daslib checklist on its own. - **A diff that adds a field to `CodeOfPolicies` (`code_of_policies.h`) adds it to `DAS_MODULE_CACHE_POLICY_FIELDS` in `src/builtin/module_builtin_ast_serialize.cpp`, in the @@ -31,13 +31,15 @@ checklist on its own. template under this folder that generated code runs for every evaluated expression. An added load, branch, call, copy, or counter, a direct call becoming indirect, a static dispatch becoming virtual, or an unboxed value becoming a boxed round-trip is that defect unless the - PR names the check showing the shipped build costs no more: its codegen unchanged, or a - measurement of the new code against the code it replaces - a diff cannot show optimized - codegen. + PR names the check showing the shipped build costs no more: its codegen unchanged, a + measurement of the new code against the code it replaces, or the addition landing its + sanctioned-additions entry per the rule below - a diff cannot show optimized codegen. The + baseline is what already answered correctly: a slot that returned a wrong constant costs more + once it computes the right one, and that is not this defect. - **A diff that adds work to the hot path - whether or not the shipped build flattens it - lands its entry under `ARCHITECTURE.md`'s sanctioned hot-path additions in the same diff: - what was added, where, why correctness required it, and the alternative that was rejected.** + what was added, where, why correctness requires it, and the alternative that was rejected.** Replacing a hot-path body with code that performs the same per-evaluation operations - no load, branch, call, copy, or counter the old body did not have - and measures no slower on the build the repo ships is not added work. A body that gains one of those operations is diff --git a/include/daScript/simulate/simulate_nodes.h b/include/daScript/simulate/simulate_nodes.h index c91a076c9f..0d58f9ad2e 100644 --- a/include/daScript/simulate/simulate_nodes.h +++ b/include/daScript/simulate/simulate_nodes.h @@ -3182,13 +3182,26 @@ SIM_NODE_AT_VECTOR(Float, float) SimFunction * fun = context.functions + subexpr.valueU; return cast::from(fun); } + virtual char * evalPtr ( Context & context ) override { + return cast::to(eval(context)); + } + virtual int64_t evalInt64 ( Context & context ) override { + return cast::to(eval(context)); + } + virtual uint64_t evalUInt64 ( Context & context ) override { + return cast::to(eval(context)); + } #define EVAL_NODE(TYPE,CTYPE) \ virtual CTYPE eval##TYPE ( Context & context ) override { \ DAS_PROFILE_NODE \ DAS_ASSERT(0); \ return 0; \ } - DAS_EVAL_NODE + EVAL_NODE(Int,int32_t); + EVAL_NODE(UInt,uint32_t); + EVAL_NODE(Float,float); + EVAL_NODE(Double,double); + EVAL_NODE(Bool,bool); #undef EVAL_NODE }; @@ -3204,13 +3217,26 @@ SIM_NODE_AT_VECTOR(Float, float) DAS_ASSERT(fun==nullptr || fun->mangledNameHash==subexpr.valueU64); return cast::from(fun); } + virtual char * evalPtr ( Context & context ) override { + return cast::to(eval(context)); + } + virtual int64_t evalInt64 ( Context & context ) override { + return cast::to(eval(context)); + } + virtual uint64_t evalUInt64 ( Context & context ) override { + return cast::to(eval(context)); + } #define EVAL_NODE(TYPE,CTYPE) \ virtual CTYPE eval##TYPE ( Context & context ) override { \ DAS_PROFILE_NODE \ DAS_ASSERT(0); \ return 0; \ } - DAS_EVAL_NODE + EVAL_NODE(Int,int32_t); + EVAL_NODE(UInt,uint32_t); + EVAL_NODE(Float,float); + EVAL_NODE(Double,double); + EVAL_NODE(Bool,bool); #undef EVAL_NODE }; diff --git a/tests/language/func_addr.das b/tests/language/func_addr.das index b588c3c28f..0f4f4894bf 100644 --- a/tests/language/func_addr.das +++ b/tests/language/func_addr.das @@ -2,10 +2,28 @@ options gen2 options no_unused_function_arguments = false require dastest/testing_boost public +typedef FaFn = function<(a, b : int) : int> + def fa_add(a, b : int) : int { return a + b } +def fa_mul(a, b : int) : int { + return a * b +} + +struct FaSlot { + handle : uint64 +} + +def fa_handle_of(f : FaFn) : uint64 { + return unsafe(reinterpret(f)) +} + +def fa_inline_handle : uint64 { + return unsafe(reinterpret(@@fa_add)) +} + [test] def test_func_addr(t : T?) { t |> run("function pointer invoke") @(t : T?) { @@ -24,6 +42,40 @@ def test_func_addr(t : T?) { let qq : function t |> equal(qq == null, true) } + t |> run("a function address is a non-null uint64 handle in every store position") @(t : T?) { + // not its own baseline - the direct spelling is what this block tests + let handle_via_param = fa_handle_of(@@fa_add) + t |> success(handle_via_param != 0ul, "the handle through a parameter is not null") + unsafe { + var assigned : uint64 + assigned = reinterpret(@@fa_add) // nolint:STYLE011 - the split IS the case under test: a store into a live local, not an initializer + let declared = reinterpret(@@fa_add) + var fixarr : uint64[2] + fixarr[1] = reinterpret(@@fa_add) + var slot : FaSlot + slot.handle = reinterpret(@@fa_add) + let literal = FaSlot(handle = reinterpret(@@fa_add)) + let arith = 0ul + reinterpret(@@fa_add) + t |> equal(assigned, handle_via_param, "assignment to a local") + t |> equal(declared, handle_via_param, "declaration with initializer") + t |> equal(fixarr[1], handle_via_param, "fixed-array element store") + t |> equal(slot.handle, handle_via_param, "struct field assignment") + t |> equal(literal.handle, handle_via_param, "struct literal field") + t |> equal(arith, handle_via_param, "arithmetic operand") + t |> equal(reinterpret(@@fa_add), int64(handle_via_param), "cast to int64") + } + t |> equal(fa_inline_handle(), handle_via_param, "the direct spelling inside a return") + } + t |> run("distinct functions get distinct handles") @(t : T?) { + t |> success(fa_handle_of(@@fa_add) != fa_handle_of(@@fa_mul), + "two functions do not share a handle") + } + t |> run("a uint64 handle casts back to a callable") @(t : T?) { + unsafe { + let h = reinterpret(@@fa_add) + t |> equal(invoke(reinterpret(h), 20, 22), 42) + } + } t |> run("invoke null function panics") @(t : T?) { var failed = false try { diff --git a/tests/language/func_addr_solid.das b/tests/language/func_addr_solid.das new file mode 100644 index 0000000000..2d32d65a59 --- /dev/null +++ b/tests/language/func_addr_solid.das @@ -0,0 +1,32 @@ +options gen2 +options solid_context +options indenting = 4 + +require dastest/testing_boost public + +// Under `options solid_context` a function address simulates as an index into the context's +// function table rather than a mangled-name lookup, which is a different node with its own +// typed reads. The handle has to come out the same in every store position here too. + +typedef SolidFn = function<(a, b : int) : int> + +def sa_add(a, b : int) : int { + return a + b +} + +def sa_handle_of(f : SolidFn) : uint64 { + return unsafe(reinterpret(f)) +} + +[test] +def test_solid_context_function_address(t : T?) { + let handle_via_param = sa_handle_of(@@sa_add) + t |> success(handle_via_param != 0ul, "the handle through a parameter is not null") + unsafe { + let declared = reinterpret(@@sa_add) + let as_int64 = reinterpret(@@sa_add) + t |> equal(declared, handle_via_param, "declaration with initializer") + t |> equal(as_int64, int64(handle_via_param), "cast to int64") + t |> equal(invoke(reinterpret(declared), 20, 22), 42, "a handle casts back to a callable") + } +} From 42a8e9e3d5cb25386e46c767f68691c34e57a42f Mon Sep 17 00:00:00 2001 From: Churkin Aleksey Date: Wed, 9 Sep 2026 00:44:34 +0300 Subject: [PATCH 3/3] aot: reinterpret to a pointer reads a Func's target, not the temporary's address `das_cast` bound a `Func` to its catch-all `cast(const QQ &)`, which returns `&expr`. The emitter spells `@@fn` as a `Func(...)` temporary, so `reinterpret(@@fn)` answered a stack address where the interpreter and the JIT answer the function. A `Func` overload reads `PTR`, the way the smart-pointer overloads beside it read `get()`. The two function-address tests gain their `void?` cells, which could not be green before this. --- include/daScript/simulate/aot.h | 3 +++ tests/language/func_addr.das | 3 +++ tests/language/func_addr_solid.das | 2 ++ 3 files changed, 8 insertions(+) diff --git a/include/daScript/simulate/aot.h b/include/daScript/simulate/aot.h index 9b0deab833..d493b0a138 100644 --- a/include/daScript/simulate/aot.h +++ b/include/daScript/simulate/aot.h @@ -593,6 +593,9 @@ namespace das { static __forceinline TT * cast ( const QQ & expr ) { return const_cast(reinterpret_cast(&expr)); } + static __forceinline TT * cast ( const Func & expr ) { + return (TT *) expr.PTR; + } #if defined(_MSC_VER) #pragma warning(push) #pragma warning(disable:4312) // reinterpret_cast used between related classes diff --git a/tests/language/func_addr.das b/tests/language/func_addr.das index 0f4f4894bf..5676f57c83 100644 --- a/tests/language/func_addr.das +++ b/tests/language/func_addr.das @@ -56,13 +56,16 @@ def test_func_addr(t : T?) { slot.handle = reinterpret(@@fa_add) let literal = FaSlot(handle = reinterpret(@@fa_add)) let arith = 0ul + reinterpret(@@fa_add) + let as_ptr = reinterpret(@@fa_add) t |> equal(assigned, handle_via_param, "assignment to a local") t |> equal(declared, handle_via_param, "declaration with initializer") t |> equal(fixarr[1], handle_via_param, "fixed-array element store") t |> equal(slot.handle, handle_via_param, "struct field assignment") t |> equal(literal.handle, handle_via_param, "struct literal field") t |> equal(arith, handle_via_param, "arithmetic operand") + t |> equal(reinterpret(as_ptr), handle_via_param, "cast to void?") t |> equal(reinterpret(@@fa_add), int64(handle_via_param), "cast to int64") + t |> equal(invoke(reinterpret(as_ptr), 20, 22), 42, "a void? handle casts back to a callable") } t |> equal(fa_inline_handle(), handle_via_param, "the direct spelling inside a return") } diff --git a/tests/language/func_addr_solid.das b/tests/language/func_addr_solid.das index 2d32d65a59..9638d578a7 100644 --- a/tests/language/func_addr_solid.das +++ b/tests/language/func_addr_solid.das @@ -25,8 +25,10 @@ def test_solid_context_function_address(t : T?) { unsafe { let declared = reinterpret(@@sa_add) let as_int64 = reinterpret(@@sa_add) + let as_ptr = reinterpret(@@sa_add) t |> equal(declared, handle_via_param, "declaration with initializer") t |> equal(as_int64, int64(handle_via_param), "cast to int64") + t |> equal(reinterpret(as_ptr), handle_via_param, "cast to void?") t |> equal(invoke(reinterpret(declared), 20, 22), 42, "a handle casts back to a callable") } }