diff --git a/.github/workflows/extended_checks.yml b/.github/workflows/extended_checks.yml index 7a3162d00f..b675a06e1c 100644 --- a/.github/workflows/extended_checks.yml +++ b/.github/workflows/extended_checks.yml @@ -228,7 +228,7 @@ jobs: # No sccache launcher: this lane is nightly-only and runs uncached. cmake --no-warn-unused-cli -B./build -G "${{ matrix.cmake_generator }}" -DCMAKE_BUILD_TYPE:STRING=Release \ $ACTIVE_MODULES -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" - cmake --build ./build --config Release --target daslang daslang_static --parallel + cmake --build ./build --config Release --target daslang daslang_static dasbind_probe --parallel ;; linux) echo "BIN=./bin" >> $GITHUB_ENV @@ -245,7 +245,7 @@ jobs: -DDAS_CLANG_BIND_DISABLED=ON \ -DDAS_GLFW_DISABLED=OFF \ -G "${{ matrix.cmake_generator }}" $ACTIVE_MODULES - cmake --build ./build --config Release --target daslang daslang_static + cmake --build ./build --config Release --target daslang daslang_static dasbind_probe ;; *) echo "BIN=./bin" >> $GITHUB_ENV @@ -254,7 +254,7 @@ jobs: -DCMAKE_OSX_ARCHITECTURES="${{ matrix.architecture_string }}" \ -DDAS_GLFW_DISABLED=OFF \ -G "${{ matrix.cmake_generator }}" $ACTIVE_MODULES - cmake --build ./build --config Release --target daslang daslang_static + cmake --build ./build --config Release --target daslang daslang_static dasbind_probe ;; esac diff --git a/CMakeLists.txt b/CMakeLists.txt index 44a817832d..4aed71ff86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1634,6 +1634,9 @@ if (NOT ${DAS_TOOLS_DISABLED}) # test time + dasModuleSQLITE; both must already be defined as targets). add_subdirectory(tests/exe-paths) + # The probe library tests/dasbind binds through [extern] (lands in lib/). + add_subdirectory(tests/dasbind) + # Build daslang utilities as standalone executables (-exe mode) add_subdirectory(utils) diff --git a/src/builtin/ARCHITECTURE.md b/src/builtin/ARCHITECTURE.md index 0e0ceb1250..467e527fd8 100644 --- a/src/builtin/ARCHITECTURE.md +++ b/src/builtin/ARCHITECTURE.md @@ -67,3 +67,53 @@ set; `0` disables eviction), never the record just written. `install` touches th reads, so a record in use is the newest and a stale variant the oldest. Only the default directory is pruned - an explicit `-module-cache ` is the user's - and the limit variable is the one `DAS*` name the record key skips, since it decides nothing about a compile. + +## 3. The interpreter's `[extern]` call + +An `[extern]` function (`module_builtin_dasbind.cpp`) is called from the interpreter through +a wrapper `vec4f (*)(void * fn, vec4f * args)`: the node evaluates every argument into a +`vec4f` lane and the wrapper calls `fn` through a C prototype. The wrappers are generated - +`generate_x86_64_calls.das` writes `win_x86_64_wrapper.inc`, `systemV_64_wrapper.inc` and +`systemV_64_extra_wrapper.inc` - one per argument count, result class and float mask of the +first register-class arguments, indexed from 0: four positions on Windows, six on SystemV, +with every integer-class argument spelled `int64_t`, every float-class one `double`, and the +result `int64_t` or `vec4f`. Float class is a `float` or `double` by value; a ref, whatever it +points to, is a pointer and so integer class, on the table and the layout alike. A SystemV call with more than six arguments and a float at index +6 or later needs a prototype from the extra table, keyed by argument count, result class (1 for +a float or double result, as the main table indexes it) and the full mask; its entries are the +hand-listed `systemV_extra` rows in the generator - a new shape is added there and the +generator re-run - and a shape the list lacks is a compile error naming the mask. + +The C prototype passes a stack argument as an 8-byte slot. That fits Windows x64, where the +mask covers four positions and everything past the fourth argument is an 8-byte stack slot, +float included. It fits SystemV x86-64 too: its float registers past index 5 are exactly the +extra list's cases. It does not fit arm64: eight float registers, so a float at index 6 or 7 +belongs in d6 or d7, and on Apple the stack is packed at natural size and alignment - a 32-bit +`int` in 4 bytes, a `bool` in 1 - where Linux and Android keep 8-byte slots. So on arm64 +(Apple, Linux, Android; a Windows arm64 build keeps the table, since `DAS_BIND_ARM64_LAYOUT` +turns on for `__APPLE__` and `__linux__` only while `DAS_BIND_EXTERNAL` is on for every 64-bit +Windows) a call with more than six arguments leaves the table: at bind time the +function's das types produce an `Arm64Layout` - which argument rides in x0-x7, which in d0-d7, +and at which byte offset of a stack image the rest sit, packed on Apple and slotted elsewhere - +and the node calls `das_arm64_call`, an assembly trampoline that copies the image below its +own frame, loads the sixteen argument registers, calls, and stores x0 and d0 for the node to +return. A call of six or fewer arguments has no stack argument and no float past the mask +under any of these ABIs, so it keeps the table there too. A packed width comes from the das +type: a ref, pointer or string is 8 bytes, a scalar its `getBaseSizeOf`, and anything else (an +aggregate by value, which the binder does not support) rides as 8 bytes, as the table would +pass it. A layout lives for the process - `computeArm64Layout` keeps every layout in a static +vector it never frees - because a re-applied `[extern]` replaces the module's function object +while an earlier context's call node, which carries the layout pointer, may still run. + +The JIT uses neither: `modules/dasLLVM/daslib/llvm_jit.das` (repo root) emits an extern call +with the function's own LLVM types, so a compiled call is right on every platform. The wrapper +table and the layout are the interpreter's alone, which is why an ABI defect there shows only +in interpreted code - a macro context, a `[no_jit]` function, a plain run. `tests/dasbind` +probes both paths against `tests/dasbind/probe`, a library whose functions sum their arguments +under distinct multipliers, so one argument in the wrong register or slot changes the total. +`tests/dasbind/CMakeLists.txt` builds the probe on a 64-bit host that builds and dlopens a +shared library at test time - not wasm, Android or iOS, none of which walks `tests/` - and +`tests/.das_test` skips the suite only on a 32-bit host, where the probe is not built, and under +dastest's `--ser`/`--deser` sweep, because a deserialized program never applies `[extern]` and +so never manufactures the `__dasbind__` function it names in the `dasbind` module; a 64-bit +desktop tree without the library fails the suite instead of skipping it. diff --git a/src/builtin/REVIEW.md b/src/builtin/REVIEW.md index faae95e0f1..79aaccbd70 100644 --- a/src/builtin/REVIEW.md +++ b/src/builtin/REVIEW.md @@ -8,9 +8,9 @@ `strings` and `jit`. What the scan enforces, and which shared generic helpers it exempts, is read from the scan itself. -- **A diff that adds or changes a bind in a module the scan covers rebuilds the binary from - that diff before the folder's gate runs** - the scan reads the binds compiled into the running - binary, so a stale binary is a false green. +- **A diff that adds or changes a bind - any `addExtern*` call - in a module the scan covers + rebuilds the binary from that diff before the folder's gate runs** - the scan reads the binds + compiled into the running binary, so a stale binary is a false green. - **A diff that adds a module under this folder adds it to `review_nttp.das`'s `require` list in the same change - directly, or through the daslib wrapper that requires it.** A module the list @@ -47,13 +47,18 @@ in the same change.** A replayed start never runs the descriptor, so an effect the recorder does not see is an effect every warm start silently lacks. -- **A diff that moves a bind between modules - an `addExtern` or `addExternInline` call whose - module changes, or a builtin whose `vector` functions follow a type to another module - - bumps `LLVM_JIT_CODEGEN_VERSION` in `modules/dasLLVM/daslib/llvm_jit_plan.das` (repo root), in - the same change.** The JIT's DLL cache key folds the codegen version and each function's AST +- **A diff that moves a bind between modules - any `addExtern*` call whose module changes, or a + builtin whose `vector` functions follow a type to another module - bumps + `LLVM_JIT_CODEGEN_VERSION` in `modules/dasLLVM/daslib/llvm_jit_plan.das` (repo root), in the + same change.** The JIT's DLL cache key folds the codegen version and each function's AST hash, never the module an extern lives in, so a cached DLL binds the old name and crashes on the hit. +- **A diff that changes the wrapper tables, the `systemV_extra` list, the arm64 layout or the + `das_arm64_call` trampoline updates section 3 of `ARCHITECTURE.md` in the same change.** Two + comments in `module_builtin_dasbind.cpp` cite that section instead of restating it, so a + stale section is what the next reader trusts. + - **A diff that changes what `ModuleFileCache::defaultPath` folds into the module-cache key - the binary, the command line, the environment names, or which script arguments count - updates the cache-key paragraph of `ARCHITECTURE.md` in the same change.** The key is what stops a diff --git a/src/builtin/generate_x86_64_calls.das b/src/builtin/generate_x86_64_calls.das index 9419b5f97b..bda23762db 100644 --- a/src/builtin/generate_x86_64_calls.das +++ b/src/builtin/generate_x86_64_calls.das @@ -85,9 +85,17 @@ let ARG12_D = (1 << 12) let ARG13_D = (1 << 13) let ARG14_D = (1 << 14) let ARG15_D = (1 << 15) +let ARG16_D = (1 << 16) +let ARG17_D = (1 << 17) +let ARG18_D = (1 << 18) +let ARG19_D = (1 << 19) +let ARG20_D = (1 << 20) +let ARG21_D = (1 << 21) +let ARG22_D = (1 << 22) +let ARG23_D = (1 << 23) -let RES_D = 0 -let RES_X = 1 +let RES_FLOAT = 1 +let RES_INT = 0 struct WrapperFn { nargs : int @@ -110,18 +118,27 @@ def genExtraWrapper(name : string; extra : array) { let systemV_extra <- [WrapperFn( // glPrimitiveBoundingBoxARB - nargs=8, res=1, perm=0 | ARG0_D | ARG1_D | ARG2_D | ARG3_D | ARG4_D | ARG5_D | ARG6_D | ARG7_D), WrapperFn( + nargs=8, res=RES_INT, perm=0 | ARG0_D | ARG1_D | ARG2_D | ARG3_D | ARG4_D | ARG5_D | ARG6_D | ARG7_D), WrapperFn( // glDrawVkImageNV - nargs=11, res=1, perm=0 | ARG2_D | ARG3_D | ARG4_D | ARG5_D | ARG6_D | ARG7_D | ARG8_D | ARG9_D | ARG10_D), WrapperFn( + nargs=11, res=RES_INT, perm=0 | ARG2_D | ARG3_D | ARG4_D | ARG5_D | ARG6_D | ARG7_D | ARG8_D | ARG9_D | ARG10_D), WrapperFn( // glPathGlyphsNV - nargs=10, res=1, perm=0 | ARG9_D), WrapperFn( + nargs=10, res=RES_INT, perm=0 | ARG9_D), WrapperFn( // glPathGlyphRangeNV, glPathMemoryGlyphIndexArrayNV - nargs=9, res=1, perm=0 | ARG8_D), WrapperFn( + nargs=9, res=RES_INT, perm=0 | ARG8_D), WrapperFn( // glPathGlyphIndexArrayNV - nargs=8, res=1, perm=0 | ARG7_D), WrapperFn( -// DUMMY - nargs = 13, res = RES_X, perm=ARG0_D | ARG13_D), WrapperFn( - nargs = 13, res = RES_D, perm=ARG0_D | ARG13_D + nargs=8, res=RES_INT, perm=0 | ARG7_D), WrapperFn( +// glMatrixFrustumEXT, glMatrixOrthoEXT + nargs=7, res=RES_INT, perm=0 | ARG1_D | ARG2_D | ARG3_D | ARG4_D | ARG5_D | ARG6_D), WrapperFn( +// glNamedProgramLocalParameter4dEXT, glNamedProgramLocalParameter4fEXT + nargs=7, res=RES_INT, perm=0 | ARG3_D | ARG4_D | ARG5_D | ARG6_D), WrapperFn( +// glGetPathSpacingNV + nargs=9, res=RES_INT, perm=0 | ARG5_D | ARG6_D), WrapperFn( +// tests/dasbind probe_dbl_at6 + nargs=8, res=RES_FLOAT, perm=0 | ARG6_D), WrapperFn( +// tests/dasbind probe_dbl_at67 + nargs=8, res=RES_FLOAT, perm=0 | ARG6_D | ARG7_D), WrapperFn( +// tests/dasbind probe_stack_narrow + nargs=20, res=RES_FLOAT, perm=0 | ARG0_D | ARG1_D | ARG2_D | ARG3_D | ARG4_D | ARG5_D | ARG6_D | ARG7_D | ARG18_D )]; [export] diff --git a/src/builtin/module_builtin_dasbind.cpp b/src/builtin/module_builtin_dasbind.cpp index 574eb8c9e8..d49e32835e 100644 --- a/src/builtin/module_builtin_dasbind.cpp +++ b/src/builtin/module_builtin_dasbind.cpp @@ -12,8 +12,67 @@ #include "daScript/misc/sysos.h" #include "daScript/misc/env_cfg.h" +#include +#include #include +#if DAS_BIND_EXTERNAL && defined(__aarch64__) && (defined(__APPLE__) || defined(__linux__)) +#define DAS_BIND_ARM64_LAYOUT 1 +#else +#define DAS_BIND_ARM64_LAYOUT 0 +#endif +#if DAS_BIND_ARM64_LAYOUT && defined(__APPLE__) +#define DAS_BIND_ARM64_PACKED_STACK 1 +#else +#define DAS_BIND_ARM64_PACKED_STACK 0 +#endif + +#if DAS_BIND_ARM64_LAYOUT +#if defined(__APPLE__) +#define DAS_ARM64_CALL_SYMBOL "_das_arm64_call" +#else +#define DAS_ARM64_CALL_SYMBOL "das_arm64_call" +#endif +// src/builtin/ARCHITECTURE.md sec.3 +__asm__( + ".text\n" + ".p2align 2\n" + ".globl " DAS_ARM64_CALL_SYMBOL "\n" + DAS_ARM64_CALL_SYMBOL ":\n" + " stp x29, x30, [sp, #-32]!\n" + " stp x19, x20, [sp, #16]\n" + " mov x29, sp\n" + " mov x19, x0\n" + " mov x20, x5\n" + " sub sp, sp, x4\n" + " mov x9, #0\n" + "1: cmp x9, x4\n" + " b.hs 2f\n" + " ldr x10, [x3, x9]\n" + " str x10, [sp, x9]\n" + " add x9, x9, #8\n" + " b 1b\n" + "2: ldp d0, d1, [x2]\n" + " ldp d2, d3, [x2, #16]\n" + " ldp d4, d5, [x2, #32]\n" + " ldp d6, d7, [x2, #48]\n" + " ldp x6, x7, [x1, #48]\n" + " ldp x4, x5, [x1, #32]\n" + " ldp x2, x3, [x1, #16]\n" + " ldp x0, x1, [x1]\n" + " blr x19\n" + " str x0, [x20]\n" + " str d0, [x20, #8]\n" + " mov sp, x29\n" + " ldp x19, x20, [sp, #16]\n" + " ldp x29, x30, [sp], #32\n" + " ret\n" +); +struct Arm64Result { uint64_t x0, d0; }; +extern "C" void das_arm64_call ( void * fn, const uint64_t * gpr, const uint64_t * fpr, + const uint8_t * stack, uint64_t stackBytes, Arm64Result * out ); +#endif + namespace das { #if DAS_BIND_EXTERNAL @@ -85,6 +144,12 @@ namespace das { FastCallWrapper wrapper; }; + enum { RES_INT = 0, RES_FLOAT = 1 }; + + static bool isFloatClass ( const TypeDeclPtr & type ) { + return !type->isRef() && (type->baseType==Type::tFloat || type->baseType==Type::tDouble); + } + __forceinline vec4f Rx ( int64_t x ) { return v_cast_vec4f(v_splatsi64(x)); } #define AX(i) (*(uint64_t *)(args+(i))) @@ -110,6 +175,68 @@ FastCallWrapper getExtraWrapper ( int nargs, int res, int perm ) { #undef AX #undef AD + // src/builtin/ARCHITECTURE.md sec.3 + struct Arm64Layout { + struct Slot { uint8_t argIndex, bytes; uint16_t offset; }; + uint8_t gprArg[8] = {}, fprArg[8] = {}; + int ngpr = 0, nfpr = 0; + vector stack; + uint32_t stackBytes = 0; + bool fpResult = false; + }; + +#if DAS_BIND_ARM64_LAYOUT + static uint8_t arm64StackArgumentBytes ( const TypeDeclPtr & type ) { + if ( !DAS_BIND_ARM64_PACKED_STACK || type->isRef() ) return 8; + auto size = type->getBaseSizeOf(); + return (size==1 || size==2 || size==4) ? uint8_t(size) : 8; + } + + static const Arm64Layout * computeArm64Layout ( Function * fun ) { + if ( fun->arguments.size()<=6 ) return nullptr; + auto layout = make_unique(); + uint32_t offset = 0; + for ( int a=0, as=int(fun->arguments.size()); aarguments[a]->type; + bool fp = isFloatClass(type); + int & count = fp ? layout->nfpr : layout->ngpr; + if ( count<8 ) { + (fp ? layout->fprArg : layout->gprArg)[count++] = uint8_t(a); + } else { + uint8_t bytes = arm64StackArgumentBytes(type); + offset = (offset + bytes - 1) & ~uint32_t(bytes - 1); + layout->stack.push_back({uint8_t(a), bytes, uint16_t(offset)}); + offset += bytes; + } + } + layout->stackBytes = (offset + 15) & ~15u; + layout->fpResult = isFloatClass(fun->result); + static mutex layoutsMutex; + static vector> layouts; + lock_guard guard(layoutsMutex); + layouts.push_back(das::move(layout)); + return layouts.back().get(); + } + + static vec4f arm64Call ( void * fn, vec4f * args, const Arm64Layout * layout ) { + uint64_t gpr[8] = {}, fpr[8] = {}; + Arm64Result out = {}; + for ( int i=0; i!=layout->ngpr; ++i ) memcpy(gpr+i, args+layout->gprArg[i], 8); + for ( int i=0; i!=layout->nfpr; ++i ) memcpy(fpr+i, args+layout->fprArg[i], 8); + alignas(16) uint8_t stack[DAS_MAX_FUNCTION_ARGUMENTS*8 + 16] = {}; + for ( const auto & slot : layout->stack ) memcpy(stack+slot.offset, args+slot.argIndex, slot.bytes); + das_arm64_call(fn, gpr, fpr, stack, layout->stackBytes, &out); + uint64_t raw = layout->fpResult ? out.d0 : out.x0; + int64_t bits; + memcpy(&bits, &raw, sizeof(bits)); + return v_cast_vec4f(v_splatsi64(bits)); + } +#else + static const Arm64Layout * computeArm64Layout ( Function * ) { + return nullptr; + } +#endif + struct BoundFunction { void * fun; string name; @@ -246,8 +373,9 @@ FastCallWrapper getExtraWrapper ( int nargs, int res, int perm ) { } struct SimNode_DasBindCall : SimNode_ExtFuncCallBase { - SimNode_DasBindCall ( const LineInfo & a, const char * fnName, uint64_t hc, FastCallWrapper wrp, void * fun, ApiType api_ ) - : SimNode_ExtFuncCallBase(a, fnName), code(hc), wrapper(wrp), fnptr(fun), api(api_) { + SimNode_DasBindCall ( const LineInfo & a, const char * fnName, uint64_t hc, FastCallWrapper wrp, void * fun, ApiType api_, + const Arm64Layout * layout_ ) + : SimNode_ExtFuncCallBase(a, fnName), code(hc), wrapper(wrp), fnptr(fun), api(api_), layout(layout_) { } void bind ( Context & context ) { string crash_and_burn; @@ -270,20 +398,23 @@ FastCallWrapper getExtraWrapper ( int nargs, int res, int perm ) { } vec4f argValues[DAS_MAX_FUNCTION_ARGUMENTS]; evalArgs(context, argValues); +#if DAS_BIND_ARM64_LAYOUT + if ( layout ) return arm64Call(fnptr, argValues, layout); +#endif return wrapper(fnptr, argValues); } uint64_t code = 0; FastCallWrapper wrapper = nullptr; void * fnptr = nullptr; ApiType api = ApiType::api_unknown; + const Arm64Layout * layout = nullptr; }; FastCallWrapper getWrapper ( Function * fun, int nReg ) { - int args = ( fun->result->baseType==Type::tFloat || fun->result->baseType==Type::tDouble ) ? (1<result) ? (1<arguments.size()); aarguments[a]->type->baseType; - if ( tp==Type::tFloat || tp==Type::tDouble ) { + if ( a==nReg ) break; + if ( isFloatClass(fun->arguments[a]->type) ) { args |= (1< & ) override { const char * fnName = context.code->allocateName(this->name); - return context.code->makeNode(at, fnName, hc, wrapper, dllAddress, api); + return context.code->makeNode(at, fnName, hc, wrapper, dllAddress, api, layout); } }; @@ -391,15 +524,14 @@ FastCallWrapper getExtraWrapper ( int nargs, int res, int perm ) { #else if ( fun->arguments.size()>6 ) { int nargs = int(fun->arguments.size()); - int res = ( fun->result->baseType==Type::tFloat || fun->result->baseType==Type::tDouble ) ? 0 : 1; + int res = isFloatClass(fun->result) ? RES_FLOAT : RES_INT; int perm = 0; for ( size_t ai=0, ais=fun->arguments.size(); ai!=ais; ++ai ) { - const auto & a = fun->arguments[ai]; - if ( a->type->isSimpleType(Type::tFloat) || a->type->isSimpleType(Type::tDouble) ) { + if ( isFloatClass(fun->arguments[ai]->type) ) { perm |= (1<=(1<<7) ) { + if ( perm>=(1<<6) ) { return getExtraWrapper(nargs, res, perm); } } @@ -446,17 +578,16 @@ FastCallWrapper getExtraWrapper ( int nargs, int res, int perm ) { return false; } #ifndef _MSC_VER - if ( fun->arguments.size()>6 ) { + if ( fun->arguments.size()>6 && !DAS_BIND_ARM64_LAYOUT ) { int perm=0; int nargs = int(fun->arguments.size()); - int res = ( fun->result->baseType==Type::tFloat || fun->result->baseType==Type::tDouble ) ? 0 : 1; + int res = isFloatClass(fun->result) ? RES_FLOAT : RES_INT; for ( size_t ai=0, ais=fun->arguments.size(); ai!=ais; ++ai ) { - const auto & arg = fun->arguments[ai]; - if ( arg->type->isSimpleType(Type::tFloat) || arg->type->isSimpleType(Type::tDouble) ) { + if ( isFloatClass(fun->arguments[ai]->type) ) { perm |= (1<=(1<<7) ) { + if ( perm>=(1<<6) ) { auto wrp = getExtraWrapper(nargs,res,perm); if ( !wrp ) { string argText; @@ -496,7 +627,7 @@ FastCallWrapper getExtraWrapper ( int nargs, int res, int perm ) { } auto wrp = computeWrapper(fun); uint64_t code = lateBind(ba.fn_name, ba.library, funptr); - auto bif = new DasBindFunction(bindName, code, funptr, ba, wrp); + auto bif = new DasBindFunction(bindName, code, funptr, ba, wrp, computeArm64Layout(fun)); bif->result = fun->result; for ( auto & a : fun->arguments ) { auto newArg = new Variable(); diff --git a/src/builtin/systemV_64_extra_wrapper.inc b/src/builtin/systemV_64_extra_wrapper.inc index 42ba514e2c..4418d216a9 100644 --- a/src/builtin/systemV_64_extra_wrapper.inc +++ b/src/builtin/systemV_64_extra_wrapper.inc @@ -1,45 +1,69 @@ -vec4f fastcall64_8_1_255 ( void * fn, vec4f * args ) { - using call_kind = vec4f ( * ) ( double,double,double,double,double,double,double,double ); - return call_kind(fn) ( AD(0),AD(1),AD(2),AD(3),AD(4),AD(5),AD(6),AD(7) ); +vec4f fastcall64_8_0_255 ( void * fn, vec4f * args ) { + using call_kind = int64_t ( * ) ( double,double,double,double,double,double,double,double ); + return Rx ( call_kind(fn) ( AD(0),AD(1),AD(2),AD(3),AD(4),AD(5),AD(6),AD(7) ) ); } -vec4f fastcall64_11_1_2044 ( void * fn, vec4f * args ) { - using call_kind = vec4f ( * ) ( int64_t,int64_t,double,double,double,double,double,double,double,double,double ); - return call_kind(fn) ( AX(0),AX(1),AD(2),AD(3),AD(4),AD(5),AD(6),AD(7),AD(8),AD(9),AD(10) ); +vec4f fastcall64_11_0_2044 ( void * fn, vec4f * args ) { + using call_kind = int64_t ( * ) ( int64_t,int64_t,double,double,double,double,double,double,double,double,double ); + return Rx ( call_kind(fn) ( AX(0),AX(1),AD(2),AD(3),AD(4),AD(5),AD(6),AD(7),AD(8),AD(9),AD(10) ) ); } -vec4f fastcall64_10_1_512 ( void * fn, vec4f * args ) { - using call_kind = vec4f ( * ) ( int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,double ); - return call_kind(fn) ( AX(0),AX(1),AX(2),AX(3),AX(4),AX(5),AX(6),AX(7),AX(8),AD(9) ); +vec4f fastcall64_10_0_512 ( void * fn, vec4f * args ) { + using call_kind = int64_t ( * ) ( int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,double ); + return Rx ( call_kind(fn) ( AX(0),AX(1),AX(2),AX(3),AX(4),AX(5),AX(6),AX(7),AX(8),AD(9) ) ); } -vec4f fastcall64_9_1_256 ( void * fn, vec4f * args ) { - using call_kind = vec4f ( * ) ( int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,double ); - return call_kind(fn) ( AX(0),AX(1),AX(2),AX(3),AX(4),AX(5),AX(6),AX(7),AD(8) ); +vec4f fastcall64_9_0_256 ( void * fn, vec4f * args ) { + using call_kind = int64_t ( * ) ( int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,double ); + return Rx ( call_kind(fn) ( AX(0),AX(1),AX(2),AX(3),AX(4),AX(5),AX(6),AX(7),AD(8) ) ); } -vec4f fastcall64_8_1_128 ( void * fn, vec4f * args ) { - using call_kind = vec4f ( * ) ( int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,double ); - return call_kind(fn) ( AX(0),AX(1),AX(2),AX(3),AX(4),AX(5),AX(6),AD(7) ); +vec4f fastcall64_8_0_128 ( void * fn, vec4f * args ) { + using call_kind = int64_t ( * ) ( int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,double ); + return Rx ( call_kind(fn) ( AX(0),AX(1),AX(2),AX(3),AX(4),AX(5),AX(6),AD(7) ) ); } -vec4f fastcall64_13_1_8193 ( void * fn, vec4f * args ) { - using call_kind = vec4f ( * ) ( double,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t ); - return call_kind(fn) ( AD(0),AX(1),AX(2),AX(3),AX(4),AX(5),AX(6),AX(7),AX(8),AX(9),AX(10),AX(11),AX(12) ); +vec4f fastcall64_7_0_126 ( void * fn, vec4f * args ) { + using call_kind = int64_t ( * ) ( int64_t,double,double,double,double,double,double ); + return Rx ( call_kind(fn) ( AX(0),AD(1),AD(2),AD(3),AD(4),AD(5),AD(6) ) ); } -vec4f fastcall64_13_0_8193 ( void * fn, vec4f * args ) { - using call_kind = int64_t ( * ) ( double,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t ); - return Rx ( call_kind(fn) ( AD(0),AX(1),AX(2),AX(3),AX(4),AX(5),AX(6),AX(7),AX(8),AX(9),AX(10),AX(11),AX(12) ) ); +vec4f fastcall64_7_0_120 ( void * fn, vec4f * args ) { + using call_kind = int64_t ( * ) ( int64_t,int64_t,int64_t,double,double,double,double ); + return Rx ( call_kind(fn) ( AX(0),AX(1),AX(2),AD(3),AD(4),AD(5),AD(6) ) ); +} + +vec4f fastcall64_9_0_96 ( void * fn, vec4f * args ) { + using call_kind = int64_t ( * ) ( int64_t,int64_t,int64_t,int64_t,int64_t,double,double,int64_t,int64_t ); + return Rx ( call_kind(fn) ( AX(0),AX(1),AX(2),AX(3),AX(4),AD(5),AD(6),AX(7),AX(8) ) ); +} + +vec4f fastcall64_8_1_64 ( void * fn, vec4f * args ) { + using call_kind = vec4f ( * ) ( int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,double,int64_t ); + return call_kind(fn) ( AX(0),AX(1),AX(2),AX(3),AX(4),AX(5),AD(6),AX(7) ); +} + +vec4f fastcall64_8_1_192 ( void * fn, vec4f * args ) { + using call_kind = vec4f ( * ) ( int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,double,double ); + return call_kind(fn) ( AX(0),AX(1),AX(2),AX(3),AX(4),AX(5),AD(6),AD(7) ); +} + +vec4f fastcall64_20_1_262399 ( void * fn, vec4f * args ) { + using call_kind = vec4f ( * ) ( double,double,double,double,double,double,double,double,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,int64_t,double,int64_t ); + return call_kind(fn) ( AD(0),AD(1),AD(2),AD(3),AD(4),AD(5),AD(6),AD(7),AX(8),AX(9),AX(10),AX(11),AX(12),AX(13),AX(14),AX(15),AX(16),AX(17),AD(18),AX(19) ); } FastCallExtraWrapper fastcall64_extra_table [] = { - 8,1,255,&fastcall64_8_1_255, - 11,1,2044,&fastcall64_11_1_2044, - 10,1,512,&fastcall64_10_1_512, - 9,1,256,&fastcall64_9_1_256, - 8,1,128,&fastcall64_8_1_128, - 13,1,8193,&fastcall64_13_1_8193, - 13,0,8193,&fastcall64_13_0_8193, + 8,0,255,&fastcall64_8_0_255, + 11,0,2044,&fastcall64_11_0_2044, + 10,0,512,&fastcall64_10_0_512, + 9,0,256,&fastcall64_9_0_256, + 8,0,128,&fastcall64_8_0_128, + 7,0,126,&fastcall64_7_0_126, + 7,0,120,&fastcall64_7_0_120, + 9,0,96,&fastcall64_9_0_96, + 8,1,64,&fastcall64_8_1_64, + 8,1,192,&fastcall64_8_1_192, + 20,1,262399,&fastcall64_20_1_262399, }; diff --git a/tests/.das_test b/tests/.das_test index 3b51cbc972..2b5230fe88 100644 --- a/tests/.das_test +++ b/tests/.das_test @@ -5,6 +5,22 @@ require daslib/rtti [export, pinvoke] def can_visit_folder(folder_name : string; var result : bool?) { + // the probe suite runs on 64-bit hosts only - tests/dasbind/CMakeLists.txt builds its library nowhere + // else; a 64-bit tree missing that library fails the suite loudly. It skips the ser/deser sweep: a + // deserialized program never applies [extern], so the bound function it names is not in dasbind + if (folder_name == "dasbind") { + *result = typeinfo sizeof(type) == 8 + if (*result) { + let args <- get_command_line_arguments() + for (arg in args) { + if (arg == "--ser" || arg == "--deser") { + *result = false + return + } + } + } + return + } if (folder_name == "clipboard") { *result = has_module("clipboard_core") return diff --git a/tests/REVIEW.md b/tests/REVIEW.md new file mode 100644 index 0000000000..e49641cb84 --- /dev/null +++ b/tests/REVIEW.md @@ -0,0 +1,8 @@ +# Tests Code Review Checklist + +**Read `REVIEW_COMMON.md` (repo root) first - its contract binds this checklist.** + +- **A diff that widens the `dasbind` skip in `.das_test` or drops a probe shape from + `dasbind/test_extern_abi.das` is a defect.** The suite is the only check of which register or + stack slot an interpreted `[extern]` call puts each argument in - the JIT never takes that + path - so coverage lost there reports green on every lane. diff --git a/tests/aot/CMakeLists.txt b/tests/aot/CMakeLists.txt index cbd2c5f89e..7b6a127698 100644 --- a/tests/aot/CMakeLists.txt +++ b/tests/aot/CMakeLists.txt @@ -389,7 +389,7 @@ set(TEST_AOT_ALL_DAS "") # test-body .das (not module libs); LLVM-AOT corpus e set(DAS_AOT_SUITES algorithm apply archive assert_once ast_match async bare_block base64 bitfields bool_array - class_boost daslib dasgltf daspeg data_walker debug debug_agent decs dynamic_cast_rtti fio + class_boost dasbind daslib dasgltf daspeg data_walker debug debug_agent decs dynamic_cast_rtti fio fixed_array flatten fs functional gc glsl handle_types hash_map interfaces jit jobque json jsonrpc language linq lint long_array_table loops lpipe lsp macro_boost macro_call match math mcp md_boost module_cache module_tests option promote quote diff --git a/tests/dasbind/CMakeLists.txt b/tests/dasbind/CMakeLists.txt new file mode 100644 index 0000000000..5de3e0f0a7 --- /dev/null +++ b/tests/dasbind/CMakeLists.txt @@ -0,0 +1,24 @@ +# The probe library tests/dasbind binds through [extern]: C functions whose argument shapes +# reach past the callee's registers. Built only on a host that builds and dlopens a shared +# library at test time - the mobile and wasm targets that also run the extern path share an +# ABI with a desktop lane that does - and placed in /lib, the last directory dasbind's +# library search tries, so the test names it by bare file name everywhere. +if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8 OR EMSCRIPTEN OR ANDROID OR IOS) + return() +endif() + +add_library(dasbind_probe SHARED probe/dasbind_probe.cpp) +set(_dasbind_probe_dir "${PROJECT_SOURCE_DIR}/lib") +set_target_properties(dasbind_probe PROPERTIES + PREFIX "" + LIBRARY_OUTPUT_DIRECTORY "${_dasbind_probe_dir}" + RUNTIME_OUTPUT_DIRECTORY "${_dasbind_probe_dir}" + LIBRARY_OUTPUT_DIRECTORY_DEBUG "${_dasbind_probe_dir}" + LIBRARY_OUTPUT_DIRECTORY_MINSIZEREL "${_dasbind_probe_dir}" + LIBRARY_OUTPUT_DIRECTORY_RELEASE "${_dasbind_probe_dir}" + LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO "${_dasbind_probe_dir}" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${_dasbind_probe_dir}" + RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${_dasbind_probe_dir}" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${_dasbind_probe_dir}" + RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${_dasbind_probe_dir}" +) diff --git a/tests/dasbind/_reapply_fixture.das b/tests/dasbind/_reapply_fixture.das new file mode 100644 index 0000000000..06c3425f7f --- /dev/null +++ b/tests/dasbind/_reapply_fixture.das @@ -0,0 +1,13 @@ +options gen2 +options no_aot + +require dasbind + +[extern(cdecl, name="dasbind_probe_stack14", windows_library="dasbind_probe", linux_library="dasbind_probe.so", macos_library="dasbind_probe.dylib")] +def probe_stack14(a0 : int8; a1 : int16; a2 : int; a3 : int64; a4 : float; a5 : double; a6 : int; + a7 : int8; a8 : int8; a9 : int16; a10 : int; a11 : int64; a12 : bool; a13 : int) : int64 { return 0l; } + +[export] +def main { + probe_stack14(int8(1), int16(2), 3, 4l, 0.5, 0.25lf, 6, int8(7), int8(8), int16(9), 10, 11l, true, 13) +} diff --git a/tests/dasbind/probe/dasbind_probe.cpp b/tests/dasbind/probe/dasbind_probe.cpp new file mode 100644 index 0000000000..fc3fc5799b --- /dev/null +++ b/tests/dasbind/probe/dasbind_probe.cpp @@ -0,0 +1,54 @@ +#include + +#if defined(_WIN32) +#define DASBIND_PROBE_API extern "C" __declspec(dllexport) +#else +#define DASBIND_PROBE_API extern "C" __attribute__((visibility("default"))) +#endif + +DASBIND_PROBE_API double dasbind_probe_dbl_at45 ( int64_t a0, int64_t a1, int64_t a2, int64_t a3, double a4, double a5 ) { + return double(a0) + double(a1)*10 + double(a2)*100 + double(a3)*1000 + a4*2 + a5*3; +} + +DASBIND_PROBE_API double dasbind_probe_dbl_at6 ( int64_t a0, int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, double a6, int64_t a7 ) { + return double(a0) + double(a1)*10 + double(a2)*100 + double(a3)*1000 + double(a4)*10000 + double(a5)*100000 + a6*2 + double(a7)*1000000; +} + +DASBIND_PROBE_API double dasbind_probe_dbl_at67 ( int64_t a0, int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, double a6, double a7 ) { + return double(a0) + double(a1)*10 + double(a2)*100 + double(a3)*1000 + double(a4)*10000 + double(a5)*100000 + a6*2 + a7*3; +} + +DASBIND_PROBE_API int64_t dasbind_probe_regs7 ( int8_t a0, int16_t a1, int32_t a2, int64_t a3, float a4, double a5, int32_t a6 ) { + return a0 + a1*10 + a2*100 + a3*1000 + int64_t(a4*4) + int64_t(a5*8) + int64_t(a6)*7; +} + +DASBIND_PROBE_API int64_t dasbind_probe_stack14 ( int8_t a0, int16_t a1, int32_t a2, int64_t a3, float a4, double a5, int32_t a6, + int8_t a7, int8_t a8, int16_t a9, int32_t a10, int64_t a11, bool a12, int32_t a13 ) { + return a0 + a1*10 + a2*100 + a3*1000 + int64_t(a4*4) + int64_t(a5*8) + int64_t(a6)*7 + + a7*11 + a8*13 + a9*17 + int64_t(a10)*19 + a11*23 + (a12 ? 29 : 0) + int64_t(a13)*31; +} + +DASBIND_PROBE_API float dasbind_probe_stack_narrow ( float a0, float a1, float a2, float a3, float a4, float a5, float a6, float a7, + int64_t a8, int64_t a9, int64_t a10, int64_t a11, int64_t a12, int64_t a13, int64_t a14, int64_t a15, + int8_t a16, int16_t a17, float a18, int32_t a19 ) { + return a0*1 + a1*2 + a2*3 + a3*4 + a4*5 + a5*6 + a6*7 + a7*8 + + float(a8*9 + a9*10 + a10*11 + a11*12 + a12*13 + a13*14 + a14*15 + a15*16) + + float(a16)*17 + float(a17)*18 + a18*19 + float(a19)*20; +} + +DASBIND_PROBE_API int64_t dasbind_probe_ref_short ( const float * r0, int64_t a1, const int32_t * r2 ) { + return int64_t(*r0 * 2) + a1*3 + int64_t(*r2)*5; +} + +DASBIND_PROBE_API int64_t dasbind_probe_ref_args ( const float * r0, int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, int64_t a6, int64_t a7, const int32_t * r8 ) { + return int64_t(*r0 * 2) + a1*3 + a2*4 + a3*5 + a4*6 + a5*7 + a6*8 + a7*9 + int64_t(*r8)*10; +} + +DASBIND_PROBE_API const float * dasbind_probe_ref_result ( int64_t a0, int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5, int64_t a6, const float * src ) { + return (a0 + a1 + a2 + a3 + a4 + a5 + a6) == 28 ? src : nullptr; +} + +DASBIND_PROBE_API double dasbind_probe_stack_dbl_result ( double a0, int64_t a1, int64_t a2, int64_t a3, int64_t a4, + int64_t a5, int64_t a6, int64_t a7, int64_t a8, int32_t a9 ) { + return a0 + double(a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8) + double(a9)*2; +} diff --git a/tests/dasbind/test_extern_abi.das b/tests/dasbind/test_extern_abi.das new file mode 100644 index 0000000000..7071b03a35 --- /dev/null +++ b/tests/dasbind/test_extern_abi.das @@ -0,0 +1,97 @@ +options gen2 +options no_aot +options rtti = true +options strict_smart_pointers = false + +require dastest/testing_boost public +require dasbind +require daslib/ast +require daslib/rtti + +//! every probe sums its arguments under distinct multipliers, so one argument landing in the +//! wrong register or stack slot changes the total; the library is tests/dasbind/probe + +//! declares this file's probe_stack14 again, so compiling it re-applies the extern in the process +let REAPPLY_FIXTURE = "{get_das_root()}/tests/dasbind/_reapply_fixture.das" + +[extern(cdecl, name="dasbind_probe_dbl_at45", windows_library="dasbind_probe", linux_library="dasbind_probe.so", macos_library="dasbind_probe.dylib")] +def probe_dbl_at45(a0, a1, a2, a3 : int64; a4, a5 : double) : double { return 0.0lf; } + +[extern(cdecl, name="dasbind_probe_dbl_at6", windows_library="dasbind_probe", linux_library="dasbind_probe.so", macos_library="dasbind_probe.dylib")] +def probe_dbl_at6(a0, a1, a2, a3, a4, a5 : int64; a6 : double; a7 : int64) : double { return 0.0lf; } + +[extern(cdecl, name="dasbind_probe_dbl_at67", windows_library="dasbind_probe", linux_library="dasbind_probe.so", macos_library="dasbind_probe.dylib")] +def probe_dbl_at67(a0, a1, a2, a3, a4, a5 : int64; a6, a7 : double) : double { return 0.0lf; } + +[extern(cdecl, name="dasbind_probe_regs7", windows_library="dasbind_probe", linux_library="dasbind_probe.so", macos_library="dasbind_probe.dylib")] +def probe_regs7(a0 : int8; a1 : int16; a2 : int; a3 : int64; a4 : float; a5 : double; a6 : int) : int64 { return 0l; } + +[extern(cdecl, name="dasbind_probe_stack14", windows_library="dasbind_probe", linux_library="dasbind_probe.so", macos_library="dasbind_probe.dylib")] +def probe_stack14(a0 : int8; a1 : int16; a2 : int; a3 : int64; a4 : float; a5 : double; a6 : int; + a7 : int8; a8 : int8; a9 : int16; a10 : int; a11 : int64; a12 : bool; a13 : int) : int64 { return 0l; } + +[extern(cdecl, name="dasbind_probe_stack_narrow", windows_library="dasbind_probe", linux_library="dasbind_probe.so", macos_library="dasbind_probe.dylib")] +def probe_stack_narrow(a0, a1, a2, a3, a4, a5, a6, a7 : float; a8, a9, a10, a11, a12, a13, a14, a15 : int64; + a16 : int8; a17 : int16; a18 : float; a19 : int) : float { return 0.0; } + +[extern(cdecl, name="dasbind_probe_ref_short", windows_library="dasbind_probe", linux_library="dasbind_probe.so", macos_library="dasbind_probe.dylib")] +def probe_ref_short(r0 : float const&; a1 : int64; r2 : int const&) : int64 { return 0l; } + +[extern(cdecl, name="dasbind_probe_ref_args", windows_library="dasbind_probe", linux_library="dasbind_probe.so", macos_library="dasbind_probe.dylib")] +def probe_ref_args(r0 : float const&; a1, a2, a3, a4, a5, a6, a7 : int64; r8 : int const&) : int64 { return 0l; } + +[extern(cdecl, name="dasbind_probe_ref_result", windows_library="dasbind_probe", linux_library="dasbind_probe.so", macos_library="dasbind_probe.dylib")] +def probe_ref_result(a0, a1, a2, a3, a4, a5, a6 : int64; src : float const&) : float const& { return src; } + +[extern(cdecl, name="dasbind_probe_stack_dbl_result", windows_library="dasbind_probe", linux_library="dasbind_probe.so", macos_library="dasbind_probe.dylib")] +def probe_stack_dbl_result(a0 : double; a1, a2, a3, a4, a5, a6, a7, a8 : int64; a9 : int) : double { return 0.0lf; } + +[test] +def test_extern_argument_passing(t : T?) { + t |> run("doubles in positions 4 and 5 reach the callee's float registers") @(t : T?) { + t |> equal(probe_dbl_at45(1l, 2l, 3l, 4l, 1.5lf, 2.5lf), 4331.5lf) + } + t |> run("a double in position 6, followed by an integer so no register residue can stand in") @(t : T?) { + t |> equal(probe_dbl_at6(1l, 2l, 3l, 4l, 5l, 6l, 1.5lf, 7l), 7654324.0lf) + } + t |> run("doubles in positions 6 and 7") @(t : T?) { + t |> equal(probe_dbl_at67(1l, 2l, 3l, 4l, 5l, 6l, 1.5lf, 2.5lf), 654331.5lf) + } + t |> run("seven mixed-width arguments, all in registers") @(t : T?) { + t |> equal(probe_regs7(int8(1), int16(2), 3, 4l, 0.5, 0.25lf, 6), 4367l) + } + t |> run("fourteen mixed-width arguments, the tail on the stack") @(t : T?) { + t |> equal(probe_stack14(int8(1), int16(2), 3, 4l, 0.5, 0.25lf, 6, int8(7), int8(8), int16(9), 10, 11l, true, 13), 5576l) + } + t |> run("eight floats and eight integers fill the registers; int8, int16, float and int land on the stack; float result") @(t : T?) { + t |> equal(probe_stack_narrow(0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8l, 9l, 10l, 11l, 12l, 13l, 14l, 15l, int8(16), int16(17), 18.5, 19), 2687.5) + } + t |> run("a double result from a call with a stack argument") @(t : T?) { + t |> equal(probe_stack_dbl_result(0.5lf, 1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l, 9), 54.5lf) + } + t |> run("a float reference is a pointer, not a float, in the wrapper table's mask") @(t : T?) { + let fv = 1.5 + let iv = 7 + t |> equal(probe_ref_short(fv, 4l, iv), 50l) + } + t |> run("a float reference rides an integer register and an int reference an 8-byte stack slot") @(t : T?) { + let fv = 1.5 + let iv = 7 + t |> equal(probe_ref_args(fv, 1l, 2l, 3l, 4l, 5l, 6l, 7l, iv), 269l) + } + t |> run("a reference result comes back in the integer register, whatever it points to") @(t : T?) { + let fv = 1.5 + t |> equal(probe_ref_result(1l, 2l, 3l, 4l, 5l, 6l, 7l, fv), 1.5) + } + t |> run("the call survives the extern being applied again by another program in the process") @(t : T?) { + var inscope access <- make_file_access("") + using() $(var mg : ModuleGroup) { + using() $(var cop : CodeOfPolicies) { + compile_file(REAPPLY_FIXTURE, access, unsafe(addr(mg)), cop) $(ok, program, issues) { + t |> success(ok, "the fixture compiles: {issues}") + } + } + } + t |> equal(probe_stack14(int8(1), int16(2), 3, 4l, 0.5, 0.25lf, 6, int8(7), int8(8), int16(9), 10, 11l, true, 13), 5576l) + } +}