From 2e93819929fe220cd548e4f98f998e8e7f5ce481 Mon Sep 17 00:00:00 2001 From: Boris Batkin Date: Tue, 8 Sep 2026 08:06:19 -0700 Subject: [PATCH] Context finds a function or a global by name and by mangled hash through one sealed perfect-hash object each (functionLookup / variableLookup replace tabMnLookup / tabGMnLookup): findFunction, findFunctions, findFunction(name, isUnique) and findVariable were a strcmp walk over the whole table - an editor calling invoke_in_context by name per scene node on a program with thousands of functions spent 96% of its frame there - and are now one probe, one 64-bit compare and a same-name chain in function-index order, with no strcmp at all since hash equality is name equality everywhere else in the runtime; a distinct-name hash collision fails the seal the way a mangled-hash collision already does, and a failed seal or a never-sealed object answers every probe with a miss. fnByMangledName and globalOffsetByMangledName - the latter on every global-by-hash access in all three tiers - read one entry (2.2 ns against das_hash_map's 5.8 on 8k keys, an eighth of the memory). A simulated program builds the tables (buildMNLookup / buildGMNLookup insert then seal once, the blob is owned and freed with the object); a standalone exe and a standalone AOT context adopt them: the emitter seals the same object at code-generation time through the rtti_core name_lookup_* builtins and writes the arrays into the artifact as constant data in the word layout name_lookup.h pins, so the generated constructor builds and owns nothing - llvm_exe.das emits private constant globals and adopts them right after the context is created, before any registration call (jit_register_standalone_variable now carries index, name and the shared flag; codegen version 0x77, emitter pin re-hashed for a comment), aot_standalone.das emits static const aggregates and verifies every function's index and every global's runtime offset against the emitted tables. FillFunction no longer touches the table and the relocation walk is gone since entries hold indices. A standalone exe's globals now have names, which findVariable used to strcmp as null; the old buildGMNLookup collision path indexed globalVariables by byte offset. nano shares name_lookup.cpp unmodified (the SDK install ships it) and its findFunction / findVariable use the same tables. The new builtins are documented; the hot-path, nano and dasLLVM checklists carry the rulings the review round reached. Tests: tests-cpp/small/test_name_lookup (a 10k-key suffix-only adversarial table, an adopted copy answering like its builder and owning nothing, a name that dies right after its insert, a fresh object and a failed seal both missing, a Context's overloads, uniqueness, misses, a fork sharing the tables, a context that never simulated), modules/dasLLVM/tests/llvm_exe_name_lookup (interpreted and as an exe), cases in the standalone and nano big tests including a shared global. Co-Authored-By: Claude Fable 5.1 --- CMakeLists.txt | 4 + daslib/aot_standalone.das | 85 ++++- doc/reflections/das2rst.das | 1 + .../handmade/annotation-rtti-NameLookup.rst | 1 + ...i-name_lookup_count-0x9780e5f677f52ae5.rst | 1 + ...-name_lookup_create-0xdc119a2c4b28198f.rst | 1 + ...-name_lookup_destroy-0x3fdb742796d6d4b.rst | 1 + ..._lookup_entry_index-0xa8e92693addbad91.rst | 1 + ...me_lookup_entry_mnh-0x2dbf39242b1b1cfd.rst | 1 + ...e_lookup_entry_next-0xde41b4c5e481bd98.rst | 1 + ..._lookup_entry_value-0x8770c19fdc477823.rst | 1 + ...-name_lookup_insert-0x1df6f40370eafa48.rst | 1 + ..._lookup_mnh_buckets-0x623f630fac05a289.rst | 1 + ...ame_lookup_mnh_disp-0x5f1d16a55ceea87c.rst | 1 + ...me_lookup_mnh_slots-0xacc87eebdfa17415.rst | 1 + ..._lookup_name_buckets-0xc771560f6c131fb.rst | 1 + ...me_lookup_name_disp-0xaa40dba56bcd5663.rst | 1 + ...me_lookup_name_hash-0xaaa1df5fdee04233.rst | 1 + ...me_lookup_name_head-0x1db1092788d8cc73.rst | 1 + ...name_lookup_name_slots-0x843def8a75a15.rst | 1 + ...tti-name_lookup_seal-0xe859d3033156b01.rst | 1 + include/daScript/simulate/ARCHITECTURE.md | 35 +++ include/daScript/simulate/REVIEW.md | 15 +- include/daScript/simulate/aot_builtin_rtti.h | 18 ++ include/daScript/simulate/name_lookup.h | 142 +++++++++ include/daScript/simulate/simulate.h | 15 +- modules/dasLLVM/REVIEW.md | 11 +- modules/dasLLVM/daslib/llvm_exe.das | 111 ++++++- modules/dasLLVM/daslib/llvm_jit.das | 4 +- modules/dasLLVM/daslib/llvm_jit_run.das | 4 +- modules/dasLLVM/tests/_name_lookup_root.das | 34 ++ .../dasLLVM/tests/llvm_exe_name_lookup.das | 77 +++++ nano/ARCHITECTURE.md | 5 +- nano/CMakeLists.txt | 1 + nano/REVIEW.md | 21 +- nano/include/daScript/simulate/simulate.h | 15 +- nano/src/nano_context.cpp | 36 +-- src/ast/ast_simulate.cpp | 57 ++-- src/builtin/module_builtin_rtti.cpp | 100 ++++++ src/builtin/module_jit.cpp | 43 ++- src/runtime/context.cpp | 78 ++--- src/simulate/name_lookup.cpp | 197 ++++++++++++ src/simulate/standalone_ctx_utils.cpp | 1 - tests-cpp/big/nano_ctx/test_nano_ctx.cpp | 11 + .../standalone_init_fixture.das | 10 + .../standalone_ctx/test_standalone_ctx.cpp | 18 ++ tests-cpp/small/test_name_lookup.cpp | 290 ++++++++++++++++++ tests-cpp/small/test_name_lookup.das | 31 ++ 48 files changed, 1306 insertions(+), 182 deletions(-) create mode 100644 doc/source/stdlib/handmade/annotation-rtti-NameLookup.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_count-0x9780e5f677f52ae5.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_create-0xdc119a2c4b28198f.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_destroy-0x3fdb742796d6d4b.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_entry_index-0xa8e92693addbad91.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_entry_mnh-0x2dbf39242b1b1cfd.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_entry_next-0xde41b4c5e481bd98.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_entry_value-0x8770c19fdc477823.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_insert-0x1df6f40370eafa48.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_mnh_buckets-0x623f630fac05a289.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_mnh_disp-0x5f1d16a55ceea87c.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_mnh_slots-0xacc87eebdfa17415.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_name_buckets-0xc771560f6c131fb.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_name_disp-0xaa40dba56bcd5663.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_name_hash-0xaaa1df5fdee04233.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_name_head-0x1db1092788d8cc73.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_name_slots-0x843def8a75a15.rst create mode 100644 doc/source/stdlib/handmade/function-rtti-name_lookup_seal-0xe859d3033156b01.rst create mode 100644 include/daScript/simulate/name_lookup.h create mode 100644 modules/dasLLVM/tests/_name_lookup_root.das create mode 100644 modules/dasLLVM/tests/llvm_exe_name_lookup.das create mode 100644 src/simulate/name_lookup.cpp create mode 100644 tests-cpp/small/test_name_lookup.cpp create mode 100644 tests-cpp/small/test_name_lookup.das 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") +}