Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions include/daScript/simulate/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<CTYPE>::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<uint64>(@@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.
20 changes: 11 additions & 9 deletions include/daScript/simulate/REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions include/daScript/simulate/aot.h
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,9 @@ namespace das {
static __forceinline TT * cast ( const QQ & expr ) {
return const_cast<TT *>(reinterpret_cast<const TT *>(&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
Expand Down
30 changes: 28 additions & 2 deletions include/daScript/simulate/simulate_nodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -3182,13 +3182,26 @@ SIM_NODE_AT_VECTOR(Float, float)
SimFunction * fun = context.functions + subexpr.valueU;
return cast<SimFunction *>::from(fun);
}
virtual char * evalPtr ( Context & context ) override {
return cast<char *>::to(eval(context));
}
virtual int64_t evalInt64 ( Context & context ) override {
return cast<int64_t>::to(eval(context));
}
virtual uint64_t evalUInt64 ( Context & context ) override {
return cast<uint64_t>::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
};

Expand All @@ -3204,13 +3217,26 @@ SIM_NODE_AT_VECTOR(Float, float)
DAS_ASSERT(fun==nullptr || fun->mangledNameHash==subexpr.valueU64);
return cast<SimFunction *>::from(fun);
}
virtual char * evalPtr ( Context & context ) override {
return cast<char *>::to(eval(context));
}
virtual int64_t evalInt64 ( Context & context ) override {
return cast<int64_t>::to(eval(context));
}
virtual uint64_t evalUInt64 ( Context & context ) override {
return cast<uint64_t>::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
};

Expand Down
18 changes: 12 additions & 6 deletions modules/dasLLVM/REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
11 changes: 10 additions & 1 deletion modules/dasLLVM/daslib/llvm_boost.das
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,16 @@ def LLVMLookupIntrinsicID(name : string) {
return LLVMLookupIntrinsicID(name, uint64(long_length(name)))
}

def LLVMGetIntrinsicDeclaration(mod : LLVMOpaqueModule?; id : uint; var argTypes : array<LLVMOpaqueType?>) {
//! 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<LLVMOpaqueType?>) : 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)))
}

Expand Down
20 changes: 16 additions & 4 deletions modules/dasLLVM/daslib/llvm_jit.das
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) ]
Expand Down Expand Up @@ -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}")
Expand All @@ -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}")
Expand Down Expand Up @@ -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<uint64>(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<Fn>(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) {
Expand Down
Loading
Loading