diff --git a/docs/setup.md b/docs/setup.md index dff3e86..94f5c72 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -46,6 +46,8 @@ UCE also requires two non-vendored dependencies. WASI SDK is load-bearing at run - `/opt/wasi-sdk/bin/clang++` - `/opt/wasi-sdk/bin/wasm-ld` - `/opt/wasi-sdk/bin/llvm-objcopy` + - `/opt/wasi-sdk/bin/llvm-nm` + - `/opt/wasi-sdk/bin/llvm-dwarfdump` You can use different install locations by setting environment variables before building and in the systemd service environment: @@ -100,6 +102,8 @@ The expected directories are: /opt/wasi-sdk/bin/clang++ /opt/wasi-sdk/bin/wasm-ld /opt/wasi-sdk/bin/llvm-objcopy +/opt/wasi-sdk/bin/llvm-nm +/opt/wasi-sdk/bin/llvm-dwarfdump ``` Install the WASI SDK: @@ -135,6 +139,8 @@ test -f /opt/wasmtime/lib/libwasmtime.so /opt/wasi-sdk/bin/clang++ --version /opt/wasi-sdk/bin/wasm-ld --version /opt/wasi-sdk/bin/llvm-objcopy --version +/opt/wasi-sdk/bin/llvm-nm --version +/opt/wasi-sdk/bin/llvm-dwarfdump --version ``` If your paths differ, export the variables for manual builds: @@ -687,7 +693,7 @@ Common compile footguns: - `WASM_COMPILE_SCRIPT` is unset or points at a removed script such as `scripts/compile`; set it to `scripts/compile_wasm_unit`. - `scripts/check_unit_wasm.py` is missing or not executable; `scripts/compile_wasm_unit` calls it after linking each unit. -- `WASI_SDK` does not point at the pinned tree with `clang++`, `wasm-ld`, `llvm-objcopy`, and `llvm-nm`; run `scripts/install_wasi_sdk.sh --check-only`. +- `WASI_SDK` does not point at the pinned tree with `clang++`, `wasm-ld`, `llvm-objcopy`, `llvm-nm`, and `llvm-dwarfdump`; run `scripts/install_wasi_sdk.sh --check-only`. - `WASMTIME_HOME` does not point at a tree with Wasmtime headers and `libwasmtime.so`. - A previous failed compile left stale `.compile.txt`, `.wasm-check.txt`, or partial `.wasm` files under `BIN_DIRECTORY`. diff --git a/docs/wasi-sdk-toolchain.md b/docs/wasi-sdk-toolchain.md index 37a328a..bb19bc7 100644 --- a/docs/wasi-sdk-toolchain.md +++ b/docs/wasi-sdk-toolchain.md @@ -31,9 +31,13 @@ UCE expects these executables on each deployment host: /opt/wasi-sdk/bin/wasm-ld /opt/wasi-sdk/bin/llvm-objcopy /opt/wasi-sdk/bin/llvm-nm +/opt/wasi-sdk/bin/llvm-dwarfdump ``` `llvm-nm` is used by `scripts/check_unit_wasm.py`, which is called by `scripts/compile_wasm_unit` after linking each unit. +`llvm-dwarfdump` is used at unit compile time to extract the compact, out-of-band +source map before debug sections are stripped from the runtime artifact. The +map is consulted only on a wasm trap. ## Upgrade policy diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index e831b55..b0f4aba 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -222,8 +222,8 @@ Freshness still stats every distinct source on every entry check. Exact repeated load paths are deduplicated before canonicalization, while distinct aliases are resolved independently so symlink retargets remain immediately visible. When a current serialized module exists, the worker scans wasm section headers -and reads only `dylink.0` and `uce.abi`; it does not fault the multi-megabyte code -and data bodies into every new worker. A missing/stale/invalid serialized module +and reads only `dylink.0`, `uce.abi`, and the tiny `uce.module` identity; it does +not fault the code and data bodies into every new worker. A missing/stale/invalid serialized module still reads, validates, compiles, and republishes the complete wasm artifact. The proactive compiler also creates that serialization immediately after source compilation, keeping first-worker native compilation off the request path. @@ -257,6 +257,19 @@ intentional unreadable-source regression request as an expected source-read failure, keeping production log scans focused on real runtime failures while preserving the failing compile result and diagnostic artifacts. +Unit linking retains DWARF only long enough for +`scripts/build_unit_source_map.py` to extract a compact address/file/line table. +The published `.wasm` is debug-stripped and the table is stored beside it as +`.wasm.source-map`, keyed to the exact temporary module identity recorded in +the wasm's `uce.module` custom section. Normal module loading never reads this +sidecar. On a Wasmtime trap, the worker uses structured frame module offsets to +load only the matching map and appends source locations to the error. A missing, +stale, or malformed map is deliberately non-fatal: the ordinary named wasm +backtrace remains available. Generated C++ uses a `#line` directive naming the +original `.uce` file, so application frames resolve to application source rather +than the generated cache file. Artifact invalidation removes the wasm, serialized +module, and source map together. + --- ## 4. The workspace runtime diff --git a/scripts/build_unit_source_map.py b/scripts/build_unit_source_map.py new file mode 100755 index 0000000..d6cc57f --- /dev/null +++ b/scripts/build_unit_source_map.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Extract a compact address-to-source table before a unit's DWARF is stripped.""" + +import argparse +import ast +import os +import re +import subprocess + + +DIRECTORY = re.compile(r'^include_directories\[\s*(\d+)\] = (".*")$') +FILE = re.compile(r'^file_names\[\s*(\d+)\]:$') +ROW = re.compile(r'^0x([0-9a-fA-F]+)\s+(\d+)\s+(\d+)\s+(\d+)\s+') + + +def quoted(value: str) -> str: + return ast.literal_eval(value) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--dwarfdump", required=True) + parser.add_argument("--wasm", required=True) + parser.add_argument("--module", required=True) + parser.add_argument("--output", required=True) + args = parser.parse_args() + + text = subprocess.run( + [args.dwarfdump, "--debug-line", args.wasm], + check=True, + stdout=subprocess.PIPE, + text=True, + ).stdout + directories: dict[int, str] = {} + files: dict[int, tuple[str, int]] = {} + rows: list[tuple[int, int, int, int]] = [] + current_file = 0 + for line in text.splitlines(): + if match := DIRECTORY.match(line): + directories[int(match.group(1))] = quoted(match.group(2)) + continue + if match := FILE.match(line): + current_file = int(match.group(1)) + continue + stripped = line.strip() + if current_file and stripped.startswith("name: "): + files[current_file] = (quoted(stripped[6:]), 0) + continue + if current_file and stripped.startswith("dir_index: "): + name, _ = files[current_file] + files[current_file] = (name, int(stripped[11:])) + continue + if match := ROW.match(line): + rows.append(tuple(int(value, 16 if index == 0 else 10) for index, value in enumerate(match.groups()))) + + paths: dict[int, str] = {} + for file_id, (name, directory_id) in files.items(): + directory = directories.get(directory_id, "") + path = name if os.path.isabs(name) else os.path.normpath(os.path.join(directory, name)) + if not os.path.isabs(path): + path = os.path.abspath(path) + if "\t" in path or "\n" in path or "\r" in path: + raise ValueError(f"source-map path contains a control character: {path!r}") + paths[file_id] = path + + with open(args.output, "w", encoding="utf-8") as output: + output.write(f"UCE_SOURCE_MAP_V1\t{args.module}\n") + for file_id, path in sorted(paths.items()): + output.write(f"F\t{file_id}\t{path}\n") + for address, line, column, file_id in rows: + output.write(f"L\t{address:x}\t{file_id}\t{line}\t{column}\n") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_unit_wasm.py b/scripts/check_unit_wasm.py index 05491fe..887b459 100755 --- a/scripts/check_unit_wasm.py +++ b/scripts/check_unit_wasm.py @@ -182,6 +182,9 @@ def main() -> int: for needle in required: if needle not in abi_text: errors.append(f"uce.abi missing {needle!r}") + module_payloads = customs.get("uce.module", []) + if not module_payloads or not module_payloads[-1]: + errors.append("missing uce.module custom section") export_names = {name for name, _ in exports} forbidden_exports = {"uce_alloc", "uce_free"} for name in sorted(export_names & forbidden_exports): diff --git a/scripts/compile_wasm_unit b/scripts/compile_wasm_unit index b04de17..ed1cacb 100755 --- a/scripts/compile_wasm_unit +++ b/scripts/compile_wasm_unit @@ -15,7 +15,9 @@ ABI_VERSION=${UCE_UNIT_ABI_VERSION:-6} ROOT=$(pwd) OBJ_FN="$DEST_DIR/$PP_FN.wasm.o" ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt" +MODULE_TMP="$DEST_DIR/$PP_FN.uce-module.txt" WASM_TMP="$DEST_DIR/$WASM_FN.tmp.$$" +MAP_TMP="$DEST_DIR/$WASM_FN.source-map.tmp.$$" PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1} PCH_DIR=${UCE_WASM_PCH_DIR:-/tmp/uce/wasm-w2/pch} COMMON_FLAGS=( @@ -33,7 +35,7 @@ COMMON_FLAGS=( -DPLATFORM_NAME=\"wasm32-wasip1\" ) -if [ ! -x "$SDK/bin/clang++" ] || [ ! -x "$SDK/bin/wasm-ld" ] || [ ! -x "$SDK/bin/llvm-objcopy" ]; then +if [ ! -x "$SDK/bin/clang++" ] || [ ! -x "$SDK/bin/wasm-ld" ] || [ ! -x "$SDK/bin/llvm-objcopy" ] || [ ! -x "$SDK/bin/llvm-dwarfdump" ]; then echo "wasi-sdk tools not found; set WASI_SDK" >&2 exit 1 fi @@ -45,7 +47,7 @@ PCH_KEY=$(printf '%s\n%s\n%s\n%s\n' "$ABI_VERSION" "$TOOLCHAIN_ID" "$HEADER_HASH PCH_FN="$PCH_DIR/uce_lib-wasm-unit-$PCH_KEY.pch" mkdir -p "$DEST_DIR" >/dev/null 2>&1 -trap 'rm -f "$OBJ_FN" "$ABI_TMP" "$WASM_TMP"' EXIT +trap 'rm -f "$OBJ_FN" "$ABI_TMP" "$MODULE_TMP" "$WASM_TMP" "$MAP_TMP"' EXIT build_pch_if_needed() { if [ "$PCH_ENABLED" = "0" ]; then @@ -67,6 +69,7 @@ unit_abi_version=$ABI_VERSION toolchain=$TOOLCHAIN_ID source=$SRC_FN EOF +printf '%s' "$(basename "$WASM_TMP")" > "$MODULE_TMP" build_pch_if_needed PCH_FLAGS=() @@ -93,9 +96,19 @@ fi --export-if-defined=__uce_once \ --export-if-defined=__uce_init -"$SDK/bin/llvm-objcopy" --strip-debug --add-section=uce.abi="$ABI_TMP" "$WASM_TMP" +python3 scripts/build_unit_source_map.py \ + --dwarfdump "$SDK/bin/llvm-dwarfdump" \ + --wasm "$WASM_TMP" \ + --module "$(basename "$WASM_TMP")" \ + --output "$MAP_TMP" + +"$SDK/bin/llvm-objcopy" --strip-debug \ + --add-section=uce.abi="$ABI_TMP" \ + --add-section=uce.module="$MODULE_TMP" \ + "$WASM_TMP" python3 scripts/check_unit_wasm.py "$WASM_TMP" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm" +mv "$MAP_TMP" "$DEST_DIR/$WASM_FN.source-map" mv "$WASM_TMP" "$DEST_DIR/$WASM_FN" -rm -f "$OBJ_FN" "$ABI_TMP" +rm -f "$OBJ_FN" "$ABI_TMP" "$MODULE_TMP" diff --git a/scripts/install_wasi_sdk.sh b/scripts/install_wasi_sdk.sh index 24eaa3a..40ef086 100755 --- a/scripts/install_wasi_sdk.sh +++ b/scripts/install_wasi_sdk.sh @@ -53,7 +53,7 @@ require_command() { verify_tree() { local root="$1" - for tool in clang++ wasm-ld llvm-objcopy llvm-nm; do + for tool in clang++ wasm-ld llvm-objcopy llvm-nm llvm-dwarfdump; do if [[ ! -x "$root/bin/$tool" ]]; then echo "Missing WASI SDK tool: $root/bin/$tool" >&2 return 1 diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh index 6903cc6..1335513 100755 --- a/scripts/run_cli_tests.sh +++ b/scripts/run_cli_tests.sh @@ -77,5 +77,6 @@ if [[ "$action" == "run" ]]; then scripts/test_raw_http_request_log.sh scripts/test_component_resolution_ttl.sh scripts/test_unit_export_surface.sh + scripts/test_wasm_source_locations.sh scripts/test_socket_activation.sh fi diff --git a/scripts/test_unit_export_surface.sh b/scripts/test_unit_export_surface.sh index 9c4d590..86eb9ad 100755 --- a/scripts/test_unit_export_surface.sh +++ b/scripts/test_unit_export_surface.sh @@ -50,7 +50,7 @@ fi entry_wasm="$artifact_dir/entry.uce.wasm" named_wasm="$artifact_dir/named.uce.wasm" -python3 - "$entry_wasm" "$named_wasm" <<'PY' +python3 - "$entry_wasm" "$named_wasm" "$absolute_source_dir" <<'PY' import sys from pathlib import Path @@ -60,9 +60,10 @@ cases = [ (Path(sys.argv[1]), {"__wasm_call_ctors", "__uce_set_current_request", "__uce_cli", "visibility_shared"}, b"uce-private-unused-marker-8f61d2"), (Path(sys.argv[2]), {"__wasm_call_ctors", "__uce_set_current_request", "__uce_component_NAMED", "named_shared"}, b"uce-named-unused-marker-4ae973"), ] +source_dir = Path(sys.argv[3]) for path, allowed, unused_marker in cases: data = path.read_bytes() - _, imports, exports = collect(path) + customs, imports, exports = collect(path) names = {name for name, _ in exports} unexpected = names - allowed missing = allowed - names @@ -74,6 +75,18 @@ for path, allowed, unused_marker in cases: raise SystemExit(f"{path}: retained {len(imports)} imports (expected fewer than 40)") if path.stat().st_size >= 1024 * 1024: raise SystemExit(f"{path}: artifact is {path.stat().st_size} bytes (expected under 1 MiB)") + source_map = Path(str(path) + ".source-map") + if not source_map.is_file() or source_map.stat().st_size >= 256 * 1024: + raise SystemExit(f"{path}: missing or oversized source map") + module = customs["uce.module"][-1].decode() + lines = source_map.read_text().splitlines() + if not lines or lines[0] != f"UCE_SOURCE_MAP_V1\t{module}": + raise SystemExit(f"{path}: source map does not match wasm module identity") + expected_source = str(source_dir / path.name.removesuffix(".wasm")) + if not any(line.startswith("F\t") and line.endswith("\t" + expected_source) for line in lines): + raise SystemExit(f"{path}: source map does not identify {expected_source}") + if not any(line.startswith("L\t") for line in lines): + raise SystemExit(f"{path}: source map has no address rows") PY echo "unit export surface passed" diff --git a/scripts/test_wasm_source_locations.sh b/scripts/test_wasm_source_locations.sh new file mode 100755 index 0000000..c8c2b94 --- /dev/null +++ b/scripts/test_wasm_source_locations.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +test_name="wasm-source-location-test-$$" +site_directory="${UCE_TEST_SITE_DIRECTORY:-site}" +bin_directory="${UCE_TEST_BIN_DIRECTORY:-/tmp/uce/work}" +if [[ -r /etc/uce/settings.cfg ]]; then + configured_site_directory=$(awk -F= '/^[[:space:]]*HTTP_DOCUMENT_ROOT[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg) + configured_bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg) + [[ -n "${UCE_TEST_SITE_DIRECTORY:-}" ]] || site_directory="${configured_site_directory:-$site_directory}" + [[ -n "${UCE_TEST_BIN_DIRECTORY:-}" ]] || bin_directory="${configured_bin_directory:-$bin_directory}" +fi +source_dir="$site_directory/$test_name" +absolute_source_dir="" +artifact_dir="" + +cleanup() { + rm -rf "$source_dir" + [[ -z "$artifact_dir" ]] || rm -rf "$artifact_dir" +} +trap cleanup EXIT +mkdir -p "$source_dir" +absolute_source_dir=$(realpath "$source_dir") +artifact_dir="$bin_directory$absolute_source_dir" +printf '%s\n' 'CLI(Request& context) { __builtin_trap(); }' >"$source_dir/entry.uce" + +set +e +output=$(scripts/uce-cli "/$test_name/entry.uce" 2>&1) +status=$? +set -e +if [[ $status -eq 0 ]]; then + echo "trapping wasm unit unexpectedly succeeded" >&2 + exit 1 +fi +expected="$absolute_source_dir/entry.uce:1:25" +if [[ "$output" != *'wasm `unreachable` instruction executed'* || "$output" != *"source locations:"* || "$output" != *"$expected"* ]]; then + echo "wasm trap omitted its source location:" >&2 + echo "$output" >&2 + exit 1 +fi + +rm "$artifact_dir/entry.uce.wasm.source-map" +set +e +without_map=$(scripts/uce-cli "/$test_name/entry.uce" 2>&1) +status=$? +set -e +if [[ $status -eq 0 || "$without_map" != *'wasm `unreachable` instruction executed'* || "$without_map" == *"source locations:"* ]]; then + echo "missing-map trap fallback failed:" >&2 + echo "$without_map" >&2 + exit 1 +fi + +echo "wasm source locations passed" diff --git a/src/lib/compiler-parser.cpp b/src/lib/compiler-parser.cpp index 8949f0c..6293020 100644 --- a/src/lib/compiler-parser.cpp +++ b/src/lib/compiler-parser.cpp @@ -497,8 +497,8 @@ String compiler_preprocess_shared_unit_char_wise(Request* context, SharedUnit* s "#include \"uce_lib.h\" \n"+ file_get_contents( context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"] - )+ - "#line 1\n"; + )+ + "#line 1 " + json_escape(su->file_name) + "\n"; CompilerCodeState code_state; String current_line = ""; String literal_buffer = ""; diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index ad4ea01..fb62c92 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -16,7 +16,7 @@ namespace { -const u64 UCE_UNIT_ABI_VERSION = 9; +const u64 UCE_UNIT_ABI_VERSION = 10; struct SharedUnitFilesystemState { @@ -294,10 +294,16 @@ String compiler_cached_wasm_path(String wasm_path) return(wasm_path + ".cwasm"); } +String compiler_source_map_path(String wasm_path) +{ + return(wasm_path + ".source-map"); +} + void compiler_unlink_unit_wasm_artifacts(SharedUnit* su) { file_unlink(su->wasm_name); file_unlink(compiler_cached_wasm_path(su->wasm_name)); + file_unlink(compiler_source_map_path(su->wasm_name)); } String compiler_wasm_compile_script(Request* context) @@ -937,7 +943,7 @@ void compile_shared_unit(Request* context, SharedUnit* su) file_put_contents(su->compile_output_file_name, su->compiler_messages + "\n"); file_put_contents(su->wasm_check_file_name, su->compiler_messages + "\n"); file_put_contents(su->meta_file_name, compiler_unit_metadata_text(context, su)); - file_unlink(su->wasm_name); + compiler_unlink_unit_wasm_artifacts(su); compiler_record_compile_result(su, time_precise() - comp_start, false, "compile_error", su->compiler_messages); printf("%s \n", compiler_format_source_read_failure(context, su, su->compiler_messages).c_str()); compiler_mark_source_generation(context); diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index 833e47e..ced3201 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -69,9 +69,24 @@ struct WasmAbiInfo { u32 version = 0; String toolchain; + String module_name; bool found = false; }; +struct WasmSourceMap +{ + struct Row + { + u64 address = 0; + u32 file = 0; + u32 line = 0; + u32 column = 0; + }; + String module_name; + std::map files; + std::vector rows; +}; + enum class WasmUnitImportKind { Memory, @@ -725,6 +740,8 @@ static bool wasm_parse_sections(const std::vector& bytes, WasmDylinkInfo& dy line_start = line_end + 1; } } + else if(name == "uce.module") + abi.module_name.assign((const char*)bytes.data() + cursor, end - cursor); } pos = end; } @@ -748,6 +765,53 @@ static bool wasm_read_file(const String& path, std::vector& out) return((bool)in); } +static bool wasm_source_map_load(const String& path, WasmSourceMap& map) +{ + std::ifstream input(path); + if(!input) + return(false); + String line; + if(!std::getline(input, line) || line.rfind("UCE_SOURCE_MAP_V1\t", 0) != 0) + return(false); + map.module_name = line.substr(18); + while(std::getline(input, line)) + { + auto fields = split(line, "\t"); + if(fields.size() == 3 && fields[0] == "F") + map.files[(u32)strtoul(fields[1].c_str(), 0, 10)] = fields[2]; + else if(fields.size() == 5 && fields[0] == "L") + { + WasmSourceMap::Row row; + row.address = strtoull(fields[1].c_str(), 0, 16); + row.file = (u32)strtoul(fields[2].c_str(), 0, 10); + row.line = (u32)strtoul(fields[3].c_str(), 0, 10); + row.column = (u32)strtoul(fields[4].c_str(), 0, 10); + map.rows.push_back(row); + } + } + return(!map.rows.empty()); +} + +static String wasm_source_map_lookup(const WasmSourceMap& map, u64 address) +{ + const WasmSourceMap::Row* found = 0; + for(auto& row : map.rows) + { + if(row.address > address) + break; + found = &row; + } + if(!found || found->line == 0) + return(""); + auto file = map.files.find(found->file); + if(file == map.files.end()) + return(""); + String result = file->second + ":" + std::to_string(found->line); + if(found->column) + result += ":" + std::to_string(found->column); + return(result); +} + static bool wasm_pread_all(int fd, u64 offset, u8* out, size_t size, u64& bytes_read) { size_t done = 0; @@ -858,7 +922,7 @@ static bool wasm_read_metadata_file(const String& path, std::vector& metadat break; } } - if(name == "dylink.0" || name == "uce.abi") + if(name == "dylink.0" || name == "uce.abi" || name == "uce.module") { if(section_size > 1024 * 1024) { @@ -1771,9 +1835,78 @@ private: return(pos == String::npos ? String("") : path.substr(0, pos)); } - static String trap_text(const wasmtime::TrapError& error) + String trap_text(const wasmtime::TrapError& error) { - return(wasm_trace_collapse(String(error.message()))); + String result = wasm_trace_collapse(String(error.message())); + struct Frame + { + String module; + String function; + u64 offset = 0; + }; + std::vector frames; + auto collect = [&](const wasmtime::Trace& trace) { + if(trace.size() == 0) + return; + for(auto& frame : trace) + { + Frame item; + if(auto name = frame.module_name()) + item.module = String(*name); + if(auto name = frame.func_name()) + item.function = wasm_trace_demangle(String(*name)); + item.offset = frame.module_offset(); + frames.push_back(item); + } + }; + if(auto* trap = std::get_if(&error.data)) + { + auto trace = trap->trace(); + collect(trace); + } + else if(auto* runtime_error = std::get_if(&error.data)) + { + auto trace = runtime_error->trace(); + collect(trace); + } + + std::map maps; + std::set unavailable; + std::vector locations; + for(size_t index = 0; index < frames.size() && locations.size() < 12; index++) + { + const WasmUnitModule* unit = 0; + for(auto& loaded : units) + if(loaded.mod->abi.module_name == frames[index].module) + { + unit = loaded.mod.get(); + break; + } + if(!unit) + continue; + String map_path = unit->wasm_path + ".source-map"; + if(unavailable.find(map_path) != unavailable.end()) + continue; + auto loaded_map = maps.find(map_path); + if(loaded_map == maps.end()) + { + WasmSourceMap source_map; + if(!wasm_source_map_load(map_path, source_map) || source_map.module_name != frames[index].module) + { + unavailable.insert(map_path); + continue; + } + loaded_map = maps.emplace(map_path, std::move(source_map)).first; + } + String location = wasm_source_map_lookup(loaded_map->second, frames[index].offset); + if(location == "") + continue; + String label = frames[index].function == "" ? "wasm function" : frames[index].function; + locations.push_back("#" + std::to_string(index) + " " + label + " at " + location); + } + if(!locations.empty()) + result += "\nsource locations:\n " + join(locations, "\n "); + return(result); } // ---- guest memory access (pointer re-derived per call: it moves) ------