diff --git a/CMakeLists.txt b/CMakeLists.txt index 90197d63e9..44a817832d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1154,6 +1154,8 @@ include/daScript/simulate/simulate_visit.h include/daScript/simulate/simulate_visit_op.h include/daScript/simulate/simulate_visit_op_undef.h include/daScript/simulate/sim_policy.h +include/daScript/simulate/name_lookup.h +src/simulate/name_lookup.cpp src/simulate/data_walker.cpp include/daScript/simulate/data_walker.h src/simulate/debug_print.cpp @@ -1767,6 +1769,7 @@ SET(DAS_SIMULATE_INCLUDES include/daScript/simulate/simulate_fusion_op2_set_perm.h include/daScript/simulate/simulate_fusion_op2_vec_settings.h include/daScript/simulate/simulate.h + include/daScript/simulate/name_lookup.h include/daScript/simulate/simulate_nodes.h include/daScript/simulate/simulate_visit.h include/daScript/simulate/simulate_visit_op.h @@ -1861,6 +1864,7 @@ install(FILES ${PROJECT_SOURCE_DIR}/src/simulate/debug_info.cpp ${PROJECT_SOURCE_DIR}/src/simulate/escape_string.cpp ${PROJECT_SOURCE_DIR}/src/simulate/heap.cpp + ${PROJECT_SOURCE_DIR}/src/simulate/name_lookup.cpp ${PROJECT_SOURCE_DIR}/src/simulate/runtime_array.cpp ${PROJECT_SOURCE_DIR}/src/simulate/runtime_iterator.cpp ${PROJECT_SOURCE_DIR}/src/simulate/runtime_table.cpp diff --git a/daslib/aot_standalone.das b/daslib/aot_standalone.das index f8ed6448ac..c24d064760 100644 --- a/daslib/aot_standalone.das +++ b/daslib/aot_standalone.das @@ -150,6 +150,72 @@ def private writeGlobalVarInfos(var helper : AotDebugInfoHelper?; infos : array< } } +//! the sealed lookup, written as the constant arrays the generated constructor adopts: a NameLookup::StaticTable +//! named `_table` and the four arrays it points at +def private writeLookupTables(var tw : StringBuilderWriter; prefix : string; lookup : NameLookup?) { + write(tw, " static const NameLookup::Entry {prefix}_entries[] = \{\n") + for (i in urange(name_lookup_mnh_slots(lookup))) { + write(tw, " \{0x{name_lookup_entry_mnh(lookup, i):x}ull, {name_lookup_entry_value(lookup, i):d}u, {name_lookup_entry_index(lookup, i):d}u, {name_lookup_entry_next(lookup, i)}, 0\},\n") + } + write(tw, " \};\n") + write(tw, " static const NameLookup::NameSlot {prefix}_names[] = \{\n") + for (i in urange(name_lookup_name_slots(lookup))) { + write(tw, " \{0x{name_lookup_name_hash(lookup, i):x}ull, {name_lookup_name_head(lookup, i)}, 0\},\n") + } + write(tw, " \};\n") + write(tw, " static const uint32_t {prefix}_mnh_disp[] = \{") + for (i in urange(name_lookup_mnh_buckets(lookup))) { + write(tw, " {name_lookup_mnh_disp(lookup, i):d}u,") + } + write(tw, " \};\n") + write(tw, " static const uint32_t {prefix}_name_disp[] = \{") + for (i in urange(name_lookup_name_buckets(lookup))) { + write(tw, " {name_lookup_name_disp(lookup, i):d}u,") + } + write(tw, " \};\n") + write(tw, " static const NameLookup::StaticTable {prefix}_table = \{ {name_lookup_mnh_buckets(lookup):d}u, {name_lookup_mnh_slots(lookup):d}u, {name_lookup_name_buckets(lookup):d}u, {name_lookup_name_slots(lookup):d}u, {name_lookup_count(lookup):d}u, 0u, {prefix}_mnh_disp, {prefix}_name_disp, {prefix}_entries, {prefix}_names \};\n") +} + +//! the standalone context's function lookup: its table is dense, one slot per emitted function in emission +//! order, so the index is the position in `usedFunctions`, never the compiler's function index +def private writeFunctionLookup(var tw : StringBuilderWriter; usedFunctions : array) { + var lookup = unsafe(name_lookup_create()) + var index = 0u + for (pfun in usedFunctions) { + name_lookup_insert(lookup, pfun.getMangledNameHash, string(pfun.name), index, index) + index++ + } + name_lookup_seal(lookup) + writeLookupTables(tw, "fn_lookup", lookup) + unsafe { + name_lookup_destroy(lookup) + } +} + +//! the standalone context's global lookup: the value is the offset InitGlobalVariable assigns at construction - +//! the 16-byte-rounded running size of the shared or the plain globals, in declaration order - and the +//! generated constructor verifies every global against it, so the two computations cannot drift apart silently +def private writeVariableLookup(var tw : StringBuilderWriter; globals : array) { + var lookup = unsafe(name_lookup_create()) + var globalsSize = 0u + var sharedSize = 0u + for (pvar in globals) { + let size = (uint(pvar._type.sizeOf) + 15u) & ~15u + let offset = pvar.flags.global_shared ? sharedSize : globalsSize + if (pvar.flags.global_shared) { + sharedSize += size + } else { + globalsSize += size + } + name_lookup_insert(lookup, hash(pvar |> get_mangled_name()), string(pvar.name), uint(pvar.index), offset) + } + name_lookup_seal(lookup) + writeLookupTables(tw, "var_lookup", lookup) + unsafe { + name_lookup_destroy(lookup) + } +} + def writeStandaloneCtor(cfg : StandaloneContextCfg; initFunctions : string; var tw : StringBuilderWriter, program : ProgramPtr; lookupVariableTable : array; var context : Context) { // nolint:STYLE038 - flat emitter - one write per ctor section let min_init_stack = 16384 @@ -157,7 +223,8 @@ def writeStandaloneCtor(cfg : StandaloneContextCfg; initFunctions : string; var let requested_stack = stack_arg ?as tInt ?? int(program.policies.stack) let stack_base = requested_stack > 0 && stack_arg is tInt ? requested_stack : max(requested_stack, min_init_stack) let stack_size = stack_base + int(program.globalInitStackSize) - let usedFunctionCount = length(collectProgramUsedFunctions(program, true, false)) + let usedFunctions <- collectProgramUsedFunctions(program, true, false) + let usedFunctionCount = length(usedFunctions) write(tw, "{cfg.class_name}::{cfg.class_name}() : Context({stack_size}/*stack*/) \{\n"); write(tw, " auto & context = *this;\n"); write(tw, " CodeOfPolicies policies;"); @@ -192,8 +259,9 @@ def writeStandaloneCtor(cfg : StandaloneContextCfg; initFunctions : string; var write(tw, "/*(policies.threadlock_context || policies.debugger)*/ ) \{\n"); write(tw, " context.contextMutex = new recursive_mutex;\n"); write(tw, " }\n"); - write(tw, " context.tabMnLookup = make_shared>();\n"); - write(tw, " context.tabMnLookup->clear();\n"); + writeFunctionLookup(tw, usedFunctions) + write(tw, " context.functionLookup = make_shared();\n"); + write(tw, " context.functionLookup->adopt(fn_lookup_table);\n"); if (!empty(initFunctions)) { write(tw, " // start totalFunctions\n"); @@ -206,17 +274,18 @@ def writeStandaloneCtor(cfg : StandaloneContextCfg; initFunctions : string; var write(tw, " for (const auto& [index, func_info, debug_info]: usedFunctions) \{\n"); write(tw, " InitAotFunction(context, &context.functions[index], func_info);\n"); write(tw, " context.functions[index].debugInfo = debug_info;\n"); - write(tw, " (*context.tabMnLookup)[func_info.mnh] = context.functions + index;\n"); + write(tw, " DAS_VERIFYF(context.functionLookup->valueByMnh(func_info.mnh)==uint32_t(index), \"standalone function '%s' sits at index %d, its emitted lookup says %u\", func_info.name.c_str(), int(index), context.functionLookup->valueByMnh(func_info.mnh));\n"); write(tw, " id_to_funcs.emplace_back(func_info.aotHash, &context.functions[index]);\n"); write(tw, " anyPInvoke |= func_info.pinvoke;\n"); write(tw, " \}\n"); } - write(tw, " context.tabGMnLookup = make_shared>();\n"); - write(tw, " context.tabGMnLookup->clear();\n"); + writeVariableLookup(tw, lookupVariableTable) + write(tw, " context.variableLookup = make_shared();\n"); + write(tw, " context.variableLookup->adopt(var_lookup_table);\n"); write(tw, " for ( int i=0, is=context.totalVariables; i!=is; ++i ) \{\n"); - write(tw, " auto mnh = context.globalVariables[i].mangledNameHash;\n"); - write(tw, " (*context.tabGMnLookup)[mnh] = context.globalVariables[i].offset;\n"); + write(tw, " auto & gvar = context.globalVariables[i];\n"); + write(tw, " DAS_VERIFYF(context.variableLookup->valueByMnh(gvar.mangledNameHash)==gvar.offset, \"standalone global '%s' sits at offset %u, its emitted lookup says %u\", gvar.name, gvar.offset, context.variableLookup->valueByMnh(gvar.mangledNameHash));\n"); write(tw, " \}\n"); write(tw, " context.tabAdLookup = make_shared>();\n"); program.get_ptr() |> for_each_module_no_order($(pm) { diff --git a/doc/reflections/das2rst.das b/doc/reflections/das2rst.das index 847565be78..18bed235e5 100644 --- a/doc/reflections/das2rst.das +++ b/doc/reflections/das2rst.das @@ -454,6 +454,7 @@ def document_module_rtti(_root : string) { group_by_regex("Data walking and printing", mod, %regex~(sprint_data|sprint_json_at|sscan_json_at|describe|get_mangled_name)$%%), group_by_regex("Function and mangled name hash", mod, %regex~(get_function_by_mangled_name_hash|get_function_mangled_name_hash|get_function_address)$%%), group_by_regex("Context and mutex locking", mod, %regex~(lock_this_context|lock_context|lock_mutex)$%%), + group_by_regex("Name lookup builder", mod, %regex~name_lookup_.*%%), group_by_regex("Runtime data access", mod, %regex~(get_table_key_index)$%%), group_by_regex("Tuple and variant access", mod, %regex~(get_tuple_field_offset|get_variant_field_offset)$%%), group_by_regex("Lint suppression", mod, %regex~(rtti_get_source_line|rtti_is_nolint_suppressed|is_lint_suppressed|was_nolint_consumed|extract_lint_code)$%%), diff --git a/doc/source/stdlib/handmade/annotation-rtti-NameLookup.rst b/doc/source/stdlib/handmade/annotation-rtti-NameLookup.rst new file mode 100644 index 0000000000..a18adbbf83 --- /dev/null +++ b/doc/source/stdlib/handmade/annotation-rtti-NameLookup.rst @@ -0,0 +1 @@ +Handled type wrapping the runtime's sealed function or global name lookup (``das::NameLookup``): a perfect-hash table over mangled-name hashes and plain names. The standalone emitters build one at code-generation time through the ``name_lookup_*`` functions and write its sealed arrays into the generated artifact as constant data. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_count-0x9780e5f677f52ae5.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_count-0x9780e5f677f52ae5.rst new file mode 100644 index 0000000000..93fd4a8dd7 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_count-0x9780e5f677f52ae5.rst @@ -0,0 +1 @@ +Number of entries a sealed lookup holds. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_create-0xdc119a2c4b28198f.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_create-0xdc119a2c4b28198f.rst new file mode 100644 index 0000000000..6b0b997802 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_create-0xdc119a2c4b28198f.rst @@ -0,0 +1 @@ +Creates an empty ``NameLookup`` builder and returns a raw pointer the caller owns; release it with ``name_lookup_destroy``. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_destroy-0x3fdb742796d6d4b.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_destroy-0x3fdb742796d6d4b.rst new file mode 100644 index 0000000000..07b367f16a --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_destroy-0x3fdb742796d6d4b.rst @@ -0,0 +1 @@ +Deletes a ``NameLookup`` created by ``name_lookup_create``. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_entry_index-0xa8e92693addbad91.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_entry_index-0xa8e92693addbad91.rst new file mode 100644 index 0000000000..98780d1885 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_entry_index-0xa8e92693addbad91.rst @@ -0,0 +1 @@ +Index stored in one entries slot - the position in the context's function or global array; ``0xffffffff`` in an empty slot. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_entry_mnh-0x2dbf39242b1b1cfd.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_entry_mnh-0x2dbf39242b1b1cfd.rst new file mode 100644 index 0000000000..da86fa8fa2 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_entry_mnh-0x2dbf39242b1b1cfd.rst @@ -0,0 +1 @@ +Mangled-name hash stored in one entries slot, for ``slot`` below ``name_lookup_mnh_slots``; zero in an empty slot. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_entry_next-0xde41b4c5e481bd98.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_entry_next-0xde41b4c5e481bd98.rst new file mode 100644 index 0000000000..ef97b490bb --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_entry_next-0xde41b4c5e481bd98.rst @@ -0,0 +1 @@ +Entries slot of the next entry carrying the same plain name, ``-1`` at the end of the chain. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_entry_value-0x8770c19fdc477823.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_entry_value-0x8770c19fdc477823.rst new file mode 100644 index 0000000000..36b8eb465b --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_entry_value-0x8770c19fdc477823.rst @@ -0,0 +1 @@ +Value stored in one entries slot - the function index or global byte offset a mangled-name probe answers; ``0xffffffff`` in an empty slot. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_insert-0x1df6f40370eafa48.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_insert-0x1df6f40370eafa48.rst new file mode 100644 index 0000000000..ad3e370968 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_insert-0x1df6f40370eafa48.rst @@ -0,0 +1 @@ +Stages one entry in an unsealed lookup: ``mnh`` is the mangled-name hash the runtime probes with, ``name`` the plain name (copied, so the string need not outlive the call), ``index`` the position in the context's function or global array, and ``value`` what a mangled-name probe answers - a function index, or a global's byte offset. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_mnh_buckets-0x623f630fac05a289.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_mnh_buckets-0x623f630fac05a289.rst new file mode 100644 index 0000000000..0b5a0f5dd4 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_mnh_buckets-0x623f630fac05a289.rst @@ -0,0 +1 @@ +Bucket count of a sealed lookup's mangled-name hash - the length of the displacement array ``name_lookup_mnh_disp`` reads. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_mnh_disp-0x5f1d16a55ceea87c.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_mnh_disp-0x5f1d16a55ceea87c.rst new file mode 100644 index 0000000000..f95aecb7e9 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_mnh_disp-0x5f1d16a55ceea87c.rst @@ -0,0 +1 @@ +Displacement of one bucket of the mangled-name hash, for ``bucket`` below ``name_lookup_mnh_buckets``. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_mnh_slots-0xacc87eebdfa17415.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_mnh_slots-0xacc87eebdfa17415.rst new file mode 100644 index 0000000000..bd2be08604 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_mnh_slots-0xacc87eebdfa17415.rst @@ -0,0 +1 @@ +Slot count of a sealed lookup's mangled-name hash - the length of the entries array the ``name_lookup_entry_*`` functions read. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_name_buckets-0xc771560f6c131fb.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_name_buckets-0xc771560f6c131fb.rst new file mode 100644 index 0000000000..260b1ace6a --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_name_buckets-0xc771560f6c131fb.rst @@ -0,0 +1 @@ +Bucket count of a sealed lookup's plain-name hash - the length of the displacement array ``name_lookup_name_disp`` reads. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_name_disp-0xaa40dba56bcd5663.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_name_disp-0xaa40dba56bcd5663.rst new file mode 100644 index 0000000000..a3d4590694 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_name_disp-0xaa40dba56bcd5663.rst @@ -0,0 +1 @@ +Displacement of one bucket of the plain-name hash, for ``bucket`` below ``name_lookup_name_buckets``. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_name_hash-0xaaa1df5fdee04233.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_name_hash-0xaaa1df5fdee04233.rst new file mode 100644 index 0000000000..9a31769757 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_name_hash-0xaaa1df5fdee04233.rst @@ -0,0 +1 @@ +Plain-name hash stored in one name slot, for ``slot`` below ``name_lookup_name_slots``; zero in an empty slot. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_name_head-0x1db1092788d8cc73.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_name_head-0x1db1092788d8cc73.rst new file mode 100644 index 0000000000..f1eb0e4380 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_name_head-0x1db1092788d8cc73.rst @@ -0,0 +1 @@ +Entries slot of the first entry whose plain name hashes into this name slot, ``-1`` when the slot is empty. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_name_slots-0x843def8a75a15.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_name_slots-0x843def8a75a15.rst new file mode 100644 index 0000000000..2fafdd1f03 --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_name_slots-0x843def8a75a15.rst @@ -0,0 +1 @@ +Slot count of a sealed lookup's plain-name hash - the length of the name-slot array ``name_lookup_name_hash`` and ``name_lookup_name_head`` read. diff --git a/doc/source/stdlib/handmade/function-rtti-name_lookup_seal-0xe859d3033156b01.rst b/doc/source/stdlib/handmade/function-rtti-name_lookup_seal-0xe859d3033156b01.rst new file mode 100644 index 0000000000..c22f52361d --- /dev/null +++ b/doc/source/stdlib/handmade/function-rtti-name_lookup_seal-0xe859d3033156b01.rst @@ -0,0 +1 @@ +Builds the two perfect hashes over every staged entry and freezes the lookup; panics when two entries share a mangled-name hash or two different names share a name hash, naming both entries. diff --git a/include/daScript/simulate/ARCHITECTURE.md b/include/daScript/simulate/ARCHITECTURE.md index b083cfbec3..d950a8aeb7 100644 --- a/include/daScript/simulate/ARCHITECTURE.md +++ b/include/daScript/simulate/ARCHITECTURE.md @@ -49,6 +49,41 @@ reads as zero, so two such keys merge wherever they meet in the probe sequence. for the bit compare with the `0.0 == -0.0` tie, which its two distinct hashes never honoured anyway. `tests/language/table_vector_keys.das` covers both patterns. +## Function and global lookup + +A `Context` finds a function or a global by mangled-name hash and by plain name through one +object each, `functionLookup` and `variableLookup` (`name_lookup.h`, shared between a context and +its forks and clones the way the functions array is). A simulated program builds them: +`Program::buildMNLookup` / `buildGMNLookup` insert every entry and seal once, and the sealed blob is +owned by the object and freed with it. A standalone exe and a standalone AOT context adopt them: +the emitter (`modules/dasLLVM/daslib/llvm_exe.das` and `daslib/aot_standalone.das`, repo root) +builds the same object at code-generation time through the `name_lookup_*` builtins of the +`rtti_core` module, writes the sealed arrays into the artifact as constant data in the word layout +the header pins, and the generated constructor hands that `StaticTable` to `adopt` - nothing is +built or allocated at startup and the object owns nothing. The standalone C++ constructor also +verifies every global's runtime offset against the emitted table, since the emitter computes those +offsets with `InitGlobalVariable`'s rule rather than reading them back. An insert or an adopt after +the seal stops the program, and a seal that finds two entries on one mangled-name hash, or two +different names on one name hash, fails and names both entries - the same footing the runtime +already gives every 64-bit string hash. + +The seal builds two perfect hashes (compress-hash-displace over the distinct keys, five keys per +bucket, five percent empty slots), so a lookup is one probe and one 64-bit compare with no +collision chain: `fnByMangledName` and `globalOffsetByMangledName` - the latter on the hot path of +every global access by hash in all three tiers - read one entry, and a by-name lookup hashes the +string, reads one slot, and walks the same-name chain the seal linked in function-index order. +`findFunction(name, isUnique)` answers from the head's link, `findFunctions` is the walk, and a +missing key of either kind answers `NOT_FOUND` / `-1` without touching a name string. The +entry's `value` is what the hash probe hands back - a function index, a global's byte offset - +and `index` is the position in `functions` / `globalVariables`, which is what the by-name API +returns. Measured against `das_hash_map` on 8 000 keys the hash probe is 2.2 ns against 5.8 and +the by-name probe 25 ns against 41, in one eighth and one half the memory; sealing both hashes +for 8 000 entries takes single-digit milliseconds, paid once per `Program::simulate` and never at +the startup of an artifact that adopts. A fresh object, and one whose seal failed, points at a +static empty table and answers every probe with a miss, so a context that never simulated, a +compile that failed at the seal, and a standalone exe between its creation and its adopt all +answer `NOT_FOUND` rather than reading through a null pointer. + ## Sanctioned hot-path additions The ledger the checklist's hot-path rules route to. Each entry: what was added, where, why diff --git a/include/daScript/simulate/REVIEW.md b/include/daScript/simulate/REVIEW.md index e2697b2a4e..6768857734 100644 --- a/include/daScript/simulate/REVIEW.md +++ b/include/daScript/simulate/REVIEW.md @@ -1,7 +1,8 @@ # Simulate Headers Code Review Checklist **Read `REVIEW_COMMON.md` (repo root) first - its contract binds this checklist.** Architecture doc: -`ARCHITECTURE.md`. A diff changing a `debug_info.h` struct layout applies +`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` @@ -30,14 +31,18 @@ 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's codegen unchanged - the burden is the - author's, because a diff cannot show optimized codegen. + 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. - **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.** - A change that costs more only under a relaxed-math or otherwise non-default compiler flag - states which flavor and how much in its PR description. + 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 + added work, even at no measured cost. A change that costs more only under a relaxed-math or + otherwise non-default compiler flag states which flavor and how much in its PR description. - **A diff that changes the layout of a `debug_info.h` struct - a field added, removed, reordered, or retyped, or a base changed - states a per-consumer verdict (updated / no diff --git a/include/daScript/simulate/aot_builtin_rtti.h b/include/daScript/simulate/aot_builtin_rtti.h index a3ea6a99ea..17bc1882c7 100644 --- a/include/daScript/simulate/aot_builtin_rtti.h +++ b/include/daScript/simulate/aot_builtin_rtti.h @@ -89,6 +89,24 @@ namespace das { DAS_API vec4f rtti_contextVariableInfo ( Context & context, SimNode_CallBase *, vec4f * ); DAS_API int32_t rtti_contextTotalFunctions(Context & context); DAS_API int32_t rtti_contextTotalVariables(Context & context); + struct NameLookup; + DAS_API NameLookup * rtti_name_lookup_create (); + DAS_API void rtti_name_lookup_destroy ( NameLookup * lookup ); + DAS_API void rtti_name_lookup_insert ( NameLookup * lookup, uint64_t mnh, const char * name, uint32_t index, uint32_t value ); + DAS_API void rtti_name_lookup_seal ( NameLookup * lookup, Context * context, LineInfoArg * at ); + DAS_API uint32_t rtti_name_lookup_count ( NameLookup * lookup ); + DAS_API uint32_t rtti_name_lookup_mnh_buckets ( NameLookup * lookup ); + DAS_API uint32_t rtti_name_lookup_mnh_slots ( NameLookup * lookup ); + DAS_API uint32_t rtti_name_lookup_name_buckets ( NameLookup * lookup ); + DAS_API uint32_t rtti_name_lookup_name_slots ( NameLookup * lookup ); + DAS_API uint32_t rtti_name_lookup_mnh_disp ( NameLookup * lookup, uint32_t bucket ); + DAS_API uint32_t rtti_name_lookup_name_disp ( NameLookup * lookup, uint32_t bucket ); + DAS_API uint64_t rtti_name_lookup_entry_mnh ( NameLookup * lookup, uint32_t slot ); + DAS_API uint32_t rtti_name_lookup_entry_value ( NameLookup * lookup, uint32_t slot ); + DAS_API uint32_t rtti_name_lookup_entry_index ( NameLookup * lookup, uint32_t slot ); + DAS_API int32_t rtti_name_lookup_entry_next ( NameLookup * lookup, uint32_t slot ); + DAS_API uint64_t rtti_name_lookup_name_hash ( NameLookup * lookup, uint32_t slot ); + DAS_API int32_t rtti_name_lookup_name_head ( NameLookup * lookup, uint32_t slot ); __forceinline Context & thisContext ( Context * context ) { return *context; } diff --git a/include/daScript/simulate/name_lookup.h b/include/daScript/simulate/name_lookup.h new file mode 100644 index 0000000000..5df6558a0d --- /dev/null +++ b/include/daScript/simulate/name_lookup.h @@ -0,0 +1,142 @@ +#pragma once + +#include "daScript/misc/platform.h" + +namespace das { + + //! the function or global table of a Context, indexed by mangled-name hash and by plain name. + //! Built at runtime - insert every entry, seal once; the sealed blob is owned and freed with + //! the object - or adopted from constant data an emitter sealed at code-generation time, which + //! the object never owns. Every lookup after that is one perfect-hash probe plus one 64-bit + //! key compare + struct DAS_API NameLookup { + static constexpr uint32_t NOT_FOUND = 0xffffffffu; + + struct Entry { + uint64_t mnh; + uint32_t value; //! what the mangled-name probe answers: a function index, a global's byte offset + uint32_t index; //! position in the Context's functions or globalVariables array + int32_t next; //! entries slot of the next same-name entry, -1 at the chain end + int32_t pad_; + }; + struct NameSlot { + uint64_t nameHash; + int32_t head; //! entries slot of the first same-name entry, -1 when empty + int32_t pad_; + }; + //! compress-hash-displace: a key's bucket picks a displacement, the displaced mix of the key + //! picks the slot; built once over distinct keys so no two land on one slot. The key is + //! scrambled first: an FNV hash of two names that differ only in their last characters + //! differs only in its low bits, and the bucket reads the high ones + struct PerfectHash { + uint32_t nbuckets = 1; + uint32_t nslots = 1; + const uint32_t * disp = nullptr; + //! slack is the number of five-percent empty-slot margins; a build that fails to + //! converge is retried with more + bool build ( const vector & keys, vector & dispOut, uint32_t slack = 1 ); + __forceinline uint32_t slotOf ( uint64_t key ) const { + uint64_t h = scramble(key); + uint32_t bucket = fastRange(uint32_t(h >> 32), nbuckets); + return fastRange(mix(h, disp[bucket]), nslots); + } + static __forceinline uint64_t scramble ( uint64_t key ) { + return key * 0x9E3779B97F4A7C15ull; + } + static __forceinline uint32_t fastRange ( uint32_t x, uint32_t n ) { + return uint32_t((uint64_t(x) * uint64_t(n)) >> 32); + } + static __forceinline uint32_t mix ( uint64_t h, uint32_t d ) { + uint64_t x = h ^ (uint64_t(d) * 0xD6E8FEB86659FD93ull); + x *= 0xBF58476D1CE4E5B9ull; + x ^= x >> 32; + return uint32_t(x); + } + }; + //! a sealed lookup as an emitter writes it into an artifact: the four arrays and their sizes + struct StaticTable { + uint32_t mnhBuckets; + uint32_t mnhSlots; + uint32_t nameBuckets; + uint32_t nameSlots; + uint32_t count; + uint32_t pad_; + const uint32_t * mnhDisp; + const uint32_t * nameDisp; + const Entry * entries; + const NameSlot * names; + }; + + //! a fresh object, and one whose seal failed, answers every probe with a miss + NameLookup (); + NameLookup ( const NameLookup & ) = delete; + NameLookup & operator = ( const NameLookup & ) = delete; + ~NameLookup (); + + //! value is what the mangled-name probe answers (a function index, a global byte offset); + //! index is the position in the Context's functions or globalVariables array + void insert ( uint64_t mnh, const char * name, uint32_t index, uint32_t value ); + //! false when two entries share a mangled-name hash or two different names share a name + //! hash; failure then names both entries + bool seal ( string * failure = nullptr ); + //! seal from constant data: the table and everything it points at outlive this object + void adopt ( const StaticTable & table ); + + __forceinline uint32_t valueByMnh ( uint64_t mnh ) const { + const auto & e = entries[byMnh.slotOf(mnh)]; + return e.mnh==mnh ? e.value : NOT_FOUND; + } + //! entries slot of the first entry with this name, -1 when none + __forceinline int32_t headByName ( const char * name ) const { + uint64_t nameHash = hashName(name); + const auto & n = names[byName.slotOf(nameHash)]; + return n.nameHash==nameHash ? n.head : -1; + } + __forceinline int32_t nextSameName ( int32_t slot ) const { return entries[slot].next; } + __forceinline uint32_t indexAt ( int32_t slot ) const { return entries[slot].index; } + __forceinline uint32_t valueAt ( int32_t slot ) const { return entries[slot].value; } + + uint32_t size () const { return count; } + bool isSealed () const { return sealed; } + bool isOwned () const { return blob!=nullptr; } + + static uint64_t hashName ( const char * name ); + + //! the sealed state, readable so an emitter can write it out as a StaticTable + PerfectHash byMnh; + PerfectHash byName; + const Entry * entries = nullptr; + const NameSlot * names = nullptr; + uint32_t count = 0; + bool sealed = false; + + private: + //! the name is copied into the staging arena: a caller's string need only outlive the insert + struct Staged { + uint64_t mnh; + uint64_t nameHash; + uint32_t nameOffset; + uint32_t index; + uint32_t value; + }; + const char * stagedName ( const Staged & s ) const { return nameBytes.data() + s.nameOffset; } + void pointAtEmpty (); + vector staged; + vector nameBytes; + void * blob = nullptr; + }; + + //! the emitters write these layouts as words: an entry is {mnh, value | index << 32, next} and + //! a name slot is {nameHash, head}, little-endian, so the pins hold them to that shape + static_assert(sizeof(NameLookup::Entry)==24, "NameLookup::Entry layout is emitted as three 64-bit words"); + static_assert(offsetof(NameLookup::Entry, value)==8 && offsetof(NameLookup::Entry, index)==12 && offsetof(NameLookup::Entry, next)==16, "NameLookup::Entry layout is emitted as three 64-bit words"); + static_assert(sizeof(NameLookup::NameSlot)==16 && offsetof(NameLookup::NameSlot, head)==8, "NameLookup::NameSlot layout is emitted as two 64-bit words"); + static_assert(sizeof(NameLookup::StaticTable)==6*4+4*sizeof(void *), "NameLookup::StaticTable layout is emitted as six 32-bit words and four pointers"); + static_assert(offsetof(NameLookup::StaticTable, mnhBuckets)==0 && offsetof(NameLookup::StaticTable, mnhSlots)==4 + && offsetof(NameLookup::StaticTable, nameBuckets)==8 && offsetof(NameLookup::StaticTable, nameSlots)==12 + && offsetof(NameLookup::StaticTable, count)==16, "NameLookup::StaticTable layout is emitted as six 32-bit words and four pointers"); + static_assert(offsetof(NameLookup::StaticTable, mnhDisp)==24 && offsetof(NameLookup::StaticTable, nameDisp)==24+sizeof(void *) + && offsetof(NameLookup::StaticTable, entries)==24+2*sizeof(void *) && offsetof(NameLookup::StaticTable, names)==24+3*sizeof(void *), + "NameLookup::StaticTable layout is emitted as six 32-bit words and four pointers"); + +} diff --git a/include/daScript/simulate/simulate.h b/include/daScript/simulate/simulate.h index 677cc17aa3..ed05400b8d 100644 --- a/include/daScript/simulate/simulate.h +++ b/include/daScript/simulate/simulate.h @@ -8,6 +8,7 @@ #include "daScript/simulate/runtime_string.h" #include "daScript/simulate/debug_info.h" #include "daScript/simulate/heap.h" +#include "daScript/simulate/name_lookup.h" #include "daScript/simulate/simulate_visit_op.h" @@ -528,9 +529,9 @@ namespace das } __forceinline uint32_t globalOffsetByMangledName ( uint64_t mnh ) const { - auto it = tabGMnLookup->find(mnh); - DAS_ASSERT(it!=tabGMnLookup->end()); - return it->second; + auto offset = variableLookup->valueByMnh(mnh); + DAS_ASSERT(offset!=NameLookup::NOT_FOUND); + return offset; } __forceinline uint64_t adBySid ( uint64_t sid ) const { auto it = tabAdLookup->find(sid); @@ -539,8 +540,8 @@ namespace das } __forceinline SimFunction * fnByMangledName ( uint64_t mnh ) { if ( mnh==0 ) return nullptr; - auto it = tabMnLookup->find(mnh); - return it!=tabMnLookup->end() ? it->second : nullptr; + auto index = functionLookup->valueByMnh(mnh); + return index!=NameLookup::NOT_FOUND ? functions + index : nullptr; } SimFunction * findFunction ( const char * name ) const; @@ -898,8 +899,8 @@ namespace das public: bool debugger = false; public: - shared_ptr> tabMnLookup; - shared_ptr> tabGMnLookup; + shared_ptr functionLookup; + shared_ptr variableLookup; shared_ptr> tabAdLookup; public: class Program * thisProgram = nullptr; diff --git a/modules/dasLLVM/REVIEW.md b/modules/dasLLVM/REVIEW.md index e87b0dfdd8..81531e8f6a 100644 --- a/modules/dasLLVM/REVIEW.md +++ b/modules/dasLLVM/REVIEW.md @@ -8,9 +8,9 @@ `tests/README.md` here). The suite is outside the core `tests/` sweep, so no other lane covers it. -- **A diff that adds or changes a branch on `get_platform_name()`, `get_architecture_name()`, - `cpu_supports()`, or `host_llvm_feature()` runs the module-owned suite on a machine that - takes the new branch.** +- **A diff that adds or changes a branch keyed on what `get_platform_name()`, + `get_architecture_name()`, `cpu_supports()`, or `host_llvm_feature()` returns runs the + module-owned suite on a machine that takes the new branch.** - **A test under `tests/` (beside this file) never creates, overwrites, or deletes a git-tracked path.** @@ -22,8 +22,9 @@ - **A test under `tests/` here that spawns a daslang child keeps the child's artifacts inside the directory it created for this process: `-output /...` for a `-exe` build, `-no-module-cache` or `-module-cache /...` for a run that compiles through the front-end - cache, and `-jit-no-cache` or a pinned `jit_output_path` for a `-jit` run.** A child writes - its caches relative to the cwd otherwise, which is the tree two concurrent runs share. + cache, and `-jit-no-cache` or a pinned `jit_output_path` for a `-jit` run that executes the + script.** A child writes its caches relative to the cwd otherwise, which is the tree two + concurrent runs share. - **A diff that adds or changes a branch on the target triple records in its PR body the cross-compile (`write_exe`) for that target that exercised the behavior.** The suite runs on diff --git a/modules/dasLLVM/daslib/llvm_exe.das b/modules/dasLLVM/daslib/llvm_exe.das index e0774cf777..dc2481ce61 100644 --- a/modules/dasLLVM/daslib/llvm_exe.das +++ b/modules/dasLLVM/daslib/llvm_exe.das @@ -43,6 +43,10 @@ bitfield TabOperation { class public CollectExternVisitor : AstVisitor { uid : UidNodes? + //! every function registered with the exe's context, once each - the set its function lookup carries + registered : array> + registered_index : table + // Let's keep order of traversal during initialization. // In this way we need `seen` table to not initialize twice. table_calls : array> @@ -250,6 +254,10 @@ class public CollectExternVisitor : AstVisitor { let fnmna = uid.get_dll_fn_name_ptr(fn) let fn_publ = LLVMGetNamedFunction(g_mod, fnmna.publ()) if (fn_publ == null) return null + if (!(registered_index |> key_exists(fn.index))) { + registered_index |> insert(fn.index) + registered |> push((index = fn.index, mnh = fn.getMangledNameHash, name = string(fn.name))) + } let fn_ptr_cast = LLVMBuildPointerCast(builder, fn_publ, types.LLVMVoidPtrType(), "") var reg_args = fixed_array( standalone_ctx, @@ -610,7 +618,7 @@ def collect_external_functions(standalone_context : LLVMOpaqueValue?; ctx : LLVM var funcs : array; var uids : UidNodes?; var used_modules : table&; dynamic_modules : table; - prog : Program?; register_all_modules : bool = false) : bool { + prog : Program?; register_all_modules : bool = false) : tuple>> { var extern_resolver = new CollectExternVisitor(standalone_context, ctx, builder, mod, types, uids, register_all_modules, LlvmJitMode.EXE) extern_resolver.dynamic_modules := dynamic_modules make_visitor(*extern_resolver) $(adapter_resolve) { @@ -628,7 +636,7 @@ def collect_external_functions(standalone_context : LLVMOpaqueValue?; ctx : LLVM } } } - // Register [pinvoke]/[export] functions in tabMnLookup so they're findable by name at runtime. + // Register [pinvoke]/[export] functions with the exe's context so they're findable by name at runtime. for (fn in funcs) { if (!fn.flags.builtIn && (fn.moreFlags.pinvoke || fn.flags.exports)) { extern_resolver.uid.reset(fn) @@ -691,8 +699,85 @@ def collect_external_functions(standalone_context : LLVMOpaqueValue?; ctx : LLVM LLVMDisposeBuilder(ib) extern_resolver.uid = null extern_resolver.types = null + var registered <- extern_resolver.registered unsafe { delete extern_resolver; } - return whole_lib + return (needs_whole_lib = whole_lib, registered <- registered) +} + +def private emit_const_words(name : string; words : array; elem : LLVMOpaqueType?; var types : PrimitiveTypes?) : LLVMOpaqueValue? { + let wide = elem == types.t_int64 + var consts <- [for (w in words); wide ? types.ConstI64(w) : types.ConstI32(w)] + let arr_type = LLVMArrayType(elem, uint(length(consts))) + var glob = LLVMAddGlobal(g_mod, arr_type, name) + LLVMSetInitializer(glob, LLVMConstArray(elem, unsafe(addr(consts[0])), uint(length(consts)))) + LLVMSetGlobalConstant(glob, 1) + LLVMSetLinkage(glob, LLVMLinkage.LLVMPrivateLinkage) + LLVMSetAlignment(glob, 16u) + return glob +} + +//! one sealed lookup as the exe carries it: the four constant arrays and the NameLookup::StaticTable that +//! points at them, in the word layout name_lookup.h pins - an entry is {mnh, value | index << 32, next}, a +//! name slot {nameHash, head}, the table six 32-bit counts then four pointers +def private emit_static_lookup(prefix : string; lookup : NameLookup?; var types : PrimitiveTypes?; ctx : LLVMContextRef) : LLVMOpaqueValue? { + var entries : array + entries |> reserve(int(name_lookup_mnh_slots(lookup)) * 3) + for (i in urange(name_lookup_mnh_slots(lookup))) { + entries |> push(name_lookup_entry_mnh(lookup, i)) + entries |> push(uint64(name_lookup_entry_value(lookup, i)) | (uint64(name_lookup_entry_index(lookup, i)) << 32ul)) + entries |> push(uint64(uint(name_lookup_entry_next(lookup, i)))) + } + var names : array + names |> reserve(int(name_lookup_name_slots(lookup)) * 2) + for (i in urange(name_lookup_name_slots(lookup))) { + names |> push(name_lookup_name_hash(lookup, i)) + names |> push(uint64(uint(name_lookup_name_head(lookup, i)))) + } + let mnh_disp <- [for (i in urange(name_lookup_mnh_buckets(lookup))); uint64(name_lookup_mnh_disp(lookup, i))] + let name_disp <- [for (i in urange(name_lookup_name_buckets(lookup))); uint64(name_lookup_name_disp(lookup, i))] + var g_entries = emit_const_words(".{prefix}_entries", entries, types.t_int64, types) + var g_names = emit_const_words(".{prefix}_names", names, types.t_int64, types) + var g_mnh_disp = emit_const_words(".{prefix}_mnh_disp", mnh_disp, types.t_int32, types) + var g_name_disp = emit_const_words(".{prefix}_name_disp", name_disp, types.t_int32, types) + var fields <- array( + types.ConstI32(uint64(name_lookup_mnh_buckets(lookup))), + types.ConstI32(uint64(name_lookup_mnh_slots(lookup))), + types.ConstI32(uint64(name_lookup_name_buckets(lookup))), + types.ConstI32(uint64(name_lookup_name_slots(lookup))), + types.ConstI32(uint64(name_lookup_count(lookup))), + types.ConstI32(0ul), + g_mnh_disp, g_name_disp, g_entries, g_names) + let static_table = LLVMConstStructInContext(ctx, unsafe(addr(fields[0])), uint(length(fields)), 0) + var glob = LLVMAddGlobal(g_mod, LLVMTypeOf(static_table), ".{prefix}_table") + LLVMSetInitializer(glob, static_table) + LLVMSetGlobalConstant(glob, 1) + LLVMSetLinkage(glob, LLVMLinkage.LLVMPrivateLinkage) + LLVMSetAlignment(glob, 8u) + return glob +} + +//! the exe's function and global lookups, sealed here at code-generation time and adopted by the runtime +//! context as constant data: the functions are the registered set, the globals every one of the program's +def private emit_exe_lookups(builder : LLVMBuilderRef; ctx : LLVMContextRef; var types : PrimitiveTypes?; global_context : LLVMOpaqueValue?; registered : array>; program_context : Context?) { + var fns = unsafe(name_lookup_create()) + for (fn in registered) { + name_lookup_insert(fns, fn.mnh, fn.name, uint(fn.index), uint(fn.index)) + } + name_lookup_seal(fns) + var vars = unsafe(name_lookup_create()) + for (i in range(program_context.totalVariables)) { + name_lookup_insert(vars, get_global_variable_mnh(program_context, i), get_global_variable_name(program_context, i), uint(i), uint(get_global_variable_offset(program_context, i))) + } + name_lookup_seal(vars) + let g_fns = emit_static_lookup("fn_lookup", fns, types, ctx) + let g_vars = emit_static_lookup("var_lookup", vars, types, ctx) + unsafe { + name_lookup_destroy(fns) + name_lookup_destroy(vars) + } + let adopt_type = LLVMFunctionType(types.t_void, fixed_array(types.LLVMVoidPtrType(), types.LLVMVoidPtrType(), types.LLVMVoidPtrType())) + let adopt_fn = LLVMAddFunctionWithType(g_mod, "jit_adopt_standalone_lookups", adopt_type) + LLVMBuildCall2(builder, adopt_type, adopt_fn, fixed_array(global_context, g_fns, g_vars), "") } // --jit-check-abi: emit into initialize_modules() a host-vs-target layout safe-check for every handled @@ -1190,9 +1275,9 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; // noli clone_init_glob = cpair._0 } - let sort_needs_whole_lib = collect_external_functions(global_context, ctx, builder, mod, types, funcs, uids, used_modules, dynamic_modules, prog, register_all_modules) + let collected <- collect_external_functions(global_context, ctx, builder, mod, types, funcs, uids, used_modules, dynamic_modules, prog, register_all_modules) // Determine if exe needs the whole compiler lib (rtti, ast, debugger, jit, sort-with-cblock) - var needs_whole_lib = sort_needs_whole_lib + var needs_whole_lib = collected.needs_whole_lib // ast_core / network_core live in the compiler lib, so registering them pulls compiler-only // symbols -> whole-lib link. (rtti_core used to be here but its compile builtins moved to ast, // so it's now self-contained in the runtime lib.) @@ -1242,12 +1327,15 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; // noli LLVMBuildCall2(builder, reg_fusion_type, reg_fusion_fn, array(), "") } - // void jit_register_standalone_variable ( Context * ctx, uint64_t mnh, uint64_t offset ) + // void jit_register_standalone_variable ( Context * ctx, uint64_t index, const char * name, uint64_t mnh, uint64_t offset, int shared ) let init_global_var_type = LLVMFunctionType(types.t_void, fixed_array( types.LLVMVoidPtrType(), // Context * + types.t_int64, // uint64_t index + types.LLVMVoidPtrType(), // const char * name types.t_int64, // uint64_t mnh types.t_int64, // uint64_t offset + types.t_int32, // int shared ) ) var init_global_var = LLVMAddFunctionWithType( @@ -1257,11 +1345,22 @@ def public inject_main(program_context : Context?; ctx : LLVMContextRef; // noli for (i in range(vars)) { var var_params = fixed_array( global_context, + types.ConstI64(i |> uint64), + get_string_constant_ptr(builder, get_global_variable_name(program_context, i)), types.ConstI64(get_global_variable_mnh(program_context, i)), types.ConstI64(get_global_variable_offset(program_context, i)), + types.ConstI32(uint64(get_global_variable_shared(program_context, i))), ) LLVMBuildCall2(builder, init_global_var_type, init_global_var, var_params, "") } + // the sealed lookups are adopted right after the context is created - before any registration + // call, so no JIT code can ever probe an unadopted table, however LLVM orders the calls + { + let resume_block = LLVMGetInsertBlock(builder) + LLVMPositionBuilderBefore(builder, LLVMGetNextInstruction(global_context)) + emit_exe_lookups(builder, ctx, types, global_context, collected.registered, program_context) + LLVMPositionBuilderAtEnd(builder, resume_block) + } var main_params = fixed_array( diff --git a/modules/dasLLVM/daslib/llvm_jit.das b/modules/dasLLVM/daslib/llvm_jit.das index 4b5775f8c0..038f49935d 100644 --- a/modules/dasLLVM/daslib/llvm_jit.das +++ b/modules/dasLLVM/daslib/llvm_jit.das @@ -8090,8 +8090,8 @@ def public generate_globals_initialization_fn(ctx : Context?; prog : Program?; } // nolint:STYLE014 // Populate globalVariables[] (offset/debugInfo/shared) for EVERY global so the GC - // can trace standalone-exe globals. The exe otherwise leaves globalVariables zeroed - // (only tabGMnLookup is filled), so collectHeap would deref a NULL debugInfo and + // can trace standalone-exe globals. Registration fills name, hash, offset and the + // shared flag but not debugInfo, so collectHeap would deref a NULL debugInfo and // crash on any `options gc` heap_collect. Emitted once (main init, not the // shared-skipping clone variant); debugInfo is the exe-resident TypeInfo. if (!skip_shared) { diff --git a/modules/dasLLVM/daslib/llvm_jit_run.das b/modules/dasLLVM/daslib/llvm_jit_run.das index 21f2784167..6f472f2547 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 = 0x77ul // 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 = 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))) // 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 = 0xe08c02674b3653d9ul +let LLVM_JIT_EMITTER_HASH : uint64 = 0xa85abd6c7514b352ul let JIT_FNV_PRIME : uint64 = 1099511628211ul diff --git a/modules/dasLLVM/tests/_name_lookup_root.das b/modules/dasLLVM/tests/_name_lookup_root.das new file mode 100644 index 0000000000..bea45ed3da --- /dev/null +++ b/modules/dasLLVM/tests/_name_lookup_root.das @@ -0,0 +1,34 @@ +options gen2 +require debugapi + +var g_counter = 7 + +[export] +def foo(x : int) : int { + return x + 1 +} + +[export] +def foo(x : float) : float { + return x * 2.0 +} + +[export, sideeffects] +def bump : int { + g_counter++ + return g_counter +} + +[export] +def main { + print("HAS_FOO {has_function(this_context(), "foo")}\n") + print("HAS_BUMP {has_function(this_context(), "bump")}\n") + print("HAS_NOPE {has_function(this_context(), "nope")}\n") + print("BUMP {invoke("bump")}\n") + unsafe { + var counter = reinterpret(get_context_global_variable(addr(this_context()), "g_counter")) + print("G_COUNTER {counter != null ? *counter : -1}\n") + var missing = get_context_global_variable(addr(this_context()), "nope") + print("G_NOPE {missing == null}\n") + } +} diff --git a/modules/dasLLVM/tests/llvm_exe_name_lookup.das b/modules/dasLLVM/tests/llvm_exe_name_lookup.das new file mode 100644 index 0000000000..271246eab0 --- /dev/null +++ b/modules/dasLLVM/tests/llvm_exe_name_lookup.das @@ -0,0 +1,77 @@ +options gen2 +require dastest/testing_boost public +require daslib/fio +require daslib/strings_boost +require strings +require math + +// A standalone exe registers its functions and globals one at a time and seals the lookup tables +// before its init script runs; by-name lookup - has_function, invoke by name, a global by name - +// then answers from the sealed tables, the same way it does in the interpreter. + +def private das_exe() : string { + let argv <- get_command_line_arguments() + return find(to_generic_path(argv[0]), "/") < 0 ? argv[0] : get_full_file_name(normalize(argv[0])) +} + +def private tail(s : string) : string { + return slice(s, max(0, length(s) - 400)) +} + +def private run_capture(cmd : string; var out : string&) : int { + return unsafe(popen_timeout(cmd, 300.0, $(f) { + if (f != null) { + out = fread(f) + } + })) +} + +def private build_and_run(t : T?; dir : string; tag : string) : string { + let root = "modules/dasLLVM/tests/_name_lookup_root.das" + let exe = path_join(dir, "{tag}.exe") + var bout = "" + let argv <- [das_exe(), "-jit", "-exe", "-output", path_join(dir, tag), root] + let brc = run_and_capture(argv, bout, 300.0) + t |> equal(0, brc, "the -exe build ({tag}) succeeds: {tail(bout)}") + if (brc != 0 || !stat(exe).is_valid) { + t |> success(stat(exe).is_valid, "the exe artifact exists at -output ({tag})") + return "" + } + let bin_dir = dir_name(das_exe()) + var cmd = "PATH=\"{bin_dir}:$PATH\" \"{exe}\" 2>&1" + if (get_platform_name() == "windows") { + cmd = "set \"PATH={bin_dir};%PATH%\"&& \"{exe}\" 2>&1" + } + var out = "" + let rc = run_capture(cmd, out) + t |> equal(0, rc, "the exe ({tag}) runs clean: {tail(out)}") + return out +} + +def private expect_lines(t : T?; out : string; what : string) { + t |> success(find(out, "HAS_FOO true") >= 0, "{what}: has_function finds an overloaded name: {tail(out)}") + t |> success(find(out, "HAS_BUMP true") >= 0, "{what}: has_function finds a unique name: {tail(out)}") + t |> success(find(out, "HAS_NOPE false") >= 0, "{what}: has_function misses an unknown name: {tail(out)}") + t |> success(find(out, "BUMP 8") >= 0, "{what}: invoke by name reaches the unique function: {tail(out)}") + t |> success(find(out, "G_COUNTER 8") >= 0, "{what}: a global resolves by name after the bump: {tail(out)}") + t |> success(find(out, "G_NOPE true") >= 0, "{what}: an unknown global name answers null: {tail(out)}") +} + +[test] +def test_exe_name_lookup(t : T?) { + let dir_r = create_temp_directory_result("llvm_exe_name_lookup") + if (!(dir_r is value)) { + t |> failure("temp dir failed") + return + } + let dir = unsafe(dir_r.value) + t |> run("the fixture answers the same by-name lookups interpreted and as a standalone exe") @(t : T?) { + var hout = "" + let hrc = run_and_capture([das_exe(), "-no-module-cache", "modules/dasLLVM/tests/_name_lookup_root.das"], hout, 300.0) + t |> equal(0, hrc, "the fixture runs interpreted on the host: {tail(hout)}") + expect_lines(t, hout, "interpreted") + let out = build_and_run(t, dir, "lookup") + expect_lines(t, out, "exe") + } + rmdir_rec_result(dir) +} diff --git a/nano/ARCHITECTURE.md b/nano/ARCHITECTURE.md index e535f370ed..b5d78f40bf 100644 --- a/nano/ARCHITECTURE.md +++ b/nano/ARCHITECTURE.md @@ -72,7 +72,8 @@ cannot silently get a runtime nano was not built to be - the panic path in parti `setjmp`/`longjmp`, not a C++ exception, and there is no second version of it here. `daScript/simulate/simulate.h` - the `Context` subset standalone AOT actually touches: a stack, -two heaps, the function and global tables, the three mangled-name lookups, and a panic path. +two heaps, the function and global tables, their two sealed name lookups +(`name_lookup.h`, upstream and unmodified) plus the annotation-data table, and a panic path. Gone: debug agents, stack walkers, GC roots, job-fork pools, the profiler, JIT hooks, instrumentation, context cloning, code relocation, the init and shutdown scripts (the generated constructor runs the init script itself). @@ -97,7 +98,7 @@ The header is upstream and unmodified; only fmt had to go, and it was reached th ## What nano reuses verbatim -Fourteen sources compile straight out of `src/`, listed in `nano/CMakeLists.txt`. Adding one is a +Fifteen sources compile straight out of `src/`, listed in `nano/CMakeLists.txt`. Adding one is a decision: it must compile with no edit to the shared tree. When it needs an edit, the fix goes upstream as a **carve** - splitting the runtime half of a file away from its compiler half - not into a fork here. `src/simulate/simulate_gc_pod.cpp`, `src/simulate/annotation_arguments.cpp`, diff --git a/nano/CMakeLists.txt b/nano/CMakeLists.txt index fda63e2177..dafdf2cc44 100644 --- a/nano/CMakeLists.txt +++ b/nano/CMakeLists.txt @@ -35,6 +35,7 @@ set(NANO_SHARED_SRC ${DASLANG_NANO_ROOT}/src/simulate/debug_info.cpp ${DASLANG_NANO_ROOT}/src/simulate/escape_string.cpp ${DASLANG_NANO_ROOT}/src/simulate/heap.cpp + ${DASLANG_NANO_ROOT}/src/simulate/name_lookup.cpp ${DASLANG_NANO_ROOT}/src/simulate/runtime_array.cpp ${DASLANG_NANO_ROOT}/src/simulate/runtime_iterator.cpp ${DASLANG_NANO_ROOT}/src/simulate/runtime_table.cpp diff --git a/nano/REVIEW.md b/nano/REVIEW.md index 3283549ec5..9a29cf1dc2 100644 --- a/nano/REVIEW.md +++ b/nano/REVIEW.md @@ -22,20 +22,23 @@ return turns a missing feature into a wrong answer. nano compiles unchanged from the full runtime, or generated code, already refers to it.** This header is a subset, not a copy: a name added for later use is one nobody will know to remove. -**Never rename a member of this folder's `Context` or change its type - it keeps the name and -the type it has in `include/daScript/simulate/simulate.h` at the repo root.** Generated code -and the reused headers are written against those names, so a rename is a compile error at best -and a different field at worst. +**A diff that renames a member of this folder's `Context`, or changes its type, makes the +identical change to `include/daScript/simulate/simulate.h` at the repo root in the same +change.** Generated code and the reused headers are written against those names, so a member +the two headers spell differently is a compile error at best and a different field at worst. **A diff that changes what nano leaves out also updates the tier table in `ARCHITECTURE.md` and the "What it leaves out" list in `README.md`, in the same change.** Both are read by embedders deciding whether their script fits. -**A diff that adds a tier (a group of script features nano supports as a unit) or a -fail-closed seam (a place where nano stops the program instead of returning a default) also -adds an example under `examples/standalone/` and a case in -`tests-cpp/big/nano_ctx/test_nano_ctx.cpp`, in the same change.** A tier with no program -linking it stops working and no test fails. +**A diff that adds a tier (a group of script features nano supports as a unit) also adds an +example under `examples/standalone/` and a case in `tests-cpp/big/nano_ctx/test_nano_ctx.cpp`, +in the same change.** A tier with no program linking it stops working and no test fails. + +**A diff that adds a fail-closed seam in this folder's `src/` or `include/` (a place that stops +the program instead of returning a default) also adds a case in +`tests-cpp/big/nano_ctx/test_nano_ctx.cpp`, in the same change.** A seam no test reaches has +never been shown to stop the program. **Never put an estimated number in `README.md` - measure it, and name in the table the toolchain and the targets that produced it.** A reader reproduces the number from those two diff --git a/nano/include/daScript/simulate/simulate.h b/nano/include/daScript/simulate/simulate.h index fa47da8701..a93c9dd310 100644 --- a/nano/include/daScript/simulate/simulate.h +++ b/nano/include/daScript/simulate/simulate.h @@ -21,6 +21,7 @@ #include "daScript/simulate/runtime_string.h" #include "daScript/simulate/debug_info.h" #include "daScript/simulate/heap.h" +#include "daScript/simulate/name_lookup.h" #include "daScript/simulate/code_of_policies.h" #include "daScript/simulate/simulate_visit_op.h" @@ -347,9 +348,9 @@ namespace das } __forceinline uint32_t globalOffsetByMangledName ( uint64_t mnh ) const { - auto it = tabGMnLookup->find(mnh); - DAS_ASSERT(it!=tabGMnLookup->end()); - return it->second; + auto offset = variableLookup->valueByMnh(mnh); + DAS_ASSERT(offset!=NameLookup::NOT_FOUND); + return offset; } __forceinline uint64_t adBySid ( uint64_t sid ) const { auto it = tabAdLookup->find(sid); @@ -358,8 +359,8 @@ namespace das } __forceinline SimFunction * fnByMangledName ( uint64_t mnh ) { if ( mnh==0 ) return nullptr; - auto it = tabMnLookup->find(mnh); - return it!=tabMnLookup->end() ? it->second : nullptr; + auto index = functionLookup->valueByMnh(mnh); + return index!=NameLookup::NOT_FOUND ? functions + index : nullptr; } SimFunction * findFunction ( const char * name ) const; @@ -560,8 +561,8 @@ namespace das public: SimNode * aotInitScript = nullptr; public: - shared_ptr> tabMnLookup; - shared_ptr> tabGMnLookup; + shared_ptr functionLookup; + shared_ptr variableLookup; shared_ptr> tabAdLookup; public: vec4f result; diff --git a/nano/src/nano_context.cpp b/nano/src/nano_context.cpp index c254e74615..9df07bab72 100644 --- a/nano/src/nano_context.cpp +++ b/nano/src/nano_context.cpp @@ -86,36 +86,24 @@ namespace das { } SimFunction * Context::findFunction ( const char * name ) const { - for ( int fni = 0; fni != totalFunctions; ++fni ) { - if ( strcmp(functions[fni].name, name)==0 ) { - return functions + fni; - } - } - return nullptr; + if ( !functionLookup || !name ) return nullptr; + auto slot = functionLookup->headByName(name); + return slot>=0 ? functions + functionLookup->indexAt(slot) : nullptr; } SimFunction * Context::findFunction ( const char * name, bool & isUnique ) const { - SimFunction * found = nullptr; - isUnique = true; - for ( int fni = 0; fni != totalFunctions; ++fni ) { - if ( strcmp(functions[fni].name, name)==0 ) { - if ( found ) { - isUnique = false; - return found; - } - found = functions + fni; - } - } - return found; + isUnique = false; + if ( !functionLookup || !name ) return nullptr; + auto slot = functionLookup->headByName(name); + if ( slot<0 ) return nullptr; + isUnique = functionLookup->nextSameName(slot) < 0; + return functions + functionLookup->indexAt(slot); } int Context::findVariable ( const char * name ) const { - for ( int vi = 0; vi != totalVariables; ++vi ) { - if ( strcmp(globalVariables[vi].name, name)==0 ) { - return vi; - } - } - return -1; + if ( !variableLookup || !name ) return -1; + auto slot = variableLookup->headByName(name); + return slot>=0 ? int(variableLookup->indexAt(slot)) : -1; } void Context::to_out ( const LineInfo *, int level, const char * message ) { diff --git a/src/ast/ast_simulate.cpp b/src/ast/ast_simulate.cpp index d131a14f74..01a6ad8b1d 100644 --- a/src/ast/ast_simulate.cpp +++ b/src/ast/ast_simulate.cpp @@ -2623,7 +2623,7 @@ namespace das // rewrites such refs to their literal init before simulate. But rtti-exposed ASTs // (e.g. struct field defaults) bypass folding, so the original ExprVar reference // survives. Re-simulating it would emit a GetSharedMnh / GetGlobalMnh that looks - // up a mnh not in tabGMnLookup -> crash. Emit the const init directly instead. + // up a mnh the variable lookup does not hold -> crash. Emit the const init directly instead. if ( expr->variable->index < 0 && expr->variable->init && expr->variable->init->rtti_isConstant() ) { setE(expr, simulateExpression(expr->variable->init)); @@ -3562,59 +3562,38 @@ namespace das } void Program::buildGMNLookup ( Context & context, TextWriter & logs ) { - context.tabGMnLookup = make_shared>(); - context.tabGMnLookup->clear(); + context.variableLookup = make_shared(); for ( int i=0, is=context.totalVariables; i!=is; ++i ) { auto & gvar = context.globalVariables[i]; - auto mnh = gvar.mangledNameHash; - auto it = context.tabGMnLookup->find(mnh); - if ( it != context.tabGMnLookup->end() ) { - GlobalVariable * collision = context.globalVariables + it->second; - LineInfo * errorAt = nullptr; - TextWriter message; - message << "internal compiler error: global variable mangled name hash collision '" - << gvar.name << ": " << debug_type(gvar.debugInfo) << "'" - << " hash=" << HEX << gvar.mangledNameHash << DEC - << " offset=" << gvar.offset; - if ( collision ) { - message << " and '" << collision->name << ": " << debug_type(collision->debugInfo) << "'" - << " hash=" << HEX << collision->mangledNameHash << DEC - << " offset=" << collision->offset; - } - if ( gvar.init ) { - errorAt = &gvar.init->debugInfo; - } else if ( collision && collision->init ) { - errorAt = &collision->init->debugInfo; - } - error(message.str(), "", "", errorAt ? *errorAt : LineInfo(), CompilationError::internal_global); - return; - } - context.tabGMnLookup->insert({mnh, context.globalVariables[i].offset}); + context.variableLookup->insert(gvar.mangledNameHash, gvar.name, uint32_t(i), gvar.offset); + } + string failure; + if ( !context.variableLookup->seal(&failure) ) { + error("internal compiler error: global variable " + failure, "", "", LineInfo(), CompilationError::internal_global); + return; } if ( options.getBoolOption("log_gmn_hash",false) ) { logs << "totalGlobals: " << context.totalVariables << "\n" - << "tabGMnLookup:" << context.tabGMnLookup->size() << "\n"; + << "variableLookup:" << context.variableLookup->size() << "\n"; } } void Program::buildMNLookup ( Context & context, const vector & lookupFunctions, TextWriter & logs ) { - context.tabMnLookup = make_shared>(); - context.tabMnLookup->clear(); + context.functionLookup = make_shared(); for ( const auto & fn : lookupFunctions ) { - auto mnh = fn->getMangledNameHash(); - auto it = context.tabMnLookup->find(mnh); - if ( it != context.tabMnLookup->end() ) { - error("internal compiler error: function mangled name hash collision '" + fn->name + "'", - "", "", LineInfo(), CompilationError::internal_function); - return; - } - context.tabMnLookup->insert({mnh, context.functions + fn->index}); + auto & sfn = context.functions[fn->index]; + context.functionLookup->insert(sfn.mangledNameHash, sfn.name, uint32_t(fn->index), uint32_t(fn->index)); + } + string failure; + if ( !context.functionLookup->seal(&failure) ) { + error("internal compiler error: function " + failure, "", "", LineInfo(), CompilationError::internal_function); + return; } if ( options.getBoolOption("log_mn_hash",false) ) { logs << "totalFunctions: " << context.totalFunctions << "\n" - << "tabMnLookup:" << context.tabMnLookup->size() << "\n"; + << "functionLookup:" << context.functionLookup->size() << "\n"; } } diff --git a/src/builtin/module_builtin_rtti.cpp b/src/builtin/module_builtin_rtti.cpp index 3cb2c5521f..faf85fa361 100644 --- a/src/builtin/module_builtin_rtti.cpp +++ b/src/builtin/module_builtin_rtti.cpp @@ -46,6 +46,7 @@ IMPLEMENT_EXTERNAL_TYPE_FACTORY(CodeOfPolicies,CodeOfPolicies) IMPLEMENT_EXTERNAL_TYPE_FACTORY(ModuleGroup,ModuleGroup) IMPLEMENT_EXTERNAL_TYPE_FACTORY(recursive_mutex,das::recursive_mutex) IMPLEMENT_EXTERNAL_TYPE_FACTORY(AstSerializer,das::AstSerializerState) +MAKE_TYPE_FACTORY(NameLookup,das::NameLookup) class EnumerationCompilationError : public das::Enumeration { private: @@ -1134,6 +1135,69 @@ namespace das { return context.getTotalVariables(); } + // the emitters build a context's function and global lookups here, at code-generation time, + // and read the sealed state back to write it into the artifact as constant data + // a null lookup or an out-of-range slot stops the emitter: it is writing an artifact, and a + // silently wrong word would only surface in the artifact's run + static NameLookup & nameLookupOf ( NameLookup * lookup, const char * what ) { + if ( !lookup ) DAS_FATAL_ERROR("%s: null NameLookup\n", what); + return *lookup; + } + static uint32_t nameLookupAt ( uint32_t at, uint32_t count, const char * what ) { + if ( at >= count ) DAS_FATAL_ERROR("%s: %u is past the %u the sealed table holds\n", what, at, count); + return at; + } + NameLookup * rtti_name_lookup_create () { + return new NameLookup(); + } + void rtti_name_lookup_destroy ( NameLookup * lookup ) { + delete lookup; + } + void rtti_name_lookup_insert ( NameLookup * lookup, uint64_t mnh, const char * name, uint32_t index, uint32_t value ) { + nameLookupOf(lookup, "name_lookup_insert").insert(mnh, name, index, value); + } + void rtti_name_lookup_seal ( NameLookup * lookup, Context * context, LineInfoArg * at ) { + string failure; + if ( !nameLookupOf(lookup, "name_lookup_seal").seal(&failure) ) context->throw_error_at(at, "name lookup: %s", failure.c_str()); + } + uint32_t rtti_name_lookup_count ( NameLookup * lookup ) { return nameLookupOf(lookup, "name_lookup_count").count; } + uint32_t rtti_name_lookup_mnh_buckets ( NameLookup * lookup ) { return nameLookupOf(lookup, "name_lookup_mnh_buckets").byMnh.nbuckets; } + uint32_t rtti_name_lookup_mnh_slots ( NameLookup * lookup ) { return nameLookupOf(lookup, "name_lookup_mnh_slots").byMnh.nslots; } + uint32_t rtti_name_lookup_name_buckets ( NameLookup * lookup ) { return nameLookupOf(lookup, "name_lookup_name_buckets").byName.nbuckets; } + uint32_t rtti_name_lookup_name_slots ( NameLookup * lookup ) { return nameLookupOf(lookup, "name_lookup_name_slots").byName.nslots; } + uint32_t rtti_name_lookup_mnh_disp ( NameLookup * lookup, uint32_t bucket ) { + auto & l = nameLookupOf(lookup, "name_lookup_mnh_disp"); + return l.byMnh.disp[nameLookupAt(bucket, l.byMnh.nbuckets, "name_lookup_mnh_disp")]; + } + uint32_t rtti_name_lookup_name_disp ( NameLookup * lookup, uint32_t bucket ) { + auto & l = nameLookupOf(lookup, "name_lookup_name_disp"); + return l.byName.disp[nameLookupAt(bucket, l.byName.nbuckets, "name_lookup_name_disp")]; + } + uint64_t rtti_name_lookup_entry_mnh ( NameLookup * lookup, uint32_t slot ) { + auto & l = nameLookupOf(lookup, "name_lookup_entry_mnh"); + return l.entries[nameLookupAt(slot, l.byMnh.nslots, "name_lookup_entry_mnh")].mnh; + } + uint32_t rtti_name_lookup_entry_value ( NameLookup * lookup, uint32_t slot ) { + auto & l = nameLookupOf(lookup, "name_lookup_entry_value"); + return l.entries[nameLookupAt(slot, l.byMnh.nslots, "name_lookup_entry_value")].value; + } + uint32_t rtti_name_lookup_entry_index ( NameLookup * lookup, uint32_t slot ) { + auto & l = nameLookupOf(lookup, "name_lookup_entry_index"); + return l.entries[nameLookupAt(slot, l.byMnh.nslots, "name_lookup_entry_index")].index; + } + int32_t rtti_name_lookup_entry_next ( NameLookup * lookup, uint32_t slot ) { + auto & l = nameLookupOf(lookup, "name_lookup_entry_next"); + return l.entries[nameLookupAt(slot, l.byMnh.nslots, "name_lookup_entry_next")].next; + } + uint64_t rtti_name_lookup_name_hash ( NameLookup * lookup, uint32_t slot ) { + auto & l = nameLookupOf(lookup, "name_lookup_name_hash"); + return l.names[nameLookupAt(slot, l.byName.nslots, "name_lookup_name_hash")].nameHash; + } + int32_t rtti_name_lookup_name_head ( NameLookup * lookup, uint32_t slot ) { + auto & l = nameLookupOf(lookup, "name_lookup_name_head"); + return l.names[nameLookupAt(slot, l.byName.nslots, "name_lookup_name_head")].head; + } + void rtti_builtin_context_for_each_init_function ( Context & ctx, const TBlock & block, Context * context, LineInfoArg * at ) { for ( int i=0, is=ctx.getTotalInitFunctions(); i!=is; ++i ) { vec4f args[1] = { cast::from(ctx.getInitFunction(i)->mangledNameHash) }; @@ -1813,6 +1877,42 @@ namespace das { addCtor(*this,lib,"LineInfo","LineInfo"); addAnnotation(new DummyTypeAnnotation("recursive_mutex","recursive_mutex",sizeof(recursive_mutex),alignof(recursive_mutex))); addUsing(*this, lib, "das::recursive_mutex"); + // name lookup builder for the standalone emitters + addAnnotation(new DummyTypeAnnotation("NameLookup","das::NameLookup",sizeof(NameLookup),alignof(NameLookup))); + addExtern(*this, lib, "name_lookup_create", + SideEffects::modifyExternal, "rtti_name_lookup_create")->unsafeOperation = true; + addExtern(*this, lib, "name_lookup_destroy", + SideEffects::modifyExternal, "rtti_name_lookup_destroy")->arg("lookup")->unsafeOperation = true; + addExtern(*this, lib, "name_lookup_insert", + SideEffects::modifyExternal, "rtti_name_lookup_insert")->args({"lookup","mnh","name","index","value"}); + addExtern(*this, lib, "name_lookup_seal", + SideEffects::modifyExternal, "rtti_name_lookup_seal")->args({"lookup","context","at"}); + addExtern(*this, lib, "name_lookup_count", + SideEffects::none, "rtti_name_lookup_count")->arg("lookup"); + addExtern(*this, lib, "name_lookup_mnh_buckets", + SideEffects::none, "rtti_name_lookup_mnh_buckets")->arg("lookup"); + addExtern(*this, lib, "name_lookup_mnh_slots", + SideEffects::none, "rtti_name_lookup_mnh_slots")->arg("lookup"); + addExtern(*this, lib, "name_lookup_name_buckets", + SideEffects::none, "rtti_name_lookup_name_buckets")->arg("lookup"); + addExtern(*this, lib, "name_lookup_name_slots", + SideEffects::none, "rtti_name_lookup_name_slots")->arg("lookup"); + addExtern(*this, lib, "name_lookup_mnh_disp", + SideEffects::none, "rtti_name_lookup_mnh_disp")->args({"lookup","bucket"}); + addExtern(*this, lib, "name_lookup_name_disp", + SideEffects::none, "rtti_name_lookup_name_disp")->args({"lookup","bucket"}); + addExtern(*this, lib, "name_lookup_entry_mnh", + SideEffects::none, "rtti_name_lookup_entry_mnh")->args({"lookup","slot"}); + addExtern(*this, lib, "name_lookup_entry_value", + SideEffects::none, "rtti_name_lookup_entry_value")->args({"lookup","slot"}); + addExtern(*this, lib, "name_lookup_entry_index", + SideEffects::none, "rtti_name_lookup_entry_index")->args({"lookup","slot"}); + addExtern(*this, lib, "name_lookup_entry_next", + SideEffects::none, "rtti_name_lookup_entry_next")->args({"lookup","slot"}); + addExtern(*this, lib, "name_lookup_name_hash", + SideEffects::none, "rtti_name_lookup_name_hash")->args({"lookup","slot"}); + addExtern(*this, lib, "name_lookup_name_head", + SideEffects::none, "rtti_name_lookup_name_head")->args({"lookup","slot"}); addAnnotation(new ContextAnnotation(lib)); addAnnotation(new ErrorAnnotation(lib)); addAnnotation(new FileAccessAnnotation(lib)); diff --git a/src/builtin/module_jit.cpp b/src/builtin/module_jit.cpp index 2ac6d3ab1e..44cf63fbae 100644 --- a/src/builtin/module_jit.cpp +++ b/src/builtin/module_jit.cpp @@ -348,8 +348,8 @@ extern "C" { functions[i].name = (char *) "unimplemented"; functions[i].debugInfo = &stubInfo[i]; } - tabMnLookup = make_shared>(); - tabGMnLookup = make_shared>(); + functionLookup = make_shared(); + variableLookup = make_shared(); } void *registerJitFunction ( uint64_t index, const char * funcName, const char * mangledName, @@ -375,19 +375,28 @@ extern "C" { auto node = code->makeNode(LineInfo{}, (JitFunction) fnPtr); fn.code = node; fn.jitFunction = fnPtr; // the invoke-fastpath mirror - (*tabMnLookup)[mnh] = &fn; return &fn; } - void registerJitGlobalVariable(uint64_t mnh, size_t offset) { - (*tabGMnLookup)[mnh] = offset; + void registerJitGlobalVariable(uint64_t index, const char * name, uint64_t mnh, size_t offset, bool shared) { + DAS_ASSERT(index < (uint64_t) totalVariables); + auto & gv = globalVariables[index]; + gv.name = code->allocateName(name); + gv.mangledNameHash = mnh; + gv.offset = (uint32_t) offset; + gv.flags = shared ? 1u : 0u; + } + + // the exe carries both lookups as constant data the emitter sealed; nothing is built or owned here + void adoptLookups(const NameLookup::StaticTable * functions, const NameLookup::StaticTable * variables) { + functionLookup->adopt(*functions); + variableLookup->adopt(*variables); } - // A standalone -exe leaves globalVariables[] zeroed (registerJitGlobalVariable - // only fills tabGMnLookup). collectHeap walks globalVariables[i] via - // .offset/.debugInfo/.shared, so they must be populated or the GC dereferences - // a NULL debugInfo. Called from the JIT'd init function (debugInfo is the - // exe-resident TypeInfo emitted by create_type_info_global). + // registerJitGlobalVariable fills name, hash, offset and the shared flag; the + // debugInfo is the exe-resident TypeInfo emitted by create_type_info_global, which + // only the JIT'd init function can wire. collectHeap walks globalVariables[i] via + // .offset/.debugInfo/.shared, so a NULL debugInfo is a GC crash. void setStandaloneGlobalInfo(uint64_t index, uint64_t offset, void* debugInfo, int shared) { DAS_ASSERT(index < (uint64_t) totalVariables); auto & gv = globalVariables[index]; @@ -429,8 +438,12 @@ extern "C" { fnPtr, cmres, fastcall, pinvoke, nArguments); } - DAS_API void jit_register_standalone_variable ( Context * ctx, uint64_t mangledNameHash, uint64_t offset ) { - static_cast(ctx)->registerJitGlobalVariable(mangledNameHash, offset); + DAS_API void jit_register_standalone_variable ( Context * ctx, uint64_t index, const char * name, uint64_t mangledNameHash, uint64_t offset, int shared ) { + static_cast(ctx)->registerJitGlobalVariable(index, name, mangledNameHash, offset, shared != 0); + } + + DAS_API void jit_adopt_standalone_lookups ( Context * ctx, const void * functions, const void * variables ) { + static_cast(ctx)->adoptLookups((const NameLookup::StaticTable *) functions, (const NameLookup::StaticTable *) variables); } // Populate globalVariables[index] so the GC can trace standalone-exe globals. @@ -956,6 +969,10 @@ extern "C" { return ctx->getGlobalVariable(id).mangledNameHash; } + const char * das_get_global_variable_name( const Context * ctx, int id ) { + return ctx->getGlobalVariable(id).name; + } + void * das_get_global_variable_debug_info( const Context * ctx, int id ) { return (void *) ctx->getGlobalVariable(id).debugInfo; } @@ -1602,6 +1619,8 @@ extern "C" { SideEffects::none, "das_get_global_variable_offset"); addExternInline(*this, lib, "get_global_variable_mnh", SideEffects::none, "das_get_global_variable_mnh"); + addExternInline(*this, lib, "get_global_variable_name", + SideEffects::none, "das_get_global_variable_name"); addExternInline(*this, lib, "get_global_variable_debug_info", SideEffects::none, "das_get_global_variable_debug_info"); addExternInline(*this, lib, "get_global_variable_shared", diff --git a/src/runtime/context.cpp b/src/runtime/context.cpp index 0cbb6e8d7a..659a61e487 100644 --- a/src/runtime/context.cpp +++ b/src/runtime/context.cpp @@ -162,9 +162,9 @@ namespace das if ( code ) { tw << "\tcode: " << code->bytesAllocated() << " of " << code->totalAlignedMemoryAllocated() << ", depth = " << code->depth() << "\n"; - tw << "\t\ttableMN[" << tabMnLookup->size() << "]\n"; - tw << "\t\ttableGMN[" << tabGMnLookup->size() << "]\n"; - tw << "\t\ttableAd[" << tabAdLookup->size() << "]\n"; + tw << "\t\tfunctionLookup[" << (functionLookup ? functionLookup->size() : 0u) << "]\n"; + tw << "\t\tvariableLookup[" << (variableLookup ? variableLookup->size() : 0u) << "]\n"; + tw << "\t\ttableAd[" << (tabAdLookup ? tabAdLookup->size() : 0u) << "]\n"; int aotf = 0; for ( int i=0, is=totalFunctions; i!=is; ++i ) { if ( functions[i].aotFunction ) aotf++; @@ -246,8 +246,8 @@ namespace das totalFunctions = ctx.totalFunctions; // mangled name table - tabMnLookup = ctx.tabMnLookup; - tabGMnLookup = ctx.tabGMnLookup; + functionLookup = ctx.functionLookup; + variableLookup = ctx.variableLookup; tabAdLookup = ctx.tabAdLookup; } @@ -343,8 +343,8 @@ namespace das initFunctions = ctx.initFunctions; totalInitFunctions = ctx.totalInitFunctions; // mangled name table - tabMnLookup = ctx.tabMnLookup; - tabGMnLookup = ctx.tabGMnLookup; + functionLookup = ctx.functionLookup; + variableLookup = ctx.variableLookup; tabAdLookup = ctx.tabAdLookup; // jit init script jitInitScript = ctx.jitInitScript; @@ -481,7 +481,6 @@ namespace das } rel.newCode->prefixWithHeader = pwh; rel.newCode->setInitialSize(codeSize); - SimFunction * oldFunctions = functions; if ( totalFunctions ) { SimFunction * newFunctions = (SimFunction *) rel.newCode->allocate(totalFunctions*sizeof(SimFunction)); memcpy ( newFunctions, functions, totalFunctions*sizeof(SimFunction)); @@ -499,19 +498,6 @@ namespace das } globalVariables = newVariables; } - // relocate mangle-name lookup - for ( auto & kv : *tabMnLookup ) { - auto fn = kv.second; - if ( fn!=nullptr ) { - if ( fn>=oldFunctions && fn<(oldFunctions+totalFunctions) ) { - ptrdiff_t index = fn - oldFunctions; - kv.second = functions + index; - DAS_ASSERT(fn->mangledNameHash == kv.second->mangledNameHash); - DAS_ASSERT(kv.second>=functions && kv.second<(functions+totalFunctions)); - // printf("%3i - MNH 0x%8x: %s [move %p -> %p]\n", i, fn->mangledNameHash, fn->name, fn, kv.second ); - } - } - } // relocate variables if ( totalVariables ) { for ( int j=0, js=totalVariables; j!=js; ++j ) { @@ -657,46 +643,32 @@ namespace das vector Context::findFunctions ( const char * fnname ) const { vector res; - for ( auto & kv : *tabMnLookup ) { - auto fn = kv.second; - if ( fn!=nullptr && strcmp(fn->name, fnname)==0 ) { - res.push_back(fn); - } + if ( !functionLookup || !fnname ) return res; + for ( auto slot=functionLookup->headByName(fnname); slot>=0; slot=functionLookup->nextSameName(slot) ) { + res.push_back(functions + functionLookup->indexAt(slot)); } return res; } SimFunction * Context::findFunction ( const char * fnname ) const { - for ( auto & kv : *tabMnLookup ) { - auto fn = kv.second; - if ( fn!=nullptr && strcmp(fn->name, fnname)==0 ) { - return fn; - } - } - return nullptr; + if ( !functionLookup || !fnname ) return nullptr; + auto slot = functionLookup->headByName(fnname); + return slot>=0 ? functions + functionLookup->indexAt(slot) : nullptr; } SimFunction * Context::findFunction ( const char * fnname, bool & isUnique ) const { - int candidates = 0; - SimFunction * found = nullptr; - for ( auto & kv : *tabMnLookup ) { - auto fn = kv.second; - if ( fn!=nullptr && strcmp(fn->name, fnname)==0 ) { - found = fn; - candidates++; - } - } - isUnique = candidates == 1; - return found; - } - - int Context::findVariable ( const char * fnname ) const { - for ( int vni=0, vnis=totalVariables; vni!=vnis; ++vni ) { - if ( strcmp(globalVariables[vni].name, fnname)==0 ) { - return vni; - } - } - return -1; + isUnique = false; + if ( !functionLookup || !fnname ) return nullptr; + auto slot = functionLookup->headByName(fnname); + if ( slot<0 ) return nullptr; + isUnique = functionLookup->nextSameName(slot) < 0; + return functions + functionLookup->indexAt(slot); + } + + int Context::findVariable ( const char * name ) const { + if ( !variableLookup || !name ) return -1; + auto slot = variableLookup->headByName(name); + return slot>=0 ? int(variableLookup->indexAt(slot)) : -1; } void Context::stackWalk( const LineInfo * at, bool showArguments, bool showLocalVariables ) { diff --git a/src/simulate/name_lookup.cpp b/src/simulate/name_lookup.cpp new file mode 100644 index 0000000000..b2c209f332 --- /dev/null +++ b/src/simulate/name_lookup.cpp @@ -0,0 +1,197 @@ +#include "daScript/misc/platform.h" + +#include "daScript/simulate/name_lookup.h" +#include "daScript/misc/anyhash.h" + +namespace das { + + static const uint32_t g_emptyDisp[1] = { 0 }; + static const NameLookup::Entry g_emptyEntries[1] = { { 0, NameLookup::NOT_FOUND, NameLookup::NOT_FOUND, -1, 0 } }; + static const NameLookup::NameSlot g_emptyNames[1] = { { 0, -1, 0 } }; + + NameLookup::NameLookup () { + pointAtEmpty(); + } + + NameLookup::~NameLookup () { + if ( blob ) das_aligned_free16(blob); + } + + void NameLookup::pointAtEmpty () { + byMnh = PerfectHash(); + byMnh.disp = g_emptyDisp; + byName = PerfectHash(); + byName.disp = g_emptyDisp; + entries = g_emptyEntries; + names = g_emptyNames; + count = 0; + } + + uint64_t NameLookup::hashName ( const char * name ) { + return hash_blockz64((const uint8_t *) (name ? name : "")); + } + + void NameLookup::insert ( uint64_t mnh, const char * name, uint32_t index, uint32_t value ) { + if ( sealed ) DAS_FATAL_ERROR("NameLookup::insert after seal: '%s'\n", name ? name : ""); + if ( !name ) name = ""; + auto nameOffset = uint32_t(nameBytes.size()); + nameBytes.insert(nameBytes.end(), name, name + strlen(name) + 1); + staged.push_back(Staged{mnh, hashName(name), nameOffset, index, value}); + } + + void NameLookup::adopt ( const StaticTable & table ) { + if ( sealed ) DAS_FATAL_ERROR("NameLookup::adopt after seal\n"); + byMnh.nbuckets = table.mnhBuckets; + byMnh.nslots = table.mnhSlots; + byMnh.disp = table.mnhDisp; + byName.nbuckets = table.nameBuckets; + byName.nslots = table.nameSlots; + byName.disp = table.nameDisp; + entries = table.entries; + names = table.names; + count = table.count; + sealed = true; + } + + bool NameLookup::PerfectHash::build ( const vector & keys, vector & dispOut, uint32_t slack ) { + uint32_t n = uint32_t(keys.size()); + nbuckets = das::max(1u, (n + 4) / 5); + nslots = n + das::max(1u, n / 20) * slack; + vector scrambled(n); + vector bucketOf(n); + vector bucketSize(nbuckets, 0); + for ( uint32_t i=0; i!=n; ++i ) { + scrambled[i] = scramble(keys[i]); + bucketOf[i] = fastRange(uint32_t(scrambled[i] >> 32), nbuckets); + bucketSize[bucketOf[i]] ++; + } + vector bucketStart(nbuckets + 1, 0); + for ( uint32_t b=0; b!=nbuckets; ++b ) bucketStart[b + 1] = bucketStart[b] + bucketSize[b]; + vector members(n); + { + vector fill(bucketStart.begin(), bucketStart.end() - 1); + for ( uint32_t i=0; i!=n; ++i ) members[fill[bucketOf[i]]++] = i; + } + vector order(nbuckets); + for ( uint32_t b=0; b!=nbuckets; ++b ) order[b] = b; + das::stable_sort(order.begin(), order.end(), [&](uint32_t a, uint32_t b) { + return bucketSize[a] > bucketSize[b]; + }); + dispOut.assign(nbuckets, 0); + vector occupied(nslots, 0); + vector slots; + const uint32_t dispLimit = 1u << 24; + for ( uint32_t b : order ) { + uint32_t first = bucketStart[b], last = bucketStart[b + 1]; + if ( first==last ) break; + for ( uint32_t d=0; ; ++d ) { + slots.clear(); + bool fits = true; + for ( uint32_t m=first; m!=last && fits; ++m ) { + uint32_t s = fastRange(mix(scrambled[members[m]], d), nslots); + if ( occupied[s] ) { fits = false; break; } + for ( uint32_t t : slots ) if ( t==s ) { fits = false; break; } + slots.push_back(s); + } + if ( fits ) { + for ( uint32_t s : slots ) occupied[s] = 1; + dispOut[b] = d; + break; + } + if ( d==dispLimit ) return false; + } + } + disp = dispOut.data(); + return true; + } + + bool NameLookup::seal ( string * failure ) { + if ( sealed ) { + if ( failure ) *failure = "sealed twice"; + return false; + } + das::sort(staged.begin(), staged.end(), [](const Staged & a, const Staged & b) { + return a.mnh < b.mnh; + }); + for ( size_t i=1; i keys; + keys.reserve(staged.size()); + for ( auto & s : staged ) keys.push_back(s.mnh); + vector mnhDisp; + bool built = false; + for ( uint32_t slack=1; slack<=4 && !built; ++slack ) built = byMnh.build(keys, mnhDisp, slack); + if ( !built ) { + pointAtEmpty(); + if ( failure ) *failure = "mangled name perfect hash did not converge"; + return false; + } + vector builtEntries(byMnh.nslots, Entry{0, NOT_FOUND, NOT_FOUND, -1, 0}); + for ( auto & s : staged ) { + builtEntries[byMnh.slotOf(s.mnh)] = Entry{s.mnh, s.value, s.index, -1, 0}; + } + das::sort(staged.begin(), staged.end(), [](const Staged & a, const Staged & b) { + return a.nameHash!=b.nameHash ? a.nameHash < b.nameHash : a.index < b.index; + }); + keys.clear(); + for ( size_t i=0; i nameDisp; + built = false; + for ( uint32_t slack=1; slack<=4 && !built; ++slack ) built = byName.build(keys, nameDisp, slack); + if ( !built ) { + pointAtEmpty(); + if ( failure ) *failure = "name perfect hash did not converge"; + return false; + } + vector builtNames(byName.nslots, NameSlot{0, -1, 0}); + for ( size_t i=0; i().swap(staged); + vector().swap(nameBytes); + sealed = true; + return true; + } + +} diff --git a/src/simulate/standalone_ctx_utils.cpp b/src/simulate/standalone_ctx_utils.cpp index 9fab7e7cc5..9171961c4b 100644 --- a/src/simulate/standalone_ctx_utils.cpp +++ b/src/simulate/standalone_ctx_utils.cpp @@ -48,7 +48,6 @@ namespace das { fn->aot = true; auto fcb = (SimNode_CallBase *) fn->code; fn->aotFunction = fcb->aotFunction; - (*ctx.tabMnLookup)[fn->mangledNameHash] = fn; } else if (!fn->builtin) { // Can't fill noAot functions. DAS_ASSERT(false); diff --git a/tests-cpp/big/nano_ctx/test_nano_ctx.cpp b/tests-cpp/big/nano_ctx/test_nano_ctx.cpp index 2ade6e738c..5593b33889 100644 --- a/tests-cpp/big/nano_ctx/test_nano_ctx.cpp +++ b/tests-cpp/big/nano_ctx/test_nano_ctx.cpp @@ -89,6 +89,17 @@ int main () { // `options stack = 4096` is honored exactly, plus the headroom the // global initializers need - not rounded up to the 16k default. expect_int("explicit stack is honored", ctx.stack.size() >= 4096 && ctx.stack.size() < 16384 ? 1 : 0, 1); + // by-name lookups answer from the sealed tables the generated constructor built + auto dot = ctx.findFunction("dot"); + expect_int("findFunction(dot)", dot != nullptr ? 1 : 0, 1); + expect_int("fnByMangledName(dot)", dot && ctx.fnByMangledName(dot->mangledNameHash) == dot ? 1 : 0, 1); + bool unique = false; + expect_int("findFunction(dot, unique)", ctx.findFunction("dot", unique) == dot && unique ? 1 : 0, 1); + expect_int("findFunction(nope)", ctx.findFunction("nope") == nullptr ? 1 : 0, 1); + bool missing_unique = true; + expect_int("findFunction(nope, unique) misses and is not unique", ctx.findFunction("nope", missing_unique) == nullptr && !missing_unique ? 1 : 0, 1); + expect_int("findVariable(TAPS)", ctx.findVariable("TAPS") >= 0 ? 1 : 0, 1); + expect_int("findVariable(nope)", ctx.findVariable("nope"), -1); } { // tier B - the das heap diff --git a/tests-cpp/big/standalone_ctx/standalone_init_fixture.das b/tests-cpp/big/standalone_ctx/standalone_init_fixture.das index c0a4981f04..75ed20f363 100644 --- a/tests-cpp/big/standalone_ctx/standalone_init_fixture.das +++ b/tests-cpp/big/standalone_ctx/standalone_init_fixture.das @@ -19,6 +19,7 @@ var g_second = next_stamp() var g_reads_forward = read_forward() + 100 var g_later = 7 var g_init_fn_stamp = 0 +let shared g_shared_taps = fixed_array(1, 2, 3) [init] def record_init { @@ -50,6 +51,15 @@ def get_later : int { return g_later } +[export] +def get_shared_total : int { + var total = 0 + for (t in g_shared_taps) { + total += t + } + return total +} + struct Pair { a : int b : int diff --git a/tests-cpp/big/standalone_ctx/test_standalone_ctx.cpp b/tests-cpp/big/standalone_ctx/test_standalone_ctx.cpp index 9db27ba8dc..d406ead92b 100644 --- a/tests-cpp/big/standalone_ctx/test_standalone_ctx.cpp +++ b/tests-cpp/big/standalone_ctx/test_standalone_ctx.cpp @@ -34,5 +34,23 @@ int main( int, char * [] ) { expect("apply_lambda(10)", ctx.apply_lambda(10), 16); expect("call_through_pointer(21)", ctx.call_through_pointer(21), 42); expect("sum_generator(5)", ctx.sum_generator(5), 10); + auto getFirst = ctx.findFunction("get_first"); + expect("findFunction(get_first)", getFirst != nullptr ? 1 : 0, 1); + expect("fnByMangledName(get_first)", getFirst && ctx.fnByMangledName(getFirst->mangledNameHash) == getFirst ? 1 : 0, 1); + bool unique = false; + expect("findFunction(get_first, unique)", ctx.findFunction("get_first", unique) == getFirst && unique ? 1 : 0, 1); + expect("findFunctions(get_first).size()", int32_t(ctx.findFunctions("get_first").size()), 1); + expect("findFunction(nope)", ctx.findFunction("nope") == nullptr ? 1 : 0, 1); + expect("findFunctions(nope).size()", int32_t(ctx.findFunctions("nope").size()), 0); + int later = ctx.findVariable("g_later"); + expect("findVariable(g_later)", later >= 0 ? 1 : 0, 1); + expect("getVariable(g_later)", later >= 0 ? *(int32_t *) ctx.getVariable(later) : -1, 7); + expect("findVariable(nope)", ctx.findVariable("nope"), -1); + // a shared global sits in the shared block: its emitted offset is the shared running size + expect("get_shared_total()", ctx.get_shared_total(), 6); + int sharedTaps = ctx.findVariable("g_shared_taps"); + expect("findVariable(g_shared_taps)", sharedTaps >= 0 ? 1 : 0, 1); + expect("getVariable(g_shared_taps)[0]", sharedTaps >= 0 ? ((int32_t *) ctx.getVariable(sharedTaps))[0] : -1, 1); + expect("getVariable(g_shared_taps)[2]", sharedTaps >= 0 ? ((int32_t *) ctx.getVariable(sharedTaps))[2] : -1, 3); return failures ? 1 : 0; } diff --git a/tests-cpp/small/test_name_lookup.cpp b/tests-cpp/small/test_name_lookup.cpp new file mode 100644 index 0000000000..918a90ae9f --- /dev/null +++ b/tests-cpp/small/test_name_lookup.cpp @@ -0,0 +1,290 @@ +#include + +#include "daScript/daScript.h" +#include "daScript/simulate/name_lookup.h" + +#include +#include +#include +#include + +using namespace das; + +#define SCRIPT_PATH "/tests-cpp/small/test_name_lookup.das" + +namespace { + +std::string mangledOf ( uint32_t i ) { + return "fn_" + std::to_string(i / 4) + " (" + std::to_string(i % 4) + ")"; +} + +} + +TEST_CASE("NameLookup - sealed perfect hash answers every key and no other") { + const uint32_t N = 10000; + std::vector names, mangled; + std::vector mnhs; + for ( uint32_t i=0; i!=N; ++i ) { + names.push_back("fn_" + std::to_string(i / 4)); + mangled.push_back(mangledOf(i)); + mnhs.push_back(NameLookup::hashName(mangled.back().c_str())); + } + NameLookup lookup; + for ( uint32_t i=0; i!=N; ++i ) lookup.insert(mnhs[i], names[i].c_str(), i, i * 16); + CHECK_FALSE(lookup.isSealed()); + string failure; + REQUIRE_MESSAGE(lookup.seal(&failure), failure); + CHECK(lookup.isSealed()); + CHECK_EQ(lookup.size(), N); + for ( uint32_t i=0; i!=N; ++i ) { + CHECK_EQ(lookup.valueByMnh(mnhs[i]), i * 16); + } + CHECK_EQ(lookup.valueByMnh(0), NameLookup::NOT_FOUND); + CHECK_EQ(lookup.valueByMnh(NameLookup::hashName("not a mangled name")), NameLookup::NOT_FOUND); + for ( uint32_t g=0; g!=N / 4; ++g ) { + std::string name = "fn_" + std::to_string(g); + int32_t slot = lookup.headByName(name.c_str()); + REQUIRE_MESSAGE(slot >= 0, name); + uint32_t seen = 0; + for ( ; slot >= 0; slot = lookup.nextSameName(slot), ++seen ) { + CHECK_EQ(lookup.indexAt(slot), g * 4 + seen); + CHECK_EQ(lookup.valueAt(slot), (g * 4 + seen) * 16); + } + CHECK_EQ(seen, 4u); + } + CHECK_EQ(lookup.headByName("fn_"), -1); + CHECK_EQ(lookup.headByName("nope"), -1); + CHECK_EQ(lookup.headByName(""), -1); + CHECK_EQ(lookup.headByName(nullptr), -1); +} + +TEST_CASE("NameLookup - empty and single-entry tables") { + SUBCASE("empty") { + NameLookup lookup; + REQUIRE(lookup.seal()); + CHECK_EQ(lookup.size(), 0u); + CHECK_EQ(lookup.valueByMnh(12345), NameLookup::NOT_FOUND); + CHECK_EQ(lookup.headByName("x"), -1); + } + SUBCASE("single") { + NameLookup lookup; + lookup.insert(777, "only", 0, 99); + REQUIRE(lookup.seal()); + CHECK_EQ(lookup.valueByMnh(777), 99u); + CHECK_EQ(lookup.valueByMnh(778), NameLookup::NOT_FOUND); + int32_t slot = lookup.headByName("only"); + REQUIRE(slot >= 0); + CHECK_EQ(lookup.indexAt(slot), 0u); + CHECK_EQ(lookup.nextSameName(slot), -1); + } +} + +TEST_CASE("NameLookup - an adopted table answers like the one that built it and owns nothing") { + NameLookup built; + std::vector names, mangled; + for ( uint32_t i=0; i!=300; ++i ) { + names.push_back("g_" + std::to_string(i / 3)); + mangled.push_back(names.back() + "#" + std::to_string(i % 3)); + built.insert(NameLookup::hashName(mangled.back().c_str()), names.back().c_str(), i, i * 16); + } + REQUIRE(built.seal()); + CHECK(built.isOwned()); + // the arrays an emitter would write out, held here in the caller's own storage + std::vector entries(built.entries, built.entries + built.byMnh.nslots); + std::vector slots(built.names, built.names + built.byName.nslots); + std::vector mnhDisp(built.byMnh.disp, built.byMnh.disp + built.byMnh.nbuckets); + std::vector nameDisp(built.byName.disp, built.byName.disp + built.byName.nbuckets); + NameLookup::StaticTable table = { built.byMnh.nbuckets, built.byMnh.nslots, built.byName.nbuckets, built.byName.nslots, + built.count, 0, mnhDisp.data(), nameDisp.data(), entries.data(), slots.data() }; + { + NameLookup adopted; + adopted.adopt(table); + CHECK(adopted.isSealed()); + CHECK_FALSE(adopted.isOwned()); + CHECK_EQ(adopted.size(), built.size()); + for ( uint32_t i=0; i!=300; ++i ) { + auto mnh = NameLookup::hashName(mangled[i].c_str()); + CHECK_EQ(adopted.valueByMnh(mnh), built.valueByMnh(mnh)); + } + for ( uint32_t g=0; g!=100; ++g ) { + std::string name = "g_" + std::to_string(g); + int32_t a = adopted.headByName(name.c_str()), b = built.headByName(name.c_str()); + for ( ; a >= 0 && b >= 0; a = adopted.nextSameName(a), b = built.nextSameName(b) ) { + CHECK_EQ(adopted.indexAt(a), built.indexAt(b)); + } + CHECK_EQ(a, b); + } + CHECK_EQ(adopted.headByName("g_100"), -1); + CHECK_EQ(adopted.valueByMnh(1), NameLookup::NOT_FOUND); + } + // the adopted object is gone; the storage it pointed at is untouched + CHECK_EQ(entries.size(), size_t(built.byMnh.nslots)); + CHECK_EQ(built.valueByMnh(NameLookup::hashName(mangled[7].c_str())), 7u * 16); +} + +TEST_CASE("NameLookup - an inserted name need not outlive the insert") { + NameLookup lookup; + char scratch[32]; + for ( uint32_t i=0; i!=40; ++i ) { + // one buffer, rewritten before every insert: two overloads of `same`, then names that differ + snprintf(scratch, sizeof(scratch), i < 2 ? "same" : "other_%u", i); + lookup.insert(1000 + i, scratch, i, i); + memset(scratch, 'x', sizeof(scratch) - 1); + scratch[sizeof(scratch) - 1] = 0; + } + string failure; + REQUIRE_MESSAGE(lookup.seal(&failure), failure); + int32_t slot = lookup.headByName("same"); + REQUIRE(slot >= 0); + CHECK_EQ(lookup.indexAt(slot), 0u); + REQUIRE(lookup.nextSameName(slot) >= 0); + CHECK_EQ(lookup.indexAt(lookup.nextSameName(slot)), 1u); + CHECK_EQ(lookup.nextSameName(lookup.nextSameName(slot)), -1); + CHECK(lookup.headByName("other_7") >= 0); + CHECK_EQ(lookup.headByName("xxxx"), -1); +} + +TEST_CASE("NameLookup - a duplicate mangled-name hash refuses to seal and names both entries") { + NameLookup lookup; + lookup.insert(1, "first", 0, 0); + lookup.insert(2, "second", 1, 1); + lookup.insert(1, "third", 2, 2); + string failure; + CHECK_FALSE(lookup.seal(&failure)); + CHECK(failure.find("first") != string::npos); + CHECK(failure.find("third") != string::npos); + // the failed object answers every probe with a miss, and a second seal fails the same way + CHECK_FALSE(lookup.isSealed()); + CHECK_EQ(lookup.size(), 0u); + CHECK_EQ(lookup.valueByMnh(1), NameLookup::NOT_FOUND); + CHECK_EQ(lookup.valueByMnh(2), NameLookup::NOT_FOUND); + CHECK_EQ(lookup.headByName("first"), -1); + string again; + CHECK_FALSE(lookup.seal(&again)); + CHECK_EQ(again, failure); +} + +TEST_CASE("NameLookup - a fresh object answers every probe with a miss") { + NameLookup fresh; + CHECK_FALSE(fresh.isSealed()); + CHECK_FALSE(fresh.isOwned()); + CHECK_EQ(fresh.size(), 0u); + CHECK_EQ(fresh.valueByMnh(0), NameLookup::NOT_FOUND); + CHECK_EQ(fresh.valueByMnh(0x1234567887654321ull), NameLookup::NOT_FOUND); + CHECK_EQ(fresh.headByName("anything"), -1); + CHECK_EQ(fresh.headByName(""), -1); +} + +TEST_CASE("NameLookup - a sealed table refuses a second seal and keeps answering") { + NameLookup lookup; + lookup.insert(5, "only", 0, 7); + REQUIRE(lookup.seal()); + string failure; + CHECK_FALSE(lookup.seal(&failure)); + CHECK(failure.find("sealed twice") != string::npos); + CHECK(lookup.isSealed()); + CHECK_EQ(lookup.valueByMnh(5), 7u); +} + +TEST_CASE("NameLookup - a null name inserts as the empty name") { + NameLookup lookup; + lookup.insert(9, nullptr, 3, 3); + lookup.insert(10, "named", 4, 4); + REQUIRE(lookup.seal()); + int32_t slot = lookup.headByName(""); + REQUIRE(slot >= 0); + CHECK_EQ(lookup.indexAt(slot), 3u); + CHECK_EQ(lookup.headByName(nullptr), slot); + CHECK_EQ(lookup.nextSameName(slot), -1); +} + +TEST_CASE("Context - a context that never simulated misses every by-name lookup") { + Context bare(4096); + CHECK_EQ(bare.findFunction("main"), nullptr); + bool unique = true; + CHECK_EQ(bare.findFunction("main", unique), nullptr); + CHECK_FALSE(unique); + CHECK(bare.findFunctions("main").empty()); + CHECK(bare.findFunctions(nullptr).empty()); + CHECK_EQ(bare.findVariable("g"), -1); + TextWriter report; + bare.logMemInfo(report); + CHECK(string(report.str()).find("functionLookup[0]") != string::npos); +} + +TEST_CASE("Context - by-name and by-hash function and global lookups") { + TextPrinter tout; + ModuleGroup dummyLibGroup; + auto fAccess = make_smart(); + auto program = compileDaScript(getDasRoot() + SCRIPT_PATH, fAccess, tout, dummyLibGroup); + REQUIRE_FALSE(program->failed()); + Context ctx(program->getContextStackSize()); + REQUIRE(program->simulate(ctx, tout)); + + SUBCASE("overloads share a name") { + auto foo = ctx.findFunction("foo"); + REQUIRE(foo != nullptr); + CHECK(strcmp(foo->name, "foo") == 0); + auto foos = ctx.findFunctions("foo"); + REQUIRE_EQ(foos.size(), 3u); + CHECK_EQ(foos[0], foo); + for ( size_t i=0; i!=foos.size(); ++i ) { + CHECK(strcmp(foos[i]->name, "foo") == 0); + for ( size_t j=i + 1; j!=foos.size(); ++j ) { + CHECK(strcmp(foos[i]->mangledName, foos[j]->mangledName) != 0); + } + CHECK_EQ(ctx.fnByMangledName(foos[i]->mangledNameHash), foos[i]); + } + bool unique = true; + CHECK_EQ(ctx.findFunction("foo", unique), foo); + CHECK_FALSE(unique); + } + SUBCASE("a unique name") { + bool unique = false; + auto bar = ctx.findFunction("bar", unique); + REQUIRE(bar != nullptr); + CHECK(unique); + CHECK(strcmp(bar->name, "bar") == 0); + CHECK_EQ(ctx.findFunctions("bar").size(), 1u); + CHECK_EQ(ctx.fnByMangledName(bar->mangledNameHash), bar); + } + SUBCASE("a missing name") { + bool unique = true; + CHECK_EQ(ctx.findFunction("nope"), nullptr); + CHECK_EQ(ctx.findFunction("nope", unique), nullptr); + CHECK_FALSE(unique); + CHECK(ctx.findFunctions("nope").empty()); + CHECK_EQ(ctx.findFunction("fo"), nullptr); + CHECK_EQ(ctx.findFunction("fooo"), nullptr); + CHECK_EQ(ctx.findFunction(""), nullptr); + CHECK_EQ(ctx.findFunction(nullptr), nullptr); + CHECK_EQ(ctx.fnByMangledName(0), nullptr); + CHECK_EQ(ctx.fnByMangledName(NameLookup::hashName("no such mangled name")), nullptr); + } + SUBCASE("globals by name and by hash") { + int counter = ctx.findVariable("g_counter"); + REQUIRE(counter >= 0); + CHECK(strcmp(ctx.getVariableInfo(counter)->name, "g_counter") == 0); + int label = ctx.findVariable("g_label"); + REQUIRE(label >= 0); + CHECK(strcmp(ctx.getVariableInfo(label)->name, "g_label") == 0); + CHECK_NE(counter, label); + CHECK_EQ(ctx.findVariable("nope"), -1); + CHECK_EQ(ctx.findVariable("g_"), -1); + CHECK_EQ(ctx.findVariable(nullptr), -1); + for ( int i=0; i!=ctx.getTotalVariables(); ++i ) { + auto gv = ctx.getGlobalVariable(i); + CHECK_EQ(ctx.globalOffsetByMangledName(gv.mangledNameHash), gv.offset); + CHECK_EQ(ctx.findVariable(gv.name), i); + } + } + SUBCASE("a fork shares the sealed tables") { + Context::CopyOptions opts; + Context fork(ctx, opts); + CHECK_EQ(fork.functionLookup.get(), ctx.functionLookup.get()); + CHECK_EQ(fork.variableLookup.get(), ctx.variableLookup.get()); + CHECK_EQ(fork.findFunction("bar"), ctx.findFunction("bar")); + CHECK_EQ(fork.findFunctions("foo").size(), 3u); + CHECK_EQ(fork.findVariable("g_scale"), ctx.findVariable("g_scale")); + } +} diff --git a/tests-cpp/small/test_name_lookup.das b/tests-cpp/small/test_name_lookup.das new file mode 100644 index 0000000000..5c04c7d1fd --- /dev/null +++ b/tests-cpp/small/test_name_lookup.das @@ -0,0 +1,31 @@ +options gen2 + +var g_counter = 0 +var g_label = "label" +var g_scale = 2.5 + +[export] +def foo(x : int) : int { + return x + 1 +} + +[export] +def foo(x : float) : float { + return x * 2.0 +} + +[export] +def foo(x : string) : string { + return x +} + +[export] +def bar : int { + return 42 +} + +[export] +def main { + g_counter++ + print("{g_label} {g_scale}\n") +}