diff --git a/.gitattributes b/.gitattributes index 0cb064ae..98aff87e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ *.zig text=auto eol=lf +*.abi text=auto eol=lf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23bc1682..95404278 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,6 +3,7 @@ name: Build env: ZIG_VERSION: "0.15.2" ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/.zig-global-cache + ZIG_LOCAL_CACHE_DIR: ${{ github.workspace }}/.zig-cache on: push: @@ -34,12 +35,18 @@ jobs: with: version: ${{ env.ZIG_VERSION }} + - name: Compute Zig package cache key + id: zig-pkg-cache-key + shell: bash + run: | + python scripts/zig_package_cache_key.py >> "$GITHUB_OUTPUT" + - name: Cache Zig package cache uses: actions/cache@v4 with: # Zig stores downloaded packages in the p subdirectory of the global cache. path: ${{ env.ZIG_GLOBAL_CACHE_DIR }}/p - key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ hashFiles('**/build.zig.zon') }} + key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ steps.zig-pkg-cache-key.outputs.key }} restore-keys: | zig-pkg-cache-${{ env.ZIG_VERSION }}- enableCrossOsArchive: true @@ -48,6 +55,16 @@ jobs: run: | zig build --global-cache-dir "$ZIG_GLOBAL_CACHE_DIR" ${{ matrix.platform }} + - name: Upload .zig-cache on failure + uses: actions/upload-artifact@v4 + if: ${{ failure() && (matrix.os == 'windows-latest' || matrix.os == 'macos-latest') }} + with: + name: zig-cache-build-${{ matrix.os }}-${{ matrix.platform }} + path: ${{ env.ZIG_LOCAL_CACHE_DIR }} + include-hidden-files: true + if-no-files-found: warn + retention-days: 7 + - name: Upload disk image uses: actions/upload-artifact@v4 if: ${{ matrix.os == 'ubuntu-latest' }} @@ -73,20 +90,42 @@ jobs: with: version: ${{ env.ZIG_VERSION }} + - name: Compute Zig package cache key + id: zig-pkg-cache-key + shell: bash + run: | + python scripts/zig_package_cache_key.py >> "$GITHUB_OUTPUT" + - name: Cache Zig package cache uses: actions/cache@v4 with: # Zig stores downloaded packages in the p subdirectory of the global cache. path: ${{ env.ZIG_GLOBAL_CACHE_DIR }}/p - key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ hashFiles('**/build.zig.zon') }} + key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ steps.zig-pkg-cache-key.outputs.key }} restore-keys: | zig-pkg-cache-${{ env.ZIG_VERSION }}- enableCrossOsArchive: true + - name: Install Linux GUI dependencies + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev + - name: Build Tools run: | zig build --global-cache-dir "$ZIG_GLOBAL_CACHE_DIR" tools + - name: Upload .zig-cache on failure + uses: actions/upload-artifact@v4 + if: ${{ failure() && (matrix.os == 'windows-latest' || matrix.os == 'macos-latest') }} + with: + name: zig-cache-tools-${{ matrix.os }} + path: ${{ env.ZIG_LOCAL_CACHE_DIR }} + include-hidden-files: true + if-no-files-found: warn + retention-days: 7 + - name: Upload tools uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index c12d08a8..ef5b293a 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -38,12 +38,18 @@ jobs: with: version: ${{ env.ZIG_VERSION }} + - name: Compute Zig package cache key + id: zig-pkg-cache-key + shell: bash + run: | + python scripts/zig_package_cache_key.py >> "$GITHUB_OUTPUT" + - name: Cache Zig package cache uses: actions/cache@v4 with: # Zig stores downloaded packages in the p subdirectory of the global cache. path: ${{ env.ZIG_GLOBAL_CACHE_DIR }}/p - key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ hashFiles('**/build.zig.zon') }} + key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ steps.zig-pkg-cache-key.outputs.key }} restore-keys: | zig-pkg-cache-${{ env.ZIG_VERSION }}- enableCrossOsArchive: true diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml index b107c427..9debd1b2 100644 --- a/.github/workflows/smoketest.yml +++ b/.github/workflows/smoketest.yml @@ -44,12 +44,18 @@ jobs: with: version: ${{ env.ZIG_VERSION }} + - name: Compute Zig package cache key + id: zig-pkg-cache-key + shell: bash + run: | + python scripts/zig_package_cache_key.py >> "$GITHUB_OUTPUT" + - name: Cache Zig package cache uses: actions/cache@v4 with: # Zig stores downloaded packages in the p subdirectory of the global cache. path: ${{ env.ZIG_GLOBAL_CACHE_DIR }}/p - key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ hashFiles('**/build.zig.zon') }} + key: zig-pkg-cache-${{ env.ZIG_VERSION }}-${{ steps.zig-pkg-cache-key.outputs.key }} restore-keys: | zig-pkg-cache-${{ env.ZIG_VERSION }}- enableCrossOsArchive: true @@ -57,7 +63,7 @@ jobs: - name: Install QEMU uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: qemu-system + packages: qemu-system libgtk-3-dev version: 1.1 - name: Build ${{ matrix.platform.kernel }} diff --git a/build.zig b/build.zig index 04dc047b..64ce19fc 100644 --- a/build.zig +++ b/build.zig @@ -54,6 +54,10 @@ const installed_tools: []const ToolDep = &.{ .dependency = "emulator", .artifacts = &.{ "emulator-web", "emulator" }, }, + .{ + .dependency = "abi_mapper", + .artifacts = &.{"abi-parser"}, + }, // .{ // .dependency = "agp_tester", // .artifacts = &.{"agp-tester"}, diff --git a/build.zig.zon b/build.zig.zon index ecb992ca..a8b2ab59 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -40,6 +40,9 @@ .agp_tester = .{ .path = "src/tools/agp-tester", }, + .abi_mapper = .{ + .path = "src/tools/abi-mapper", + }, // .wikitool = .{ // .path = "src/tools/wikitool", // }, diff --git a/scripts/zig_package_cache_key.py b/scripts/zig_package_cache_key.py new file mode 100644 index 00000000..3ed2531b --- /dev/null +++ b/scripts/zig_package_cache_key.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +import hashlib +import pathlib +import subprocess +import sys + + +digest = hashlib.sha256() +entries: set[str] = set() + +git_files = subprocess.run( + ["git", "ls-files", "-z"], + check=True, + capture_output=True, +).stdout.split(b"\0") + +manifest_paths = sorted( + pathlib.Path(raw.decode("utf-8")) + for raw in git_files + if raw and (raw == b"build.zig.zon" or raw.endswith(b"/build.zig.zon")) +) + +for path in manifest_paths: + print(path.as_posix(), file=sys.stderr) + +for path in manifest_paths: + for line in path.read_text().splitlines(): + stripped = line.strip() + if stripped.startswith("//"): + continue + + for key in (".url", ".hash"): + prefix = f"{key} =" + if not stripped.startswith(prefix): + continue + + _, value = stripped.split("=", 1) + value = value.strip() + + assert value.startswith('"'), f"{path}: malformed {key} entry: {line!r}" + assert value.endswith('",'), f"{path}: malformed {key} entry: {line!r}" + + entries.add(f"{key}={value}") + +for entry in sorted(entries): + digest.update(f"{entry}\n".encode("utf-8")) + +print(f"key={digest.hexdigest()}") diff --git a/src/abi/build.zig b/src/abi/build.zig index f8263cd3..c1134284 100644 --- a/src/abi/build.zig +++ b/src/abi/build.zig @@ -88,9 +88,26 @@ pub fn build(b: *std.Build) void { const abi_tests_run = b.addRunArtifact(abi_tests_exe); test_step.dependOn(&abi_tests_run.step); + + const escaping_tests_json = abi_mapper.get_json_dump(null, b.path("tests/escaping.abi")); + + for (std.enums.values(ConversionMode)) |mode| { + const escaping_tests_zig = convert_abi_file(b, render_zig_exe, escaping_tests_json, null, mode); + + const escaping_tests_exe = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = escaping_tests_zig, + .target = b.graph.host, + .optimize = .Debug, + }), + }); + const escaping_tests_run = b.addRunArtifact(escaping_tests_exe); + test_step.dependOn(&escaping_tests_run.step); + } } -pub fn convert_abi_file(b: *std.Build, render: *std.Build.Step.Compile, input: std.Build.LazyPath, patch: ?std.Build.LazyPath, mode: enum { kernel, definition }) std.Build.LazyPath { +const ConversionMode = enum { kernel, definition }; +pub fn convert_abi_file(b: *std.Build, render: *std.Build.Step.Compile, input: std.Build.LazyPath, patch: ?std.Build.LazyPath, mode: ConversionMode) std.Build.LazyPath { const generate_core_abi = b.addRunArtifact(render); generate_core_abi.addArg(@tagName(mode)); generate_core_abi.addFileArg(input); diff --git a/src/abi/db/abi-id-db.json b/src/abi/db/abi-id-db.json new file mode 100644 index 00000000..a12c0545 --- /dev/null +++ b/src/abi/db/abi-id-db.json @@ -0,0 +1,2448 @@ +{ + "entries": [ + { + "fqn": "SystemResource", + "uid": 1 + }, + { + "fqn": "resources.get_type.InvalidHandle", + "uid": 2 + }, + { + "fqn": "resources.get_type", + "uid": 3 + }, + { + "fqn": "resources.get_owners", + "uid": 4 + }, + { + "fqn": "resources.send_to_process.DeadProcess", + "uid": 5 + }, + { + "fqn": "resources.send_to_process.InvalidHandle", + "uid": 6 + }, + { + "fqn": "resources.send_to_process.SystemResources", + "uid": 7 + }, + { + "fqn": "resources.send_to_process", + "uid": 8 + }, + { + "fqn": "resources.release", + "uid": 9 + }, + { + "fqn": "resources.destroy", + "uid": 10 + }, + { + "fqn": "overlapped.ARC", + "uid": 11 + }, + { + "fqn": "overlapped.schedule.AlreadyScheduled", + "uid": 12 + }, + { + "fqn": "overlapped.schedule.SystemResources", + "uid": 13 + }, + { + "fqn": "overlapped.schedule", + "uid": 14 + }, + { + "fqn": "overlapped.await_completion.Unscheduled", + "uid": 15 + }, + { + "fqn": "overlapped.await_completion", + "uid": 16 + }, + { + "fqn": "overlapped.await_completion_of.InvalidOperation", + "uid": 17 + }, + { + "fqn": "overlapped.await_completion_of.Unscheduled", + "uid": 18 + }, + { + "fqn": "overlapped.await_completion_of", + "uid": 19 + }, + { + "fqn": "overlapped.cancel.Completed", + "uid": 20 + }, + { + "fqn": "overlapped.cancel.Unscheduled", + "uid": 21 + }, + { + "fqn": "overlapped.cancel", + "uid": 22 + }, + { + "fqn": "process.ExitCode", + "uid": 23 + }, + { + "fqn": "process.get_file_name.InvalidHandle", + "uid": 24 + }, + { + "fqn": "process.get_file_name", + "uid": 25 + }, + { + "fqn": "process.get_base_address.InvalidHandle", + "uid": 26 + }, + { + "fqn": "process.get_base_address", + "uid": 27 + }, + { + "fqn": "process.get_arguments.InvalidHandle", + "uid": 28 + }, + { + "fqn": "process.get_arguments", + "uid": 29 + }, + { + "fqn": "process.terminate", + "uid": 30 + }, + { + "fqn": "process.kill.InvalidHandle", + "uid": 31 + }, + { + "fqn": "process.kill", + "uid": 32 + }, + { + "fqn": "process.Spawn.BadExecutable", + "uid": 33 + }, + { + "fqn": "process.Spawn.DiskError", + "uid": 34 + }, + { + "fqn": "process.Spawn.FileNotFound", + "uid": 35 + }, + { + "fqn": "process.Spawn.InvalidHandle", + "uid": 36 + }, + { + "fqn": "process.Spawn.InvalidPath", + "uid": 37 + }, + { + "fqn": "process.Spawn.SystemResources", + "uid": 38 + }, + { + "fqn": "process.Spawn", + "uid": 39 + }, + { + "fqn": "process.thread.yield", + "uid": 40 + }, + { + "fqn": "process.thread.exit", + "uid": 41 + }, + { + "fqn": "process.thread.join.InvalidHandle", + "uid": 42 + }, + { + "fqn": "process.thread.join", + "uid": 43 + }, + { + "fqn": "process.thread.spawn.SystemResources", + "uid": 44 + }, + { + "fqn": "process.thread.spawn", + "uid": 45 + }, + { + "fqn": "process.thread.kill.InvalidHandle", + "uid": 46 + }, + { + "fqn": "process.thread.kill", + "uid": 47 + }, + { + "fqn": "process.debug.write_log", + "uid": 48 + }, + { + "fqn": "process.debug.breakpoint", + "uid": 49 + }, + { + "fqn": "process.memory.allocate.SystemResource", + "uid": 50 + }, + { + "fqn": "process.memory.allocate", + "uid": 51 + }, + { + "fqn": "process.memory.release", + "uid": 52 + }, + { + "fqn": "process.monitor.enumerate_processes", + "uid": 53 + }, + { + "fqn": "process.monitor.query_owned_resources.InvalidHandle", + "uid": 54 + }, + { + "fqn": "process.monitor.query_owned_resources", + "uid": 55 + }, + { + "fqn": "process.monitor.query_total_memory_usage.InvalidHandle", + "uid": 56 + }, + { + "fqn": "process.monitor.query_total_memory_usage", + "uid": 57 + }, + { + "fqn": "process.monitor.query_dynamic_memory_usage.InvalidHandle", + "uid": 58 + }, + { + "fqn": "process.monitor.query_dynamic_memory_usage", + "uid": 59 + }, + { + "fqn": "process.monitor.query_active_allocation_count.InvalidHandle", + "uid": 60 + }, + { + "fqn": "process.monitor.query_active_allocation_count", + "uid": 61 + }, + { + "fqn": "clock.monotonic", + "uid": 62 + }, + { + "fqn": "clock.Timer", + "uid": 63 + }, + { + "fqn": "datetime.now", + "uid": 64 + }, + { + "fqn": "datetime.Alarm", + "uid": 65 + }, + { + "fqn": "video.enumerate", + "uid": 66 + }, + { + "fqn": "video.acquire.NotAvailable", + "uid": 67 + }, + { + "fqn": "video.acquire.NotFound", + "uid": 68 + }, + { + "fqn": "video.acquire.SystemResources", + "uid": 69 + }, + { + "fqn": "video.acquire", + "uid": 70 + }, + { + "fqn": "video.get_resolution.InvalidHandle", + "uid": 71 + }, + { + "fqn": "video.get_resolution", + "uid": 72 + }, + { + "fqn": "video.get_video_memory.InvalidHandle", + "uid": 73 + }, + { + "fqn": "video.get_video_memory", + "uid": 74 + }, + { + "fqn": "video.WaitForVBlank.InvalidHandle", + "uid": 75 + }, + { + "fqn": "video.WaitForVBlank", + "uid": 76 + }, + { + "fqn": "random.get_soft_random", + "uid": 77 + }, + { + "fqn": "random.GetStrictRandom", + "uid": 78 + }, + { + "fqn": "input.GetEvent.InProgress", + "uid": 79 + }, + { + "fqn": "input.GetEvent.NonExclusiveAccess", + "uid": 80 + }, + { + "fqn": "input.GetEvent", + "uid": 81 + }, + { + "fqn": "network.IP_Type", + "uid": 82 + }, + { + "fqn": "network.IPv4", + "uid": 83 + }, + { + "fqn": "network.IPv6", + "uid": 84 + }, + { + "fqn": "network.IP.AnyAddr", + "uid": 85 + }, + { + "fqn": "network.IP", + "uid": 86 + }, + { + "fqn": "network.EndPoint", + "uid": 87 + }, + { + "fqn": "network.udp.create_socket.SystemResources", + "uid": 88 + }, + { + "fqn": "network.udp.create_socket", + "uid": 89 + }, + { + "fqn": "network.udp.Bind.IllegalArgument", + "uid": 90 + }, + { + "fqn": "network.udp.Bind.AddressInUse", + "uid": 91 + }, + { + "fqn": "network.udp.Bind.IllegalValue", + "uid": 92 + }, + { + "fqn": "network.udp.Bind.InvalidHandle", + "uid": 93 + }, + { + "fqn": "network.udp.Bind.SystemResources", + "uid": 94 + }, + { + "fqn": "network.udp.Bind", + "uid": 95 + }, + { + "fqn": "network.udp.Connect.AlreadyConnected", + "uid": 96 + }, + { + "fqn": "network.udp.Connect.AlreadyConnecting", + "uid": 97 + }, + { + "fqn": "network.udp.Connect.AddressInUse", + "uid": 98 + }, + { + "fqn": "network.udp.Connect.BufferError", + "uid": 99 + }, + { + "fqn": "network.udp.Connect.IllegalArgument", + "uid": 100 + }, + { + "fqn": "network.udp.Connect.IllegalValue", + "uid": 101 + }, + { + "fqn": "network.udp.Connect.InProgress", + "uid": 102 + }, + { + "fqn": "network.udp.Connect.InvalidHandle", + "uid": 103 + }, + { + "fqn": "network.udp.Connect.LowlevelInterfaceError", + "uid": 104 + }, + { + "fqn": "network.udp.Connect.OutOfMemory", + "uid": 105 + }, + { + "fqn": "network.udp.Connect.Routing", + "uid": 106 + }, + { + "fqn": "network.udp.Connect.SystemResources", + "uid": 107 + }, + { + "fqn": "network.udp.Connect.Timeout", + "uid": 108 + }, + { + "fqn": "network.udp.Connect", + "uid": 109 + }, + { + "fqn": "network.udp.Disconnect.InvalidHandle", + "uid": 110 + }, + { + "fqn": "network.udp.Disconnect.NotConnected", + "uid": 111 + }, + { + "fqn": "network.udp.Disconnect.SystemResources", + "uid": 112 + }, + { + "fqn": "network.udp.Disconnect", + "uid": 113 + }, + { + "fqn": "network.udp.Send.BufferError", + "uid": 114 + }, + { + "fqn": "network.udp.Send.IllegalArgument", + "uid": 115 + }, + { + "fqn": "network.udp.Send.IllegalValue", + "uid": 116 + }, + { + "fqn": "network.udp.Send.InProgress", + "uid": 117 + }, + { + "fqn": "network.udp.Send.InvalidHandle", + "uid": 118 + }, + { + "fqn": "network.udp.Send.LowlevelInterfaceError", + "uid": 119 + }, + { + "fqn": "network.udp.Send.NotConnected", + "uid": 120 + }, + { + "fqn": "network.udp.Send.OutOfMemory", + "uid": 121 + }, + { + "fqn": "network.udp.Send.Routing", + "uid": 122 + }, + { + "fqn": "network.udp.Send.SystemResources", + "uid": 123 + }, + { + "fqn": "network.udp.Send.Timeout", + "uid": 124 + }, + { + "fqn": "network.udp.Send", + "uid": 125 + }, + { + "fqn": "network.udp.SendTo.BufferError", + "uid": 126 + }, + { + "fqn": "network.udp.SendTo.IllegalArgument", + "uid": 127 + }, + { + "fqn": "network.udp.SendTo.IllegalValue", + "uid": 128 + }, + { + "fqn": "network.udp.SendTo.InProgress", + "uid": 129 + }, + { + "fqn": "network.udp.SendTo.InvalidHandle", + "uid": 130 + }, + { + "fqn": "network.udp.SendTo.LowlevelInterfaceError", + "uid": 131 + }, + { + "fqn": "network.udp.SendTo.OutOfMemory", + "uid": 132 + }, + { + "fqn": "network.udp.SendTo.Routing", + "uid": 133 + }, + { + "fqn": "network.udp.SendTo.SystemResources", + "uid": 134 + }, + { + "fqn": "network.udp.SendTo.Timeout", + "uid": 135 + }, + { + "fqn": "network.udp.SendTo", + "uid": 136 + }, + { + "fqn": "network.udp.ReceiveFrom.BufferError", + "uid": 137 + }, + { + "fqn": "network.udp.ReceiveFrom.IllegalArgument", + "uid": 138 + }, + { + "fqn": "network.udp.ReceiveFrom.IllegalValue", + "uid": 139 + }, + { + "fqn": "network.udp.ReceiveFrom.InProgress", + "uid": 140 + }, + { + "fqn": "network.udp.ReceiveFrom.InvalidHandle", + "uid": 141 + }, + { + "fqn": "network.udp.ReceiveFrom.LowlevelInterfaceError", + "uid": 142 + }, + { + "fqn": "network.udp.ReceiveFrom.OutOfMemory", + "uid": 143 + }, + { + "fqn": "network.udp.ReceiveFrom.Routing", + "uid": 144 + }, + { + "fqn": "network.udp.ReceiveFrom.SystemResources", + "uid": 145 + }, + { + "fqn": "network.udp.ReceiveFrom.Timeout", + "uid": 146 + }, + { + "fqn": "network.udp.ReceiveFrom", + "uid": 147 + }, + { + "fqn": "network.tcp.create_socket.SystemResources", + "uid": 148 + }, + { + "fqn": "network.tcp.create_socket", + "uid": 149 + }, + { + "fqn": "network.tcp.Bind.AddressInUse", + "uid": 150 + }, + { + "fqn": "network.tcp.Bind.IllegalValue", + "uid": 151 + }, + { + "fqn": "network.tcp.Bind.InvalidHandle", + "uid": 152 + }, + { + "fqn": "network.tcp.Bind.SystemResources", + "uid": 153 + }, + { + "fqn": "network.tcp.Bind", + "uid": 154 + }, + { + "fqn": "network.tcp.Connect.AlreadyConnected", + "uid": 155 + }, + { + "fqn": "network.tcp.Connect.AlreadyConnecting", + "uid": 156 + }, + { + "fqn": "network.tcp.Connect.BufferError", + "uid": 157 + }, + { + "fqn": "network.tcp.Connect.ConnectionAborted", + "uid": 158 + }, + { + "fqn": "network.tcp.Connect.ConnectionClosed", + "uid": 159 + }, + { + "fqn": "network.tcp.Connect.ConnectionReset", + "uid": 160 + }, + { + "fqn": "network.tcp.Connect.IllegalArgument", + "uid": 161 + }, + { + "fqn": "network.tcp.Connect.IllegalValue", + "uid": 162 + }, + { + "fqn": "network.tcp.Connect.InProgress", + "uid": 163 + }, + { + "fqn": "network.tcp.Connect.InvalidHandle", + "uid": 164 + }, + { + "fqn": "network.tcp.Connect.LowlevelInterfaceError", + "uid": 165 + }, + { + "fqn": "network.tcp.Connect.OutOfMemory", + "uid": 166 + }, + { + "fqn": "network.tcp.Connect.Routing", + "uid": 167 + }, + { + "fqn": "network.tcp.Connect.SystemResources", + "uid": 168 + }, + { + "fqn": "network.tcp.Connect.Timeout", + "uid": 169 + }, + { + "fqn": "network.tcp.Connect", + "uid": 170 + }, + { + "fqn": "network.tcp.Send.BufferError", + "uid": 171 + }, + { + "fqn": "network.tcp.Send.ConnectionAborted", + "uid": 172 + }, + { + "fqn": "network.tcp.Send.ConnectionClosed", + "uid": 173 + }, + { + "fqn": "network.tcp.Send.ConnectionReset", + "uid": 174 + }, + { + "fqn": "network.tcp.Send.IllegalArgument", + "uid": 175 + }, + { + "fqn": "network.tcp.Send.IllegalValue", + "uid": 176 + }, + { + "fqn": "network.tcp.Send.InProgress", + "uid": 177 + }, + { + "fqn": "network.tcp.Send.InvalidHandle", + "uid": 178 + }, + { + "fqn": "network.tcp.Send.LowlevelInterfaceError", + "uid": 179 + }, + { + "fqn": "network.tcp.Send.NotConnected", + "uid": 180 + }, + { + "fqn": "network.tcp.Send.OutOfMemory", + "uid": 181 + }, + { + "fqn": "network.tcp.Send.Routing", + "uid": 182 + }, + { + "fqn": "network.tcp.Send.SystemResources", + "uid": 183 + }, + { + "fqn": "network.tcp.Send.Timeout", + "uid": 184 + }, + { + "fqn": "network.tcp.Send", + "uid": 185 + }, + { + "fqn": "network.tcp.Receive.AlreadyConnected", + "uid": 186 + }, + { + "fqn": "network.tcp.Receive.AlreadyConnecting", + "uid": 187 + }, + { + "fqn": "network.tcp.Receive.BufferError", + "uid": 188 + }, + { + "fqn": "network.tcp.Receive.ConnectionAborted", + "uid": 189 + }, + { + "fqn": "network.tcp.Receive.ConnectionClosed", + "uid": 190 + }, + { + "fqn": "network.tcp.Receive.ConnectionReset", + "uid": 191 + }, + { + "fqn": "network.tcp.Receive.IllegalArgument", + "uid": 192 + }, + { + "fqn": "network.tcp.Receive.IllegalValue", + "uid": 193 + }, + { + "fqn": "network.tcp.Receive.InProgress", + "uid": 194 + }, + { + "fqn": "network.tcp.Receive.InvalidHandle", + "uid": 195 + }, + { + "fqn": "network.tcp.Receive.LowlevelInterfaceError", + "uid": 196 + }, + { + "fqn": "network.tcp.Receive.NotConnected", + "uid": 197 + }, + { + "fqn": "network.tcp.Receive.OutOfMemory", + "uid": 198 + }, + { + "fqn": "network.tcp.Receive.Routing", + "uid": 199 + }, + { + "fqn": "network.tcp.Receive.SystemResources", + "uid": 200 + }, + { + "fqn": "network.tcp.Receive.Timeout", + "uid": 201 + }, + { + "fqn": "network.tcp.Receive", + "uid": 202 + }, + { + "fqn": "fs.find_filesystem", + "uid": 203 + }, + { + "fqn": "fs.Sync.DiskError", + "uid": 204 + }, + { + "fqn": "fs.Sync", + "uid": 205 + }, + { + "fqn": "fs.GetFilesystemInfo.DiskError", + "uid": 206 + }, + { + "fqn": "fs.GetFilesystemInfo.InvalidFileSystem", + "uid": 207 + }, + { + "fqn": "fs.GetFilesystemInfo", + "uid": 208 + }, + { + "fqn": "fs.OpenDrive.DiskError", + "uid": 209 + }, + { + "fqn": "fs.OpenDrive.FileNotFound", + "uid": 210 + }, + { + "fqn": "fs.OpenDrive.InvalidFileSystem", + "uid": 211 + }, + { + "fqn": "fs.OpenDrive.InvalidPath", + "uid": 212 + }, + { + "fqn": "fs.OpenDrive.NotADir", + "uid": 213 + }, + { + "fqn": "fs.OpenDrive.SystemFdQuotaExceeded", + "uid": 214 + }, + { + "fqn": "fs.OpenDrive.SystemResources", + "uid": 215 + }, + { + "fqn": "fs.OpenDrive", + "uid": 216 + }, + { + "fqn": "fs.OpenDir.DiskError", + "uid": 217 + }, + { + "fqn": "fs.OpenDir.FileNotFound", + "uid": 218 + }, + { + "fqn": "fs.OpenDir.InvalidHandle", + "uid": 219 + }, + { + "fqn": "fs.OpenDir.InvalidPath", + "uid": 220 + }, + { + "fqn": "fs.OpenDir.NotADir", + "uid": 221 + }, + { + "fqn": "fs.OpenDir.SystemFdQuotaExceeded", + "uid": 222 + }, + { + "fqn": "fs.OpenDir.SystemResources", + "uid": 223 + }, + { + "fqn": "fs.OpenDir", + "uid": 224 + }, + { + "fqn": "fs.CloseDir.InvalidHandle", + "uid": 225 + }, + { + "fqn": "fs.CloseDir", + "uid": 226 + }, + { + "fqn": "fs.ResetDirEnumeration.DiskError", + "uid": 227 + }, + { + "fqn": "fs.ResetDirEnumeration.InvalidHandle", + "uid": 228 + }, + { + "fqn": "fs.ResetDirEnumeration.SystemResources", + "uid": 229 + }, + { + "fqn": "fs.ResetDirEnumeration", + "uid": 230 + }, + { + "fqn": "fs.EnumerateDir.DiskError", + "uid": 231 + }, + { + "fqn": "fs.EnumerateDir.InvalidHandle", + "uid": 232 + }, + { + "fqn": "fs.EnumerateDir.SystemResources", + "uid": 233 + }, + { + "fqn": "fs.EnumerateDir", + "uid": 234 + }, + { + "fqn": "fs.Delete.DiskError", + "uid": 235 + }, + { + "fqn": "fs.Delete.FileNotFound", + "uid": 236 + }, + { + "fqn": "fs.Delete.InvalidHandle", + "uid": 237 + }, + { + "fqn": "fs.Delete.InvalidPath", + "uid": 238 + }, + { + "fqn": "fs.Delete", + "uid": 239 + }, + { + "fqn": "fs.MkDir.DiskError", + "uid": 240 + }, + { + "fqn": "fs.MkDir.Exists", + "uid": 241 + }, + { + "fqn": "fs.MkDir.InvalidHandle", + "uid": 242 + }, + { + "fqn": "fs.MkDir.InvalidPath", + "uid": 243 + }, + { + "fqn": "fs.MkDir", + "uid": 244 + }, + { + "fqn": "fs.StatEntry.DiskError", + "uid": 245 + }, + { + "fqn": "fs.StatEntry.FileNotFound", + "uid": 246 + }, + { + "fqn": "fs.StatEntry.InvalidHandle", + "uid": 247 + }, + { + "fqn": "fs.StatEntry.InvalidPath", + "uid": 248 + }, + { + "fqn": "fs.StatEntry", + "uid": 249 + }, + { + "fqn": "fs.NearMove.DiskError", + "uid": 250 + }, + { + "fqn": "fs.NearMove.Exists", + "uid": 251 + }, + { + "fqn": "fs.NearMove.FileNotFound", + "uid": 252 + }, + { + "fqn": "fs.NearMove.InvalidHandle", + "uid": 253 + }, + { + "fqn": "fs.NearMove.InvalidPath", + "uid": 254 + }, + { + "fqn": "fs.NearMove", + "uid": 255 + }, + { + "fqn": "fs.FarMove.DiskError", + "uid": 256 + }, + { + "fqn": "fs.FarMove.Exists", + "uid": 257 + }, + { + "fqn": "fs.FarMove.FileNotFound", + "uid": 258 + }, + { + "fqn": "fs.FarMove.InvalidHandle", + "uid": 259 + }, + { + "fqn": "fs.FarMove.InvalidPath", + "uid": 260 + }, + { + "fqn": "fs.FarMove.NoSpaceLeft", + "uid": 261 + }, + { + "fqn": "fs.FarMove", + "uid": 262 + }, + { + "fqn": "fs.Copy.DiskError", + "uid": 263 + }, + { + "fqn": "fs.Copy.Exists", + "uid": 264 + }, + { + "fqn": "fs.Copy.FileNotFound", + "uid": 265 + }, + { + "fqn": "fs.Copy.InvalidHandle", + "uid": 266 + }, + { + "fqn": "fs.Copy.InvalidPath", + "uid": 267 + }, + { + "fqn": "fs.Copy.NoSpaceLeft", + "uid": 268 + }, + { + "fqn": "fs.Copy", + "uid": 269 + }, + { + "fqn": "fs.OpenFile.DiskError", + "uid": 270 + }, + { + "fqn": "fs.OpenFile.Exists", + "uid": 271 + }, + { + "fqn": "fs.OpenFile.FileAlreadyExists", + "uid": 272 + }, + { + "fqn": "fs.OpenFile.FileNotFound", + "uid": 273 + }, + { + "fqn": "fs.OpenFile.InvalidHandle", + "uid": 274 + }, + { + "fqn": "fs.OpenFile.InvalidPath", + "uid": 275 + }, + { + "fqn": "fs.OpenFile.NoSpaceLeft", + "uid": 276 + }, + { + "fqn": "fs.OpenFile.SystemFdQuotaExceeded", + "uid": 277 + }, + { + "fqn": "fs.OpenFile.SystemResources", + "uid": 278 + }, + { + "fqn": "fs.OpenFile.WriteProtected", + "uid": 279 + }, + { + "fqn": "fs.OpenFile", + "uid": 280 + }, + { + "fqn": "fs.CloseFile.DiskError", + "uid": 281 + }, + { + "fqn": "fs.CloseFile.InvalidHandle", + "uid": 282 + }, + { + "fqn": "fs.CloseFile.SystemResources", + "uid": 283 + }, + { + "fqn": "fs.CloseFile", + "uid": 284 + }, + { + "fqn": "fs.FlushFile.DiskError", + "uid": 285 + }, + { + "fqn": "fs.FlushFile.InvalidHandle", + "uid": 286 + }, + { + "fqn": "fs.FlushFile.SystemResources", + "uid": 287 + }, + { + "fqn": "fs.FlushFile", + "uid": 288 + }, + { + "fqn": "fs.Read.DiskError", + "uid": 289 + }, + { + "fqn": "fs.Read.InvalidHandle", + "uid": 290 + }, + { + "fqn": "fs.Read.SystemResources", + "uid": 291 + }, + { + "fqn": "fs.Read", + "uid": 292 + }, + { + "fqn": "fs.Write.DiskError", + "uid": 293 + }, + { + "fqn": "fs.Write.InvalidHandle", + "uid": 294 + }, + { + "fqn": "fs.Write.NoSpaceLeft", + "uid": 295 + }, + { + "fqn": "fs.Write.SystemResources", + "uid": 296 + }, + { + "fqn": "fs.Write.WriteProtected", + "uid": 297 + }, + { + "fqn": "fs.Write", + "uid": 298 + }, + { + "fqn": "fs.StatFile.DiskError", + "uid": 299 + }, + { + "fqn": "fs.StatFile.InvalidHandle", + "uid": 300 + }, + { + "fqn": "fs.StatFile.SystemResources", + "uid": 301 + }, + { + "fqn": "fs.StatFile", + "uid": 302 + }, + { + "fqn": "fs.Resize.DiskError", + "uid": 303 + }, + { + "fqn": "fs.Resize.InvalidHandle", + "uid": 304 + }, + { + "fqn": "fs.Resize.NoSpaceLeft", + "uid": 305 + }, + { + "fqn": "fs.Resize.SystemResources", + "uid": 306 + }, + { + "fqn": "fs.Resize", + "uid": 307 + }, + { + "fqn": "shm.create.SystemResources", + "uid": 308 + }, + { + "fqn": "shm.create", + "uid": 309 + }, + { + "fqn": "shm.get_length.InvalidHandle", + "uid": 310 + }, + { + "fqn": "shm.get_length", + "uid": 311 + }, + { + "fqn": "shm.get_pointer.InvalidHandle", + "uid": 312 + }, + { + "fqn": "shm.get_pointer", + "uid": 313 + }, + { + "fqn": "pipe.create.SystemResources", + "uid": 314 + }, + { + "fqn": "pipe.create", + "uid": 315 + }, + { + "fqn": "pipe.get_fifo_length.InvalidHandle", + "uid": 316 + }, + { + "fqn": "pipe.get_fifo_length", + "uid": 317 + }, + { + "fqn": "pipe.get_object_size.InvalidHandle", + "uid": 318 + }, + { + "fqn": "pipe.get_object_size", + "uid": 319 + }, + { + "fqn": "pipe.Write", + "uid": 320 + }, + { + "fqn": "pipe.Read", + "uid": 321 + }, + { + "fqn": "sync.create_event.SystemResources", + "uid": 322 + }, + { + "fqn": "sync.create_event", + "uid": 323 + }, + { + "fqn": "sync.notify_one.InvalidHandle", + "uid": 324 + }, + { + "fqn": "sync.notify_one", + "uid": 325 + }, + { + "fqn": "sync.notify_all.InvalidHandle", + "uid": 326 + }, + { + "fqn": "sync.notify_all", + "uid": 327 + }, + { + "fqn": "sync.WaitForEvent.InvalidHandle", + "uid": 328 + }, + { + "fqn": "sync.WaitForEvent", + "uid": 329 + }, + { + "fqn": "sync.create_mutex.SystemResources", + "uid": 330 + }, + { + "fqn": "sync.create_mutex", + "uid": 331 + }, + { + "fqn": "sync.try_lock.InvalidHandle", + "uid": 332 + }, + { + "fqn": "sync.try_lock", + "uid": 333 + }, + { + "fqn": "sync.unlock.InvalidHandle", + "uid": 334 + }, + { + "fqn": "sync.unlock", + "uid": 335 + }, + { + "fqn": "sync.Lock.InvalidHandle", + "uid": 336 + }, + { + "fqn": "sync.Lock", + "uid": 337 + }, + { + "fqn": "draw.get_system_font.FileNotFound", + "uid": 338 + }, + { + "fqn": "draw.get_system_font.SystemResources", + "uid": 339 + }, + { + "fqn": "draw.get_system_font", + "uid": 340 + }, + { + "fqn": "draw.create_font.InvalidData", + "uid": 341 + }, + { + "fqn": "draw.create_font.SystemResources", + "uid": 342 + }, + { + "fqn": "draw.create_font", + "uid": 343 + }, + { + "fqn": "draw.is_system_font.InvalidHandle", + "uid": 344 + }, + { + "fqn": "draw.is_system_font", + "uid": 345 + }, + { + "fqn": "draw.measure_text_size.InvalidHandle", + "uid": 346 + }, + { + "fqn": "draw.measure_text_size", + "uid": 347 + }, + { + "fqn": "draw.create_memory_framebuffer.SystemResources", + "uid": 348 + }, + { + "fqn": "draw.create_memory_framebuffer", + "uid": 349 + }, + { + "fqn": "draw.create_video_framebuffer.InvalidHandle", + "uid": 350 + }, + { + "fqn": "draw.create_video_framebuffer.SystemResources", + "uid": 351 + }, + { + "fqn": "draw.create_video_framebuffer", + "uid": 352 + }, + { + "fqn": "draw.create_window_framebuffer.InvalidHandle", + "uid": 353 + }, + { + "fqn": "draw.create_window_framebuffer.SystemResources", + "uid": 354 + }, + { + "fqn": "draw.create_window_framebuffer", + "uid": 355 + }, + { + "fqn": "draw.create_widget_framebuffer.InvalidHandle", + "uid": 356 + }, + { + "fqn": "draw.create_widget_framebuffer.SystemResources", + "uid": 357 + }, + { + "fqn": "draw.create_widget_framebuffer", + "uid": 358 + }, + { + "fqn": "draw.get_framebuffer_type.InvalidHandle", + "uid": 359 + }, + { + "fqn": "draw.get_framebuffer_type", + "uid": 360 + }, + { + "fqn": "draw.get_framebuffer_size.InvalidHandle", + "uid": 361 + }, + { + "fqn": "draw.get_framebuffer_size", + "uid": 362 + }, + { + "fqn": "draw.get_framebuffer_memory.InvalidHandle", + "uid": 363 + }, + { + "fqn": "draw.get_framebuffer_memory.Unsupported", + "uid": 364 + }, + { + "fqn": "draw.get_framebuffer_memory", + "uid": 365 + }, + { + "fqn": "draw.invalidate_framebuffer", + "uid": 366 + }, + { + "fqn": "draw.Render.BadCode", + "uid": 367 + }, + { + "fqn": "draw.Render.InvalidHandle", + "uid": 368 + }, + { + "fqn": "draw.Render", + "uid": 369 + }, + { + "fqn": "gui.register_widget_type.AlreadyRegistered", + "uid": 370 + }, + { + "fqn": "gui.register_widget_type.SystemResources", + "uid": 371 + }, + { + "fqn": "gui.register_widget_type", + "uid": 372 + }, + { + "fqn": "gui.ShowMessageBox", + "uid": 373 + }, + { + "fqn": "gui.create_window.InvalidDimensions", + "uid": 374 + }, + { + "fqn": "gui.create_window.InvalidHandle", + "uid": 375 + }, + { + "fqn": "gui.create_window.SystemResources", + "uid": 376 + }, + { + "fqn": "gui.create_window", + "uid": 377 + }, + { + "fqn": "gui.get_window_title.InvalidHandle", + "uid": 378 + }, + { + "fqn": "gui.get_window_title", + "uid": 379 + }, + { + "fqn": "gui.get_window_size.InvalidHandle", + "uid": 380 + }, + { + "fqn": "gui.get_window_size", + "uid": 381 + }, + { + "fqn": "gui.get_window_min_size.InvalidHandle", + "uid": 382 + }, + { + "fqn": "gui.get_window_min_size", + "uid": 383 + }, + { + "fqn": "gui.get_window_max_size.InvalidHandle", + "uid": 384 + }, + { + "fqn": "gui.get_window_max_size", + "uid": 385 + }, + { + "fqn": "gui.get_window_flags.InvalidHandle", + "uid": 386 + }, + { + "fqn": "gui.get_window_flags", + "uid": 387 + }, + { + "fqn": "gui.set_window_size.InvalidHandle", + "uid": 388 + }, + { + "fqn": "gui.set_window_size", + "uid": 389 + }, + { + "fqn": "gui.resize_window.InvalidHandle", + "uid": 390 + }, + { + "fqn": "gui.resize_window", + "uid": 391 + }, + { + "fqn": "gui.set_window_title.InvalidHandle", + "uid": 392 + }, + { + "fqn": "gui.set_window_title", + "uid": 393 + }, + { + "fqn": "gui.mark_window_urgent.InvalidHandle", + "uid": 394 + }, + { + "fqn": "gui.mark_window_urgent", + "uid": 395 + }, + { + "fqn": "gui.GetWindowEvent.Cancelled", + "uid": 396 + }, + { + "fqn": "gui.GetWindowEvent.InProgress", + "uid": 397 + }, + { + "fqn": "gui.GetWindowEvent.InvalidHandle", + "uid": 398 + }, + { + "fqn": "gui.GetWindowEvent", + "uid": 399 + }, + { + "fqn": "gui.create_widget.SystemResources", + "uid": 400 + }, + { + "fqn": "gui.create_widget.WidgetNotFound", + "uid": 401 + }, + { + "fqn": "gui.create_widget.InvalidHandle", + "uid": 402 + }, + { + "fqn": "gui.create_widget", + "uid": 403 + }, + { + "fqn": "gui.place_widget.InvalidHandle", + "uid": 404 + }, + { + "fqn": "gui.place_widget", + "uid": 405 + }, + { + "fqn": "gui.WidgetControlID", + "uid": 406 + }, + { + "fqn": "gui.control_widget.SystemResources", + "uid": 407 + }, + { + "fqn": "gui.control_widget.InvalidHandle", + "uid": 408 + }, + { + "fqn": "gui.control_widget", + "uid": 409 + }, + { + "fqn": "gui.WidgetNotifyID", + "uid": 410 + }, + { + "fqn": "gui.notify_owner.SystemResources", + "uid": 411 + }, + { + "fqn": "gui.notify_owner.InvalidHandle", + "uid": 412 + }, + { + "fqn": "gui.notify_owner", + "uid": 413 + }, + { + "fqn": "gui.get_widget_data.InvalidHandle", + "uid": 414 + }, + { + "fqn": "gui.get_widget_data", + "uid": 415 + }, + { + "fqn": "gui.get_widget_bounds.InvalidHandle", + "uid": 416 + }, + { + "fqn": "gui.get_widget_bounds", + "uid": 417 + }, + { + "fqn": "gui.create_desktop.SystemResources", + "uid": 418 + }, + { + "fqn": "gui.create_desktop", + "uid": 419 + }, + { + "fqn": "gui.get_desktop_name.InvalidHandle", + "uid": 420 + }, + { + "fqn": "gui.get_desktop_name", + "uid": 421 + }, + { + "fqn": "gui.enumerate_desktops", + "uid": 422 + }, + { + "fqn": "gui.enumerate_desktop_windows.InvalidHandle", + "uid": 423 + }, + { + "fqn": "gui.enumerate_desktop_windows", + "uid": 424 + }, + { + "fqn": "gui.get_desktop_data.InvalidHandle", + "uid": 425 + }, + { + "fqn": "gui.get_desktop_data", + "uid": 426 + }, + { + "fqn": "gui.notify_message_box.BadRequestId", + "uid": 427 + }, + { + "fqn": "gui.notify_message_box.InvalidHandle", + "uid": 428 + }, + { + "fqn": "gui.notify_message_box", + "uid": 429 + }, + { + "fqn": "gui.post_window_event.SystemResources", + "uid": 430 + }, + { + "fqn": "gui.post_window_event.InvalidHandle", + "uid": 431 + }, + { + "fqn": "gui.post_window_event", + "uid": 432 + }, + { + "fqn": "gui.send_notification.SystemResources", + "uid": 433 + }, + { + "fqn": "gui.send_notification.InvalidHandle", + "uid": 434 + }, + { + "fqn": "gui.send_notification", + "uid": 435 + }, + { + "fqn": "gui.clipboard.set.SystemResources", + "uid": 436 + }, + { + "fqn": "gui.clipboard.set", + "uid": 437 + }, + { + "fqn": "gui.clipboard.get_type.InvalidHandle", + "uid": 438 + }, + { + "fqn": "gui.clipboard.get_type", + "uid": 439 + }, + { + "fqn": "gui.clipboard.get_value.InvalidHandle", + "uid": 440 + }, + { + "fqn": "gui.clipboard.get_value.SystemResources", + "uid": 441 + }, + { + "fqn": "gui.clipboard.get_value.ConversionFailed", + "uid": 442 + }, + { + "fqn": "gui.clipboard.get_value.ClipboardEmpty", + "uid": 443 + }, + { + "fqn": "gui.clipboard.get_value", + "uid": 444 + }, + { + "fqn": "service.create.AlreadyRegistered", + "uid": 445 + }, + { + "fqn": "service.create.SystemResources", + "uid": 446 + }, + { + "fqn": "service.create", + "uid": 447 + }, + { + "fqn": "service.enumerate", + "uid": 448 + }, + { + "fqn": "service.get_name.InvalidHandle", + "uid": 449 + }, + { + "fqn": "service.get_name", + "uid": 450 + }, + { + "fqn": "service.get_process.InvalidHandle", + "uid": 451 + }, + { + "fqn": "service.get_process", + "uid": 452 + }, + { + "fqn": "service.get_functions.InvalidHandle", + "uid": 453 + }, + { + "fqn": "service.get_functions", + "uid": 454 + }, + { + "fqn": "Service", + "uid": 455 + }, + { + "fqn": "SharedMemory", + "uid": 456 + }, + { + "fqn": "Pipe", + "uid": 457 + }, + { + "fqn": "Process", + "uid": 458 + }, + { + "fqn": "Thread", + "uid": 459 + }, + { + "fqn": "TcpSocket", + "uid": 460 + }, + { + "fqn": "UdpSocket", + "uid": 461 + }, + { + "fqn": "File", + "uid": 462 + }, + { + "fqn": "Directory", + "uid": 463 + }, + { + "fqn": "VideoOutput", + "uid": 464 + }, + { + "fqn": "Font", + "uid": 465 + }, + { + "fqn": "Framebuffer", + "uid": 466 + }, + { + "fqn": "Window", + "uid": 467 + }, + { + "fqn": "Widget", + "uid": 468 + }, + { + "fqn": "Desktop", + "uid": 469 + }, + { + "fqn": "WidgetType", + "uid": 470 + }, + { + "fqn": "SyncEvent", + "uid": 471 + }, + { + "fqn": "Mutex", + "uid": 472 + }, + { + "fqn": "max_fs_name_len", + "uid": 473 + }, + { + "fqn": "max_fs_type_len", + "uid": 474 + }, + { + "fqn": "max_file_name_len", + "uid": 475 + }, + { + "fqn": "UUID", + "uid": 476 + }, + { + "fqn": "DateTime", + "uid": 477 + }, + { + "fqn": "Absolute", + "uid": 478 + }, + { + "fqn": "Duration", + "uid": 479 + }, + { + "fqn": "PipeMode", + "uid": 480 + }, + { + "fqn": "NotificationSeverity", + "uid": 481 + }, + { + "fqn": "Await_Options.Thread_Affinity", + "uid": 482 + }, + { + "fqn": "Await_Options.Wait", + "uid": 483 + }, + { + "fqn": "Await_Options", + "uid": 484 + }, + { + "fqn": "VideoOutputID", + "uid": 485 + }, + { + "fqn": "FontType", + "uid": 486 + }, + { + "fqn": "FramebufferType", + "uid": 487 + }, + { + "fqn": "MessageBoxIcon", + "uid": 488 + }, + { + "fqn": "LogLevel", + "uid": 489 + }, + { + "fqn": "FileSystemId", + "uid": 490 + }, + { + "fqn": "FileAttributes", + "uid": 491 + }, + { + "fqn": "FileAccess", + "uid": 492 + }, + { + "fqn": "FileMode", + "uid": 493 + }, + { + "fqn": "KeyUsageCode", + "uid": 494 + }, + { + "fqn": "MouseButton", + "uid": 495 + }, + { + "fqn": "SpawnProcessArg.Type", + "uid": 496 + }, + { + "fqn": "SpawnProcessArg.Value", + "uid": 497 + }, + { + "fqn": "SpawnProcessArg.String", + "uid": 498 + }, + { + "fqn": "SpawnProcessArg", + "uid": 499 + }, + { + "fqn": "WindowFlags", + "uid": 500 + }, + { + "fqn": "CreateWindowFlags", + "uid": 501 + }, + { + "fqn": "WidgetDescriptor.Flags", + "uid": 502 + }, + { + "fqn": "WidgetDescriptor", + "uid": 503 + }, + { + "fqn": "WidgetControlMessage", + "uid": 504 + }, + { + "fqn": "WidgetNotifyEvent", + "uid": 505 + }, + { + "fqn": "MessageBoxResult", + "uid": 506 + }, + { + "fqn": "MessageBoxButtons.ok", + "uid": 507 + }, + { + "fqn": "MessageBoxButtons.ok_cancel", + "uid": 508 + }, + { + "fqn": "MessageBoxButtons.yes_no", + "uid": 509 + }, + { + "fqn": "MessageBoxButtons.yes_no_cancel", + "uid": 510 + }, + { + "fqn": "MessageBoxButtons.retry_cancel", + "uid": 511 + }, + { + "fqn": "MessageBoxButtons.abort_retry_ignore", + "uid": 512 + }, + { + "fqn": "MessageBoxButtons", + "uid": 513 + }, + { + "fqn": "DesktopDescriptor", + "uid": 514 + }, + { + "fqn": "DesktopEvent.Type", + "uid": 515 + }, + { + "fqn": "DesktopEvent", + "uid": 516 + }, + { + "fqn": "DesktopWindowEvent", + "uid": 517 + }, + { + "fqn": "DesktopWindowInvalidateEvent", + "uid": 518 + }, + { + "fqn": "DesktopNotificationEvent", + "uid": 519 + }, + { + "fqn": "MessageBoxEvent.RequestID", + "uid": 520 + }, + { + "fqn": "MessageBoxEvent", + "uid": 521 + }, + { + "fqn": "Color.black", + "uid": 522 + }, + { + "fqn": "Color.white", + "uid": 523 + }, + { + "fqn": "Color.red", + "uid": 524 + }, + { + "fqn": "Color.yellow", + "uid": 525 + }, + { + "fqn": "Color.lime", + "uid": 526 + }, + { + "fqn": "Color.green", + "uid": 527 + }, + { + "fqn": "Color.cyan", + "uid": 528 + }, + { + "fqn": "Color.blue", + "uid": 529 + }, + { + "fqn": "Color.purple", + "uid": 530 + }, + { + "fqn": "Color.magenta", + "uid": 531 + }, + { + "fqn": "Color.RGB888", + "uid": 532 + }, + { + "fqn": "Color.ARGB8888", + "uid": 533 + }, + { + "fqn": "Color.ABGR8888", + "uid": 534 + }, + { + "fqn": "Color", + "uid": 535 + }, + { + "fqn": "InputEvent.Type", + "uid": 536 + }, + { + "fqn": "InputEvent", + "uid": 537 + }, + { + "fqn": "WidgetEvent.Type", + "uid": 538 + }, + { + "fqn": "WidgetEvent", + "uid": 539 + }, + { + "fqn": "WindowEvent.Type", + "uid": 540 + }, + { + "fqn": "WindowEvent", + "uid": 541 + }, + { + "fqn": "SharedEventType", + "uid": 542 + }, + { + "fqn": "MouseEvent", + "uid": 543 + }, + { + "fqn": "KeyboardEvent", + "uid": 544 + }, + { + "fqn": "KeyboardModifiers", + "uid": 545 + }, + { + "fqn": "Point.zero", + "uid": 546 + }, + { + "fqn": "Point", + "uid": 547 + }, + { + "fqn": "Size.empty", + "uid": 548 + }, + { + "fqn": "Size.max", + "uid": 549 + }, + { + "fqn": "Size", + "uid": 550 + }, + { + "fqn": "Rectangle", + "uid": 551 + }, + { + "fqn": "VideoMemory", + "uid": 552 + }, + { + "fqn": "FileSystemInfo.Flags", + "uid": 553 + }, + { + "fqn": "FileSystemInfo", + "uid": 554 + }, + { + "fqn": "FileInfo", + "uid": 555 + }, + { + "fqn": "io.serial.SerialPortID", + "uid": 556 + }, + { + "fqn": "io.serial.SerialPort", + "uid": 557 + }, + { + "fqn": "io.serial.configure.InvalidHandle", + "uid": 558 + }, + { + "fqn": "io.serial.configure.ImpreciseBaudRate", + "uid": 559 + }, + { + "fqn": "io.serial.configure.UnsupportedDataBits", + "uid": 560 + }, + { + "fqn": "io.serial.configure.UnsupportedStopBits", + "uid": 561 + }, + { + "fqn": "io.serial.configure.UnsupportedParity", + "uid": 562 + }, + { + "fqn": "io.serial.configure.UnsupportedControlFlow", + "uid": 563 + }, + { + "fqn": "io.serial.configure", + "uid": 564 + }, + { + "fqn": "io.serial.control.InvalidHandle", + "uid": 565 + }, + { + "fqn": "io.serial.control.Unsupported", + "uid": 566 + }, + { + "fqn": "io.serial.control.ControlFlowActive", + "uid": 567 + }, + { + "fqn": "io.serial.control", + "uid": 568 + }, + { + "fqn": "io.serial.query_control.InvalidHandle", + "uid": 569 + }, + { + "fqn": "io.serial.query_control.Unsupported", + "uid": 570 + }, + { + "fqn": "io.serial.query_control", + "uid": 571 + }, + { + "fqn": "io.serial.write.InvalidHandle", + "uid": 572 + }, + { + "fqn": "io.serial.write.WordSizeMismatch", + "uid": 573 + }, + { + "fqn": "io.serial.write", + "uid": 574 + }, + { + "fqn": "io.serial.read.InvalidHandle", + "uid": 575 + }, + { + "fqn": "io.serial.read.WordSizeMismatch", + "uid": 576 + }, + { + "fqn": "io.serial.read", + "uid": 577 + }, + { + "fqn": "io.serial.break.InvalidHandle", + "uid": 578 + }, + { + "fqn": "io.serial.break.Unsupported", + "uid": 579 + }, + { + "fqn": "io.serial.break", + "uid": 580 + }, + { + "fqn": "io.serial.SerialPortError", + "uid": 581 + }, + { + "fqn": "io.serial.StopBits", + "uid": 582 + }, + { + "fqn": "io.serial.Parity", + "uid": 583 + }, + { + "fqn": "io.serial.ControlFlow", + "uid": 584 + }, + { + "fqn": "io.serial.SoftwareControlFlow", + "uid": 585 + }, + { + "fqn": "io.i2c.BusID", + "uid": 586 + }, + { + "fqn": "io.i2c.enumerate", + "uid": 587 + }, + { + "fqn": "io.i2c.query_metadata.NotFound", + "uid": 588 + }, + { + "fqn": "io.i2c.query_metadata", + "uid": 589 + }, + { + "fqn": "io.i2c.Bus", + "uid": 590 + }, + { + "fqn": "io.i2c.open.NotFound", + "uid": 591 + }, + { + "fqn": "io.i2c.open.SystemResources", + "uid": 592 + }, + { + "fqn": "io.i2c.open", + "uid": 593 + }, + { + "fqn": "io.i2c.Execute.InvalidHandle", + "uid": 594 + }, + { + "fqn": "io.i2c.Execute.InvalidAddress", + "uid": 595 + }, + { + "fqn": "io.i2c.Execute.EmptyOperation", + "uid": 596 + }, + { + "fqn": "io.i2c.Execute.ExecutionFailed", + "uid": 597 + }, + { + "fqn": "io.i2c.Execute", + "uid": 598 + }, + { + "fqn": "io.i2c.Operation.Type", + "uid": 599 + }, + { + "fqn": "io.i2c.Operation.Error", + "uid": 600 + }, + { + "fqn": "io.i2c.Operation", + "uid": 601 + }, + { + "fqn": "Syscall_ID", + "uid": 602 + }, + { + "fqn": "SystemResource.Type", + "uid": 603 + }, + { + "fqn": "overlapped.ARC.Type", + "uid": 604 + }, + { + "fqn": "get_demo_mode", + "uid": 605 + }, + { + "fqn": "draw.invalidate_framebuffer.InvalidHandle", + "uid": 606 + }, + { + "fqn": "draw.Render.SystemResources", + "uid": 607 + }, + { + "fqn": "WidgetEvent.LimitSizeEvent", + "uid": 608 + }, + { + "fqn": "WidgetEvent.ClickEvent.Source", + "uid": 609 + }, + { + "fqn": "WidgetEvent.ClickEvent", + "uid": 610 + }, + { + "fqn": "Rectangle.everything", + "uid": 611 + } + ] +} \ No newline at end of file diff --git a/src/abi/src/ashet.abi b/src/abi/src/ashet.abi index bc994d05..cf79048e 100644 --- a/src/abi/src/ashet.abi +++ b/src/abi/src/ashet.abi @@ -90,15 +90,15 @@ namespace overlapped { } /// Awaits one or more scheduled asynchronous operations and returns the - /// number of `completed` elements. + /// number of @`completed` elements. /// - /// The kernel will fill out `completed` up to the returned number of elements. + /// The kernel will fill out @`completed` up to the returned number of elements. /// All other values are undefined. /// /// NOTE: For blocking operations, this function will suspend the current /// thread until the request has been completed. /// - /// RELATES: @ref await_completion_of + /// RELATES: @`await_completion_of` syscall await_completion { in completed: []*ARC; in options: Await_Options; @@ -107,26 +107,26 @@ namespace overlapped { } /// Awaits one or more explicit asynchronous operations and returns the - /// number of `events` elements. + /// number of @`events` elements. /// - /// The kernel will only await elements provided in `events` and all of those events must - /// not be awaited by another `await_completion_of`. + /// The kernel will only await elements provided in @`events` and all of those events must + /// not be awaited by another @`await_completion_of`. /// - /// When the function returns, `events` will have all completed events unchanged, and all + /// When the function returns, @`events` will have all completed events unchanged, and all /// unfinished events set to `null`. This way, a simple check via index can be done instead of - /// the need for iteration of `events` to find what was finished. + /// the need for iteration of @`events` to find what was finished. /// /// NOTE: This syscall will always return as soon as a single event has finished. /// - /// NOTE: It is invalid to await the same operation with two concurrent calls to `await_completion_of`. + /// NOTE: It is invalid to await the same operation with two concurrent calls to @`await_completion_of`. /// /// NOTE: Elements awaited with this function will be guaranteed to not be returned by - /// another concurrent call to `await_completion`. + /// another concurrent call to @`await_completion`. /// /// NOTE: For blocking operations, this function will suspend the current /// thread until the request has been completed. /// - /// RELATES: @ref await_completion + /// RELATES: @`await_completion` syscall await_completion_of { in events: []?*ARC; error InvalidOperation; @@ -138,7 +138,7 @@ namespace overlapped { /// /// NOTE: If the operation has already completed, an error will be returned saying so. /// - /// NOTE: The cancelled operation will not be returned by `await_completion` or `await_completion_of` anymore. + /// NOTE: The cancelled operation will not be returned by @`await_completion` or @`await_completion_of` anymore. syscall cancel { in aop: *ARC; error Completed; @@ -171,7 +171,7 @@ namespace process { error InvalidHandle; } - /// Returns the arguments that were passed to this process in `Spawn`. + /// Returns the arguments that were passed to this process in @`Spawn`. syscall get_arguments { in target: ?Process; in argv: ?[]SpawnProcessArg; @@ -194,12 +194,12 @@ namespace process { /// Spawns a new process async_call Spawn { - /// Relative base directory for `path`. + /// Relative base directory for @`path`. in dir: Directory; - /// File name of the executable relative to `dir`. + /// File name of the executable relative to @`dir`. in path: str; /// The arguments passed to the process. - /// If a `SystemResource` is passed, it will receive the created process as a owning process. + /// If a @`SystemResource` is passed, it will receive the created process as a owning process. /// It is safe to release the resource in this process as soon as this operation returns. in argv: []const SpawnProcessArg; /// Handle to the spawned process. @@ -232,12 +232,12 @@ namespace process { } /// Defines the signature of a thread entry point. - /// The parameter is the `arg` value passed to `spawn`. + /// The parameter is the @`spawn.arg` value passed to @`spawn`. /// The return value is the exit code of the thread. typedef ThreadFunction = fnptr (?anyptr) u32; - /// Spawns a new thread with `function` passing `arg` to it. - /// If `stack_size` is not 0, will create a stack with the given size. + /// Spawns a new thread with @`function` passing @`arg` to it. + /// If @`stack_size` is not 0, will create a stack with the given size. syscall spawn { in function: ThreadFunction; in arg: ?anyptr; @@ -246,7 +246,7 @@ namespace process { error SystemResources; } - /// Kills the given thread with `exit_code`. + /// Kills the given thread with @`exit_code`. syscall kill { in target: Thread; in exit_code: ExitCode; @@ -295,9 +295,9 @@ namespace process { /// The alignment in ptr_align: u8; - /// A non-`null` pointer that points to exactly @ref size bytes. + /// A non-`null` pointer that points to exactly @`size` bytes. /// - /// NOTE: In practise, this might point to more than @ref size bytes, + /// NOTE: In practise, this might point to more than @`size` bytes, /// but the code must not assume *any* excess bytes may exist. out memory: [*]u8; @@ -307,10 +307,10 @@ namespace process { /// Returns previously allocated memory back to the process heap. syscall release { - /// The complete chunk of memory previously allocated with @ref allocate. + /// The complete chunk of memory previously allocated with @`allocate`. in mem: []u8; - /// The alignment that was passed to @ref allocate.ptr_align previously. + /// The alignment that was passed to @`allocate.ptr_align` previously. in ptr_align: u8; } } @@ -360,7 +360,7 @@ namespace clock { out time: Absolute; } - /// Sleeps until `clock.monotonic()` returns at least `timeout`. + /// Sleeps until `clock.monotonic()` returns at least @`timeout`. async_call Timer { /// Monotonic timestamp in nanoseconds until the operation completes. in timeout: Absolute; @@ -376,7 +376,7 @@ namespace datetime { out datetime: DateTime; } - /// Sleeps until `datetime.now()` returns a point in time that comes after `when`. + /// Sleeps until `datetime.now()` returns a point in time that comes after @`when`. async_call Alarm { /// Earliest possible date time of when the alarm triggers. in when: DateTime; @@ -387,7 +387,7 @@ namespace datetime { namespace video { /// Returns a list of all video outputs. /// - /// If `ids` is `null`, the total number of available outputs is returned; + /// If @`ids` is `null`, the total number of available outputs is returned; /// otherwise, up to `ids.len` elements are written into the provided array /// and the number of written elements is returned. syscall enumerate { @@ -707,7 +707,7 @@ namespace fs { } /// Gets information about a file system. - /// Also returns a `next` id that can be used to iterate over all filesystems. + /// Also returns a @`next` id that can be used to iterate over all filesystems. /// The `system` filesystem is guaranteed to be the first one. async_call GetFilesystemInfo { in fs_id: FileSystemId; @@ -780,7 +780,7 @@ namespace fs { error InvalidPath; } - /// creates a new directory relative to dir. If `path` contains subdirectories, all + /// creates a new directory relative to dir. If @`path` contains subdirectories, all /// directories are created. async_call MkDir { in dir: Directory; @@ -927,7 +927,7 @@ namespace fs { } namespace shm { - /// Constructs a new shared memory object with `size` bytes of memory. + /// Constructs a new shared memory object with @`size` bytes of memory. /// Shared memory can be written by all processes without any memory protection. syscall create { in size: usize; @@ -952,9 +952,9 @@ namespace shm { } namespace pipe { - /// Spawns a new pipe with `fifo_length` elements of `object_size` bytes. - /// If `fifo_length` is 0, the pipe is synchronous and can only send data - /// if a `read` call is active. Otherwise, up to `fifo_length` elements can be + /// Spawns a new pipe with @`fifo_length` elements of @`object_size` bytes. + /// If @`fifo_length` is 0, the pipe is synchronous and can only send data + /// if a @`Read` call is active. Otherwise, up to @`fifo_length` elements can be /// stored in a FIFO. syscall create { in object_size: usize; @@ -977,14 +977,14 @@ namespace pipe { error InvalidHandle; } - /// Writes elements from `data` into the given pipe. + /// Writes elements from @`data` into the given pipe. async_call Write { in handle: Pipe; /// Pointer to the first element. Length defines how many elements are to be transferred. in data: bytestr; - /// Distance between each element in `data`. Can be different from the pipes element size + /// Distance between each element in @`data`. Can be different from the pipes element size /// to allow sparse data to be transferred. - /// If `0`, it will use the `object_size` property of the pipe. + /// If `0`, it will use the @`create.object_size` property of the pipe. in stride: usize; /// Defines how the write should operate. in mode: PipeMode; @@ -992,14 +992,14 @@ namespace pipe { out count: usize; } - /// Reads elements from a pipe into `buffer`. + /// Reads elements from a pipe into @`buffer`. async_call Read { in handle: Pipe; /// Points to the first element to be received. in buffer: bytebuf; - /// Distance between each element in `buffer`. Can be different from the pipes element size + /// Distance between each element in @`buffer`. Can be different from the pipes element size /// to allow sparse data to be transferred. - /// If `0`, it will use the `object_size` property of the pipe. + /// If `0`, it will use the @`create.object_size` property of the pipe. in stride: usize; /// Defines how the read should operate. in mode: PipeMode; @@ -1010,26 +1010,26 @@ namespace pipe { } namespace sync { - /// Creates a new `SyncEvent` object that can be used to synchronize + /// Creates a new @`SyncEvent` object that can be used to synchronize /// different processes. syscall create_event { out event: SyncEvent; error SystemResources; } - /// Completes one `WaitForEvent` IOP waiting for the given event. + /// Completes one @`WaitForEvent` IOP waiting for the given event. syscall notify_one { in event: SyncEvent; error InvalidHandle; } - /// Completes all `WaitForEvent` IOP waiting for the given event. + /// Completes all @`WaitForEvent` IOP waiting for the given event. syscall notify_all { in event: SyncEvent; error InvalidHandle; } - /// Waits for the given `SyncEvent` to be notified. + /// Waits for the given @`SyncEvent` to be notified. async_call WaitForEvent { in event: SyncEvent; error InvalidHandle; @@ -1048,7 +1048,7 @@ namespace sync { error InvalidHandle; } - /// Unlocks a mutual exclusion. Completes a single `Lock` IOP if it exists. + /// Unlocks a mutual exclusion. Completes a single @`Lock` IOP if it exists. syscall unlock { in mutex: Mutex; error InvalidHandle; @@ -1161,7 +1161,7 @@ namespace draw { error InvalidHandle; } - /// Renders the provided Ashet Graphics Protocol `sequence` into `target` framebuffer. + /// Renders the provided Ashet Graphics Protocol @`sequence` into @`target` framebuffer. /// /// The function will run asynchronously and will return as soon as the rendering is done. /// @@ -1247,7 +1247,7 @@ namespace gui { error InvalidHandle; } - /// Sets the `size` of `window` and returns the new actual size. + /// Sets the @`size` of @`window` and returns the new actual size. /// NOTE: This event is meant to be used from desktop APIs and will not automatically /// notify the window of the resize event. syscall set_window_size { @@ -1278,7 +1278,7 @@ namespace gui { error InvalidHandle; } - /// Waits for an event on the given `Window`, completing as soon as + /// Waits for an event on the given @`Window`, completing as soon as /// an event arrived. async_call GetWindowEvent { in window: Window; @@ -1288,8 +1288,8 @@ namespace gui { error InvalidHandle; } - /// Create a new widget identified by `uuid` on the given `window`. - /// Position and size of the widget are undetermined at start and a call to `place_widget` should be performed on success. + /// Create a new widget identified by @`uuid` on the given @`window`. + /// Position and size of the widget are undetermined at start and a call to @`place_widget` should be performed on success. syscall create_widget { in window: Window; in uuid: *const UUID; @@ -1317,7 +1317,7 @@ namespace gui { enum WidgetControlID : u32 { ... } - /// Triggers the `control` event of the widget with the given `message` as a payload. + /// Triggers the @`WidgetEvent.Type.control` event of the widget with the given @`message` as a payload. syscall control_widget { in widget: Widget; @@ -1333,8 +1333,8 @@ namespace gui { enum WidgetNotifyID : u32 { ... } - /// Puts a `widget_notify` event into the event queue of the `Window` that owns `widget`. - /// The parameters are passed as a `WidgetNotifyEvent` to the event queue. + /// Puts a @`WindowEvent.Type.widget_notify` event into the event queue of the @`Window` that owns @`widget`. + /// The parameters are passed as a @`WidgetNotifyEvent` to the event queue. syscall notify_owner { in widget: Widget; in type: WidgetNotifyID; @@ -1409,11 +1409,11 @@ namespace gui { /// /// NOTE: This function is meant to be implemented by a desktop server. /// Regular GUI applications should not use this function as they have no - /// access to a `MessageBoxEvent.RequestID`. + /// access to a @`MessageBoxEvent.RequestID`. syscall notify_message_box { /// The desktop that completed the message box. in source: Desktop; - /// The request id that was passed in `MessageBoxEvent`. + /// The request id that was passed in @`MessageBoxEvent`. in request_id: MessageBoxEvent.RequestID; /// The resulting button which the user clicked. in result: MessageBoxResult; @@ -1430,7 +1430,7 @@ namespace gui { error InvalidHandle; } - /// Sends a notification to the provided `desktop`. + /// Sends a notification to the provided @`desktop`. syscall send_notification { /// Where to show the notification? in desktop: Desktop; @@ -1462,7 +1462,7 @@ namespace gui { /// Returns the current clipboard value as the provided mime type. /// The os provides a conversion *if possible*, otherwise returns an error. - /// The returned memory for `value` is owned by the process and must be freed with `ashet.process.memory.release`. + /// The returned memory for @`value` is owned by the process and must be freed with @`process.memory.release`. syscall get_value { in desktop: Desktop; in mime: str; @@ -1476,7 +1476,7 @@ namespace gui { } namespace service { - /// Registers a new service `uuid` in the system. + /// Registers a new service @`uuid` in the system. /// Takes an array of function pointers that will be provided for IPC and a service name to be advertised. syscall create { in uuid: *const UUID; @@ -1668,7 +1668,7 @@ struct Await_Options { /// or /// b) the result array is full /// - /// NOTE: If `thread_affinity` is `.all_threads`, other threads can still + /// NOTE: If @`thread_affinity` is @`Thread_Affinity.all_threads`, other threads can still /// schedule more operations and make this function block longer. item wait_all = 2; @@ -2629,7 +2629,7 @@ struct WidgetDescriptor { field uuid: UUID; /// Number of bytes allocated in a Widget for this widget type. - /// See @ref gui.get_widget_data function for further information. + /// See @`gui.get_widget_data` function for further information. field data_size: usize; field flags: Flags; @@ -2720,7 +2720,7 @@ typedef DesktopEventHandler = fnptr (Desktop, *const DesktopEvent) void; struct DesktopDescriptor { /// Number of bytes allocated in a Window for this desktop. - /// See @ref gui.get_desktop_data function for further information. + /// See @`gui.get_desktop_data` function for further information. field window_data_size: usize; /// A function pointer to the event handler of a desktop. @@ -2752,11 +2752,11 @@ union DesktopEvent { //? user interaction: - /// `send_notification` was called and the desktop user should display + /// @`gui.send_notification` was called and the desktop user should display /// a notification. item show_notification = 3; - /// `send_notification` was called and the desktop user should display + /// @`gui.ShowMessageBox` was called and the desktop user should display /// a notification. item show_message_box = 4; @@ -2789,7 +2789,7 @@ struct MessageBoxEvent { field event_type: DesktopEvent.Type; /// The desktop-specific request id that must be passed into - /// `notify_message_box` to finish the message box request. + /// @`gui.notify_message_box` to finish the message box request. field request_id: RequestID; /// Content of the message box. @@ -2823,11 +2823,11 @@ struct MessageBoxEvent { /// /// To address these two problems, the color scheme uses a modified mapping: /// -/// - `hue` is used without special interpretation. -/// - `value` maps to a range of `[1:8]` instead of `[0:7]`, allowing 8 different +/// - @`hue` is used without special interpretation. +/// - @`value` maps to a range of `[1:8]` instead of `[0:7]`, allowing 8 different /// values that are all not black. -/// - `saturation` is used without special interpretation except for zero: -/// If the `saturation` field is zero, `hue` and `value` are interpreted together as a 6 bit +/// - @`saturation` is used without special interpretation except for zero: +/// If the @`saturation` field is zero, @`hue` and @`value` are interpreted together as a 6 bit /// integer storing the brightness of gray. /// /// This yields a color space which has the following properties: @@ -2942,7 +2942,7 @@ union WidgetEvent { /// same widget. The hovered widget *may* change in between the mouse down and mouse up, /// but the click will still be recognized. /// NOTE: A click with the keyboard is valid when, and only when: - /// `key_press` and `key_release` happen without changing the focused widget, and only when + /// @`key_press` and @`key_release` happen without changing the focused widget, and only when /// the focus giving key (space, return, ...) was pressed without any other key interrupting. item click = 3; @@ -2958,43 +2958,43 @@ union WidgetEvent { /// The mouse was moved inside the rectangle of the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item mouse_enter = 6; /// The mouse was moved outside the rectangle of the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item mouse_leave = 7; /// The mouse stopped for some time over the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item mouse_hover = 8; /// A mouse button was pressed over the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item mouse_button_press = 9; /// A mouse button was released over the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item mouse_button_release = 10; /// The mouse was moved over the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item mouse_motion = 11; /// A vertical or horizontal scroll wheel was scrolled over the widget. /// - /// NOTE: This event can only happen when `hit_test_visible` was set + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.hit_test_visible` was set /// in the widget creation flags. item scroll = 12; @@ -3002,25 +3002,25 @@ union WidgetEvent { /// The user dragged a payload into the rectangle of this widget. /// - /// NOTE: This event can only happen when `allow_drop` was set in the + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.allow_drop` was set in the /// widget creation flags. item drag_enter = 13; /// The user dragged a payload out of the rectangle of this widget. /// - /// NOTE: This event can only happen when `allow_drop` was set in the + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.allow_drop` was set in the /// widget type creation flags. item drag_leave = 14; /// The user dragged a payload over the rectangle of this widget. /// - /// NOTE: This event can only happen when `allow_drop` was set in the + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.allow_drop` was set in the /// widget type creation flags. item drag_over = 15; /// The user dropped a payload into this widget. /// - /// NOTE: This event can only happen when `allow_drop` was set in the + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.allow_drop` was set in the /// widget type creation flags. item drag_drop = 16; @@ -3028,19 +3028,19 @@ union WidgetEvent { /// The user requested a clipboard copy operation, usually by pressing 'Ctrl-C'. /// - /// NOTE: This event can only happen when `clipboard_sensitive` was set in + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.clipboard_sensitive` was set in /// the widget type creation flags. item clipboard_copy = 17; /// The user requested a clipboard paste operation, usually by pressing 'Ctrl-V'. /// - /// NOTE: This event can only happen when `clipboard_sensitive` was set in + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.clipboard_sensitive` was set in /// the widget type creation flags. item clipboard_paste = 18; /// The user requested a clipboard cut operation, usually by pressing 'Ctrl-X'. /// - /// NOTE: This event can only happen when `clipboard_sensitive` was set in + /// NOTE: This event can only happen when @`WidgetDescriptor.Flags.clipboard_sensitive` was set in /// the widget type creation flags. item clipboard_cut = 19; @@ -3142,15 +3142,19 @@ union WindowEvent { item window_restore = 10; /// The window is currently moving on the screen. Query `window.bounds` to get the new position. + /// + /// DEPRECATED: This event must be removed! item window_moving = 11; /// The window was moved on the screen. Query `window.bounds` to get the new position. + /// + /// DEPRECATED: This event must be removed! item window_moved = 12; - /// The window size is currently changing. Query `window.bounds` to get the new size. + /// The window size is currently changing. Query @`gui.get_window_size` to get the new size. item window_resizing = 13; - /// The window size changed. Query `window.bounds` to get the new size. + /// The window size changed. Query @`gui.get_window_size` to get the new size. item window_resized = 14; } } @@ -3253,10 +3257,10 @@ struct Rectangle { struct VideoMemory { /// Pointer to the first pixel of the first scanline. /// - /// Each scanline is `.stride` elements separated from - /// each other and contains `width` valid elements. + /// Each scanline is @`stride` elements separated from + /// each other and contains @`width` valid elements. /// - /// There are `height` total scanlines available. + /// There are @`height` total scanlines available. field base: [*]align(4) Color; /// Length of a scanline. @@ -3386,7 +3390,7 @@ namespace io { error InvalidHandle; - /// The actual baud rate diverges more than `acceptable_baud_error` from the requested baud rate. + /// The actual baud rate diverges more than @`acceptable_baud_error` from the requested baud rate. error ImpreciseBaudRate; /// The requested word size is not supported by this serial port. @@ -3657,7 +3661,7 @@ namespace io { /// As both a singular read and a singular write can be expressed as a batch of one operation, /// this is the only available function on the I²C bus. /// - /// **Example:** + /// *Example:* /// Typical I²C EEPROMs have an internally maintained "memory cursor" which is advanced for every /// read operation. All write operations will update the cursor also for read operations. /// @@ -3673,10 +3677,10 @@ namespace io { in bus: Bus; /// A mutable sequence of I²C operations. Will be processed first-to-last and - /// the pointed `Operation`s will be changed during their execution. + /// the pointed @`Operation`s will be changed during their execution. in sequence: []Operation; - /// The number of successfully processed elements from `sequence`. + /// The number of successfully processed elements from @`sequence`. /// /// On success, the call returns exactly the length of the sequence, otherwise /// it returns the index of the element that failed. @@ -3684,10 +3688,10 @@ namespace io { error InvalidHandle; - /// An operation in the `sequence` contained an invalid address. + /// An operation in the @`sequence` contained an invalid address. error InvalidAddress; - /// An operation that isn't `ping` was trying to process zero bytes. + /// An operation that isn't @`Operation.Type.ping` was trying to process zero bytes. error EmptyOperation; /// An error happened during processing. Read `sequence[].error` to see which operations failed. @@ -3705,12 +3709,12 @@ namespace io { /// The data which should either be written or read. /// - /// NOTE: If the operation is `Type.read`, the buffer will + /// NOTE: If the operation is @`Type.read`, the buffer will /// be overwritten by the OS. All other operations treat this /// buffer as immutable. field data: bytebuf; - /// The number of processed bytes inside `data`. + /// The number of processed bytes inside @`data`. /// /// NOTE: This field can be left uninitialized and will be overwritten by the OS /// with the result of the operation. @@ -3718,11 +3722,11 @@ namespace io { field processed: usize = 0; /// The error that happened when processing this batch item. On success, this will - /// be set to `Error.none`. + /// be set to @`Error.none`. /// /// NOTE: This field can be left uninitialized and will be overwritten by the OS /// with the result of the operation. - /// If the batch item wasn't scheduled, the resulting value will be `Error.aborted`. + /// If the batch item wasn't scheduled, the resulting value will be @`Error.aborted`. field @"error": Error; enum Type : u8 { @@ -3748,7 +3752,7 @@ namespace io { /// While writing the data, the device returned a NAK. /// - /// NOTE: The ACK/NAK during the addressing phase is handled by `device_not_found`. + /// NOTE: The ACK/NAK during the addressing phase is handled by @`device_not_found`. item no_acknowledge = 2; /// A previous batch item errored and this item wasn't executed at all. @@ -3765,4 +3769,4 @@ namespace io { } } } -} \ No newline at end of file +} diff --git a/src/abi/tests/escaping.abi b/src/abi/tests/escaping.abi new file mode 100644 index 00000000..24b78458 --- /dev/null +++ b/src/abi/tests/escaping.abi @@ -0,0 +1,36 @@ +typedef Syscall_ID = <>; +enum SystemResource : usize +{ + ... + + typedef Type = <>; +} + +namespace resources { + syscall release { + in @"resource": SystemResource; + } + +} + +resource Thread {} + +//? Must be properly escaped as @"suspend" +syscall suspend { + in target: ?Thread; + error InvalidHandle; + error ThreadStopped; +} + +struct resume {} + +namespace embedded { + //? Must not be as @"escaped_suspend" + syscall suspend { + in target: ?Thread; + error InvalidHandle; + error ThreadStopped; + } + + struct resume {} +} diff --git a/src/abi/utility/render_zig_code.zig b/src/abi/utility/render_zig_code.zig index ecc7d7fa..f6ac5233 100644 --- a/src/abi/utility/render_zig_code.zig +++ b/src/abi/utility/render_zig_code.zig @@ -5,7 +5,6 @@ const code_writer = @import("code_writer.zig"); const patch_parser = @import("patch_parser.zig"); -const fmt_id = std.zig.fmtId; const fmt_escapes = std.zig.fmtString; const model = abi_parser.model; @@ -142,7 +141,7 @@ pub fn render_kernel(writer: *CodeWriter, allocator: std.mem.Allocator, schema: defer writer.dedent(); for (schema.syscalls) |syscall| { - try writer.print("pub export fn {s}{f}(", .{ renderer.symbol_prefix, fmt_fqn(syscall.full_qualified_name, "_") }); + try writer.print("pub export fn {f}(", .{renderer.fmt_sym_name(syscall.full_qualified_name, .with_prefix)}); for (syscall.native_inputs, 0..) |input, index| { if (index > 0) @@ -174,8 +173,8 @@ pub fn render_kernel(writer: *CodeWriter, allocator: std.mem.Allocator, schema: writer.indent(); defer writer.dedent(); - try writer.println("Callbacks.before_syscall(.{f});", .{fmt_fqn(syscall.full_qualified_name, "_")}); - try writer.println("defer Callbacks.after_syscall(.{f});", .{fmt_fqn(syscall.full_qualified_name, "_")}); + try writer.println("Callbacks.before_syscall(.{f});", .{renderer.fmt_sym_name(syscall.full_qualified_name, .no_prefix)}); + try writer.println("defer Callbacks.after_syscall(.{f});", .{renderer.fmt_sym_name(syscall.full_qualified_name, .no_prefix)}); if (has_errors) { // the return value must be an error union @@ -206,7 +205,7 @@ pub fn render_kernel(writer: *CodeWriter, allocator: std.mem.Allocator, schema: } try writer.println(" = Impl.{f}(", .{ - fmt_fqn(syscall.full_qualified_name, null), + fmt_fqn(syscall.full_qualified_name), }); { writer.indent(); @@ -386,7 +385,7 @@ const ZigRenderer = struct { try zr.writer.println("pub const {f} = {s}{f};", .{ fmt_id(model.local_name(child.full_qualified_name)), zr.scope_prefix, - fmt_fqn(child.full_qualified_name, null), + fmt_fqn(child.full_qualified_name), }); }, @@ -395,6 +394,38 @@ const ZigRenderer = struct { } } + fn fmt_sym_name(zr: *ZigRenderer, sym: model.FQN, mode: FmtSymName.Mode) FmtSymName { + return .{ + .zr = zr, + .sym = sym, + .mode = mode, + }; + } + + const FmtSymName = struct { + const Mode = enum { with_prefix, no_prefix }; + zr: *ZigRenderer, + sym: model.FQN, + mode: FmtSymName.Mode, + + pub fn format(fmt: FmtSymName, writer: *std.Io.Writer) !void { + var full_name_buf: [512]u8 = undefined; + var full_name_writer: std.Io.Writer = .fixed(&full_name_buf); + + switch (fmt.mode) { + .with_prefix => try full_name_writer.writeAll(fmt.zr.symbol_prefix), + .no_prefix => {}, + } + for (fmt.sym, 0..) |node, i| { + if (i > 0) + try full_name_writer.writeAll("_"); + try full_name_writer.writeAll(node); + } + + try writer.print("{f}", .{std.zig.fmtId(full_name_writer.buffered())}); + } + }; + fn render_userland_call(zr: *ZigRenderer, syscall: *const model.GenericCall, children: []const model.Declaration) !void { std.debug.assert(children.len == 0); @@ -480,9 +511,8 @@ const ZigRenderer = struct { } } - try writer.println("const __result = {s}{f}(", .{ - zr.symbol_prefix, - fmt_fqn(syscall.full_qualified_name, "_"), + try writer.println("const __result = {f}(", .{ + zr.fmt_sym_name(syscall.full_qualified_name, .with_prefix), }); writer.indent(); @@ -538,7 +568,7 @@ const ZigRenderer = struct { }); } try writer.println("else => return __handle_unexpected(.{f}, __result),", .{ - fmt_fqn(syscall.full_qualified_name, "_"), + zr.fmt_sym_name(syscall.full_qualified_name, .no_prefix), }); writer.dedent(); try writer.writeln("}"); @@ -632,9 +662,8 @@ const ZigRenderer = struct { .local_name => try zr.writer.println("pub extern fn {f}(", .{ fmt_id(model.local_name(syscall.full_qualified_name)), }), - .full_name => try zr.writer.println("extern fn {s}{f}(", .{ - zr.symbol_prefix, - fmt_fqn(syscall.full_qualified_name, "_"), + .full_name => try zr.writer.println("extern fn {f}(", .{ + zr.fmt_sym_name(syscall.full_qualified_name, .with_prefix), }), } @@ -687,7 +716,7 @@ const ZigRenderer = struct { try zr.writer.writeln("pub const Inputs = extern struct {"); zr.writer.indent(); - try zr.writer.println("pub const Overlapped = {f};", .{fmt_fqn(arc.full_qualified_name, null)}); + try zr.writer.println("pub const Overlapped = {f};", .{fmt_fqn(arc.full_qualified_name)}); for (arc.native_inputs) |field| { try zr.render_docs(field.docs); try zr.writer.print("{f}: {f}", .{ @@ -705,7 +734,7 @@ const ZigRenderer = struct { try zr.writer.writeln("pub const Outputs = extern struct {"); zr.writer.indent(); - try zr.writer.println("pub const Overlapped = {f};", .{fmt_fqn(arc.full_qualified_name, null)}); + try zr.writer.println("pub const Overlapped = {f};", .{fmt_fqn(arc.full_qualified_name)}); for (arc.native_outputs) |field| { try zr.render_docs(field.docs); try zr.writer.print("{f}: {f}", .{ @@ -826,8 +855,9 @@ const ZigRenderer = struct { try zr.writer.writeln("}"); } if (arc.native_outputs.len == 1) { + assert_unpadded_name(arc.native_outputs[0].name); try zr.writer.writeln(""); - try zr.writer.println("pub fn get_output(arc: *const @This()) !*const @FieldType(Outputs, \"{s}\") {{", .{arc.native_outputs[0].name}); + try zr.writer.println("pub fn get_output(arc: *const @This()) !*const @FieldType(Outputs, \"{f}\") {{", .{fmt_escapes(arc.native_outputs[0].name)}); zr.writer.indent(); try zr.writer.writeln("try arc.check_error();"); try zr.writer.println("return &arc.outputs.{f};", .{fmt_id(arc.native_outputs[0].name)}); @@ -1018,10 +1048,13 @@ const ZigRenderer = struct { }); } - fn render_docs(zr: *ZigRenderer, docs: model.DocString) !void { - for (docs) |line| { - try zr.writer.println("/// {s}", .{line}); - } + fn render_docs(zr: *ZigRenderer, docs: model.DocComment) !void { + _ = zr; + _ = docs; + // TODO: COnsider if it's worth to include the doc strings inside the generated zig code + // for (docs) |line| { + // try zr.writer.println("/// {s}", .{line}); + // } } fn fmt_type(zr: *ZigRenderer, type_id: model.TypeIndex) ZigTypeFmt { @@ -1102,32 +1135,32 @@ const ZigRenderer = struct { .@"enum" => |index| try writer.print("{s}{f}", .{ zr.scope_prefix, - fmt_fqn(zr.schema.get_enum(index).full_qualified_name, null), + fmt_fqn(zr.schema.get_enum(index).full_qualified_name), }), .@"struct" => |index| try writer.print("{s}{f}", .{ zr.scope_prefix, - fmt_fqn(zr.schema.get_struct(index).full_qualified_name, null), + fmt_fqn(zr.schema.get_struct(index).full_qualified_name), }), .@"union" => |index| try writer.print("{s}{f}", .{ zr.scope_prefix, - fmt_fqn(zr.schema.get_union(index).full_qualified_name, null), + fmt_fqn(zr.schema.get_union(index).full_qualified_name), }), .bitstruct => |index| try writer.print("{s}{f}", .{ zr.scope_prefix, - fmt_fqn(zr.schema.get_bitstruct(index).full_qualified_name, null), + fmt_fqn(zr.schema.get_bitstruct(index).full_qualified_name), }), .resource => |index| try writer.print("{s}{f}", .{ zr.scope_prefix, - fmt_fqn(zr.schema.get_resource(index).full_qualified_name, null), + fmt_fqn(zr.schema.get_resource(index).full_qualified_name), }), .typedef => |typedef| try writer.print("{s}{f}", .{ zr.scope_prefix, - fmt_fqn(typedef.full_qualified_name, null), + fmt_fqn(typedef.full_qualified_name), }), .uint => |size| try writer.print("u{}", .{size}), @@ -1135,11 +1168,14 @@ const ZigRenderer = struct { .fnptr => |fptr| { try writer.writeAll("*const fn("); - for (fptr.parameters, 0..) |ptype, i| { + for (fptr.parameters, 0..) |param, i| { if (i > 0) { try writer.writeAll(", "); } - try writer.print("{f}", .{zr.fmt_type(ptype)}); + if (param.name) |name| { + try writer.print("{f}: ", .{std.zig.fmtId(name)}); + } + try writer.print("{f}", .{zr.fmt_type(param.type)}); } try writer.writeAll(") callconv(.c) "); @@ -1222,10 +1258,10 @@ const ZigRenderer = struct { } }; -fn fmt_fqn(fqn: []const []const u8, sep: ?[]const u8) FqnFmt { +fn fmt_fqn(fqn: []const []const u8) FqnFmt { return .{ .fqn = fqn, - .sep = sep orelse ".", + .sep = ".", }; } @@ -1243,6 +1279,15 @@ const FqnFmt = struct { } }; +fn assert_unpadded_name(name: []const u8) void { + std.debug.assert(std.mem.trim(u8, name, " \r\n\t").len == name.len); +} + +fn fmt_id(id: []const u8) @TypeOf(std.zig.fmtId(id)) { + assert_unpadded_name(id); + return std.zig.fmtId(id); +} + fn fmt_local(id: []const u8) std.fmt.Formatter([]const u8, format_local) { return .{ .data = id }; } diff --git a/src/os/build.zig b/src/os/build.zig index 24de2d2e..133326ee 100644 --- a/src/os/build.zig +++ b/src/os/build.zig @@ -47,8 +47,7 @@ pub fn build(b: *std.Build) void { }); const assets_dep = b.dependency("assets", .{}); - // const disk_image_dep = b.dependency("dimmer", .{ .release = true }); - const disk_image_dep = b.dependency("dimmer", .{}); + const disk_image_dep = b.dependency("dimmer", .{ .release = true }); const limine_dep = b.dependency("zig_limine_install", .{ .target = b.graph.host, .optimize = .ReleaseSafe }); diff --git a/src/tools/abi-mapper/build.zig b/src/tools/abi-mapper/build.zig index 3fb7276e..deb4f6e3 100644 --- a/src/tools/abi-mapper/build.zig +++ b/src/tools/abi-mapper/build.zig @@ -35,16 +35,25 @@ pub fn build(b: *std.Build) void { const output_file = convert_test_file.addPrefixedOutputFileArg("--output=", "coverage.json"); test_step.dependOn(&b.addInstallFile(output_file, "test/coverage.json").step); + + const testsuite_mod = b.createModule(.{ + .root_source_file = b.path("tests/testsuite.zig"), + .target = target, + .optimize = optimize, + .imports = &.{.{ .name = "abi-parser", .module = abi_parser_mod }}, + }); + test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = testsuite_mod })).step); } pub const Converter = struct { b: *std.Build, executable: *std.Build.Step.Compile, - pub fn get_json_dump(cc: Converter, id_database: std.Build.LazyPath, input: std.Build.LazyPath) std.Build.LazyPath { + pub fn get_json_dump(cc: Converter, id_database: ?std.Build.LazyPath, input: std.Build.LazyPath) std.Build.LazyPath { const generate_json = cc.b.addRunArtifact(cc.executable); - // TODO: generate_json.addPrefixedFileArg("--id-db=", id_database); - _ = id_database; + if (id_database) |db_path| { + generate_json.addPrefixedFileArg("--id-db=", db_path); + } const abi_json = generate_json.addPrefixedOutputFileArg("--output=", "abi.json"); generate_json.addFileArg(input); return abi_json; diff --git a/src/tools/abi-mapper/rework/abi-doc-format.md b/src/tools/abi-mapper/rework/abi-doc-format.md new file mode 100644 index 00000000..3400bf1d --- /dev/null +++ b/src/tools/abi-mapper/rework/abi-doc-format.md @@ -0,0 +1,1047 @@ +# Ashet IDL Documentation Comment Format + +**Status:** Draft + +## 1. Overview + +This document specifies the syntax, semantics, and data model for structured documentation comments in Ashet IDL files. It replaces the previous unstructured `docs: []const u8` (list of raw lines) representation with a validated, hyperlinked document fragment tree. + +### 1.1 Design goals + +- **Minimal migration friction.** Existing doc comments are nearly valid as-is. +- **Validated cross-references.** Every reference to an IDL declaration is resolved and checked at parse time. +- **Structured admonitions.** `NOTE`, `LORE`, `EXAMPLE`, etc. are first-class constructs, not conventions. +- **HyperDoc-compatible AST.** The output tree uses a subset of HyperDoc 2.0's semantic model, enabling shared rendering and tooling. +- **Parseable in Zig.** No complex grammar; every construct is recognizable by a simple line-prefix or character-level scan. + +### 1.2 Relationship to HyperDoc 2.0 + +The output AST of a parsed doc comment is a strict subset of HyperDoc 2.0's document model. Specifically, it uses the node types `p`, `note`, `warning`, `tip`, `ul`, `ol`, `pre`, `\mono`, `\em`, `\ref`, and `\link`, plus the custom extensions `lore`, `example`, `deprecated`, and `decision`. + +The *input syntax*, however, is a distinct lightweight format optimized for `///` comment lines, not HyperDoc surface syntax. Think of it as a convenient authoring frontend that compiles to a HyperDoc fragment. + +--- + +## 2. Source representation + +### 2.1 Comment extraction + +Documentation comments are lines beginning with `///`. The parser strips the `///` prefix and exactly one optional trailing space character (the separator between `///` and content). The resulting lines form the **raw doc text**, which is then parsed according to this specification. + +``` +/// This is a paragraph. → "This is a paragraph." +/// → "" +/// Indented continuation. → " Indented continuation." +``` + +Line comments (`//?`) are not part of the documentation and are discarded before doc parsing. + +### 2.2 Encoding + +Raw doc text inherits the encoding of the IDL source file (UTF-8). No additional encoding layer is defined. + +--- + +## 3. Document model (normative) + +A parsed doc comment produces a **DocComment** value. The model is specified here as a JSON schema; the canonical in-memory representation in Zig is derived from this. + +### 3.1 JSON schema + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ashet.org/schemas/abi-doc-comment/v1", + "title": "Ashet IDL DocComment", + + "$defs": { + + "DocComment": { + "description": "Root type. A parsed documentation comment.", + "type": "object", + "required": ["sections"], + "additionalProperties": false, + "properties": { + "sections": { + "type": "array", + "items": { "$ref": "#/$defs/Section" }, + "minItems": 0 + } + } + }, + + "Section": { + "description": "A thematic section of a doc comment. The first section with kind 'main' contains the primary description. Subsequent sections are admonitions.", + "type": "object", + "required": ["kind", "blocks"], + "additionalProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "main", + "note", + "warning", + "lore", + "example", + "deprecated", + "decision", + "learn", + ] + }, + "blocks": { + "type": "array", + "items": { "$ref": "#/$defs/Block" }, + "minItems": 1 + } + } + }, + + "Block": { + "description": "A block-level element inside a section.", + "oneOf": [ + { "$ref": "#/$defs/Paragraph" }, + { "$ref": "#/$defs/UnorderedList" }, + { "$ref": "#/$defs/OrderedList" }, + { "$ref": "#/$defs/CodeBlock" } + ] + }, + + "Paragraph": { + "type": "object", + "required": ["type", "content"], + "additionalProperties": false, + "properties": { + "type": { "const": "paragraph" }, + "content": { "$ref": "#/$defs/InlineContent" } + } + }, + + "UnorderedList": { + "type": "object", + "required": ["type", "items"], + "additionalProperties": false, + "properties": { + "type": { "const": "unordered_list" }, + "items": { + "type": "array", + "items": { "$ref": "#/$defs/InlineContent" }, + "minItems": 1 + } + } + }, + + "OrderedList": { + "type": "object", + "required": ["type", "items"], + "additionalProperties": false, + "properties": { + "type": { "const": "ordered_list" }, + "items": { + "type": "array", + "items": { "$ref": "#/$defs/InlineContent" }, + "minItems": 1 + } + } + }, + + "CodeBlock": { + "type": "object", + "required": ["type", "content"], + "additionalProperties": false, + "properties": { + "type": { "const": "code_block" }, + "syntax": { + "description": "Optional syntax identifier (HyperDoc §10.1.1 compatible).", + "type": ["string", "null"] + }, + "content": { + "description": "Raw text content of the code block. Line breaks are preserved.", + "type": "string" + } + } + }, + + "InlineContent": { + "description": "A sequence of inline spans forming a rich text run.", + "type": "array", + "items": { "$ref": "#/$defs/Inline" } + }, + + "Inline": { + "description": "A single inline element.", + "oneOf": [ + { "$ref": "#/$defs/Text" }, + { "$ref": "#/$defs/Code" }, + { "$ref": "#/$defs/Emphasis" }, + { "$ref": "#/$defs/Ref" }, + { "$ref": "#/$defs/Link" } + ] + }, + + "Text": { + "type": "object", + "required": ["type", "value"], + "additionalProperties": false, + "properties": { + "type": { "const": "text" }, + "value": { "type": "string" } + } + }, + + "Code": { + "description": "Inline monospace code span. Not validated as a reference.", + "type": "object", + "required": ["type", "value"], + "additionalProperties": false, + "properties": { + "type": { "const": "code" }, + "value": { "type": "string" } + } + }, + + "Emphasis": { + "type": "object", + "required": ["type", "content"], + "additionalProperties": false, + "properties": { + "type": { "const": "emphasis" }, + "content": { "$ref": "#/$defs/InlineContent" } + } + }, + + "Ref": { + "description": "A validated cross-reference to an IDL declaration. The fqn field always contains the fully-qualified resolved name, regardless of what the author wrote in the source.", + "type": "object", + "required": ["type", "fqn"], + "additionalProperties": false, + "properties": { + "type": { "const": "ref" }, + "fqn": { + "description": "The fully-qualified name of the referenced IDL declaration. Always stored in resolved form (e.g. 'resource.bind.target', never the shorthand 'target').", + "type": "string" + } + } + }, + + "Link": { + "description": "A hyperlink to an external resource.", + "type": "object", + "required": ["type", "url"], + "additionalProperties": false, + "properties": { + "type": { "const": "link" }, + "url": { "type": "string" }, + "content": { + "description": "Display text. If absent or empty, the URL is used as display text.", + "$ref": "#/$defs/InlineContent" + } + } + } + }, + + "$ref": "#/$defs/DocComment" +} +``` + +### 3.2 Type summary + +``` +DocComment + └─ sections: Section[] + +Section + ├─ kind: "main" | "note" | "warning" | "lore" | "example" + │ | "deprecated" | "decision" | "learn" + └─ blocks: Block[] (at least 1) + +Block = Paragraph | UnorderedList | OrderedList | CodeBlock + +Paragraph + └─ content: Inline[] + +UnorderedList + └─ items: Inline[][] (each item is one inline run) + +OrderedList + └─ items: Inline[][] + +CodeBlock + ├─ syntax: string? + └─ content: string (raw text, newlines preserved) + +Inline = Text | Code | Emphasis | Ref | Link + +Text { value: string } +Code { value: string } +Emphasis { content: Inline[] } +Ref { fqn: string } (always fully-qualified, resolved form) +Link { url: string, content?: Inline[] } +``` + +### 3.3 HyperDoc AST mapping + +For tooling that consumes HyperDoc document trees, the mapping is: + +| Doc comment type | HyperDoc element | +|---|---| +| Section(main) | sequence of child blocks (no wrapper) | +| Section(note) | `note { ... }` | +| Section(warning) | `warning { ... }` | +| Section(lore) | `lore { ... }` (extension) | +| Section(example) | `example { ... }` (extension) | +| Section(deprecated) | `deprecated { ... }` (extension) | +| Section(decision) | `decision { ... }` (extension) | +| Paragraph | `p { ... }` | +| UnorderedList | `ul { li { ... } li { ... } }` | +| OrderedList | `ol { li { ... } li { ... } }` | +| CodeBlock | `pre(syntax="...") : \| ...` | +| Text | bare inline text | +| Code | `\mono { ... }` | +| Emphasis | `\em { ... }` | +| Ref | `\ref(ref="...");` | +| Link | `\link(uri="...") { ... }` | + +--- + +## 4. Syntax + +### 4.1 Block structure + +After prefix stripping (§2.1), the raw doc text is parsed line-by-line into blocks. Blank lines are block separators. + +#### 4.1.1 Paragraphs + +Any sequence of non-blank lines that does not match another block rule forms a paragraph. Adjacent lines are joined with a single space (whitespace normalization). + +``` +/// The process handle to terminate. +/// Must be a valid, non-destroyed handle. +``` + +Produces one paragraph: `The process handle to terminate. Must be a valid, non-destroyed handle.` + +#### 4.1.2 Admonition sections + +A line matching the pattern `: ` where `` is one of the recognized admonition keywords starts a new section. The text after the colon (plus any continuation lines) forms the first block of that section. Continuation lines are either: + +- Indented by at least one space beyond the tag's colon position (aligned continuation), or +- Any non-blank, non-admonition, non-list line (unindented continuation — for compatibility with existing docs). + +Recognized tags (case-sensitive): + +| Tag | Section kind | +|---|---| +| `NOTE` | `note` | +| `WARNING` | `warning` | +| `LORE` | `lore` | +| `EXAMPLE` | `example` | +| `DEPRECATED` | `deprecated` | +| `DECISION` | `decision` | +| `LEARN` | `learn` | + +A new admonition tag or a blank line followed by different content ends the current section and starts a new one. + +``` +/// NOTE: This will *always* destroy the resource, even if it's +/// still strongly bound by a process. +``` + +Produces: `Section(note)` containing one paragraph. + +Multiple admonitions of the same kind are separate sections: + +``` +/// NOTE: First note. +/// +/// NOTE: Second note. +``` + +Produces two `Section(note)` values. + +All text before the first admonition tag belongs to `Section(main)`. If no admonition tags appear, the entire doc comment is a single `Section(main)`. + +#### 4.1.3 Unordered lists + +A line beginning with `- ` (hyphen + space) starts an unordered list item. Continuation lines must be indented by at least two spaces. + +``` +/// - Resources are created through various calls in the kernel API, +/// but their lifetime is managed through this namespace. +/// - After creation, a resource is strongly bound to the creator. +``` + +Produces: `UnorderedList` with two items. + +Adjacent list items form a single list. A blank line or non-list-item line ends the list. + +#### 4.1.4 Ordered lists + +A line beginning with `. ` (decimal number + dot + space) starts an ordered list item. Continuation lines must be indented past the number prefix. + +``` +/// 1. Allocate memory. +/// 2. Write the payload. +/// 3. Schedule the ARC. +``` + +#### 4.1.5 Fenced code blocks + +A line consisting of exactly ` ``` ` or ` ``` ` starts a fenced code block. The block continues until a closing ` ``` ` line. Lines between the fences are taken verbatim (no inline parsing, no whitespace normalization). + +``` +/// ```zig +/// const handle = try resource.open(path); +/// defer resource.close(handle); +/// ``` +``` + +Produces: `CodeBlock { syntax: "zig", content: "const handle = try resource.open(path);\ndefer resource.close(handle);" }` + +The syntax identifier, if present, follows HyperDoc §10.1.1 rules. + +### 4.2 Inline syntax + +Within paragraphs, list items, and admonition text, the following inline constructs are recognized. Inline parsing operates on the joined, whitespace-normalized text of a block. + +#### 4.2.1 Inline code: `` `...` `` + +A backtick-delimited span produces an inline `Code` node. The content between backticks is taken verbatim (no nested inline parsing). + +``` +/// The `destroy` syscall always succeeds. +``` + +Produces: `[Text("The "), Code("destroy"), Text(" syscall always succeeds.")]` + +Backtick spans must not be empty. An unmatched backtick is a parse error. + +#### 4.2.2 Cross-reference: `` @`...` `` + +An `@` character immediately followed by a backtick-delimited span produces an inline `Ref` node. The content between the backticks is interpreted as a fully-qualified name (FQN) and **must** resolve to a declaration in the IDL. + +``` +/// See @`overlapped.ARC` for the completion queue model. +``` + +Produces: `[Text("See "), Ref("overlapped.ARC"), Text(" for the completion queue model.")]` + +FQN resolution rules: + +Resolution walks from the innermost scope outward. Given a reference `@`name`` on a declaration with FQN `a.b.c`: + +1. **Self scope.** Look up `a.b.c.name` — i.e., the reference names a child of the declaration the doc comment is attached to. This is the most common case for syscall docs referencing their own parameters, struct docs referencing their own fields, enum docs referencing their own items, etc. +2. **Sibling scope.** Look up `a.b.name` — i.e., a sibling declaration in the same namespace. +3. **Parent scopes.** Walk outward: `a.name`, then `name` at global scope. +4. **Global exact match.** Look up `name` as a fully-qualified path (for when the author writes the full FQN explicitly). +5. If no match is found, emit a validation error. + +Steps 1–3 check the *unqualified* name against progressively broader scopes. Step 4 handles the case where `name` itself contains dots and is already fully qualified (e.g., `@`overlapped.ARC.tag``). + +If a name is ambiguous (matches at multiple scopes), the innermost match wins. Tooling **should** emit a warning for ambiguous references and suggest using the full FQN. + +**Examples:** + +```abi +/// Parameter @`foo` is used for stuff. +/// ↑ resolves to call.foo (self scope) +syscall call { + in foo: u32; +} +``` + +```abi +namespace overlapped { + /// See @`ARC` for the structure layout. + /// ↑ resolves to overlapped.ARC (sibling scope) + syscall schedule { + in @"arc": *ARC; + } +} +``` + +```abi +/// Uses the @`overlapped.ARC` completion model. +/// ↑ resolves as-is (global exact match) +namespace process { ... } +``` + +For references to sub-items (fields, enum items, error variants), dot notation works at any scope level: `@`ARC.tag`` inside `namespace overlapped` resolves to `overlapped.ARC.tag` via sibling scope + child traversal. + +A bare `@` not immediately followed by a backtick has no special meaning and is treated as literal text. + +#### 4.2.3 Emphasis: `*...*` + +An asterisk-delimited span produces an inline `Emphasis` node. The content between asterisks is parsed for nested inline constructs (code, references, links — but not nested emphasis). + +``` +/// This will *always* destroy the resource. +``` + +Produces: `[Text("This will "), Emphasis([Text("always")]), Text(" destroy the resource.")]` + +Emphasis spans must not be empty. Opening `*` must be preceded by whitespace or start-of-text, and closing `*` must be followed by whitespace, punctuation, or end-of-text. This prevents false matches on expressions like `a*b*c`. + +#### 4.2.4 Links: `[text](url)` and `` + +**Titled link:** A `[` character starts a link's display text, closed by `]`, immediately followed by `(url)`. The display text is parsed for nested inline constructs (code, emphasis, references). The URL is taken verbatim. + +``` +/// See [the RISC-V specification](https://riscv.org/specifications/) for details. +``` + +Produces: `Link { url: "https://riscv.org/specifications/", content: [Text("the RISC-V specification")] }` + +**Autolink:** A `<` character followed by a URL scheme (`http://`, `https://`, or `mailto:`) and closed by `>` produces a link where the display text is the URL itself. + +``` +/// More information at . +``` + +Produces: `Link { url: "https://ashet.org/docs/abi", content: [Text("https://ashet.org/docs/abi")] }` + +A `<` not followed by a recognized URL scheme is treated as literal text. This avoids ambiguity with angle brackets in prose (e.g., ``). + +### 4.3 Escape sequences + +To use the special characters literally in inline text: + +| Sequence | Produces | +|---|---| +| `` \` `` | literal `` ` `` | +| `\*` | literal `*` | +| `\[` | literal `[` | +| `\<` | literal `<` | +| `\@` | literal `@` | +| `\\` | literal `\` | + +Escapes are only recognized in inline contexts. They are not recognized inside code spans (`` `...` ``), code blocks, or URLs. + +--- + +## 5. Parsing algorithm (non-normative) + +This section describes the intended parsing strategy. It is non-normative but should produce results identical to the normative syntax rules. + +### 5.1 Block pass (line-oriented) + +``` +input: list of stripped doc lines +output: list of (SectionKind, Block) + +state: + current_section = main + current_block = null + in_code_fence = false + +for each line: + if in_code_fence: + if line == "```": + emit CodeBlock, in_code_fence = false + else: + append line to code block buffer + continue + + if line starts with "```": + flush current_block + extract optional syntax tag + in_code_fence = true + continue + + if line is blank: + flush current_block + continue + + if line matches /^(NOTE|WARNING|LORE|EXAMPLE|DEPRECATED|DECISION):\s+(.*)/: + flush current_block + current_section = matched tag + start new paragraph with captured text + continue + + if line matches /^- (.*)/: + if current_block is not unordered_list: flush, start new list + append new list item with captured text + continue + + if line matches /^(\d+)\. (.*)/: + if current_block is not ordered_list: flush, start new list + append new list item with captured text + continue + + if current_block is list and line starts with sufficient indentation: + append to current list item (continuation) + continue + + if current_block is paragraph: + append line to paragraph (continuation) + else: + flush current_block + start new paragraph with line + +flush current_block +``` + +### 5.2 Inline pass (character-oriented) + +For each paragraph or list item text, scan left-to-right: + +``` +while not end-of-text: + if char == '\\' and next is escapable: + emit Text(next), advance 2 + elif char == '`': + scan to closing '`' + emit Code(content) + elif char == '@' and next == '`': + advance past '@' + scan to closing '`' + emit Ref(content) // validated later + elif char == '*': + if valid emphasis open (preceded by whitespace/start): + scan to closing '*' (with valid close context) + recursively parse content for nested inlines + emit Emphasis(parsed_content) + else: + emit Text('*') + elif char == '[': + scan to '](' + parse display text for nested inlines + scan to closing ')' + emit Link(url, parsed_display) + elif char == '<' and followed by url scheme: + scan to '>' + emit Link(url, [Text(url)]) + else: + accumulate into Text span +``` + +### 5.3 Validation pass + +After block and inline parsing: + +1. **FQN resolution:** For every `Ref` node, resolve the FQN against the IDL symbol table using the scoped resolution rules (§4.2.2). The resolution requires knowing the FQN of the declaration the doc comment is attached to. Emit an error for unresolvable references; emit a warning for ambiguous references. +2. **Empty section check:** Sections with zero blocks are invalid (parser bug, not user error). +3. **Unclosed constructs:** Unmatched backticks, asterisks, brackets, or code fences are parse errors. +4. **TODO rejection:** If any section's text starts with `TODO:` (or a line comment `//? TODO:` was mistakenly written as `/// TODO:`), emit a warning. TODOs are development artifacts; use `//? TODO:` instead. + +--- + +## 6. Examples + +### 6.1 Simple field and parameter documentation + +Source: + +```abi +/// The process handle to query. +/// If `null`, uses the current process. +in target: ?Process; +``` + +JSON output: + +```json +{ + "sections": [{ + "kind": "main", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "The process handle to query. If " }, + { "type": "code", "value": "null" }, + { "type": "text", "value": ", uses the current process." } + ] + }] + }] +} +``` + +Self-scope reference example — a syscall doc referencing its own parameter: + +```abi +/// Binds @`resource` to a process. +/// +/// The success of this operation allows @`target` to access +/// @`resource`, and optionally gain/lose a strong binding. +syscall bind { + in resource: SystemResource; + in target: ?Process; + in binding: BindOperation; +} +``` + +Here `@`resource`` resolves to `resource.bind.resource` (self-scope), `@`target`` to `resource.bind.target`. + +### 6.2 Multiple notes + +Source: + +```abi +/// Immediately destroys the resource and releases its memory. +/// +/// NOTE: This will *always* destroy the resource, even if it's +/// still strongly bound by a process. +/// +/// NOTE: This immediately triggers tether chains and destroys +/// all tethered resources as well. +/// +/// NOTE: @`resource.destroy` always succeeds; destroying an invalid +/// or already-destroyed handle is a no-op. +``` + +JSON output: + +```json +{ + "sections": [ + { + "kind": "main", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "Immediately destroys the resource and releases its memory." } + ] + }] + }, + { + "kind": "note", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "This will " }, + { "type": "emphasis", "content": [ + { "type": "text", "value": "always" } + ]}, + { "type": "text", "value": " destroy the resource, even if it's still strongly bound by a process." } + ] + }] + }, + { + "kind": "note", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "This immediately triggers tether chains and destroys all tethered resources as well." } + ] + }] + }, + { + "kind": "note", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "ref", "fqn": "resource.destroy" }, + { "type": "text", "value": " always succeeds; destroying an invalid or already-destroyed handle is a no-op." } + ] + }] + } + ] +} +``` + +### 6.3 LORE section with list and cross-references + +Source: + +```abi +/// All syscalls related to generic resource management. +/// +/// - Resources are created through various calls in the kernel API, but their +/// lifetime is managed through calls inside this namespace. +/// - After creation, a resource is strongly bound to the process that created it. +/// - When a resource is destroyed, it becomes unusable from userland. +/// +/// NOTE: Every kernel object the userland can interact with is a @`SystemResource`. +/// +/// LORE: Originally, Ashet OS had no concept of bindings, but only of ownership. +/// But this quickly led to problems like "the desktop server also owns the +/// window, so even if the application releases the window, it is not destroyed." +/// The idea of allowing a process to access a resource without keeping it alive +/// solves this problem completely. See @`resource.bind` for the binding API. +``` + +JSON output: + +```json +{ + "sections": [ + { + "kind": "main", + "blocks": [ + { + "type": "paragraph", + "content": [ + { "type": "text", "value": "All syscalls related to generic resource management." } + ] + }, + { + "type": "unordered_list", + "items": [ + [ + { "type": "text", "value": "Resources are created through various calls in the kernel API, but their lifetime is managed through calls inside this namespace." } + ], + [ + { "type": "text", "value": "After creation, a resource is strongly bound to the process that created it." } + ], + [ + { "type": "text", "value": "When a resource is destroyed, it becomes unusable from userland." } + ] + ] + } + ] + }, + { + "kind": "note", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "Every kernel object the userland can interact with is a " }, + { "type": "ref", "fqn": "SystemResource" }, + { "type": "text", "value": "." } + ] + }] + }, + { + "kind": "lore", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "Originally, Ashet OS had no concept of bindings, but only of ownership. But this quickly led to problems like \"the desktop server also owns the window, so even if the application releases the window, it is not destroyed.\" The idea of allowing a process to access a resource without keeping it alive solves this problem completely. See " }, + { "type": "ref", "fqn": "resource.bind" }, + { "type": "text", "value": " for the binding API." } + ] + }] + } + ] +} +``` + +### 6.4 DECISION and EXAMPLE admonitions + +Source: + +```abi +/// Defines the process exit status. +/// +/// DECISION: Unlike POSIX, Ashet OS uses a single boolean for success/failure +/// rather than an integer exit code. Integer codes are overloaded in +/// practice (is 2 "worse" than 1? is 0 always success?) and the +/// meaningful information is carried by log output, not codes. +/// +/// EXAMPLE: A well-behaved application terminates with: +/// +/// ```zig +/// process.terminate(.success); +/// ``` +``` + +JSON output: + +```json +{ + "sections": [ + { + "kind": "main", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "Defines the process exit status." } + ] + }] + }, + { + "kind": "decision", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "Unlike POSIX, Ashet OS uses a single boolean for success/failure rather than an integer exit code. Integer codes are overloaded in practice (is 2 \"worse\" than 1? is 0 always success?) and the meaningful information is carried by log output, not codes." } + ] + }] + }, + { + "kind": "example", + "blocks": [ + { + "type": "paragraph", + "content": [ + { "type": "text", "value": "A well-behaved application terminates with:" } + ] + }, + { + "type": "code_block", + "syntax": "zig", + "content": "process.terminate(.success);" + } + ] + } + ] +} +``` + +### 6.5 Links + +Source: + +```abi +/// The timezone database follows the IANA format. +/// See [the IANA tz database](https://www.iana.org/time-zones) for details, +/// or the mirror at . +``` + +JSON output: + +```json +{ + "sections": [{ + "kind": "main", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "The timezone database follows the IANA format. See " }, + { "type": "link", + "url": "https://www.iana.org/time-zones", + "content": [ + { "type": "text", "value": "the IANA tz database" } + ] + }, + { "type": "text", "value": " for details, or the mirror at " }, + { "type": "link", + "url": "https://github.com/eggert/tz", + "content": [ + { "type": "text", "value": "https://github.com/eggert/tz" } + ] + }, + { "type": "text", "value": "." } + ] + }] + }] +} +``` + +### 6.6 Minimal field doc (the common case) + +Source: + +```abi +/// The time is adjusted to the first possible past point in time. +``` + +JSON output: + +```json +{ + "sections": [{ + "kind": "main", + "blocks": [{ + "type": "paragraph", + "content": [ + { "type": "text", "value": "The time is adjusted to the first possible past point in time." } + ] + }] + }] +} +``` + +--- + +## 7. Grammar (EBNF) + +This grammar describes the surface syntax after `///` prefix stripping. + +```ebnf +(* Block level — line oriented *) + +doc_comment = { blank_line } , { section } ; + +section = [ admonition_start ] , block , { blank_line , block } ; + +admonition_start = tag , ":" , ws , text_line ; +tag = "NOTE" | "WARNING" | "LORE" | "EXAMPLE" + | "DEPRECATED" | "DECISION" | "LEARN"; + +block = code_block | unordered_list | ordered_list | paragraph ; + +code_block = "```" , [ syntax_id ] , newline , + { code_line , newline } , + "```" , newline ; +syntax_id = ident_char , { ident_char | "-" | "." | ":" } ; +code_line = { any_char_except_newline } ; + +unordered_list = ul_item , { ul_item } ; +ul_item = "- " , inline_text , newline , + { " " , continuation_text , newline } ; + +ordered_list = ol_item , { ol_item } ; +ol_item = digit , { digit } , ". " , inline_text , newline , + { " " , continuation_text , newline } ; + +paragraph = text_line , { continuation_text , newline } ; + +text_line = inline_text , newline ; +continuation_text = inline_text ; + +blank_line = newline ; + +(* Inline level — character oriented *) + +inline_text = { inline_item } ; + +inline_item = ref | code_span | emphasis | titled_link + | autolink | escape | plain_text ; + +ref = "@" , "`" , fqn_chars , "`" ; +code_span = "`" , code_chars , "`" ; +emphasis = "*" , inline_text , "*" ; (* see §4.2.3 for open/close rules *) +titled_link = "[" , inline_text , "]" , "(" , url_chars , ")" ; +autolink = "<" , url_scheme , url_chars , ">" ; +escape = "\\" , escapable_char ; + +fqn_chars = fqn_char , { fqn_char } ; +fqn_char = letter | digit | "_" | "." | "@" ; (* @"..." for escaped IDL names *) +code_chars = { any_char_except_backtick }- ; (* at least one character *) +url_scheme = "http://" | "https://" | "mailto:" ; +url_chars = { any_char_except_closing }- ; +escapable_char = "`" | "*" | "[" | "<" | "@" | "\\" ; +plain_text = { text_char }- ; +text_char = ? any character not starting another inline construct ? ; +``` + +--- + +## 8. Diagnostics + +The parser **must** emit diagnostics for the following conditions: + +| Condition | Severity | +|---|---| +| Unresolvable `@\`fqn\`` | Error | +| Ambiguous `@\`fqn\`` (matches at multiple scopes) | Warning | +| Unclosed backtick span | Error | +| Unclosed emphasis span | Error | +| Unclosed code fence | Error | +| Unclosed `[text](url)` link | Error | +| Empty code span (``` `` ```) | Error | +| Empty emphasis span (`**`) | Error | +| `TODO:` used as admonition tag | Warning | +| Trailing whitespace on doc lines | Warning (non-fatal) | +| Section with no blocks (parser bug) | Error | + +--- + +## 9. Migration guide + +The following table summarizes the changes needed to migrate existing doc comments: + +| Current pattern | New pattern | Count (est.) | +|---|---|---| +| `` `fqn` `` where fqn is a real declaration | `` @`fqn` `` | ~50-100 | +| `NOTE:` | `NOTE:` (unchanged) | 454 | +| `LORE:` | `LORE:` (unchanged) | 30 | +| `EXAMPLE:` | `EXAMPLE:` (unchanged) | 7 | +| `*emphasis*` | `*emphasis*` (unchanged) | ~10 | +| `` `code` `` (non-referencing) | `` `code` `` (unchanged) | ~500 | +| `- list item` | `- list item` (unchanged) | ~20 | +| `//? TODO:` (line comments) | `//? TODO:` (unchanged) | 67 | +| `/// TODO:` (in doc comments) | Move to `//? TODO:` | 1 | + +**Effective migration:** Add `@` prefix to backtick spans that reference IDL declarations. Everything else stays the same. diff --git a/src/tools/abi-mapper/rework/findings.md b/src/tools/abi-mapper/rework/findings.md new file mode 100644 index 00000000..4c502990 --- /dev/null +++ b/src/tools/abi-mapper/rework/findings.md @@ -0,0 +1,213 @@ +# ABI Mapper Findings — ashet-1.0.abi Stress Test + +--- + +## Finding 1: Underscore digit separators in integer literals + +**File:** `tests/stress/ashet-1.0.abi:1300` + +**Code:** +``` +item infinity = 0xFFFF_FFFF_FFFF_FFFF; +``` + +**Problem:** The lexer does not support `_` as a digit separator in integer literals. +`0xFFFF` is tokenized as a number, then `_FFFF_FFFF_FFFF` is seen as an identifier, +causing an unexpected token error. + +**Accepted solution:** Add `_` digit-separator support to the lexer. Underscores +are silently skipped within numeric literals (both decimal and hex), matching +Zig/Rust conventions. + +**Workaround applied:** Removed underscores → `0xFFFFFFFFFFFFFFFF`. + +--- + +## Finding 2: Named parameters in `fnptr` types not supported + +**File:** `tests/stress/ashet-1.0.abi:8308`, `8323`, `8339` + +**Code:** +``` +typedef AsyncHandler = fnptr(context: ?*anyopaque, request: RequestToken, operation: u8, arguments: *const [8]usize) void; +typedef CancelHandler = fnptr(context: ?*anyopaque, request: RequestToken) void; +typedef Function = fnptr(context: ?*anyopaque, arguments: *const [8]usize) usize; +``` + +**Problem:** The parser expects `fnptr` parameter lists to contain only types; +`name: Type` syntax causes an unexpected `:` token error. + +**Accepted solution:** Extend the `fnptr` parser to accept optional `name:` prefixes +on each parameter. Names are parsed and stored in the model so that code generators +targeting languages that require parameter names (e.g. Zig, C with named args) can +replicate them faithfully. + +**Workaround applied:** Removed parameter names, leaving bare types. + +--- + +## Finding 3: Constant used as array size before symbol resolution + +**File:** `tests/stress/ashet-1.0.abi:5948`, `5955` + +**Code:** +``` +const max_fs_name_len = 8; +... +field name: [max_fs_name_len]u8; +``` + +**Problem:** sema panics with "symbol resolution not done yet" when a named constant +is used as an array size in a struct field. Symbol resolution and type mapping happen +in the same pass; the constant may not yet be resolved when its referencing field type +is processed. A full fix would require multi-pass resolution or lazy evaluation, which +can produce complex dependency chains. + +**Accepted solution:** Require constants to be lexically defined before use. If +`resolve_value` is called on a constant that has not yet been assigned a value, emit +a proper error: +``` +error: constant 'max_fs_name_len' must be defined before it is used here +``` +This is a simple rule with low implementation cost. It is acceptable author load when +writing `.abi` files: constant declarations naturally belong near the top of the +namespace they apply to, before any types that reference them. + +**Workaround applied:** Replaced constant references with literal values. + +--- + +## Finding 4: Non-standard integer widths not supported as enum/bitstruct backing types + +**File:** `tests/stress/ashet-1.0.abi:6132`, `8225` + +**Code:** +``` +enum FileType : u2 { ... } +enum MarshalType: u2 { ... } +``` + +**Problem:** `map_decl` requires the backing type (subtype) of an `enum` or `bitstruct` +to be a `model.StandardType` (`u8`, `u16`, `u32`, `u64`, `usize`, …). Non-power-of-8 +widths like `u2` map to `model.Type.uint` instead and are rejected, causing cascading +failures up through the parent namespace. + +**Secondary bug (from cascading failure):** When `map_node` fails for a top-level +node, `map()` does `continue` leaving the pre-allocated `root.items` slot as undefined +memory. The subsequent `resolve_namespace_doc_comment_refs` pass reads garbage pointers +from those slots, crashing with a General Protection Fault. Fix: collect successful +results into a fresh list rather than pre-sizing with `resize`. + +**Accepted solution:** +- Accept any `uint`/`int` type as the backing type of an `enum` or `bitstruct`. +- Add a `bit_count: u8` field to `model.Enumeration` (matching the existing field on + `model.BitStruct`), storing the original declared bit width (e.g. 2 for `u2`). +- The ABI-surface standard type is rounded up to the next power-of-two byte width + (`u2`→`u8`, `u3`/`u4`→`u8`, `u9`…`u16`→`u16`, etc.) and stored as the + `backing_type: StandardType`. +- This lets code generators use the standard type for languages that don't support + arbitrary-width integers, while preserving `bit_count` for precise packing in + bitstructs and for languages that do support sub-byte types. + +**Workaround applied:** Changed backing type to the smallest standard width → `u8`. +Finding 5 (bitstruct Flags overflow) is a cascading consequence and will be +automatically resolved when this finding is implemented. + +--- + +## Finding 5: `bitstruct Flags : u16` exceeds 16 bits (cascading from Finding 4 workaround) + +**File:** `tests/stress/ashet-1.0.abi:6137` + +**Note:** Cascading consequence of the Finding 4 workaround — `FileType`'s backing +type was widened from `u2` to `u8`, adding 6 extra bits to the bitstruct. This will +be automatically resolved when Finding 4 is properly implemented: `FileType.bit_count` +will be 2, so the bitstruct field contributes 2 bits and fits within `u16` again. + +**Workaround applied:** Changed `reserve u12 = 0` to `reserve u6 = 0`. + +--- + +## Finding 6: `compute_native_params` silently drops unsupported optional types + +**File:** `src/sema.zig:596`, `sema.zig:620` + +**Problem:** In `compute_native_params`, the `.optional` branch handles only +`?*T`/`?[*]T`, `?resource`, `?anyptr`, `?anyfnptr`, and the string pseudo-types. +All other optional types (`?fnptr`, `?enum`, `?struct`, `?u8`, `?u32`, `?bool`, +etc.) fall into the `else` branch which calls `std.log.err` and **does not append +the parameter**, silently producing an incorrect native call signature. + +**Accepted solution:** +- `?fnptr` is a valid C-ABI optional: a function pointer is nullable in C, so it + should be kept as-is (added directly to the native params list). +- All other unsupported optional types (`?enum`, `?struct`, `?u8`, `?u32`, `?bool`, + etc.) should emit a proper `emit_error` diagnostic instead of the silent `std.log.err`. + +--- + +## Finding 7: `unknown_named_type` in `compute_native_fields` — unreachable was semantically correct + +**File:** `src/sema.zig:864` + +**Problem:** `resolve_named_types` emits a non-fatal error when it cannot resolve a +type reference, but leaves the type slot as `.unknown_named_type`. When +`compute_native_fields` later encounters this it hits `unreachable`, panicking. + +**Accepted solution:** The `unreachable` was semantically correct: if +`unknown_named_type` is encountered here, an error diagnostic must already have been +emitted by `resolve_named_types`. Change it to a silent `continue` (skip the field) +rather than `unreachable`. Add a test that verifies the invariant — that whenever +`unknown_named_type` is reached in this code path, `ana.errors` is non-empty — so +the silent skip cannot silently hide a real bug. + +--- + +## Finding 8: Array fields in `bitstruct` not supported + +**File:** `tests/stress/ashet-1.0.abi:8270` + +**Code:** +``` +bitstruct FunctionSignature : u32 { + field inputs: [8]MarshalType; + field outputs: [8]MarshalType; +} +``` + +**Problem:** `get_type_bit_size` returns `null` for array types, so array fields +cannot appear inside a `bitstruct`. The intent is to pack 8 × 2-bit `MarshalType` +values into 16 bits per field (32 bits total). + +**Accepted solution:** Allow arrays of bit-packable element types inside bitstructs. +The bit contribution of `[N]T` is `N × bit_size(T)`. The model stores the array as a +bitstruct field. Code generators that cannot express sub-byte arrays must unroll the +field into N individual fields or emit appropriate macros/accessors. + +**Workaround applied:** Changed `bitstruct` to `struct` (loses the packing semantics). + +--- + +## Finding 9: Syscall with 2+ logic outputs surfaces an incorrect assertion in `validate_constraints` + +**File:** `src/sema.zig:2081`, trigger at `tests/stress/ashet-1.0.abi:2187` + +**Code:** +```zig +std.debug.assert(sc.logic_outputs.len <= sc.native_outputs.len); +``` + +**Problem:** The assertion IS correct as a design constraint: a syscall in the C ABI +can produce at most one return value, so having 2+ logic outputs is invalid. However, +`map_any_call` does not check this early — it accepts any number of `out` parameters +for syscalls. `validate_constraints` then fires the assertion as the first enforcement +point, turning a user error into a crash rather than a diagnostic. + +**Accepted solution:** Add an explicit check in `map_any_call` (or `map_syscall`) +that emits a proper `emit_error` / `fatal_error` when a syscall declaration contains +more than one `out` parameter. This surfaces the constraint early with a good error +message, making `validate_constraints` a true internal sanity check rather than the +first line of defence. + +**Workaround applied:** Merged the two outputs (`name_len`, `unique_id_len`) into a +single `struct DeviceMetadataLengths` output. diff --git a/src/tools/abi-mapper/src/abi-parser.zig b/src/tools/abi-mapper/src/abi-parser.zig index 057dd28c..dc15c391 100644 --- a/src/tools/abi-mapper/src/abi-parser.zig +++ b/src/tools/abi-mapper/src/abi-parser.zig @@ -4,9 +4,11 @@ const args_parser = @import("args"); pub const syntax = @import("syntax.zig"); pub const model = @import("model.zig"); pub const sema = @import("sema.zig"); +pub const doc_comment = @import("doc_comment.zig"); const CliOptions = struct { output: []const u8 = "", + @"id-db": []const u8 = "", }; pub fn main() !u8 { @@ -19,10 +21,14 @@ pub fn main() !u8 { defer args.deinit(); if (args.positionals.len != 1) { + std.debug.print("expects exactly one positional argument, found {}\n", .{args.positionals.len}); + std.debug.print("usage: abi-mapper [--id-db ] --output \n", .{}); return 1; } - if (args.options.output.len == 1) { + if (args.options.output.len == 0) { + std.debug.print("missing argument: --output \n", .{}); + std.debug.print("usage: abi-mapper [--id-db ] --output \n", .{}); return 1; } @@ -50,7 +56,33 @@ pub fn main() !u8 { return err; }; - const analyzed_document: model.Document = try sema.analyze(allocator, ast_document); + // Load UID database if --id-db was specified + const id_db_path = args.options.@"id-db"; + var uid_database: ?sema.uid_db.UidDatabase = if (id_db_path.len > 0) + try sema.uid_db.UidDatabase.load(allocator, id_db_path) + else + null; + defer if (uid_database) |*db| db.deinit(); + + var analysis_errors: std.ArrayList(sema.AnalysisError) = .empty; + defer analysis_errors.deinit(allocator); + + const analyzed_document: model.Document = sema.analyze( + allocator, + ast_document, + if (uid_database != null) &uid_database.? else null, + &analysis_errors, + ) catch |err| { + for (analysis_errors.items) |ae| { + std.log.err("{s}", .{ae.message}); + } + return err; + }; + + // Save UID database back if it was loaded + if (uid_database != null and id_db_path.len > 0) { + try uid_database.?.save(id_db_path); + } var atomic_buffer: [4096]u8 = undefined; var atomic_output = try std.fs.cwd().atomicFile( diff --git a/src/tools/abi-mapper/src/doc_comment.zig b/src/tools/abi-mapper/src/doc_comment.zig new file mode 100644 index 00000000..c47c1d46 --- /dev/null +++ b/src/tools/abi-mapper/src/doc_comment.zig @@ -0,0 +1,511 @@ +const std = @import("std"); +const model = @import("model.zig"); + +const DocComment = model.DocComment; + +pub const RefLookupFn = *const fn ( + context: ?*anyopaque, + allocator: std.mem.Allocator, + local_qn: []const u8, +) error{OutOfMemory}!?[]const u8; + +pub const ParseOptions = struct { + ref_lookup: ?RefLookupFn = null, + ref_lookup_context: ?*anyopaque = null, +}; + +pub const ParseError = error{ + UnclosedCodeFence, + UnclosedInlineReference, + UnclosedInlineCode, + UnclosedInlineLink, + MalformedInlineLink, + UnclosedAutolink, +}; + +pub fn describe_parse_error(err: ParseError) []const u8 { + return switch (err) { + error.UnclosedCodeFence => "unclosed fenced code block", + error.UnclosedInlineReference => "unclosed inline reference", + error.UnclosedInlineCode => "unclosed inline code span", + error.UnclosedInlineLink => "unclosed inline link", + error.MalformedInlineLink => "malformed inline link", + error.UnclosedAutolink => "unclosed autolink", + }; +} + +/// A parsed doc comment together with the arena that owns its memory. +/// Call deinit() when the DocComment is no longer needed. +pub const ParsedDocComment = struct { + arena: std.heap.ArenaAllocator, + comment: DocComment, + + pub fn deinit(self: *ParsedDocComment) void { + self.arena.deinit(); + } +}; + +/// Parses raw doc comment lines into a freshly allocated arena. +/// The caller owns the result and must call deinit() to release memory. +/// +/// Each raw line should be `token.text[3..]` where token.text starts with `///`. +pub fn parse(backing_allocator: std.mem.Allocator, raw_lines: []const []const u8, options: ParseOptions) (ParseError || error{OutOfMemory})!ParsedDocComment { + var result: ParsedDocComment = .{ + .arena = std.heap.ArenaAllocator.init(backing_allocator), + .comment = undefined, + }; + errdefer result.arena.deinit(); + result.comment = try parse_into_arena(&result.arena, raw_lines, options); + return result; +} + +/// Parses raw doc comment lines into an existing arena. +/// The caller owns the arena and is responsible for its lifetime. +/// +/// Each raw line should be `token.text[3..]` where token.text starts with `///`. +pub fn parse_into_arena(arena: *std.heap.ArenaAllocator, raw_lines: []const []const u8, options: ParseOptions) (ParseError || error{OutOfMemory})!DocComment { + if (raw_lines.len == 0) return .empty; + + var ctx: ParseContext = .{ + .allocator = arena.allocator(), + .ref_lookup = options.ref_lookup, + .ref_lookup_context = options.ref_lookup_context, + }; + return ctx.parse_doc(raw_lines); +} + +const AccKind = enum { none, paragraph, unordered_list, ordered_list }; + +const ParseContext = struct { + allocator: std.mem.Allocator, + ref_lookup: ?RefLookupFn, + ref_lookup_context: ?*anyopaque, + + fn parse_doc(ctx: *ParseContext, raw_lines: []const []const u8) (ParseError || error{OutOfMemory})!DocComment { + // Normalize lines: strip one optional leading space (the /// separator), right-trim. + var norm_lines: std.ArrayList([]const u8) = .empty; + defer norm_lines.deinit(ctx.allocator); + + for (raw_lines) |raw| { + const stripped = if (raw.len > 0 and raw[0] == ' ') raw[1..] else raw; + try norm_lines.append(ctx.allocator, std.mem.trimRight(u8, stripped, " \t")); + } + const lines = norm_lines.items; + + var sections: std.ArrayList(DocComment.Section) = .empty; + defer sections.deinit(ctx.allocator); + + var current_kind: DocComment.Section.Kind = .main; + var blocks: std.ArrayList(DocComment.Block) = .empty; + defer blocks.deinit(ctx.allocator); + + // Current paragraph lines accumulator + var para_lines: std.ArrayList([]const u8) = .empty; + defer para_lines.deinit(ctx.allocator); + + // Current list items (each item is a list of lines) + var list_items: std.ArrayList(std.ArrayList([]const u8)) = .empty; + defer { + for (list_items.items) |*item| item.deinit(ctx.allocator); + list_items.deinit(ctx.allocator); + } + + var acc_kind: AccKind = .none; + + // Code fence state + var in_fence = false; + var fence_syntax: ?[]const u8 = null; + var fence_lines: std.ArrayList([]const u8) = .empty; + defer fence_lines.deinit(ctx.allocator); + + for (lines) |line| { + if (in_fence) { + if (std.mem.eql(u8, line, "```")) { + const content = try std.mem.join(ctx.allocator, "\n", fence_lines.items); + try blocks.append(ctx.allocator, .{ .code_block = .{ .syntax = fence_syntax, .content = content } }); + in_fence = false; + fence_syntax = null; + fence_lines.clearRetainingCapacity(); + } else { + try fence_lines.append(ctx.allocator, line); + } + continue; + } + + // Code fence start + if (std.mem.startsWith(u8, line, "```")) { + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); + const syn = std.mem.trim(u8, line[3..], " "); + fence_syntax = if (syn.len > 0) try ctx.allocator.dupe(u8, syn) else null; + in_fence = true; + continue; + } + + // Blank line: flush current block + if (line.len == 0) { + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); + continue; + } + + // Admonition tag: NOTE:, WARNING:, LORE:, EXAMPLE:, DEPRECATED:, DECISION: + if (parse_admonition(line)) |adm| { + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); + // Emit current section if it has content + if (blocks.items.len > 0) { + try sections.append(ctx.allocator, .{ .kind = current_kind, .blocks = try blocks.toOwnedSlice(ctx.allocator) }); + } + current_kind = adm.kind; + acc_kind = .paragraph; + if (adm.text.len > 0) { + try para_lines.append(ctx.allocator, adm.text); + } + continue; + } + + // Unordered list item: starts with "- " + if (std.mem.startsWith(u8, line, "- ")) { + if (acc_kind != .unordered_list) { + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); + acc_kind = .unordered_list; + } + var new_item: std.ArrayList([]const u8) = .empty; + try new_item.append(ctx.allocator, line[2..]); + try list_items.append(ctx.allocator, new_item); + continue; + } + + // Ordered list item: starts with "N. " + if (parse_ordered_item(line)) |text| { + if (acc_kind != .ordered_list) { + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); + acc_kind = .ordered_list; + } + var new_item: std.ArrayList([]const u8) = .empty; + try new_item.append(ctx.allocator, text); + try list_items.append(ctx.allocator, new_item); + continue; + } + + // Continuation of current list item (indented by 2+ spaces) + if ((acc_kind == .unordered_list or acc_kind == .ordered_list) and + list_items.items.len > 0 and + std.mem.startsWith(u8, line, " ")) + { + const cont = std.mem.trimLeft(u8, line, " "); + try list_items.items[list_items.items.len - 1].append(ctx.allocator, cont); + continue; + } + + // Paragraph (default): accumulate lines, trimming leading whitespace + // so that admonition continuation indentation is normalized. + if (acc_kind != .paragraph) { + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); + acc_kind = .paragraph; + } + try para_lines.append(ctx.allocator, std.mem.trimLeft(u8, line, " \t")); + } + + if (in_fence) { + return error.UnclosedCodeFence; + } + + // Flush whatever remains + try ctx.flush_acc(&blocks, &acc_kind, ¶_lines, &list_items); + + // Emit the final section + if (blocks.items.len > 0) { + try sections.append(ctx.allocator, .{ .kind = current_kind, .blocks = try blocks.toOwnedSlice(ctx.allocator) }); + } + + if (sections.items.len == 0) return .empty; + + return .{ .sections = try sections.toOwnedSlice(ctx.allocator) }; + } + + fn flush_acc( + ctx: *ParseContext, + blocks: *std.ArrayList(DocComment.Block), + acc_kind: *AccKind, + para_lines: *std.ArrayList([]const u8), + list_items: *std.ArrayList(std.ArrayList([]const u8)), + ) (ParseError || error{OutOfMemory})!void { + switch (acc_kind.*) { + .none => {}, + .paragraph => { + if (para_lines.items.len > 0) { + const text = try std.mem.join(ctx.allocator, " ", para_lines.items); + const content = try ctx.parse_inline(text); + try blocks.append(ctx.allocator, .{ .paragraph = .{ .content = content } }); + para_lines.clearRetainingCapacity(); + } + }, + .unordered_list, .ordered_list => { + const items = try ctx.allocator.alloc([]const DocComment.Inline, list_items.items.len); + for (list_items.items, 0..) |item_lines, j| { + const text = try std.mem.join(ctx.allocator, " ", item_lines.items); + items[j] = try ctx.parse_inline(text); + } + for (list_items.items) |*item| item.deinit(ctx.allocator); + list_items.clearRetainingCapacity(); + if (acc_kind.* == .unordered_list) { + try blocks.append(ctx.allocator, .{ .unordered_list = .{ .items = items } }); + } else { + try blocks.append(ctx.allocator, .{ .ordered_list = .{ .items = items } }); + } + }, + } + acc_kind.* = .none; + } + + fn is_escapable_inline_char(c: u8) bool { + return switch (c) { + '`', '*', '[', '<', '@', '\\' => true, + else => false, + }; + } + + fn find_inline_code_end(_: *ParseContext, text: []const u8, code_start: usize) ?usize { + var i = code_start; + while (i < text.len) { + if (text[i] == '\\' and i + 1 < text.len and is_escapable_inline_char(text[i + 1])) { + if (text[i + 1] == '`') { + if (i + 2 < text.len and text[i + 2] == '`') { + i += 2; + continue; + } + return i + 1; + } + i += 2; + continue; + } + if (text[i] == '`') { + return i; + } + i += 1; + } + return null; + } + + fn unescape_inline_text(ctx: *ParseContext, text: []const u8) error{OutOfMemory}![]const u8 { + var i: usize = 0; + while (i + 1 < text.len) : (i += 1) { + if (text[i] == '\\' and is_escapable_inline_char(text[i + 1])) break; + } + if (i + 1 >= text.len) { + return text; + } + + var unescaped: std.ArrayList(u8) = .empty; + defer unescaped.deinit(ctx.allocator); + + var text_start: usize = 0; + i = 0; + while (i < text.len) { + if (i + 1 < text.len and text[i] == '\\' and is_escapable_inline_char(text[i + 1])) { + try unescaped.appendSlice(ctx.allocator, text[text_start..i]); + try unescaped.append(ctx.allocator, text[i + 1]); + i += 2; + text_start = i; + continue; + } + i += 1; + } + + try unescaped.appendSlice(ctx.allocator, text[text_start..]); + return unescaped.toOwnedSlice(ctx.allocator); + } + + fn parse_inline(ctx: *ParseContext, text: []const u8) (ParseError || error{OutOfMemory})![]const DocComment.Inline { + var result: std.ArrayList(DocComment.Inline) = .empty; + defer result.deinit(ctx.allocator); + + var text_start: usize = 0; + var i: usize = 0; + + while (i < text.len) { + const c = text[i]; + + // Escape sequence: \` \* \[ \< \@ \\ + if (c == '\\' and i + 1 < text.len) { + const next = text[i + 1]; + if (is_escapable_inline_char(next)) { + if (i > text_start) { + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); + } + try result.append(ctx.allocator, .{ .text = .{ .value = text[i + 1 .. i + 2] } }); + i += 2; + text_start = i; + continue; + } + } + + // Cross-reference: @`fqn` + if (c == '@' and i + 1 < text.len and text[i + 1] == '`') { + if (i > text_start) { + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); + } + const ref_start = i + 2; + if (std.mem.indexOfScalar(u8, text[ref_start..], '`')) |rel_end| { + const local_qn = text[ref_start .. ref_start + rel_end]; + const fqn = if (ctx.ref_lookup) |lookup| + (try lookup(ctx.ref_lookup_context, ctx.allocator, local_qn)) orelse local_qn + else + local_qn; + try result.append(ctx.allocator, .{ .ref = .{ .fqn = fqn } }); + i = ref_start + rel_end + 1; + text_start = i; + } else { + return error.UnclosedInlineReference; + } + continue; + } + + // Inline code: `text` + if (c == '`') { + if (i > text_start) { + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); + } + const code_start = i + 1; + if (ctx.find_inline_code_end(text, code_start)) |code_end| { + const code_val = try ctx.unescape_inline_text(text[code_start..code_end]); + try result.append(ctx.allocator, .{ .code = .{ .value = code_val } }); + i = code_end + 1; + text_start = i; + } else { + return error.UnclosedInlineCode; + } + continue; + } + + // Emphasis: *content* + // Opening * must be preceded by whitespace or start-of-text. + if (c == '*') { + const at_word_start = (i == 0 or text[i - 1] == ' ' or text[i - 1] == '\t'); + if (at_word_start and i + 1 < text.len) { + const em_start = i + 1; + var j = em_start; + var found_close: ?usize = null; + while (j < text.len) : (j += 1) { + if (text[j] == '*') { + // Closing * must be followed by whitespace, non-alphanumeric, or end-of-text + const after_close = j + 1; + const valid_close = (after_close >= text.len or + !std.ascii.isAlphanumeric(text[after_close])); + if (valid_close and j > em_start) { + found_close = j; + break; + } + } + } + if (found_close) |close_pos| { + if (i > text_start) { + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); + } + const inner = text[em_start..close_pos]; + const inner_content = try ctx.parse_inline(inner); + try result.append(ctx.allocator, .{ .emphasis = .{ .content = inner_content } }); + i = close_pos + 1; + text_start = i; + continue; + } + } + } + + // Titled link: [display](url) + if (c == '[') { + if (std.mem.indexOfScalarPos(u8, text, i + 1, ']')) |close_bracket| { + if (close_bracket + 1 < text.len and text[close_bracket + 1] == '(') { + const close_paren = std.mem.indexOfScalarPos(u8, text, close_bracket + 2, ')') orelse + return error.UnclosedInlineLink; + if (i > text_start) { + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); + } + const display = text[i + 1 .. close_bracket]; + const url = text[close_bracket + 2 .. close_paren]; + const content = try ctx.parse_inline(display); + try result.append(ctx.allocator, .{ .link = .{ .url = url, .content = content } }); + i = close_paren + 1; + text_start = i; + continue; + } + } + } + + // Autolink: + if (c == '<') { + const url_schemes = [_][]const u8{ "http://", "https://", "mailto:" }; + var matched_scheme = false; + for (url_schemes) |scheme| { + if (i + 1 + scheme.len <= text.len and + std.mem.eql(u8, text[i + 1 .. i + 1 + scheme.len], scheme)) + { + matched_scheme = true; + break; + } + } + if (matched_scheme) { + const close_angle = std.mem.indexOfScalarPos(u8, text, i + 1, '>') orelse + return error.UnclosedAutolink; + if (i > text_start) { + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..i] } }); + } + const url = text[i + 1 .. close_angle]; + const content = try ctx.allocator.alloc(DocComment.Inline, 1); + content[0] = .{ .text = .{ .value = url } }; + try result.append(ctx.allocator, .{ .link = .{ .url = url, .content = content } }); + i = close_angle + 1; + text_start = i; + continue; + } + } + + i += 1; + } + + // Flush remaining literal text + if (text_start < text.len) { + try result.append(ctx.allocator, .{ .text = .{ .value = text[text_start..] } }); + } + + return result.toOwnedSlice(ctx.allocator); + } +}; + +const AdmonitionResult = struct { + kind: DocComment.Section.Kind, + text: []const u8, +}; + +fn parse_admonition(line: []const u8) ?AdmonitionResult { + const Tag = struct { tag: []const u8, kind: DocComment.Section.Kind }; + const tags = [_]Tag{ + .{ .tag = "NOTE", .kind = .note }, + .{ .tag = "WARNING", .kind = .warning }, + .{ .tag = "LORE", .kind = .lore }, + .{ .tag = "EXAMPLE", .kind = .example }, + .{ .tag = "DEPRECATED", .kind = .deprecated }, + .{ .tag = "DECISION", .kind = .decision }, + .{ .tag = "LEARN", .kind = .learn }, + }; + + for (tags) |entry| { + if (std.mem.startsWith(u8, line, entry.tag)) { + const rest = line[entry.tag.len..]; + if (std.mem.startsWith(u8, rest, ": ")) { + return .{ .kind = entry.kind, .text = rest[2..] }; + } else if (std.mem.eql(u8, rest, ":")) { + return .{ .kind = entry.kind, .text = "" }; + } + } + } + return null; +} + +fn parse_ordered_item(line: []const u8) ?[]const u8 { + var i: usize = 0; + while (i < line.len and std.ascii.isDigit(line[i])) : (i += 1) {} + if (i == 0 or i >= line.len) return null; + if (line[i] != '.') return null; + if (i + 1 >= line.len or line[i + 1] != ' ') return null; + return line[i + 2 ..]; +} diff --git a/src/tools/abi-mapper/src/model.zig b/src/tools/abi-mapper/src/model.zig index b442852d..ce8ee389 100644 --- a/src/tools/abi-mapper/src/model.zig +++ b/src/tools/abi-mapper/src/model.zig @@ -25,8 +25,250 @@ pub fn to_json_str(document: Document, writer: anytype) !void { /// A full qualified name is a name consisting of a sequence of namespaces and ending with the actual name. pub const FQN = []const []const u8; -/// A documentation string is a sequence of text lines. -pub const DocString = []const []const u8; +/// A documentation comment with structured content. +pub const DocComment = struct { + sections: []const Section, + + pub fn is_empty(docs: DocComment) bool { + return docs.sections.len == 0; + } + + pub const empty: DocComment = .{ .sections = &.{} }; + + pub const Section = struct { + kind: Kind, + blocks: []const Block, + + pub const Kind = enum { + main, + note, + warning, + lore, + example, + deprecated, + decision, + learn, + }; + }; + + pub const Block = union(enum) { + paragraph: Paragraph, + unordered_list: UnorderedList, + ordered_list: OrderedList, + code_block: CodeBlock, + + pub const Paragraph = struct { + content: []const Inline, + }; + + pub const UnorderedList = struct { + items: []const []const Inline, + }; + + pub const OrderedList = struct { + items: []const []const Inline, + }; + + pub const CodeBlock = struct { + syntax: ?[]const u8, + content: []const u8, + }; + + pub fn jsonStringify(block: Block, jws: anytype) !void { + try jws.beginObject(); + switch (block) { + .paragraph => |p| { + try jws.objectField("type"); + try jws.write("paragraph"); + try jws.objectField("content"); + try jws.write(p.content); + }, + .unordered_list => |ul| { + try jws.objectField("type"); + try jws.write("unordered_list"); + try jws.objectField("items"); + try jws.write(ul.items); + }, + .ordered_list => |ol| { + try jws.objectField("type"); + try jws.write("ordered_list"); + try jws.objectField("items"); + try jws.write(ol.items); + }, + .code_block => |cb| { + try jws.objectField("type"); + try jws.write("code_block"); + try jws.objectField("syntax"); + try jws.write(cb.syntax); + try jws.objectField("content"); + try jws.write(cb.content); + }, + } + try jws.endObject(); + } + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) std.json.ParseError(@TypeOf(source.*))!Block { + const dynamic = try std.json.innerParse(std.json.Value, allocator, source, options); + return block_from_value(allocator, dynamic) catch return error.UnexpectedToken; + } + }; + + pub const Inline = union(enum) { + text: Text, + code: Code, + emphasis: Emphasis, + ref: Ref, + link: Link, + + pub const Text = struct { value: []const u8 }; + pub const Code = struct { value: []const u8 }; + pub const Emphasis = struct { content: []const Inline }; + pub const Ref = struct { + fqn: []const u8, + }; + pub const Link = struct { url: []const u8, content: []const Inline }; + + pub fn jsonStringify(inl: Inline, jws: anytype) !void { + try jws.beginObject(); + switch (inl) { + .text => |t| { + try jws.objectField("type"); + try jws.write("text"); + try jws.objectField("value"); + try jws.write(t.value); + }, + .code => |c| { + try jws.objectField("type"); + try jws.write("code"); + try jws.objectField("value"); + try jws.write(c.value); + }, + .emphasis => |e| { + try jws.objectField("type"); + try jws.write("emphasis"); + try jws.objectField("content"); + try jws.write(e.content); + }, + .ref => |r| { + try jws.objectField("type"); + try jws.write("ref"); + try jws.objectField("fqn"); + try jws.write(r.fqn); + }, + .link => |l| { + try jws.objectField("type"); + try jws.write("link"); + try jws.objectField("url"); + try jws.write(l.url); + try jws.objectField("content"); + try jws.write(l.content); + }, + } + try jws.endObject(); + } + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) std.json.ParseError(@TypeOf(source.*))!Inline { + const dynamic = try std.json.innerParse(std.json.Value, allocator, source, options); + return inline_from_value(allocator, dynamic) catch return error.UnexpectedToken; + } + }; +}; + +fn block_from_value(allocator: std.mem.Allocator, value: std.json.Value) !DocComment.Block { + const obj = switch (value) { + .object => |o| o, + else => return error.UnexpectedToken, + }; + const type_str = switch (obj.get("type") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + }; + if (std.mem.eql(u8, type_str, "paragraph")) { + const cv = obj.get("content") orelse return error.UnexpectedToken; + return .{ .paragraph = .{ .content = try inline_array_from_value(allocator, cv) } }; + } else if (std.mem.eql(u8, type_str, "unordered_list")) { + const iv = obj.get("items") orelse return error.UnexpectedToken; + return .{ .unordered_list = .{ .items = try inline_array_array_from_value(allocator, iv) } }; + } else if (std.mem.eql(u8, type_str, "ordered_list")) { + const iv = obj.get("items") orelse return error.UnexpectedToken; + return .{ .ordered_list = .{ .items = try inline_array_array_from_value(allocator, iv) } }; + } else if (std.mem.eql(u8, type_str, "code_block")) { + const syntax_v = obj.get("syntax"); + const syntax: ?[]const u8 = if (syntax_v) |sv| switch (sv) { + .string => |s| s, + .null => null, + else => return error.UnexpectedToken, + } else null; + const content = switch (obj.get("content") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + }; + return .{ .code_block = .{ .syntax = syntax, .content = content } }; + } + return error.UnexpectedToken; +} + +fn inline_from_value(allocator: std.mem.Allocator, value: std.json.Value) !DocComment.Inline { + const obj = switch (value) { + .object => |o| o, + else => return error.UnexpectedToken, + }; + const type_str = switch (obj.get("type") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + }; + if (std.mem.eql(u8, type_str, "text")) { + return .{ .text = .{ .value = switch (obj.get("value") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + } } }; + } else if (std.mem.eql(u8, type_str, "code")) { + return .{ .code = .{ .value = switch (obj.get("value") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + } } }; + } else if (std.mem.eql(u8, type_str, "emphasis")) { + const cv = obj.get("content") orelse return error.UnexpectedToken; + return .{ .emphasis = .{ .content = try inline_array_from_value(allocator, cv) } }; + } else if (std.mem.eql(u8, type_str, "ref")) { + return .{ .ref = .{ .fqn = switch (obj.get("fqn") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + } } }; + } else if (std.mem.eql(u8, type_str, "link")) { + const url = switch (obj.get("url") orelse return error.UnexpectedToken) { + .string => |s| s, + else => return error.UnexpectedToken, + }; + const cv = obj.get("content") orelse return error.UnexpectedToken; + return .{ .link = .{ .url = url, .content = try inline_array_from_value(allocator, cv) } }; + } + return error.UnexpectedToken; +} + +fn inline_array_from_value(allocator: std.mem.Allocator, value: std.json.Value) error{ OutOfMemory, UnexpectedToken }![]const DocComment.Inline { + const arr = switch (value) { + .array => |a| a, + else => return error.UnexpectedToken, + }; + const result = try allocator.alloc(DocComment.Inline, arr.items.len); + for (arr.items, 0..) |item, i| { + result[i] = try inline_from_value(allocator, item); + } + return result; +} + +fn inline_array_array_from_value(allocator: std.mem.Allocator, value: std.json.Value) ![]const []const DocComment.Inline { + const arr = switch (value) { + .array => |a| a, + else => return error.UnexpectedToken, + }; + const result = try allocator.alloc([]const DocComment.Inline, arr.items.len); + for (arr.items, 0..) |item, i| { + result[i] = try inline_array_from_value(allocator, item); + } + return result; +} /// Returns the last item of the full qualified name. pub fn local_name(fqn: FQN) []const u8 { @@ -93,7 +335,7 @@ pub const ConstantIndex = GenericIndex(Constant, "constants"); pub const TypeIndex = GenericIndex(Type, "types"); pub const Declaration = struct { - docs: DocString, + docs: DocComment, full_qualified_name: FQN, children: []const Declaration, data: Data, @@ -149,7 +391,7 @@ pub const Type = union(enum) { /// to be reified into `enum MyName : u32 { item … } unset_magic_type: MagicType, - pub fn is_c_abi_compatible(t: Type) bool { + pub fn is_c_abi_compatible(t: Type, types: []const Type) bool { return switch (t) { .@"struct" => true, .@"union" => true, @@ -182,10 +424,35 @@ pub const Type = union(enum) { .one, .unknown => true, .slice => false, }, - .optional => false, // TODO: ?*T and ?[*]T are C-abi-compatible + .optional => |inner_idx| blk: { + // ?*T, ?[*]T, ?fnptr(...) are C-ABI compatible (nullable pointers). + // Resolve aliases and typedefs to reach the concrete inner type. + var idx = inner_idx; + const inner = while (true) { + const inner_t = types[@intFromEnum(idx)]; + switch (inner_t) { + .alias => |a| idx = a, + .typedef => |td| idx = td.alias, + else => break inner_t, + } + }; + break :blk switch (inner) { + .ptr => |ptr| switch (ptr.size) { + .one, .unknown => true, + .slice => false, + }, + .resource => true, // resources are also represented as nullable pointers in C-ABI + .fnptr => true, // nullable function pointer is C-ABI compatible + .well_known => |id| switch (id) { + .anyptr, .anyfnptr => true, + else => false, + }, + else => false, + }; + }, .fnptr => true, - // TODO: These types should not exist anymore when C-ABI check is performed + // These types should not exist anymore when C-ABI check is performed .alias => true, .unknown_named_type => false, .unset_magic_type => false, @@ -229,8 +496,14 @@ pub const PointerSize = enum { unknown, }; +pub const FunctionPointerParam = struct { + /// Optional parameter name. `null` means unnamed. + name: ?[]const u8, + type: TypeIndex, +}; + pub const FunctionPointer = struct { - parameters: []const TypeIndex, + parameters: []const FunctionPointerParam, return_type: TypeIndex, }; @@ -242,20 +515,20 @@ pub const ArrayType = struct { pub const TypeId = std.meta.Tag(Type); pub const ExternalType = struct { - docs: DocString, + docs: DocComment, full_qualified_name: FQN, alias: []const u8, }; pub const TypeDefition = struct { - docs: DocString, + docs: DocComment, full_qualified_name: FQN, alias: TypeIndex, }; pub const BitStruct = struct { uid: UniqueID, - docs: DocString, + docs: DocComment, full_qualified_name: FQN, backing_type: StandardType, bit_count: u8, @@ -264,7 +537,7 @@ pub const BitStruct = struct { }; pub const BitStructField = struct { - docs: DocString, + docs: DocComment, name: ?[]const u8, // null is reserved type: TypeIndex, default: ?Value, @@ -275,14 +548,14 @@ pub const BitStructField = struct { pub const Struct = struct { uid: UniqueID, - docs: DocString, + docs: DocComment, full_qualified_name: FQN, logic_fields: []const StructField, native_fields: []const StructField, }; pub const StructField = struct { - docs: DocString, + docs: DocComment, name: []const u8, type: TypeIndex, default: ?Value, @@ -298,9 +571,12 @@ pub const StructFieldRole = union(enum) { pub const Enumeration = struct { uid: UniqueID, - docs: DocString, + docs: DocComment, full_qualified_name: FQN, backing_type: StandardType, + /// The actual declared bit width (e.g. 2 for `u2`). May be less than + /// `backing_type.size_in_bits()` when a non-standard width was declared. + bit_count: u8, kind: Kind, items: []const EnumItem, @@ -312,14 +588,14 @@ pub const Enumeration = struct { }; pub const EnumItem = struct { - docs: DocString, + docs: DocComment, name: []const u8, value: i65, }; pub const GenericCall = struct { uid: UniqueID, - docs: DocString, + docs: DocComment, full_qualified_name: FQN, no_return: bool, @@ -333,7 +609,7 @@ pub const GenericCall = struct { }; pub const Parameter = struct { - docs: DocString, + docs: DocComment, name: []const u8, type: TypeIndex, default: ?Value, @@ -377,19 +653,19 @@ pub const ParameterRole = union(enum) { pub const Resource = struct { uid: UniqueID, - docs: DocString, + docs: DocComment, full_qualified_name: FQN, }; pub const Error = struct { - docs: DocString, + docs: DocComment, name: []const u8, value: u32, }; pub const Constant = struct { uid: UniqueID, - docs: DocString, + docs: DocComment, full_qualified_name: FQN, type: ?TypeIndex, value: Value, diff --git a/src/tools/abi-mapper/src/sema.zig b/src/tools/abi-mapper/src/sema.zig index b08e941d..0826cbc3 100644 --- a/src/tools/abi-mapper/src/sema.zig +++ b/src/tools/abi-mapper/src/sema.zig @@ -1,10 +1,16 @@ const std = @import("std"); const model = @import("model.zig"); const syntax = @import("syntax.zig"); +const doc_comment_parser = @import("doc_comment.zig"); +pub const uid_db = @import("uid_db.zig"); const Location = syntax.Location; -pub fn analyze(allocator: std.mem.Allocator, document: syntax.Document) !model.Document { +pub const AnalysisError = struct { + message: []const u8, +}; + +pub fn analyze(allocator: std.mem.Allocator, document: syntax.Document, uid_database: ?*uid_db.UidDatabase, errors_out: *std.ArrayList(AnalysisError)) !model.Document { var analyzer: Analyzer = .{ .allocator = allocator, .scope_stack = .empty, @@ -21,11 +27,14 @@ pub fn analyze(allocator: std.mem.Allocator, document: syntax.Document) !model.D .resources = .init(allocator), .constants = .init(allocator), .types = .init(allocator), + + .uid_db = uid_database, }; try analyzer.scope_map.put(&.{}, &analyzer.root_scope); try analyzer.map(document); + try analyzer.resolve_doc_comment_refs(); try analyzer.resolve_named_types(); @@ -41,16 +50,13 @@ pub fn analyze(allocator: std.mem.Allocator, document: syntax.Document) !model.D // TODO: Compute type sizes, field offsets - if (analyzer.errors.items.len > 0) { - for (analyzer.errors.items) |err| { - std.log.err("{s}", .{err}); - } - return error.AnalysisFailed; - } + try analyzer.fail_if_errors(errors_out); // TODO: Implement garbage collection for unreferenced things - // analyzer.validate_constraints(); + try analyzer.validate_constraints(); + + try analyzer.fail_if_errors(errors_out); return .{ .root = try analyzer.root.toOwnedSlice(analyzer.allocator), @@ -117,6 +123,7 @@ const Analyzer = struct { types: Collector(model.TypeIndex), uid_base: u32 = 1, + uid_db: ?*uid_db.UidDatabase = null, const Scope = struct { parent: ?*Scope, @@ -137,8 +144,19 @@ const Analyzer = struct { }; /// Returns a unique ID based on the `fqn` of the object. + /// When a UID database is present, IDs are stable across re-runs for a + /// given FQN. Without a database, IDs are sequentially assigned. fn get_uid(ana: *Analyzer, fqn: model.FQN) error{OutOfMemory}!model.UniqueID { - _ = fqn; // TODO: Implement derivation from FQN and a UID database. + if (ana.uid_db) |db| { + var key: std.ArrayList(u8) = .empty; + defer key.deinit(ana.allocator); + for (fqn, 0..) |part, i| { + if (i > 0) try key.append(ana.allocator, '.'); + try key.appendSlice(ana.allocator, part); + } + const uid_val = try db.get_or_assign(key.items); + return @enumFromInt(uid_val); + } const uid: model.UniqueID = @enumFromInt(ana.uid_base); ana.uid_base += 1; return uid; @@ -152,13 +170,13 @@ const Analyzer = struct { const current_name = ana.current_scope_name(); const current_scope = ana.scope_map.get(current_name) orelse { - std.log.err("current scope: {f}", .{dotJoin(current_name)}); + std.debug.print("current scope: {f}\n", .{dotJoin(current_name)}); @panic("BUG: No current scope found!"); }; const inserted = if (current_scope.children.get(name)) |existing_child| blk: { if (scope_type != existing_child.type) { - std.log.err("scope mismatch for scope {f}: types {s} and {s} don't match", .{ + std.debug.print("scope mismatch for scope {f}: types {s} and {s} don't match\n", .{ std.zig.fmtId(name), @tagName(scope_type), @tagName(existing_child.type), @@ -200,14 +218,14 @@ const Analyzer = struct { } fn map(ana: *Analyzer, doc: syntax.Document) error{OutOfMemory}!void { - try ana.root.resize(ana.allocator, doc.nodes.len); - for (ana.root.items, doc.nodes) |*out, node| { - out.* = ana.map_node(node) catch |err| switch (err) { + for (doc.nodes) |node| { + const decl = ana.map_node(node) catch |err| switch (err) { // swallow silently here, all nodes are independent from each other error.FatalAnalysisError => continue, error.OutOfMemory => |e| return e, }; + try ana.root.append(ana.allocator, decl); } } @@ -256,9 +274,18 @@ const Analyzer = struct { .bitstruct => |index| .{ .bitstruct = index }, .resource => |index| .{ .resource = index }, .typedef => |index| .{ .alias = index }, - .syscall => @panic("TODO: Invalid type reference!"), - .async_call => @panic("TODO: Invalid type reference!"), - .constant => @panic("TODO: Invalid type reference!"), + .syscall => blk: { + try ana.emit_error(Location.empty, "type reference '{f}' resolves to a syscall, which cannot be used as a type", .{dotJoin(unknown_type.local_qualified_name)}); + break :blk .{ .well_known = .void }; + }, + .async_call => blk: { + try ana.emit_error(Location.empty, "type reference '{f}' resolves to an async_call, which cannot be used as a type", .{dotJoin(unknown_type.local_qualified_name)}); + break :blk .{ .well_known = .void }; + }, + .constant => blk: { + try ana.emit_error(Location.empty, "type reference '{f}' resolves to a constant, which cannot be used as a type", .{dotJoin(unknown_type.local_qualified_name)}); + break :blk .{ .well_known = .void }; + }, }; // std.log.debug(" ! candidate found {s} ({s})!", .{ sub_scope.name, @tagName(sub_scope.type) }); @@ -266,7 +293,7 @@ const Analyzer = struct { continue :element_resolution; } } - std.log.err("no candidate found for type {f} at {f}!", .{ + try ana.emit_error(Location.empty, "unknown type '{f}' referenced from scope '{f}'", .{ dotJoin(unknown_type.local_qualified_name), dotJoin(unknown_type.declared_scope), }); @@ -305,7 +332,7 @@ const Analyzer = struct { const collector = &@field(ana, collector_name); - for (collector.items, 1..) |item, index| { + for (collector.items) |item| { var item_name: std.ArrayList(u8) = .empty; defer item_name.deinit(ana.allocator); @@ -324,20 +351,20 @@ const Analyzer = struct { } } - // TODO: Implement stable item id assignment! - try items.append(ana.allocator, .{ - .docs = &.{}, + .docs = .empty, .name = try item_name.toOwnedSlice(ana.allocator), - .value = @intCast(index), + .value = @intCast(@intFromEnum(item.uid)), }); } }, } + const magic_backing = convert_enum(model.StandardType, magic_type.size); const enum_id = try ana.enums.append(.{ .uid = try ana.get_uid(type_def.full_qualified_name), - .backing_type = convert_enum(model.StandardType, magic_type.size), + .backing_type = magic_backing, + .bit_count = magic_backing.size_in_bits() orelse 0, .docs = type_def.docs, .full_qualified_name = type_def.full_qualified_name, @@ -395,18 +422,21 @@ const Analyzer = struct { std.debug.assert(bitstruct.backing_type.is_integer()); std.debug.assert(bitstruct.backing_type.size_in_bits() != null); // Assert we don't use `usize` or `isize` here! - const expected_size = bitstruct.backing_type.size_in_bits().?; - - // std.log.err("bitstruct {s}", .{bitstruct.full_qualified_name}); + const expected_size = bitstruct.bit_count; var struct_size: u8 = 0; + var has_error = false; for (@constCast(bitstruct.fields)) |*field| { const field_type = ana.get_resolved_type(field.type); - const maybe_type_size = get_type_bit_size(field_type); - // std.log.err(" {?s} => {} ({?} bits)", .{ field.name, field_type, maybe_type_size }); + const maybe_type_size = ana.get_type_bit_size(field_type); const type_size = maybe_type_size orelse { - @panic("TODO: error report for 'type not bit-packable'"); + try ana.emit_error(Location.empty, "bitstruct '{s}': field '{s}' has a type that cannot be packed into bits", .{ + model.local_name(bitstruct.full_qualified_name), + field.name orelse "", + }); + has_error = true; + continue; }; field.bit_shift = struct_size; @@ -415,16 +445,26 @@ const Analyzer = struct { struct_size += type_size; } - if (struct_size > expected_size) { - @panic("TODO: error reporting for 'fields too big'"); - } else if (struct_size < expected_size) { - @panic("TODO: error reporting for 'fields too little'"); + if (!has_error) { + if (struct_size > expected_size) { + try ana.emit_error(Location.empty, "bitstruct '{s}': fields occupy {d} bits but backing type has {d} bits (too large)", .{ + model.local_name(bitstruct.full_qualified_name), + struct_size, + expected_size, + }); + } else if (struct_size < expected_size) { + try ana.emit_error(Location.empty, "bitstruct '{s}': fields occupy {d} bits but backing type has {d} bits (use 'reserve' to add padding)", .{ + model.local_name(bitstruct.full_qualified_name), + struct_size, + expected_size, + }); + } } } } /// `tvalue` must be fully resolved and must not be any type alias - fn get_type_bit_size(tvalue: model.Type) ?u8 { + fn get_type_bit_size(ana: *Analyzer, tvalue: model.Type) ?u8 { return switch (tvalue) { .alias => unreachable, .typedef => unreachable, @@ -436,12 +476,16 @@ const Analyzer = struct { .well_known => |stdtype| stdtype.size_in_bits(), - .@"enum" => @panic("TODO"), - .bitstruct => @panic("TODO"), + .@"enum" => |idx| ana.enums.get(idx).bit_count, + .bitstruct => |idx| ana.bitstructs.get(idx).bit_count, .fnptr => null, .ptr => null, - .array => null, + .array => |arr| blk: { + const elem_type = ana.get_resolved_type(arr.child); + const elem_bits = ana.get_type_bit_size(elem_type) orelse break :blk null; + break :blk std.math.cast(u8, @as(u64, elem_bits) * arr.size); + }, .optional => null, .external => null, .resource => null, @@ -504,7 +548,7 @@ const Analyzer = struct { fn render(a: *Analyzer, list: *std.ArrayList(model.Parameter), params: []model.Parameter, mode: RenderMode) !void { for (params) |*param| { const resolved = a.get_resolved_type(param.type); - if (resolved.is_c_abi_compatible()) { + if (resolved.is_c_abi_compatible(a.types.items)) { try list.append(a.allocator, param.*); continue; } @@ -556,7 +600,7 @@ const Analyzer = struct { try list.append(a.allocator, param.*); }, else => { - std.log.err("unsupported optional builtin type {}", .{inner}); + try a.emit_error(Location.empty, "parameter '{s}' has optional type '?{s}' which cannot appear in a native call signature", .{ param.name, @tagName(id) }); }, }, .ptr => |ptr| switch (ptr.size) { @@ -579,16 +623,21 @@ const Analyzer = struct { .resource => { try list.append(a.allocator, param.*); }, + .fnptr => { + // A function pointer is nullable in C — keep as-is. + try list.append(a.allocator, param.*); + }, else => { - std.log.err("unsupported optional type {}", .{inner}); + try a.emit_error(Location.empty, "parameter '{s}' has optional type '?{s}' which cannot appear in a native call signature", .{ param.name, @tagName(inner) }); }, } }, else => { - - // TODO! - std.log.err("implement type resolution for {}", .{a.get_resolved_type(param.type)}); + try a.emit_error(Location.empty, "parameter '{s}' has type '{s}' which cannot appear in a native call signature", .{ + param.name, + @tagName(a.get_resolved_type(param.type)), + }); }, } } @@ -620,9 +669,9 @@ const Analyzer = struct { .default = null, }); try list.append(a.allocator, .{ - .docs = try a.allocator.dupe([]const u8, &.{ + .docs = try a.synthetic_doc( try a.format("The number of elements referenced by {s}_ptr.", .{param.name}), - }), + ), .name = len_name, .type = try a.map_model_type(.{ .well_known = .usize }), .role = switch (mode) { @@ -649,7 +698,7 @@ const Analyzer = struct { try native_outputs.append(ana.allocator, .{ .name = "error_code", .default = null, - .docs = &.{}, + .docs = .empty, .role = .@"error", .type = try ana.map_model_type(.{ .well_known = .u16 }), }); @@ -684,7 +733,7 @@ const Analyzer = struct { fn emit_slice( h: @This(), basename: []const u8, - docs: model.DocString, + docs: model.DocComment, ptr_type: model.Type, ) !void { try h.nf.append(h.ana.allocator, .{ @@ -695,9 +744,9 @@ const Analyzer = struct { .default = null, }); try h.nf.append(h.ana.allocator, .{ - .docs = try h.ana.allocator.dupe([]const u8, &.{ + .docs = try h.ana.synthetic_doc( try h.ana.format("The number of elements referenced by {s}_ptr.", .{basename}), - }), + ), .name = try h.ana.format("{s}_len", .{basename}), .type = try h.ana.map_model_type(.{ .well_known = .usize }), .role = .{ .slice_len = basename }, @@ -727,9 +776,9 @@ const Analyzer = struct { .default = null, }); try native_fields.append(ana.allocator, .{ - .docs = try ana.allocator.dupe([]const u8, &.{ + .docs = try ana.synthetic_doc( try ana.format("The amount of bytes referenced by {s}_ptr.", .{fld.name}), - }), + ), .name = try ana.format("{s}_len", .{fld.name}), .type = try ana.map_model_type(.{ .well_known = .usize }), .role = .{ .slice_len = fld.name }, @@ -761,9 +810,9 @@ const Analyzer = struct { .default = null, }); try native_fields.append(ana.allocator, .{ - .docs = try ana.allocator.dupe([]const u8, &.{ + .docs = try ana.synthetic_doc( try ana.format("The amount of bytes referenced by {s}_ptr.", .{fld.name}), - }), + ), .name = try ana.format("{s}_len", .{fld.name}), .type = try ana.map_model_type(.{ .well_known = .usize }), .role = .{ .slice_len = fld.name }, @@ -819,11 +868,16 @@ const Analyzer = struct { .fnptr => .keep, .uint, .int => .keep, .array => .keep, - .typedef => .keep, // TODO: Check if slice! + .typedef => unreachable, // get_resolved_type always resolves through typedefs .external => .keep, .alias => unreachable, - .unknown_named_type => unreachable, + .unknown_named_type => { + // resolve_named_types already emitted an error for this type. + // Skip the field silently rather than crashing. + std.debug.assert(ana.errors.items.len > 0); + break :blk .discard; + }, .unset_magic_type => unreachable, }; @@ -840,6 +894,16 @@ const Analyzer = struct { return error.FatalAnalysisError; } + fn fail_if_errors(ana: *Analyzer, errors_out: *std.ArrayList(AnalysisError)) !void { + if (ana.errors.items.len == 0) { + return; + } + for (ana.errors.items) |msg| { + try errors_out.append(ana.allocator, .{ .message = msg }); + } + return error.AnalysisFailed; + } + fn emit_error(ana: *Analyzer, location: Location, comptime fmt: []const u8, args: anytype) error{OutOfMemory}!void { const msg = try std.fmt.allocPrint( ana.allocator, @@ -859,8 +923,6 @@ const Analyzer = struct { }; fn map_node(ana: *Analyzer, node: syntax.Node) MapError!model.Declaration { - errdefer std.log.err("failed to map node at {f}", .{node.location}); - return switch (node.type) { .declaration => try ana.map_decl(node), .typedef => try ana.map_typedef(node), @@ -884,7 +946,7 @@ const Analyzer = struct { const full_name, const scope = try ana.push_scope(typedef.name, .typedef); defer ana.pop_scope(); - const doc_comment = try ana.allocator.dupe([]const u8, node.doc_comment); + const doc_comment = try ana.map_doc_comment(node.doc_comment); const alias_id = try ana.map_type(typedef.alias); @@ -912,11 +974,10 @@ const Analyzer = struct { const full_name, const scope = try ana.push_scope(constant.name, .constant); defer ana.pop_scope(); - const doc_comment = try ana.allocator.dupe([]const u8, node.doc_comment); + const doc_comment = try ana.map_doc_comment(node.doc_comment); const value = try ana.resolve_value(constant.value.?); - // TODO: Implement explicit constant typing! const type_id: ?model.TypeIndex = if (constant.type) |type_node| try ana.map_type(type_node) else @@ -942,63 +1003,434 @@ const Analyzer = struct { const NodeInfo = struct { full_name: model.FQN, - docs: model.DocString, - sub_type: ?model.StandardType, + docs: model.DocComment, + sub_type: ?SubTypeInfo, location: Location, + + const SubTypeInfo = struct { + /// The ABI-surface standard type (rounded up to the nearest power-of-two byte width). + backing: model.StandardType, + /// The actual declared bit width (e.g. 2 for `u2`, 32 for `u32`). + bit_count: u8, + }; }; - /// Strips empty heads and tails, then left-aligns a doc comment - fn map_doc_comment(ana: *Analyzer, doc_comment: []const []const u8) !model.DocString { - const ws = " "; + /// Parses a raw doc comment into a structured DocComment. + fn map_doc_comment(ana: *Analyzer, raw_lines: []const []const u8) !model.DocComment { + var arena = std.heap.ArenaAllocator.init(ana.allocator); + return doc_comment_parser.parse_into_arena(&arena, raw_lines, .{ + .ref_lookup = lookup_doc_comment_ref, + .ref_lookup_context = @ptrCast(ana), + }) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + error.UnclosedCodeFence, + error.UnclosedInlineReference, + error.UnclosedInlineCode, + error.UnclosedInlineLink, + error.MalformedInlineLink, + error.UnclosedAutolink, + => |parse_err| { + try ana.emit_error(Location.empty, "invalid doc comment markup: {s}", .{ + doc_comment_parser.describe_parse_error(parse_err), + }); + return .empty; + }, + }; + } - var output = try ana.allocator.dupe([]const u8, doc_comment); + fn lookup_doc_comment_ref(context: ?*anyopaque, allocator: std.mem.Allocator, local_qn: []const u8) error{OutOfMemory}!?[]const u8 { + const raw = context orelse return null; + const ana: *Analyzer = @ptrCast(@alignCast(raw)); + return ana.resolve_doc_comment_ref_with_scope( + allocator, + ana.current_scope_name(), + local_qn, + false, + ); + } - // right-trim all lines - for (output) |*item| { - item.* = std.mem.trimRight(u8, item.*, ws); + fn resolve_doc_comment_ref_with_scope( + ana: *Analyzer, + allocator: std.mem.Allocator, + declared_scope: []const []const u8, + local_qn: []const u8, + report_errors: bool, + ) error{OutOfMemory}!?[]const u8 { + var local_parts: std.ArrayList([]const u8) = .empty; + defer local_parts.deinit(allocator); + + var iter = std.mem.splitScalar(u8, local_qn, '.'); + while (iter.next()) |part| { + if (part.len == 0) { + if (report_errors) { + try ana.emit_error(Location.empty, "invalid doc reference '@`{s}`' in scope '{f}'", .{ + local_qn, + dotJoin(declared_scope), + }); + } + return null; + } + try local_parts.append(allocator, part); } - // trim empty heads: - while (output.len > 0 and output[0].len == 0) { - output = output[1..]; + if (local_parts.items.len == 0) { + if (report_errors) { + try ana.emit_error(Location.empty, "invalid doc reference '@`{s}`' in scope '{f}'", .{ + local_qn, + dotJoin(declared_scope), + }); + } + return null; } - // trim empty tails: - while (output.len > 0 and output[output.len - 1].len == 0) { - output = output[0 .. output.len - 1]; + if (try ana.resolve_prefixed_doc_reference( + allocator, + declared_scope, + local_parts.items, + )) |resolved| { + return resolved; } - // Determine common whitespace prefix length: - var common_prefix_len: usize = std.math.maxInt(usize); - for (output) |line| { - if (line.len == 0) - continue; + if (try ana.resolve_contained_doc_reference( + allocator, + declared_scope, + local_parts.items, + )) |resolved| { + return resolved; + } + + if (report_errors) { + try ana.emit_error(Location.empty, "unknown doc reference '@`{s}`' in scope '{f}'", .{ + local_qn, + dotJoin(declared_scope), + }); + } + return null; + } + + fn resolve_prefixed_doc_reference( + ana: *Analyzer, + allocator: std.mem.Allocator, + declared_scope: []const []const u8, + local_parts: []const []const u8, + ) error{OutOfMemory}!?[]const u8 { + var search_scope: ?*Scope = ana.resolve_declared_scope_or_parent(declared_scope); + + while (search_scope) |base_scope| : (search_scope = base_scope.parent) { + var resolved_scope: *Scope = base_scope; + var matched_parts: usize = 0; + + while (matched_parts < local_parts.len) : (matched_parts += 1) { + const next_scope = resolved_scope.children.get(local_parts[matched_parts]) orelse break; + resolved_scope = next_scope; + } + + if (matched_parts > 0) { + if (try ana.build_doc_reference_from_scope( + allocator, + resolved_scope, + local_parts[matched_parts..], + )) |resolved| { + return resolved; + } + } + } - const prefix_len = for (line, 0..) |c, i| { - if (std.mem.indexOfScalar(u8, ws, c) == null) - break i; - } else unreachable; // lines are non-empty, and they must contain at least a non-space char + return null; + } + + fn build_doc_reference_from_scope( + ana: *Analyzer, + allocator: std.mem.Allocator, + scope: *Scope, + remaining_parts: []const []const u8, + ) error{OutOfMemory}!?[]const u8 { + const scope_fqn = try ana.scope_to_fqn_string(allocator, scope); + errdefer allocator.free(scope_fqn); + + if (remaining_parts.len == 0) { + return scope_fqn; + } + + if (remaining_parts.len != 1) { + return null; + } - common_prefix_len = @min(common_prefix_len, prefix_len); + const link = scope.link orelse return null; + if (!ana.link_contains_doc_reference_target(link, remaining_parts[0])) { + return null; } - // trim common prefix: - for (output) |*line| { - if (line.len == 0) + var full: std.ArrayList(u8) = .empty; + defer full.deinit(allocator); + if (scope_fqn.len > 0) { + try full.appendSlice(allocator, scope_fqn); + try full.append(allocator, '.'); + } + try full.appendSlice(allocator, remaining_parts[0]); + allocator.free(scope_fqn); + return @as(?[]const u8, try full.toOwnedSlice(allocator)); + } + + fn resolve_declared_scope_or_parent(ana: *Analyzer, declared_scope: []const []const u8) ?*Scope { + var scope_len = declared_scope.len; + while (true) { + if (ana.scope_map.get(declared_scope[0..scope_len])) |scope| { + return scope; + } + if (scope_len == 0) { + break; + } + scope_len -= 1; + } + return null; + } + + fn resolve_contained_doc_reference( + ana: *Analyzer, + allocator: std.mem.Allocator, + declared_scope: []const []const u8, + local_parts: []const []const u8, + ) error{OutOfMemory}!?[]const u8 { + if (local_parts.len == 0) { + return null; + } + + var search_scope: ?*Scope = ana.resolve_declared_scope_or_parent(declared_scope); + while (search_scope) |scope| : (search_scope = scope.parent) { + const link = scope.link orelse continue; + if (!ana.link_contains_doc_reference_target(link, local_parts[0])) { continue; + } + + const scope_fqn = try ana.scope_to_fqn_string(allocator, scope); + defer allocator.free(scope_fqn); + var full: std.ArrayList(u8) = .empty; + defer full.deinit(allocator); + if (scope_fqn.len > 0) { + try full.appendSlice(allocator, scope_fqn); + try full.append(allocator, '.'); + } + try full.appendSlice(allocator, local_parts[0]); + for (local_parts[1..]) |part| { + try full.append(allocator, '.'); + try full.appendSlice(allocator, part); + } + return @as(?[]const u8, try full.toOwnedSlice(allocator)); + } + + return null; + } + + fn link_contains_doc_reference_target(ana: *Analyzer, link: Scope.Link, local_name: []const u8) bool { + return switch (link) { + .namespace, + .resource, + .constant, + .typedef, + => false, + + .@"struct" => |idx| blk: { + const item = ana.structs.get(idx); + break :blk find_field_by_name(item.logic_fields, local_name) != null or + find_field_by_name(item.native_fields, local_name) != null; + }, + .@"union" => |idx| blk: { + const item = ana.unions.get(idx); + break :blk find_field_by_name(item.logic_fields, local_name) != null or + find_field_by_name(item.native_fields, local_name) != null; + }, + .@"enum" => |idx| has_enum_item_by_name(ana.enums.get(idx).items, local_name), + .bitstruct => |idx| has_bitstruct_field_by_name(ana.bitstructs.get(idx).fields, local_name), + .syscall => |idx| blk: { + const call = ana.syscalls.get(idx); + break :blk find_param_by_name(call.logic_inputs, local_name) != null or + find_param_by_name(call.logic_outputs, local_name) != null or + has_error_by_name(call.errors, local_name); + }, + .async_call => |idx| blk: { + const call = ana.async_calls.get(idx); + break :blk find_param_by_name(call.logic_inputs, local_name) != null or + find_param_by_name(call.logic_outputs, local_name) != null or + has_error_by_name(call.errors, local_name); + }, + }; + } + + fn scope_to_fqn_string(ana: *Analyzer, allocator: std.mem.Allocator, scope: *const Scope) error{OutOfMemory}![]const u8 { + _ = ana; + + var segments: std.ArrayList([]const u8) = .empty; + defer segments.deinit(allocator); + + var current: ?*const Scope = scope; + while (current) |node| : (current = node.parent) { + if (node.parent == null) break; + try segments.append(allocator, node.name); + } + + std.mem.reverse([]const u8, segments.items); + return std.mem.join(allocator, ".", segments.items); + } + + fn resolve_doc_comment_refs(ana: *Analyzer) !void { + try ana.resolve_namespace_doc_comment_refs(ana.root.items); + + for (ana.structs.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + for (@constCast(item.logic_fields)) |*field| { + try ana.resolve_doc_comment_in_scope(&field.docs, item.full_qualified_name); + } + for (@constCast(item.native_fields)) |*field| { + try ana.resolve_doc_comment_in_scope(&field.docs, item.full_qualified_name); + } + } + + for (ana.unions.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + for (@constCast(item.logic_fields)) |*field| { + try ana.resolve_doc_comment_in_scope(&field.docs, item.full_qualified_name); + } + for (@constCast(item.native_fields)) |*field| { + try ana.resolve_doc_comment_in_scope(&field.docs, item.full_qualified_name); + } + } + + for (ana.enums.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + for (@constCast(item.items)) |*enum_item| { + try ana.resolve_doc_comment_in_scope(&enum_item.docs, item.full_qualified_name); + } + } + + for (ana.bitstructs.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + for (@constCast(item.fields)) |*field| { + try ana.resolve_doc_comment_in_scope(&field.docs, item.full_qualified_name); + } + } + + for (ana.syscalls.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + for (@constCast(item.logic_inputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.logic_outputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.native_inputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.native_outputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.errors)) |*api_error| { + try ana.resolve_doc_comment_in_scope(&api_error.docs, item.full_qualified_name); + } + } + + for (ana.async_calls.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + for (@constCast(item.logic_inputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.logic_outputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.native_inputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.native_outputs)) |*param| { + try ana.resolve_doc_comment_in_scope(¶m.docs, item.full_qualified_name); + } + for (@constCast(item.errors)) |*api_error| { + try ana.resolve_doc_comment_in_scope(&api_error.docs, item.full_qualified_name); + } + } + + for (ana.resources.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + } - const prefix = line.*[0..common_prefix_len]; + for (ana.constants.items) |*item| { + try ana.resolve_doc_comment_in_scope(&item.docs, item.full_qualified_name); + } - // Prefix must be only whitespace: - std.debug.assert(for (prefix) |c| { - if (std.mem.indexOfScalar(u8, ws, c) == null) - break false; - } else true); + for (ana.types.items) |*item| { + switch (item.*) { + .typedef => |*typedef| { + try ana.resolve_doc_comment_in_scope(&typedef.docs, typedef.full_qualified_name); + }, + else => {}, + } + } + } + + fn resolve_namespace_doc_comment_refs(ana: *Analyzer, declarations: []const model.Declaration) !void { + for (@constCast(declarations)) |*decl| { + if (decl.data == .namespace) { + try ana.resolve_doc_comment_in_scope(&decl.docs, decl.full_qualified_name); + } + try ana.resolve_namespace_doc_comment_refs(decl.children); + } + } + + fn resolve_doc_comment_in_scope(ana: *Analyzer, docs: *model.DocComment, declared_scope: []const []const u8) !void { + for (@constCast(docs.sections)) |*section| { + for (@constCast(section.blocks)) |*block| { + switch (block.*) { + .paragraph => |*paragraph| { + try ana.resolve_inline_refs(@constCast(paragraph.content), declared_scope); + }, + .unordered_list => |*list| { + for (@constCast(list.items)) |*item| { + try ana.resolve_inline_refs(@constCast(item.*), declared_scope); + } + }, + .ordered_list => |*list| { + for (@constCast(list.items)) |*item| { + try ana.resolve_inline_refs(@constCast(item.*), declared_scope); + } + }, + .code_block => {}, + } + } + } + } - line.* = line.*[common_prefix_len..]; + fn resolve_inline_refs(ana: *Analyzer, inlines: []model.DocComment.Inline, declared_scope: []const []const u8) !void { + for (inlines) |*inl| { + switch (inl.*) { + .ref => |*ref_data| { + if (try ana.resolve_doc_comment_ref_with_scope( + ana.allocator, + declared_scope, + ref_data.fqn, + true, + )) |resolved| { + ref_data.fqn = resolved; + } + }, + .emphasis => |*emphasis| { + try ana.resolve_inline_refs(@constCast(emphasis.content), declared_scope); + }, + .link => |*link| { + try ana.resolve_inline_refs(@constCast(link.content), declared_scope); + }, + else => {}, + } } + } - return output; + /// Creates a synthetic one-paragraph DocComment from a plain text string. + fn synthetic_doc(ana: *Analyzer, text: []const u8) !model.DocComment { + const inlines = try ana.allocator.alloc(model.DocComment.Inline, 1); + inlines[0] = .{ .text = .{ .value = text } }; + const blocks = try ana.allocator.alloc(model.DocComment.Block, 1); + blocks[0] = .{ .paragraph = .{ .content = inlines } }; + const sections = try ana.allocator.alloc(model.DocComment.Section, 1); + sections[0] = .{ .kind = .main, .blocks = blocks }; + return .{ .sections = sections }; } fn map_decl(ana: *Analyzer, node: syntax.Node) !model.Declaration { @@ -1007,8 +1439,6 @@ const Analyzer = struct { const full_name, const scope = try ana.push_scope(decl.name, convert_enum(Scope.Type, decl.type)); defer ana.pop_scope(); - const doc_comment = try ana.map_doc_comment(node.doc_comment); - var children: std.ArrayList(model.Declaration) = .empty; defer children.deinit(ana.allocator); @@ -1024,6 +1454,8 @@ const Analyzer = struct { } } + const doc_comment = try ana.map_doc_comment(node.doc_comment); + const needs_subtype = switch (decl.type) { .@"enum", .bitstruct => true, .@"struct", .@"union", .async_call, .syscall, .namespace, .resource => false, @@ -1038,11 +1470,23 @@ const Analyzer = struct { } } - const sub_type: ?model.StandardType = if (needs_subtype) blk: { + const sub_type: ?NodeInfo.SubTypeInfo = if (needs_subtype) blk: { const model_type = try ana.map_type_inner(decl.subtype.?); - if (model_type != .well_known) - return ana.fatal_error(node.location, "subtype must be standard type, not a {s}", .{@tagName(model_type)}); - break :blk model_type.well_known; + break :blk switch (model_type) { + .well_known => |st| .{ + .backing = st, + .bit_count = st.size_in_bits() orelse 0, + }, + .uint => |bits| .{ + .backing = round_up_to_standard_type(bits), + .bit_count = bits, + }, + .int => |bits| .{ + .backing = round_up_to_standard_type(bits), + .bit_count = bits, + }, + else => return ana.fatal_error(node.location, "subtype must be an integer type, not a {s}", .{@tagName(model_type)}), + }; } else null; const info: NodeInfo = .{ @@ -1094,7 +1538,7 @@ const Analyzer = struct { fn map_enum(ana: *Analyzer, info: NodeInfo, decl: syntax.DeclarationNode) !model.Declaration.Data { std.debug.assert(info.sub_type != null); - if (!info.sub_type.?.is_integer()) { + if (!info.sub_type.?.backing.is_integer()) { return ana.fatal_error(info.location, "enum sub-type must be an integer", .{}); } @@ -1142,7 +1586,8 @@ const Analyzer = struct { .uid = try ana.get_uid(info.full_name), .docs = info.docs, .full_qualified_name = info.full_name, - .backing_type = info.sub_type.?, + .backing_type = info.sub_type.?.backing, + .bit_count = info.sub_type.?.bit_count, .items = try items.resolve(), .kind = kind, }); @@ -1211,8 +1656,12 @@ const Analyzer = struct { }, .@"error" => |data| { + // Build FQN for this error: [syscall_fqn..., error_name] + const error_fqn = try std.mem.concat(ana.allocator, []const u8, &.{ info.full_name, &.{data} }); + defer ana.allocator.free(error_fqn); + const error_uid = try ana.get_uid(error_fqn); try errors.append(child.location, .{ - .value = @intCast(errors.fields.items.len + 1), // TODO: Implement fqn + error name based caching in database file + .value = @intFromEnum(error_uid), .docs = try ana.map_doc_comment(child.doc_comment), .name = data, }); @@ -1233,6 +1682,10 @@ const Analyzer = struct { try ana.emit_error(info.location, "calls that are noreturn cannot have out parameters", .{}); } + if (mode == .syscall and outputs.fields.items.len > 1) { + return ana.fatal_error(info.location, "syscall '{s}' has {d} 'out' parameters, but syscalls can have at most one", .{ info.full_name[info.full_name.len - 1], outputs.fields.items.len }); + } + const output: model.GenericCall = .{ .uid = try ana.get_uid(info.full_name), .docs = info.docs, @@ -1324,8 +1777,8 @@ const Analyzer = struct { fn map_bit_struct(ana: *Analyzer, info: NodeInfo, decl: syntax.DeclarationNode) !model.Declaration.Data { std.debug.assert(info.sub_type != null); - if (!info.sub_type.?.is_integer()) { - return ana.fatal_error(info.location, "enum sub-type must be an integer", .{}); + if (!info.sub_type.?.backing.is_integer()) { + return ana.fatal_error(info.location, "bitstruct sub-type must be an integer", .{}); } var fields = ana.make_collector( @@ -1382,14 +1835,13 @@ const Analyzer = struct { .docs = info.docs, .full_qualified_name = info.full_name, .fields = try fields.resolve(), - .backing_type = info.sub_type.?, - - .bit_count = info.sub_type.?.size_in_bits() orelse 0, + .backing_type = info.sub_type.?.backing, + .bit_count = info.sub_type.?.bit_count, }); return .{ .bitstruct = index }; } - const MapTypeError = error{OutOfMemory}; + const MapTypeError = error{ OutOfMemory, FatalAnalysisError }; fn map_type(ana: *Analyzer, type_node: *const syntax.TypeNode) MapTypeError!model.TypeIndex { const decl: model.Type = try ana.map_type_inner(type_node); @@ -1428,7 +1880,15 @@ const Analyzer = struct { .external => false, .typedef => false, - .fnptr => |ptr| std.mem.eql(model.TypeIndex, ptr.parameters, other.fnptr.parameters) and ptr.return_type == other.fnptr.return_type, + .fnptr => |ptr| blk: { + if (ptr.return_type != other.fnptr.return_type) break :blk false; + if (ptr.parameters.len != other.fnptr.parameters.len) break :blk false; + for (ptr.parameters, other.fnptr.parameters) |a, b| { + if (a.type != b.type) break :blk false; + // Names are not part of type identity. + } + break :blk true; + }, .ptr => |ptr| ptr.size == other.ptr.size and ptr.alignment == other.ptr.alignment and ptr.is_const == other.ptr.is_const and ptr.child == other.ptr.child, @@ -1490,9 +1950,8 @@ const Analyzer = struct { while (iter.next()) |part| { if (part.len == 0) { - @panic("TODO: Empty parts!"); - // try ana.emit_error(); - // continue; + try ana.emit_error(Location.empty, "empty identifier segment in type name '{s}'", .{data}); + continue; } try fqn.append(ana.allocator, part); } @@ -1536,11 +1995,11 @@ const Analyzer = struct { const size: u32 = switch (size_val) { .int => |int| std.math.cast(u32, int) orelse blk: { - std.log.err("TODO: Array size too large: {}", .{int}); + try ana.emit_error(Location.empty, "array size {d} is too large (maximum is {d})", .{ int, std.math.maxInt(u32) }); break :blk 0; }, else => blk: { - std.log.err("TODO: Invalid array size {}", .{size_val}); + try ana.emit_error(Location.empty, "array size must be an integer, not a {s}", .{@tagName(size_val)}); break :blk 0; }, }; @@ -1554,13 +2013,16 @@ const Analyzer = struct { }, .fnptr => |data| { - var params: std.ArrayList(model.TypeIndex) = .empty; + var params: std.ArrayList(model.FunctionPointerParam) = .empty; defer params.deinit(ana.allocator); try params.resize(ana.allocator, data.parameters.len); - for (params.items, data.parameters) |*out, dst| { - out.* = try ana.map_type(dst); + for (params.items, data.parameters) |*out, src| { + out.* = .{ + .name = src.name, + .type = try ana.map_type(src.type), + }; } const return_type = try ana.map_type(data.return_type); @@ -1616,8 +2078,26 @@ const Analyzer = struct { .null => .null, }, .symbol_name => |symbol_name| { - std.log.err("resolve symbol '{f}'", .{std.zig.fmtString(symbol_name)}); - @panic("symbol resolution not done yet"); + // Search already-mapped constants (those defined before this point). + // The symbol may be a simple name or a dot-qualified name. + for (ana.constants.items) |constant| { + const local = model.local_name(constant.full_qualified_name); + if (std.mem.eql(u8, local, symbol_name)) { + return constant.value; + } + // Also match fully qualified dot-joined name + var buf: [256]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + for (constant.full_qualified_name, 0..) |part, i| { + if (i > 0) fbs.writer().writeByte('.') catch {}; + fbs.writer().writeAll(part) catch {}; + } + const fqn_str = fbs.getWritten(); + if (std.mem.eql(u8, fqn_str, symbol_name)) { + return constant.value; + } + } + return ana.fatal_error(Location.empty, "constant '{s}' must be defined before it is used here", .{symbol_name}); }, .uint => |int| .{ .int = int }, .compound => |compound| { @@ -1672,7 +2152,44 @@ const Analyzer = struct { } } - fn validate_constraints(ana: *Analyzer) void { + fn find_param_by_name(params: []const model.Parameter, name: []const u8) ?model.Parameter { + for (params) |p| { + if (std.mem.eql(u8, p.name, name)) return p; + } + return null; + } + + fn has_error_by_name(errs: []const model.Error, name: []const u8) bool { + for (errs) |err_item| { + if (std.mem.eql(u8, err_item.name, name)) return true; + } + return false; + } + + fn has_enum_item_by_name(items: []const model.EnumItem, name: []const u8) bool { + for (items) |item| { + if (std.mem.eql(u8, item.name, name)) return true; + } + return false; + } + + fn has_bitstruct_field_by_name(fields: []const model.BitStructField, name: []const u8) bool { + for (fields) |field| { + if (field.name) |field_name| { + if (std.mem.eql(u8, field_name, name)) return true; + } + } + return false; + } + + fn find_field_by_name(fields: []const model.StructField, name: []const u8) ?model.StructField { + for (fields) |f| { + if (std.mem.eql(u8, f.name, name)) return f; + } + return null; + } + + fn validate_constraints(ana: *Analyzer) !void { for (ana.syscalls.items) |sc| { // native calls must have either a return value // or none. @@ -1698,18 +2215,47 @@ const Analyzer = struct { } for (sc.native_inputs) |inp| { - std.debug.assert(ana.get_resolved_type(inp.type).is_c_abi_compatible()); + if (!ana.get_resolved_type(inp.type).is_c_abi_compatible(ana.types.items)) { + std.debug.panic("unexpected non-compatible type for native input {s}.{s}", .{ + sc.full_qualified_name[sc.full_qualified_name.len - 1], + inp.name, + }); + } // inputs cannot be the error role std.debug.assert(inp.role != .@"error"); - // TODO: Assert that referenced parameters exist, and that they have the right pointer type + switch (inp.role) { + .default, .output => {}, + .input_slice, .output_slice => unreachable, // slices are split into ptr+len in native params + .@"error" => unreachable, // asserted above + .input_ptr => |ref_name| { + if (find_param_by_name(sc.logic_inputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native input '{s}' (input_ptr) references unknown logic input '{s}'", .{ inp.name, ref_name }); + } + }, + .input_len => |ref_name| { + if (find_param_by_name(sc.logic_inputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native input '{s}' (input_len) references unknown logic input '{s}'", .{ inp.name, ref_name }); + } + }, + .output_ptr => |ref_name| { + if (find_param_by_name(sc.logic_outputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native input '{s}' (output_ptr) references unknown logic output '{s}'", .{ inp.name, ref_name }); + } + }, + .output_len => |ref_name| { + if (find_param_by_name(sc.logic_outputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native input '{s}' (output_len) references unknown logic output '{s}'", .{ inp.name, ref_name }); + } + }, + } } var has_error_output = false; const needs_error_output = (sc.errors.len > 0); for (sc.native_outputs) |outp| { - std.debug.assert(ana.get_resolved_type(outp.type).is_c_abi_compatible()); + std.debug.assert(ana.get_resolved_type(outp.type).is_c_abi_compatible(ana.types.items)); switch (outp.role) { .default => {}, .@"error" => { @@ -1717,11 +2263,27 @@ const Analyzer = struct { std.debug.assert(needs_error_output); has_error_output = true; }, - .input_len, .input_ptr => { - // TODO: Assert that referenced parameters exist, and that they have the right pointer type + .input_slice, .output_slice => unreachable, + .output => {}, + .input_len => |ref_name| { + if (find_param_by_name(sc.logic_inputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native output '{s}' (input_len) references unknown logic input '{s}'", .{ outp.name, ref_name }); + } + }, + .input_ptr => |ref_name| { + if (find_param_by_name(sc.logic_inputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native output '{s}' (input_ptr) references unknown logic input '{s}'", .{ outp.name, ref_name }); + } + }, + .output_len => |ref_name| { + if (find_param_by_name(sc.logic_outputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native output '{s}' (output_len) references unknown logic output '{s}'", .{ outp.name, ref_name }); + } }, - .output_len, .output_ptr => { - // TODO: Assert that referenced parameters exist, and that they have the right pointer type + .output_ptr => |ref_name| { + if (find_param_by_name(sc.logic_outputs, ref_name) == null) { + try ana.emit_error(Location.empty, "native output '{s}' (output_ptr) references unknown logic output '{s}'", .{ outp.name, ref_name }); + } }, } } @@ -1737,7 +2299,7 @@ const Analyzer = struct { } for (un.native_fields) |fld| { std.debug.assert(fld.role == .default); - std.debug.assert(ana.get_resolved_type(fld.type).is_c_abi_compatible()); + std.debug.assert(ana.get_resolved_type(fld.type).is_c_abi_compatible(ana.types.items)); } } @@ -1749,11 +2311,18 @@ const Analyzer = struct { for (str.native_fields) |fld| { switch (fld.role) { .default => {}, - .slice_len, .slice_ptr => { - // TODO: Assert the referenced slice exists + .slice_ptr => |ref_name| { + if (find_field_by_name(str.logic_fields, ref_name) == null) { + try ana.emit_error(Location.empty, "native field '{s}' (slice_ptr) references unknown logic field '{s}'", .{ fld.name, ref_name }); + } + }, + .slice_len => |ref_name| { + if (find_field_by_name(str.logic_fields, ref_name) == null) { + try ana.emit_error(Location.empty, "native field '{s}' (slice_len) references unknown logic field '{s}'", .{ fld.name, ref_name }); + } }, } - std.debug.assert(ana.get_resolved_type(fld.type).is_c_abi_compatible()); + std.debug.assert(ana.get_resolved_type(fld.type).is_c_abi_compatible(ana.types.items)); } } @@ -1766,7 +2335,9 @@ const Analyzer = struct { switch (fld_type) { .well_known => |id| std.debug.assert(id.size_in_bits() != null), .@"enum", .bitstruct => {}, - else => unreachable, + .uint, .int => {}, + .array => {}, + else => std.debug.panic("Unsupported bit type: {t}", .{fld_type}), } std.debug.assert(fld.bit_count != null); @@ -1874,6 +2445,10 @@ fn Collector(comptime I: type) type { }; } +fn round_up_to_standard_type(bits: u8) model.StandardType { + return if (bits <= 8) .u8 else if (bits <= 16) .u16 else if (bits <= 32) .u32 else .u64; +} + fn convert_enum(comptime T: type, src: anytype) T { return switch (src) { inline else => |tag| @field(T, @tagName(tag)), @@ -1895,3 +2470,75 @@ const DotJoin = struct { } } }; + +test "validate_constraints failures are surfaced through fail_if_errors" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var analyzer: Analyzer = .{ + .allocator = allocator, + .scope_stack = .empty, + .scope_map = .init(allocator), + .errors = .empty, + .root = .empty, + .structs = .init(allocator), + .unions = .init(allocator), + .enums = .init(allocator), + .bitstructs = .init(allocator), + .syscalls = .init(allocator), + .async_calls = .init(allocator), + .resources = .init(allocator), + .constants = .init(allocator), + .types = .init(allocator), + .uid_db = null, + }; + + const u8_type = try analyzer.types.append(.{ .well_known = .u8 }); + const ptr_type = try analyzer.types.append(.{ .ptr = .{ + .child = u8_type, + .is_const = true, + .alignment = null, + .size = .unknown, + } }); + + const logic_fields = try allocator.alloc(model.StructField, 1); + logic_fields[0] = .{ + .docs = .empty, + .name = "actual", + .type = u8_type, + .default = null, + .role = .default, + }; + + const native_fields = try allocator.alloc(model.StructField, 1); + native_fields[0] = .{ + .docs = .empty, + .name = "broken_ptr", + .type = ptr_type, + .default = null, + .role = .{ .slice_ptr = "missing" }, + }; + + _ = try analyzer.structs.append(.{ + .uid = @enumFromInt(1), + .docs = .empty, + .full_qualified_name = &.{"Broken"}, + .logic_fields = logic_fields, + .native_fields = native_fields, + }); + + try analyzer.validate_constraints(); + try std.testing.expectEqual(@as(usize, 1), analyzer.errors.items.len); + + var errors_out: std.ArrayList(AnalysisError) = .empty; + defer errors_out.deinit(allocator); + + try std.testing.expectError(error.AnalysisFailed, analyzer.fail_if_errors(&errors_out)); + try std.testing.expectEqual(@as(usize, 1), errors_out.items.len); + try std.testing.expect(std.mem.indexOf( + u8, + errors_out.items[0].message, + "native field 'broken_ptr' (slice_ptr) references unknown logic field 'missing'", + ) != null); +} diff --git a/src/tools/abi-mapper/src/syntax.zig b/src/tools/abi-mapper/src/syntax.zig index b3d8d370..aafeff35 100644 --- a/src/tools/abi-mapper/src/syntax.zig +++ b/src/tools/abi-mapper/src/syntax.zig @@ -55,6 +55,63 @@ const TokenType = enum { } }; +fn matchHexDigits(str: []const u8) ?usize { + var i: usize = 0; + var has_digit = false; + while (i < str.len) : (i += 1) { + const c = str[i]; + if (std.ascii.isHex(c)) { + has_digit = true; + } else if (c == '_') { + // digit separator — allowed + } else break; + } + return if (has_digit) i else null; +} + +fn matchBinaryDigits(str: []const u8) ?usize { + var i: usize = 0; + var has_digit = false; + while (i < str.len) : (i += 1) { + const c = str[i]; + if (c == '0' or c == '1') { + has_digit = true; + } else if (c == '_') { + // digit separator — allowed + } else break; + } + return if (has_digit) i else null; +} + +fn matchDecimalDigits(str: []const u8) ?usize { + if (str.len == 0) return null; + if (!std.ascii.isDigit(str[0])) return null; + var i: usize = 1; + while (i < str.len) : (i += 1) { + const c = str[i]; + if (std.ascii.isDigit(c)) { + // ok + } else if (c == '_') { + // digit separator — allowed + } else break; + } + return i; +} + +fn parse_number_token(text: []const u8) u64 { + var stripped_buf: [128]u8 = undefined; + std.debug.assert(text.len <= stripped_buf.len); + + var stripped_len: usize = 0; + for (text) |c| { + if (c != '_') { + stripped_buf[stripped_len] = c; + stripped_len += 1; + } + } + return std.fmt.parseInt(u64, stripped_buf[0..stripped_len], 0) catch unreachable; +} + pub fn match_identifier(str: []const u8) ?usize { const first_char = "_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const all_chars = first_char ++ "0123456789."; @@ -117,10 +174,10 @@ const patterns = blk: { .create(.comment, match.sequenceOf(.{ match.literal("//?"), match.takeNoneOf("\r\n") })), .create(.comment, match.literal("//?")), - .create(.number, match.sequenceOf(.{ match.literal("0x"), match.hexadecimalNumber })), - .create(.number, match.sequenceOf(.{ match.literal("0b"), match.binaryNumber })), - .create(.number, match.decimalNumber), - .create(.number, match.sequenceOf(.{ match.literal("-"), match.decimalNumber })), + .create(.number, match.sequenceOf(.{ match.literal("0x"), matchHexDigits })), + .create(.number, match.sequenceOf(.{ match.literal("0b"), matchBinaryDigits })), + .create(.number, matchDecimalDigits), + .create(.number, match.sequenceOf(.{ match.literal("-"), matchDecimalDigits })), .create(.whitespace, match.whitespace), }; @@ -436,7 +493,7 @@ pub const Parser = struct { const tok = try parser.accept(.number); - const num = std.fmt.parseInt(u64, tok.text, 0) catch unreachable; + const num = parse_number_token(tok.text); return .{ .uint = num, @@ -476,7 +533,7 @@ pub const Parser = struct { const num_tok = try parser.accept(.number); try parser.expect(.@")"); - break :blk std.fmt.parseInt(u64, num_tok.text, 0) catch unreachable; + break :blk parse_number_token(num_tok.text); } else null; const child = try parser.accept_type(); @@ -566,7 +623,7 @@ pub const Parser = struct { } if (try parser.try_accept(.fnptr)) |_| { - var params: std.ArrayList(*const TypeNode) = .empty; + var params: std.ArrayList(FnPtrParam) = .empty; defer params.deinit(parser.allocator); try parser.expect(.@"("); @@ -574,9 +631,29 @@ pub const Parser = struct { if (try parser.try_accept(.@")")) |_| break :blk; while (true) { - const param = try parser.accept_type(); + // Speculatively try to consume "name :" for a named parameter. + const save = parser.core.saveState(); + const param_name: ?[]const u8 = name_blk: { + const id_tok = parser.accept_any(&.{.identifier}) catch { + parser.core.restoreState(save); + break :name_blk null; + }; + if (try parser.try_accept(.@":")) |_| { + // Named parameter — strip @"…" escaping if present. + const raw = id_tok.text; + break :name_blk if (std.mem.startsWith(u8, raw, "@\"")) + raw[2 .. raw.len - 1] + else + raw; + } + // No colon — what we consumed was the start of the type. + // Restore and fall through to accept_type(). + parser.core.restoreState(save); + break :name_blk null; + }; - try params.append(parser.allocator, param); + const param_type = try parser.accept_type(); + try params.append(parser.allocator, .{ .name = param_name, .type = param_type }); if (try parser.try_accept(.@")")) |_| break :blk; @@ -801,6 +878,12 @@ pub const NamedValue = enum { true, }; +pub const FnPtrParam = struct { + /// Optional parameter name. `null` means the parameter is unnamed. + name: ?[]const u8, + type: *const TypeNode, +}; + pub const TypeNode = union(enum) { builtin: BuiltinType, named: []const u8, @@ -811,7 +894,7 @@ pub const TypeNode = union(enum) { size: *const ValueNode, }, fnptr: struct { - parameters: []const *const TypeNode, + parameters: []const FnPtrParam, return_type: *const TypeNode, }, unsigned_int: u8, diff --git a/src/tools/abi-mapper/src/uid_db.zig b/src/tools/abi-mapper/src/uid_db.zig new file mode 100644 index 00000000..86b498e9 --- /dev/null +++ b/src/tools/abi-mapper/src/uid_db.zig @@ -0,0 +1,103 @@ +const std = @import("std"); + +/// A database that maps fully-qualified names (as dot-joined strings) to stable +/// u32 UIDs. On first run the file is created; on subsequent runs it reads the +/// file and reuses existing IDs, allocating new ones for new FQNs. +pub const UidDatabase = struct { + allocator: std.mem.Allocator, + entries: std.StringArrayHashMap(u32), + next_id: u32, + + /// JSON schema used for persistence. + const FileFormat = struct { + entries: []const Entry, + + const Entry = struct { + fqn: []const u8, + uid: u32, + }; + }; + + pub fn init(allocator: std.mem.Allocator) UidDatabase { + return .{ + .allocator = allocator, + .entries = .init(allocator), + .next_id = 1, + }; + } + + pub fn deinit(db: *UidDatabase) void { + // Free owned key copies + for (db.entries.keys()) |key| { + db.allocator.free(key); + } + db.entries.deinit(); + db.* = undefined; + } + + /// Look up `fqn`; if not present, assign the next available ID and persist + /// that mapping. Returns the (possibly newly-assigned) UID. + pub fn get_or_assign(db: *UidDatabase, fqn: []const u8) !u32 { + if (db.entries.get(fqn)) |uid| return uid; + + const key = try db.allocator.dupe(u8, fqn); + const uid = db.next_id; + db.next_id += 1; + try db.entries.put(key, uid); + return uid; + } + + /// Load a database from `path`. If the file does not exist an empty + /// database is returned instead. + pub fn load(allocator: std.mem.Allocator, path: []const u8) !UidDatabase { + var db = init(allocator); + errdefer db.deinit(); + + const content = std.fs.cwd().readFileAlloc(allocator, path, 1 << 20) catch |err| switch (err) { + error.FileNotFound => return db, + else => return err, + }; + defer allocator.free(content); + + const parsed = try std.json.parseFromSlice(FileFormat, allocator, content, .{ + .allocate = .alloc_always, + .ignore_unknown_fields = true, + }); + defer parsed.deinit(); + + for (parsed.value.entries) |entry| { + const key = try allocator.dupe(u8, entry.fqn); + try db.entries.put(key, entry.uid); + if (entry.uid >= db.next_id) { + db.next_id = entry.uid + 1; + } + } + + return db; + } + + /// Save the database to `path` atomically. + pub fn save(db: *const UidDatabase, path: []const u8) !void { + const entries = try db.allocator.alloc(FileFormat.Entry, db.entries.count()); + defer db.allocator.free(entries); + + for (db.entries.keys(), db.entries.values(), 0..) |key, value, i| { + entries[i] = .{ .fqn = key, .uid = value }; + } + + const format: FileFormat = .{ .entries = entries }; + + var atomic_buffer: [4096]u8 = undefined; + var atomic_file = try std.fs.cwd().atomicFile(path, .{ .write_buffer = &atomic_buffer }); + defer atomic_file.deinit(); + + const writer = &atomic_file.file_writer.interface; + const options: std.json.Stringify.Options = .{ + .whitespace = .indent_2, + }; + try writer.print("{f}", .{std.json.fmt(format, options)}); + try writer.flush(); + + try atomic_file.finish(); + } +}; diff --git a/src/tools/abi-mapper/tests/bitstruct_array_field.zig b/src/tools/abi-mapper/tests/bitstruct_array_field.zig new file mode 100644 index 00000000..1b66a9d4 --- /dev/null +++ b/src/tools/abi-mapper/tests/bitstruct_array_field.zig @@ -0,0 +1,60 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "array of 2-bit enum in bitstruct packs correctly" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // 8 × 2-bit MarshalType = 16 bits per field, two fields = 32 bits = u32. + const source = + \\enum MarshalType : u2 { + \\ item none = 0; + \\ item small = 1; + \\ item large = 2; + \\ item ptr = 3; + \\} + \\bitstruct FunctionSignature : u32 { + \\ field inputs: [8]MarshalType; + \\ field outputs: [8]MarshalType; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.bitstructs.len); + const bs = doc.bitstructs[0]; + try std.testing.expectEqual(@as(u8, 32), bs.bit_count); + try std.testing.expectEqual(@as(usize, 2), bs.fields.len); + // inputs: bit_shift=0, bit_count=16 (8 × 2) + try std.testing.expectEqual(@as(?u8, 0), bs.fields[0].bit_shift); + try std.testing.expectEqual(@as(?u8, 16), bs.fields[0].bit_count); + // outputs: bit_shift=16, bit_count=16 + try std.testing.expectEqual(@as(?u8, 16), bs.fields[1].bit_shift); + try std.testing.expectEqual(@as(?u8, 16), bs.fields[1].bit_count); +} + +test "array of non-packable type in bitstruct emits error" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // [4]*u8 contains pointers which have no fixed bit size. + const source = + \\bitstruct Bad : u32 { + \\ field ptrs: [4]*u8; + \\} + ; + const result = parse_and_analyze(allocator, source); + try std.testing.expectError(error.AnalysisFailed, result); +} diff --git a/src/tools/abi-mapper/tests/constant_ordering.zig b/src/tools/abi-mapper/tests/constant_ordering.zig new file mode 100644 index 00000000..c0d0a2f6 --- /dev/null +++ b/src/tools/abi-mapper/tests/constant_ordering.zig @@ -0,0 +1,51 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "constant used as array size after definition succeeds" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // Constant defined before the struct that uses it. + const source = + \\const max_len = 8; + \\struct Buffer { + \\ field data: [max_len]u8; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.structs.len); + // The field's type is an array with size 8 + const fld = doc.structs[0].logic_fields[0]; + const fld_type = doc.get_type(fld.type); + try std.testing.expect(fld_type.* == .array); + try std.testing.expectEqual(@as(u32, 8), fld_type.array.size); +} + +test "constant used as array size before definition emits error" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // Struct references constant that isn't declared yet. + const source = + \\struct Buffer { + \\ field data: [max_len]u8; + \\} + \\const max_len = 8; + ; + const result = parse_and_analyze(allocator, source); + try std.testing.expectError(error.AnalysisFailed, result); +} diff --git a/src/tools/abi-mapper/tests/digit_separator.zig b/src/tools/abi-mapper/tests/digit_separator.zig new file mode 100644 index 00000000..7be161e4 --- /dev/null +++ b/src/tools/abi-mapper/tests/digit_separator.zig @@ -0,0 +1,85 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "hex digit separators" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\enum Foo : u64 { + \\ item infinity = 0xFFFF_FFFF_FFFF_FFFF; + \\ item half = 0x7FFF_FFFF_FFFF_FFFF; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.enums.len); + try std.testing.expectEqual(@as(usize, 2), doc.enums[0].items.len); + try std.testing.expectEqual(@as(i65, @bitCast(@as(u65, 0xFFFF_FFFF_FFFF_FFFF))), doc.enums[0].items[0].value); + try std.testing.expectEqual(@as(i65, 0x7FFF_FFFF_FFFF_FFFF), doc.enums[0].items[1].value); +} + +test "decimal digit separators" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\enum Counts : u32 { + \\ item million = 1_000_000; + \\ item billion = 1_000_000_000; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.enums.len); + try std.testing.expectEqual(@as(i65, 1_000_000), doc.enums[0].items[0].value); + try std.testing.expectEqual(@as(i65, 1_000_000_000), doc.enums[0].items[1].value); +} + +test "binary digit separators" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\enum Bits : u8 { + \\ item pattern = 0b1111_0000; + \\ item nibble = 0b0000_1111; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.enums.len); + try std.testing.expectEqual(@as(i65, 0b1111_0000), doc.enums[0].items[0].value); + try std.testing.expectEqual(@as(i65, 0b0000_1111), doc.enums[0].items[1].value); +} + +test "pointer alignment accepts digit separators" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\struct Buffer { + \\ field data: [*]align(1_6_4) u8; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.structs.len); + + const field = doc.structs[0].logic_fields[0]; + const field_type = doc.types[@intFromEnum(field.type)]; + try std.testing.expect(field_type == .ptr); + try std.testing.expectEqual(@as(?u64, 164), field_type.ptr.alignment); +} diff --git a/src/tools/abi-mapper/tests/doc_parser.zig b/src/tools/abi-mapper/tests/doc_parser.zig new file mode 100644 index 00000000..7ca8ab3b --- /dev/null +++ b/src/tools/abi-mapper/tests/doc_parser.zig @@ -0,0 +1,466 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); +const doc_comment_parser = abi_parser.doc_comment; +const DocComment = abi_parser.model.DocComment; + +fn parse_doc(raw_lines: []const []const u8) !doc_comment_parser.ParsedDocComment { + return doc_comment_parser.parse(std.testing.allocator, raw_lines, .{}); +} + +// from json + +test "empty doc comment from json" { + var comment = try std.json.parseFromSlice(DocComment, std.testing.allocator, "{ \"sections\": [] }", .{}); + defer comment.deinit(); +} + +// ── Empty / blank ──────────────────────────────────────────────────────────── + +test "empty input returns empty DocComment" { + var parsed = try parse_doc(&.{}); + defer parsed.deinit(); + + try std.testing.expectEqual(@as(usize, 0), parsed.comment.sections.len); +} + +test "only blank lines returns empty DocComment" { + var parsed = try parse_doc(&.{ "", "", "" }); + defer parsed.deinit(); + + try std.testing.expectEqual(@as(usize, 0), parsed.comment.sections.len); +} + +// ── Paragraphs ─────────────────────────────────────────────────────────────── + +test "simple paragraph" { + var parsed = try parse_doc(&.{" Hello, world!"}); + defer parsed.deinit(); + + try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections.len); + try std.testing.expectEqual(DocComment.Section.Kind.main, parsed.comment.sections[0].kind); + try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections[0].blocks.len); + + const block = parsed.comment.sections[0].blocks[0]; + try std.testing.expect(block == .paragraph); + try std.testing.expectEqual(@as(usize, 1), block.paragraph.content.len); + try std.testing.expect(block.paragraph.content[0] == .text); + try std.testing.expectEqualStrings("Hello, world!", block.paragraph.content[0].text.value); +} + +test "multi-line paragraph joined with space" { + var parsed = try parse_doc(&.{ + " First line", + " second line", + " third line", + }); + defer parsed.deinit(); + + try std.testing.expectEqual(@as(usize, 1), parsed.comment.sections[0].blocks.len); + + const block = parsed.comment.sections[0].blocks[0]; + try std.testing.expect(block == .paragraph); + try std.testing.expectEqual(@as(usize, 1), block.paragraph.content.len); + try std.testing.expectEqualStrings( + "First line second line third line", + block.paragraph.content[0].text.value, + ); +} + +test "blank line separates paragraphs" { + var parsed = try parse_doc(&.{ + " First paragraph.", + "", + " Second paragraph.", + }); + defer parsed.deinit(); + + try std.testing.expectEqual(@as(usize, 2), parsed.comment.sections[0].blocks.len); + try std.testing.expect(parsed.comment.sections[0].blocks[0] == .paragraph); + try std.testing.expect(parsed.comment.sections[0].blocks[1] == .paragraph); + + try std.testing.expectEqualStrings( + "First paragraph.", + parsed.comment.sections[0].blocks[0].paragraph.content[0].text.value, + ); + try std.testing.expectEqualStrings( + "Second paragraph.", + parsed.comment.sections[0].blocks[1].paragraph.content[0].text.value, + ); +} + +// ── Inline elements ────────────────────────────────────────────────────────── + +test "inline code span" { + var parsed = try parse_doc(&.{" Call `foo()` now."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("Call ", content[0].text.value); + try std.testing.expect(content[1] == .code); + try std.testing.expectEqualStrings("foo()", content[1].code.value); + try std.testing.expect(content[2] == .text); + try std.testing.expectEqualStrings(" now.", content[2].text.value); +} + +test "inline code span applies escapes" { + var parsed = try parse_doc(&.{" Call `\\`` now."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[1] == .code); + try std.testing.expectEqualStrings("`", content[1].code.value); +} + +test "inline code span keeps lone backslash" { + var parsed = try parse_doc(&.{" Path `\\` separator."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[1] == .code); + try std.testing.expectEqualStrings("\\", content[1].code.value); +} + +test "adjacent inline code spans do not swallow a backslash span" { + var parsed = try parse_doc(&.{" Keys `\\` `|`."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 5), content.len); + try std.testing.expect(content[1] == .code); + try std.testing.expectEqualStrings("\\", content[1].code.value); + try std.testing.expect(content[2] == .text); + try std.testing.expectEqualStrings(" ", content[2].text.value); + try std.testing.expect(content[3] == .code); + try std.testing.expectEqualStrings("|", content[3].code.value); +} + +test "cross-reference @`fqn`" { + var parsed = try parse_doc(&.{" See @`foo.bar.Baz` for details."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("See ", content[0].text.value); + try std.testing.expect(content[1] == .ref); + try std.testing.expectEqualStrings("foo.bar.Baz", content[1].ref.fqn); + try std.testing.expect(content[2] == .text); + try std.testing.expectEqualStrings(" for details.", content[2].text.value); +} + +test "legacy @ref syntax stays plain text" { + var parsed = try parse_doc(&.{" See @ref foo.bar.Baz for details."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 1), content.len); + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("See @ref foo.bar.Baz for details.", content[0].text.value); +} + +test "emphasis *text*" { + var parsed = try parse_doc(&.{" This is *important* text."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("This is ", content[0].text.value); + try std.testing.expect(content[1] == .emphasis); + try std.testing.expectEqual(@as(usize, 1), content[1].emphasis.content.len); + try std.testing.expectEqualStrings("important", content[1].emphasis.content[0].text.value); + try std.testing.expect(content[2] == .text); + try std.testing.expectEqualStrings(" text.", content[2].text.value); +} + +test "escape sequences suppress special syntax" { + var parsed = try parse_doc(&.{" Escape: \\`not code\\`."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + // None of the nodes should be a .code span + for (content) |item| { + try std.testing.expect(item != .code); + } + // First text node is "Escape: " + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("Escape: ", content[0].text.value); + // Second text node is the escaped backtick character + try std.testing.expect(content[1] == .text); + try std.testing.expectEqualStrings("`", content[1].text.value); +} + +test "titled link [display](url)" { + var parsed = try parse_doc(&.{" See [the docs](https://example.com/docs)."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("See ", content[0].text.value); + try std.testing.expect(content[1] == .link); + try std.testing.expectEqualStrings("https://example.com/docs", content[1].link.url); + try std.testing.expectEqual(@as(usize, 1), content[1].link.content.len); + try std.testing.expectEqualStrings("the docs", content[1].link.content[0].text.value); + try std.testing.expect(content[2] == .text); + try std.testing.expectEqualStrings(".", content[2].text.value); +} + +test "autolink " { + var parsed = try parse_doc(&.{" Visit ."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[0] == .text); + try std.testing.expectEqualStrings("Visit ", content[0].text.value); + try std.testing.expect(content[1] == .link); + try std.testing.expectEqualStrings("https://example.com", content[1].link.url); + try std.testing.expectEqual(@as(usize, 1), content[1].link.content.len); + try std.testing.expectEqualStrings("https://example.com", content[1].link.content[0].text.value); + try std.testing.expect(content[2] == .text); + try std.testing.expectEqualStrings(".", content[2].text.value); +} + +test "autolink " { + var parsed = try parse_doc(&.{" Mail us."}); + defer parsed.deinit(); + + const content = parsed.comment.sections[0].blocks[0].paragraph.content; + try std.testing.expectEqual(@as(usize, 3), content.len); + try std.testing.expect(content[1] == .link); + try std.testing.expectEqualStrings("mailto:foo@example.com", content[1].link.url); +} + +test "unclosed inline code span is rejected" { + try std.testing.expectError(error.UnclosedInlineCode, parse_doc(&.{" Broken `code"})); +} + +test "unclosed inline reference is rejected" { + try std.testing.expectError(error.UnclosedInlineReference, parse_doc(&.{" Broken @`Type.member"})); +} + +test "unclosed titled link is rejected" { + try std.testing.expectError(error.UnclosedInlineLink, parse_doc(&.{" Broken [docs](https://example.com"})); +} + +test "unclosed autolink is rejected" { + try std.testing.expectError(error.UnclosedAutolink, parse_doc(&.{" Broken 0); +} + +fn analyze_and_roundtrip_json(allocator: std.mem.Allocator, path: []const u8) !std.json.Parsed(model.Document) { + const analyzed_document = try analyze_file(allocator, path); + + var json: std.ArrayList(u8) = .empty; + defer json.deinit(allocator); + try model.to_json_str(analyzed_document, json.writer(allocator)); + + return model.from_json_str(allocator, json.items); +} + +fn analyze_file(allocator: std.mem.Allocator, path: []const u8) !model.Document { + const abi_source = try std.fs.cwd().readFileAlloc(allocator, path, 1 << 20); + + var tokenizer: abi_parser.syntax.Tokenizer = .init(abi_source, path); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast_document = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast_document, null, &errors); +} + +fn find_syscall_by_fqn( + syscalls: []const model.GenericCall, + expected: []const u8, +) ?model.GenericCall { + for (syscalls) |syscall| { + if (fqn_equals(syscall.full_qualified_name, expected)) { + return syscall; + } + } + return null; +} + +fn find_struct_by_fqn( + structs: []const model.Struct, + expected: []const u8, +) ?model.Struct { + for (structs) |item| { + if (fqn_equals(item.full_qualified_name, expected)) { + return item; + } + } + return null; +} + +fn find_enum_by_fqn( + enums: []const model.Enumeration, + expected: []const u8, +) ?model.Enumeration { + for (enums) |item| { + if (fqn_equals(item.full_qualified_name, expected)) { + return item; + } + } + return null; +} + +fn find_enum_item_by_name(items: []const model.EnumItem, name: []const u8) ?model.EnumItem { + for (items) |item| { + if (std.mem.eql(u8, item.name, name)) { + return item; + } + } + return null; +} + +fn fqn_equals(fqn: model.FQN, expected: []const u8) bool { + var parts = std.mem.splitScalar(u8, expected, '.'); + var index: usize = 0; + while (parts.next()) |part| { + if (part.len == 0 or index >= fqn.len) { + return false; + } + if (!std.mem.eql(u8, fqn[index], part)) { + return false; + } + index += 1; + } + return index == fqn.len; +} + +fn has_ref_fqn(docs: model.DocComment, expected: []const u8) bool { + for (docs.sections) |section| { + for (section.blocks) |block| { + switch (block) { + .paragraph => |paragraph| { + if (inlines_have_ref_fqn(paragraph.content, expected)) return true; + }, + .unordered_list => |list| { + for (list.items) |item| { + if (inlines_have_ref_fqn(item, expected)) return true; + } + }, + .ordered_list => |list| { + for (list.items) |item| { + if (inlines_have_ref_fqn(item, expected)) return true; + } + }, + .code_block => {}, + } + } + } + return false; +} + +fn inlines_have_ref_fqn(inlines: []const model.DocComment.Inline, expected: []const u8) bool { + for (inlines) |inl| { + switch (inl) { + .ref => |r| { + if (std.mem.eql(u8, r.fqn, expected)) return true; + }, + .emphasis => |e| { + if (inlines_have_ref_fqn(e.content, expected)) return true; + }, + .link => |l| { + if (inlines_have_ref_fqn(l.content, expected)) return true; + }, + .text, .code => {}, + } + } + return false; +} + +fn has_code_value(docs: model.DocComment, expected: []const u8) bool { + for (docs.sections) |section| { + for (section.blocks) |block| { + switch (block) { + .paragraph => |paragraph| { + if (inlines_have_code_value(paragraph.content, expected)) return true; + }, + .unordered_list => |list| { + for (list.items) |item| { + if (inlines_have_code_value(item, expected)) return true; + } + }, + .ordered_list => |list| { + for (list.items) |item| { + if (inlines_have_code_value(item, expected)) return true; + } + }, + .code_block => {}, + } + } + } + return false; +} + +fn inlines_have_code_value(inlines: []const model.DocComment.Inline, expected: []const u8) bool { + for (inlines) |inl| { + switch (inl) { + .code => |code| { + if (std.mem.eql(u8, code.value, expected)) return true; + }, + .emphasis => |e| { + if (inlines_have_code_value(e.content, expected)) return true; + }, + .link => |l| { + if (inlines_have_code_value(l.content, expected)) return true; + }, + .text, .ref => {}, + } + } + return false; +} diff --git a/src/tools/abi-mapper/tests/doc_ref_resolution.abi b/src/tools/abi-mapper/tests/doc_ref_resolution.abi new file mode 100644 index 00000000..87ca8a58 --- /dev/null +++ b/src/tools/abi-mapper/tests/doc_ref_resolution.abi @@ -0,0 +1,44 @@ +namespace overlapped { + /// Handle to an asynchronously running operation. + struct ARC { + field tag: usize; + } + + /// Awaits one or more scheduled asynchronous operations and returns the + /// number of @`completed` elements. + /// + /// The kernel will fill out @`completed` up to @`completed_count` elements. + /// + /// RELATES: @`await_completion_of` + syscall await_completion { + in completed: []*ARC; + out completed_count: usize; + error Unscheduled; + } + + /// Awaits one or more explicit asynchronous operations. + /// + /// NOTE: Use @`await_completion` when ordering of @`events` is irrelevant. + syscall await_completion_of { + in events: []?*ARC; + out completed_count: usize; + error Unscheduled; + } + + struct Await_Options { + field wait: Wait; + field thread_affinity: Thread_Affinity; + + enum Thread_Affinity : u8 { + item all_threads; + item this_thread; + } + + enum Wait : u8 { + item wait_one = 1; + + /// NOTE: If @`thread_affinity` is @`Thread_Affinity.all_threads`, waiting can take longer. + item wait_all = 2; + } + } +} diff --git a/src/tools/abi-mapper/tests/doc_ref_resolution.zig b/src/tools/abi-mapper/tests/doc_ref_resolution.zig new file mode 100644 index 00000000..1db8b70e --- /dev/null +++ b/src/tools/abi-mapper/tests/doc_ref_resolution.zig @@ -0,0 +1,208 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); +const model = abi_parser.model; + +test "doc references resolve to contained syscall elements" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const abi_source = try std.fs.cwd().readFileAlloc( + allocator, + "tests/doc_ref_resolution.abi", + 1 << 20, + ); + + var tokenizer: abi_parser.syntax.Tokenizer = .init(abi_source, "tests/doc_ref_resolution.abi"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast_document = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + const analyzed_document = try abi_parser.sema.analyze(allocator, ast_document, null, &errors); + + const await_completion = find_syscall_by_fqn( + analyzed_document.syscalls, + "overlapped.await_completion", + ) orelse return error.TestUnexpectedResult; + + try std.testing.expect(has_ref_fqn( + await_completion.docs, + "overlapped.await_completion.completed", + )); + try std.testing.expect(has_ref_fqn( + await_completion.docs, + "overlapped.await_completion.completed_count", + )); + try std.testing.expect(has_ref_fqn( + await_completion.docs, + "overlapped.await_completion_of", + )); + try std.testing.expect(!has_ref_fqn(await_completion.docs, "completed")); + + const await_completion_of = find_syscall_by_fqn( + analyzed_document.syscalls, + "overlapped.await_completion_of", + ) orelse return error.TestUnexpectedResult; + + try std.testing.expect(has_ref_fqn( + await_completion_of.docs, + "overlapped.await_completion", + )); + try std.testing.expect(has_ref_fqn( + await_completion_of.docs, + "overlapped.await_completion_of.events", + )); + + const wait_enum = find_enum_by_fqn( + analyzed_document.enums, + "overlapped.Await_Options.Wait", + ) orelse return error.TestUnexpectedResult; + const wait_all_item = find_enum_item_by_name( + wait_enum.items, + "wait_all", + ) orelse return error.TestUnexpectedResult; + + try std.testing.expect(has_ref_fqn( + wait_all_item.docs, + "overlapped.Await_Options.thread_affinity", + )); + try std.testing.expect(has_ref_fqn( + wait_all_item.docs, + "overlapped.Await_Options.Thread_Affinity.all_threads", + )); + try std.testing.expect(!has_ref_fqn(wait_all_item.docs, "thread_affinity")); +} + +test "doc references reject invalid trailing members after a resolved prefix" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\struct Type { + \\ field member: u32; + \\} + \\ + \\/// See @`Type.not_a_member`. + \\struct Container { + \\ field value: u32; + \\} + ; + + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast_document = try parser.accept_document(); + + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + + const result = abi_parser.sema.analyze(allocator, ast_document, null, &errors); + try std.testing.expectError(error.AnalysisFailed, result); + try std.testing.expect(errors.items.len > 0); + try std.testing.expect(error_messages_contain(errors.items, "unknown doc reference '@`Type.not_a_member`'")); +} + +fn find_syscall_by_fqn( + syscalls: []const model.GenericCall, + expected: []const u8, +) ?model.GenericCall { + for (syscalls) |syscall| { + if (fqn_equals(syscall.full_qualified_name, expected)) { + return syscall; + } + } + return null; +} + +fn find_enum_by_fqn( + enums: []const model.Enumeration, + expected: []const u8, +) ?model.Enumeration { + for (enums) |item| { + if (fqn_equals(item.full_qualified_name, expected)) { + return item; + } + } + return null; +} + +fn find_enum_item_by_name(items: []const model.EnumItem, name: []const u8) ?model.EnumItem { + for (items) |item| { + if (std.mem.eql(u8, item.name, name)) { + return item; + } + } + return null; +} + +fn fqn_equals(fqn: model.FQN, expected: []const u8) bool { + var parts = std.mem.splitScalar(u8, expected, '.'); + var index: usize = 0; + while (parts.next()) |part| { + if (part.len == 0 or index >= fqn.len) { + return false; + } + if (!std.mem.eql(u8, fqn[index], part)) { + return false; + } + index += 1; + } + return index == fqn.len; +} + +fn has_ref_fqn(docs: model.DocComment, expected: []const u8) bool { + for (docs.sections) |section| { + for (section.blocks) |block| { + switch (block) { + .paragraph => |paragraph| { + if (inlines_have_ref_fqn(paragraph.content, expected)) return true; + }, + .unordered_list => |list| { + for (list.items) |item| { + if (inlines_have_ref_fqn(item, expected)) return true; + } + }, + .ordered_list => |list| { + for (list.items) |item| { + if (inlines_have_ref_fqn(item, expected)) return true; + } + }, + .code_block => {}, + } + } + } + return false; +} + +fn error_messages_contain(errors: []const abi_parser.sema.AnalysisError, needle: []const u8) bool { + for (errors) |item| { + if (std.mem.indexOf(u8, item.message, needle) != null) { + return true; + } + } + return false; +} + +fn inlines_have_ref_fqn(inlines: []const model.DocComment.Inline, expected: []const u8) bool { + for (inlines) |inl| { + switch (inl) { + .ref => |r| { + if (std.mem.eql(u8, r.fqn, expected)) return true; + }, + .emphasis => |e| { + if (inlines_have_ref_fqn(e.content, expected)) return true; + }, + .link => |l| { + if (inlines_have_ref_fqn(l.content, expected)) return true; + }, + .text, .code => {}, + } + } + return false; +} diff --git a/src/tools/abi-mapper/tests/fnptr_named_params.zig b/src/tools/abi-mapper/tests/fnptr_named_params.zig new file mode 100644 index 00000000..6b38cfe7 --- /dev/null +++ b/src/tools/abi-mapper/tests/fnptr_named_params.zig @@ -0,0 +1,82 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +fn find_fnptr(doc: abi_parser.model.Document) ?abi_parser.model.FunctionPointer { + for (doc.types) |t| { + switch (t) { + .fnptr => |fp| return fp, + else => {}, + } + } + return null; +} + +test "named parameters in fnptr are stored" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\typedef Handler = fnptr(context: *u8, value: u32) void; + ; + const doc = try parse_and_analyze(allocator, source); + const fp = find_fnptr(doc) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(@as(usize, 2), fp.parameters.len); + try std.testing.expectEqualStrings("context", fp.parameters[0].name.?); + try std.testing.expectEqualStrings("value", fp.parameters[1].name.?); +} + +test "unnamed parameters in fnptr store null" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\typedef Callback = fnptr(*u8, u32) void; + ; + const doc = try parse_and_analyze(allocator, source); + const fp = find_fnptr(doc) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(@as(usize, 2), fp.parameters.len); + try std.testing.expectEqual(@as(?[]const u8, null), fp.parameters[0].name); + try std.testing.expectEqual(@as(?[]const u8, null), fp.parameters[1].name); +} + +test "mixed named and unnamed parameters" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\typedef Mixed = fnptr(ctx: *u8, u32) void; + ; + const doc = try parse_and_analyze(allocator, source); + const fp = find_fnptr(doc) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(@as(usize, 2), fp.parameters.len); + try std.testing.expectEqualStrings("ctx", fp.parameters[0].name.?); + try std.testing.expectEqual(@as(?[]const u8, null), fp.parameters[1].name); +} + +test "fnptr with no parameters" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\typedef Thunk = fnptr() void; + ; + const doc = try parse_and_analyze(allocator, source); + const fp = find_fnptr(doc) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(@as(usize, 0), fp.parameters.len); +} diff --git a/src/tools/abi-mapper/tests/nonstandard_backing_type.zig b/src/tools/abi-mapper/tests/nonstandard_backing_type.zig new file mode 100644 index 00000000..9eb5dab7 --- /dev/null +++ b/src/tools/abi-mapper/tests/nonstandard_backing_type.zig @@ -0,0 +1,101 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "enum with u2 backing type" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\enum FileType : u2 { + \\ item unknown = 0; + \\ item file = 1; + \\ item dir = 2; + \\ item symlink = 3; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.enums.len); + const e = doc.enums[0]; + // bit_count must reflect the declared u2 width + try std.testing.expectEqual(@as(u8, 2), e.bit_count); + // backing_type must be rounded up to the next standard type (u8) + try std.testing.expectEqual(abi_parser.model.StandardType.u8, e.backing_type); + try std.testing.expectEqual(@as(usize, 4), e.items.len); +} + +test "enum with u10 backing type" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\enum Wide : u10 { + \\ item a = 0; + \\ item b = 1023; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.enums.len); + const e = doc.enums[0]; + try std.testing.expectEqual(@as(u8, 10), e.bit_count); + try std.testing.expectEqual(abi_parser.model.StandardType.u16, e.backing_type); +} + +test "enum with non-integer backing type emits error" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\struct BadType { field x: u8; } + \\enum Bad : BadType { + \\ item a = 0; + \\} + ; + const result = parse_and_analyze(allocator, source); + try std.testing.expectError(error.AnalysisFailed, result); +} + +test "bitstruct with u2 enum field packs correctly" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // FileType : u2 → bit_count = 2; bitstruct uses 2+2 = 4 bits, fits in u8. + const source = + \\enum FileType : u2 { + \\ item unknown = 0; + \\ item file = 1; + \\} + \\bitstruct Pair : u8 { + \\ field a: FileType; + \\ field b: FileType; + \\ reserve u4 = 0; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.bitstructs.len); + const bs = doc.bitstructs[0]; + // Two 2-bit fields + 4-bit reserve = 8 bits total + try std.testing.expectEqual(@as(u8, 8), bs.bit_count); + try std.testing.expectEqual(@as(usize, 3), bs.fields.len); + // First field: bit_shift=0, bit_count=2 + try std.testing.expectEqual(@as(?u8, 0), bs.fields[0].bit_shift); + try std.testing.expectEqual(@as(?u8, 2), bs.fields[0].bit_count); + // Second field: bit_shift=2, bit_count=2 + try std.testing.expectEqual(@as(?u8, 2), bs.fields[1].bit_shift); + try std.testing.expectEqual(@as(?u8, 2), bs.fields[1].bit_count); +} diff --git a/src/tools/abi-mapper/tests/optional_type_handling.zig b/src/tools/abi-mapper/tests/optional_type_handling.zig new file mode 100644 index 00000000..4ade294f --- /dev/null +++ b/src/tools/abi-mapper/tests/optional_type_handling.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "optional fnptr parameter passes through to native params" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\typedef Callback = fnptr() void; + \\syscall do_thing { + \\ in cb: ?Callback; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.syscalls.len); + const sc = doc.syscalls[0]; + // The optional fnptr should pass through to native_inputs unchanged. + try std.testing.expectEqual(@as(usize, 1), sc.native_inputs.len); + try std.testing.expectEqualStrings("cb", sc.native_inputs[0].name); +} + +test "optional u32 parameter emits error" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\syscall bad_call { + \\ in x: ?u32; + \\} + ; + const result = parse_and_analyze(allocator, source); + try std.testing.expectError(error.AnalysisFailed, result); +} diff --git a/src/tools/abi-mapper/tests/syscall_output_count.zig b/src/tools/abi-mapper/tests/syscall_output_count.zig new file mode 100644 index 00000000..19841607 --- /dev/null +++ b/src/tools/abi-mapper/tests/syscall_output_count.zig @@ -0,0 +1,75 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "syscall with zero out params is valid" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\syscall do_work { + \\ in x: u32; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.syscalls.len); + try std.testing.expectEqual(@as(usize, 0), doc.syscalls[0].logic_outputs.len); +} + +test "syscall with one out param is valid" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\syscall get_value { + \\ out result: u32; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.syscalls.len); + try std.testing.expectEqual(@as(usize, 1), doc.syscalls[0].logic_outputs.len); +} + +test "syscall with two out params emits error" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\syscall two_outputs { + \\ out a: u32; + \\ out b: u32; + \\} + ; + const result = parse_and_analyze(allocator, source); + try std.testing.expectError(error.AnalysisFailed, result); +} + +test "async_call with multiple out params is valid (not a syscall)" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const source = + \\async_call read_data { + \\ out bytes_read: u32; + \\ out eof: bool; + \\} + ; + const doc = try parse_and_analyze(allocator, source); + try std.testing.expectEqual(@as(usize, 1), doc.async_calls.len); + try std.testing.expectEqual(@as(usize, 2), doc.async_calls[0].logic_outputs.len); +} diff --git a/src/tools/abi-mapper/tests/testsuite.zig b/src/tools/abi-mapper/tests/testsuite.zig new file mode 100644 index 00000000..f5fd0877 --- /dev/null +++ b/src/tools/abi-mapper/tests/testsuite.zig @@ -0,0 +1,13 @@ +comptime { + _ = @import("doc_parser.zig"); + _ = @import("doc_ref_emission.zig"); + _ = @import("doc_ref_resolution.zig"); + _ = @import("digit_separator.zig"); + _ = @import("constant_ordering.zig"); + _ = @import("nonstandard_backing_type.zig"); + _ = @import("optional_type_handling.zig"); + _ = @import("unknown_named_type.zig"); + _ = @import("bitstruct_array_field.zig"); + _ = @import("syscall_output_count.zig"); + _ = @import("fnptr_named_params.zig"); +} diff --git a/src/tools/abi-mapper/tests/unknown_named_type.zig b/src/tools/abi-mapper/tests/unknown_named_type.zig new file mode 100644 index 00000000..e838fa54 --- /dev/null +++ b/src/tools/abi-mapper/tests/unknown_named_type.zig @@ -0,0 +1,30 @@ +const std = @import("std"); +const abi_parser = @import("abi-parser"); + +fn parse_and_analyze(allocator: std.mem.Allocator, source: []const u8) !abi_parser.model.Document { + var tokenizer: abi_parser.syntax.Tokenizer = .init(source, "test"); + var parser: abi_parser.syntax.Parser = .{ + .allocator = allocator, + .core = .init(&tokenizer), + }; + const ast = try parser.accept_document(); + var errors: std.ArrayList(abi_parser.sema.AnalysisError) = .empty; + defer errors.deinit(allocator); + return abi_parser.sema.analyze(allocator, ast, null, &errors); +} + +test "struct with undefined field type emits error without crashing" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + // References a type that does not exist — should produce an error, not a panic. + const source = + \\struct Broken { + \\ field x: NonExistentType; + \\ field y: u32; + \\} + ; + const result = parse_and_analyze(allocator, source); + try std.testing.expectError(error.AnalysisFailed, result); +} diff --git a/src/website/build.zig b/src/website/build.zig index 887dc0e1..6cfbec72 100644 --- a/src/website/build.zig +++ b/src/website/build.zig @@ -4,7 +4,7 @@ const kernel_package = @import("kernel"); const Machine = kernel_package.Machine; -pub fn build(b: *std.Build) void { +pub fn build(b: *std.Build) void { // $ls root_id 1 const install_step = b.getInstallStep(); const os_dep = b.dependency("os", .{ diff --git a/src/website/src/syscalls-gen.zig b/src/website/src/syscalls-gen.zig index 399e7072..dcd96630 100644 --- a/src/website/src/syscalls-gen.zig +++ b/src/website/src/syscalls-gen.zig @@ -9,6 +9,7 @@ const model = abi_parser.model; const render_page_file = website_gen.render_page_file; const fmt_html = website_gen.fmt_html; const fmt_url = website_gen.fmt_url; +const fmt_attr = website_gen.fmt_attr; const Context = struct { root_dir: std.fs.Dir, @@ -41,9 +42,7 @@ pub fn render(output_dir: std.fs.Dir, schema: abi_parser.model.Document, allocat defer tree.stack.deinit(allocator); try tree.render_declaration(syscalls_dst_dir, "ashet", 0, .{ - .docs = &.{ - // TODO: can we add top-level namespace docs?! - }, + .docs = .empty, // TODO: can we add top-level namespace docs?! .children = schema.root, .full_qualified_name = &.{}, .data = .namespace, @@ -123,10 +122,10 @@ const PageRenderer = struct { }, ); - if (decl.docs.len > 0) { + if (!decl.docs.is_empty()) { try html.writer.writeAll("
\n"); try html.writer.print("

Documentation

\n", .{}); - try html.writer.print("{f}\n", .{fmt_docs(decl.docs)}); + try html.writer.print("{f}\n", .{html.fmt_docs(decl.docs)}); try html.writer.writeAll("
\n"); } @@ -139,7 +138,10 @@ const PageRenderer = struct { try html.begin_dl(); for (item.logic_fields) |field| { - try html.writer.writeAll("
\n"); + try html.writer.print("
\n", .{ + fmt_fqn(item.full_qualified_name), + field.name, + }); try html.writer.print("
{s}: {f}", .{ field.name, @@ -152,8 +154,8 @@ const PageRenderer = struct { try html.writer.writeAll("
"); - if (field.docs.len > 0) { - try html.writer.print("
{f}
\n", .{fmt_docs(field.docs)}); + if (!field.docs.is_empty()) { + try html.writer.print("
{f}
\n", .{html.fmt_docs(field.docs)}); } try html.writer.writeAll("
\n"); @@ -168,12 +170,14 @@ const PageRenderer = struct { try html.begin_dl(); for (item.logic_fields) |field| { try html.dl_item( + item.full_qualified_name, + field.name, "{s}: {f}", "{f}", .{ field.name, html.fmt_type(field.type), - fmt_docs(field.docs), + html.fmt_docs(field.docs), }, ); } @@ -189,18 +193,22 @@ const PageRenderer = struct { try html.begin_dl(); for (enumeration.items) |item| { try html.dl_item( + enumeration.full_qualified_name, + item.name, "{s} = {}", "{f}", .{ item.name, item.value, - fmt_docs(item.docs), + html.fmt_docs(item.docs), }, ); } switch (enumeration.kind) { .open => try html.dl_item( + enumeration.full_qualified_name, + "...", "...", "

This enumeration is non-exhaustive and may assume all values a {s} can represent.

", .{@tagName(enumeration.backing_type)}, @@ -216,7 +224,10 @@ const PageRenderer = struct { try html.begin_dl(); for (item.fields) |field| { - try html.writer.writeAll("
\n"); + try html.writer.print("
\n", .{ + fmt_fqn(item.full_qualified_name), + field.name orelse "", + }); try html.writer.print("
{s}: {f}", .{ field.name orelse "reserved", @@ -229,8 +240,8 @@ const PageRenderer = struct { try html.writer.writeAll("
"); - if (field.docs.len > 0) { - try html.writer.print("
{f}
\n", .{fmt_docs(field.docs)}); + if (!field.docs.is_empty()) { + try html.writer.print("
{f}
\n", .{html.fmt_docs(field.docs)}); } try html.writer.writeAll("
\n"); @@ -247,12 +258,14 @@ const PageRenderer = struct { for (item.logic_inputs) |param| { // TODO: Handle "param.default" try html.dl_item( + item.full_qualified_name, + param.name, "{f}: {f}", "{f}", .{ std.zig.fmtId(param.name), html.fmt_type(param.type), - fmt_docs(param.docs), + html.fmt_docs(param.docs), }, ); } @@ -266,12 +279,14 @@ const PageRenderer = struct { for (item.logic_outputs) |param| { // TODO: Handle "param.default" try html.dl_item( + item.full_qualified_name, + param.name, "{f}: {f}", "{f}", .{ std.zig.fmtId(param.name), html.fmt_type(param.type), - fmt_docs(param.docs), + html.fmt_docs(param.docs), }, ); } @@ -284,11 +299,13 @@ const PageRenderer = struct { try html.begin_dl(); for (item.errors) |err| { try html.dl_item( + item.full_qualified_name, + err.name, "{f}", "{f}", .{ std.zig.fmtId(err.name), - fmt_docs(err.docs), + html.fmt_docs(err.docs), }, ); } @@ -305,12 +322,14 @@ const PageRenderer = struct { for (item.logic_inputs) |param| { // TODO: Handle "param.default" try html.dl_item( + item.full_qualified_name, + param.name, "{f}: {f}", "{f}", .{ std.zig.fmtId(param.name), html.fmt_type(param.type), - fmt_docs(param.docs), + html.fmt_docs(param.docs), }, ); } @@ -324,12 +343,14 @@ const PageRenderer = struct { for (item.logic_outputs) |param| { // TODO: Handle "param.default" try html.dl_item( + item.full_qualified_name, + param.name, "{f}: {f}", "{f}", .{ std.zig.fmtId(param.name), html.fmt_type(param.type), - fmt_docs(param.docs), + html.fmt_docs(param.docs), }, ); } @@ -342,11 +363,13 @@ const PageRenderer = struct { try html.begin_dl(); for (item.errors) |err| { try html.dl_item( + item.full_qualified_name, + err.name, "{f}", "{f}", .{ std.zig.fmtId(err.name), - fmt_docs(err.docs), + html.fmt_docs(err.docs), }, ); } @@ -429,9 +452,15 @@ const PageRenderer = struct { try html.writer.writeAll("\n"); } - fn dl_item(html: *PageRenderer, comptime dt_fmt: []const u8, comptime dd_fmt: []const u8, args: anytype) !void { + fn dl_item(html: *PageRenderer, fqn: []const []const u8, local_name: ?[]const u8, comptime dt_fmt: []const u8, comptime dd_fmt: []const u8, args: anytype) !void { + if (local_name) |name| { + try html.writer.print("
", .{ fmt_fqn(fqn), name }); + } else { + try html.writer.print("
", .{fmt_fqn(fqn)}); + } + try html.writer.print( - "
\n
" ++ dt_fmt ++ "
" ++ dd_fmt ++ "
\n", + "
" ++ dt_fmt ++ "
" ++ dd_fmt ++ "
\n", args, ); } @@ -458,7 +487,7 @@ const PageRenderer = struct { try html.writer.print("

{s}

\n", .{title}); - try html.writer.writeAll("
    \n"); + try html.writer.writeAll("
      \n"); for (decl.children) |child| { if (!contains_tag(child.data, tags)) continue; @@ -494,7 +523,7 @@ const PageRenderer = struct { if (child.data != tag) continue; - try html.writer.writeAll("
      \n"); + try html.writer.print("
      \n", .{fmt_fqn(child.full_qualified_name)}); try html.writer.print( \\
      {s} {s}( @@ -543,12 +572,12 @@ const PageRenderer = struct { } try html.writer.writeAll("
      "); - if (child.docs.len > 0) { + if (!child.docs.is_empty()) { try html.writer.print( \\
      {f}
      \\ , .{ - fmt_docs(child.docs), + DocFmt{ .html = html, .docs = child.docs, .mode = .only_main }, }); } @@ -610,8 +639,11 @@ const PageRenderer = struct { for (fnptr.parameters, 0..) |param, index| { if (index > 0) try writer.writeAll(", "); + if (param.name) |name| { + try writer.print("{f}: ", .{std.zig.fmtId(name)}); + } try writer.print("{f}", .{ - self.html.fmt_type(param), + self.html.fmt_type(param.type), }); } @@ -637,10 +669,14 @@ const PageRenderer = struct { fn fmt_known_type(html: *PageRenderer, writer: *std.Io.Writer, known_type: anytype) !void { try writer.print("{f}", .{ html.fmt_page_url(known_type.full_qualified_name), - fmt_fqn(known_type.full_qualified_name), + html.fmt_lqn(known_type.full_qualified_name), }); } + fn fmt_lqn(html: *PageRenderer, fqn: model.FQN) LqnFmt { + return .{ .html = html, .fqn = fqn }; + } + fn format_value(value: model.Value, writer: *std.Io.Writer) !void { return format_value_inner(value, writer, 1); } @@ -684,6 +720,45 @@ const PageRenderer = struct { } try writer.writeAll("index.html"); } + + fn fmt_docs(html: *PageRenderer, docs: model.DocComment) DocFmt { + return .{ .docs = docs, .html = html }; + } + + fn find_declaration(html: *PageRenderer, fqn_str: []const u8) ?model.Declaration { + var current: []const model.Declaration = html.schema.root; + var pos: usize = 0; + while (pos <= fqn_str.len) { + const end = std.mem.indexOfScalarPos(u8, fqn_str, pos, '.') orelse fqn_str.len; + const part = fqn_str[pos..end]; + const found = for (current) |decl| { + if (std.mem.eql(u8, decl.full_qualified_name[decl.full_qualified_name.len - 1], part)) + break decl; + } else return null; + if (end == fqn_str.len) return found; + current = found.children; + pos = end + 1; + } + return null; + } +}; + +const LqnFmt = struct { + html: *PageRenderer, + fqn: model.FQN, + + pub fn format(self: LqnFmt, writer: *std.Io.Writer) !void { + // Count how many leading components of fqn match scope_fqn[1..] (skip root "ashet") + const scope = self.html.scope_fqn[1..]; + var skip: usize = 0; + while (skip < scope.len and skip < self.fqn.len and + std.mem.eql(u8, scope[skip], self.fqn[skip])) : (skip += 1) + {} + for (self.fqn[skip..], 0..) |part, i| { + if (i > 0) try writer.writeAll("."); + try writer.writeAll(part); + } + } }; fn render_page_header(writer: *std.Io.Writer, namespace_fqn: abi_parser.model.FQN, nesting: usize) !void { @@ -738,85 +813,130 @@ fn format_fqn(fqn: []const []const u8, writer: *std.Io.Writer) !void { } } -fn fmt_docs(docs: []const []const u8) std.fmt.Alt([]const []const u8, format_docs) { - return .{ .data = docs }; -} - -fn format_docs(docs: []const []const u8, writer: *std.Io.Writer) !void { - if (docs.len == 0) - return; - - const BlockType = enum { note, lore, relates }; +const DocFmt = struct { + docs: model.DocComment, + html: *PageRenderer, + mode: enum { default, only_main } = .default, - var last_was_empty = false; - - try writer.writeAll("

      "); - - for (docs) |line| { - if (line.len == 0) { - last_was_empty = true; - continue; - } + pub fn format(self: DocFmt, writer: *std.Io.Writer) !void { + if (self.docs.is_empty()) + return; - var requires_new_paragraph = false; + for (self.docs.sections) |section| { + switch (self.mode) { + .default => {}, + .only_main => if (section.kind != .main) + continue, + } - if (last_was_empty) { - requires_new_paragraph = true; - last_was_empty = false; - } + try writer.print("

      \n", .{section.kind}); + + for (section.blocks) |block| { + switch (block) { + .paragraph => |p| { + try writer.writeAll("

      \n"); + try self.format_inlines(p.content, writer); + try writer.writeAll("

      \n"); + }, + + .ordered_list => |list| { + try writer.writeAll("
        \n"); + for (list.items) |item| { + try writer.writeAll("
      1. \n"); + try self.format_inlines(item, writer); + try writer.writeAll("
      2. \n"); + } + try writer.writeAll("
      \n"); + }, + .unordered_list => |list| { + try writer.writeAll("
        \n"); + for (list.items) |item| { + try writer.writeAll("
      • \n"); + try self.format_inlines(item, writer); + try writer.writeAll("
      • \n"); + } + try writer.writeAll("
      \n"); + }, + + .code_block => |code| { + try writer.writeAll("
      ");
      +                        try writer.print("{f}", .{fmt_attr(code.content)});
      +                        try writer.writeAll("
      \n"); + }, + } + } - var out_line = std.mem.trim(u8, line, " "); - var change_block_type: ?BlockType = null; - - if (std.mem.startsWith(u8, out_line, "NOTE:")) { - change_block_type = .note; - requires_new_paragraph = true; - out_line = out_line[5..]; - } else if (std.mem.startsWith(u8, out_line, "LORE:")) { - change_block_type = .lore; - requires_new_paragraph = true; - out_line = out_line[5..]; - } else if (std.mem.startsWith(u8, out_line, "RELATES:")) { - change_block_type = .relates; - requires_new_paragraph = true; - out_line = out_line[8..]; + try writer.writeAll("
      \n"); } + } - if (change_block_type) |block_type| { - std.debug.assert(requires_new_paragraph == true); - - try writer.writeAll("

      "); - - try writer.print("

      {s}

      ", .{ - @tagName(block_type), - switch (block_type) { - .lore => "Lore:", - .note => "Note:", - .relates => "Related Elements:", + fn format_inlines(self: DocFmt, inlines: []const model.DocComment.Inline, writer: *std.Io.Writer) !void { + for (inlines) |span| { + switch (span) { + .text => |text| try writer.print("{f}", .{fmt_html(text.value)}), + .code => |code| try writer.print("{f}", .{fmt_html(code.value)}), + .emphasis => |emphasis| { + try writer.writeAll(""); + try self.format_inlines(emphasis.content, writer); + try writer.writeAll(""); }, - }); - } else if (requires_new_paragraph) { - try writer.writeAll("

      \n

      "); - } - - out_line = std.mem.trimRight(u8, out_line, " \t\r\n"); - - var in_code = false; - for (out_line) |char| { - switch (char) { - '`' => { - in_code = !in_code; - if (in_code) { - try writer.writeAll(""); + .ref => |ref| { + var url_buffer: [512]u8 = undefined; + var url_writer: std.Io.Writer = .fixed(&url_buffer); + + try url_writer.splatBytesAll("../", self.html.scope_fqn.len - 1); + + if (self.html.find_declaration(ref.fqn) != null) { + // Declaration: all parts become path segments, links to its own index.html + var pos: usize = 0; + while (pos < ref.fqn.len) { + const end = std.mem.indexOfScalarPos(u8, ref.fqn, pos, '.') orelse ref.fqn.len; + try url_writer.print("{s}/", .{ref.fqn[pos..end]}); + pos = end + 1; + } + try url_writer.writeAll("index.html"); } else { - try writer.writeAll(""); + // Sub-item: parent path segments + fragment anchor + var pos: usize = 0; + while (pos < ref.fqn.len) { + const split = std.mem.indexOfScalarPos(u8, ref.fqn, pos, '.') orelse break; + try url_writer.print("{s}/", .{ref.fqn[pos..split]}); + pos = split + 1; + } + try url_writer.print("index.html#{s}", .{ref.fqn}); } + + try writer.print("", .{fmt_url(url_writer.buffered(), 0)}); + try writer.print("{s}", .{self.local_ref_display(ref.fqn)}); + try writer.writeAll(""); + }, + .link => |link| { + try writer.print("", .{fmt_attr(link.url)}); + try self.format_inlines(link.content, writer); + try writer.writeAll(""); }, - else => try writer.writeByte(char), } } + } - try writer.writeAll("\n"); + /// Returns the locally qualified display name for a dot-joined ref FQN + /// relative to the current scope. Strips the scope prefix (everything + /// after the root "ashet" component) when the ref shares it. + fn local_ref_display(self: DocFmt, ref_fqn: []const u8) []const u8 { + var pos: usize = 0; + // scope_fqn[0] is the root "ashet"; refs don't include it, so start at [1] + for (self.html.scope_fqn[1..]) |part| { + if (!std.mem.startsWith(u8, ref_fqn[pos..], part)) break; + const next = pos + part.len; + if (next >= ref_fqn.len or ref_fqn[next] != '.') break; + pos = next + 1; + } + return ref_fqn[pos..]; } - try writer.writeAll("

      "); -} +}; diff --git a/src/website/www/theme.css b/src/website/www/theme.css index 15dfb85f..3d57e719 100644 --- a/src/website/www/theme.css +++ b/src/website/www/theme.css @@ -225,7 +225,7 @@ div.flex-spacer { margin-bottom: 0.5em; } -.docs-container ul { +.docs-container ul.basic-list { column-width: 15em; margin-left: 2em; } @@ -242,12 +242,70 @@ div.flex-spacer { border-color: #373737; } +.docs-container dl>div:target { + background-color: #e0d9ea; +} + .docs-container dl dd p { margin-left: 2rem; margin-top: 0.5em; margin-bottom: 0.5em; } +.docs-container .doc-section { + margin-block: 0.75em; + line-height: 125%; +} + + +.docs-container .doc-section>:is(p,li,ul,pre) { + margin-block: 0.75em; + margin-left: 2em; +} + +.docs-container .doc-section-note::before { + content: 'NOTE:'; + font-weight: bold; +} + +.docs-container .doc-section-warning::before { + content: 'WARNING:'; + font-weight: bold; +} + +.docs-container .doc-section-lore::before { + content: 'LORE:'; + font-weight: bold; +} + +.docs-container .doc-section-example::before { + content: 'EXAMPLE:'; + font-weight: bold; +} + +.docs-container .doc-section-deprecated::before { + content: 'DEPRECATED:'; + font-weight: bold; +} + +.docs-container .doc-section-decision::before { + content: 'DECISION:'; + font-weight: bold; +} + +.docs-container .doc-section-learn::before { + content: 'LEARN:'; + font-weight: bold; +} + +.docs-container .doc-section-main>:is(p,li,ul,pre) { + margin-left: 0; +} + +.docs-container .doc-section :is(ul,ol) { + padding-left: 2em; +} + /************************************************* * Syntax Highlighting * *************************************************/