From 10609b025a88647caa46f73c945d634fbfb76b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Sat, 3 Oct 2026 16:09:42 +0200 Subject: [PATCH] Upgrade to Zig 0.17 Dependencies: - v8: zig-v8-fork 0.17 branch (lightpanda-io/zig-v8-fork#218). - sqlite3: build the amalgamation directly; the allyourcodebase wrapper has no 0.17 support yet. - pcre2: master commit with 0.17 support (no release yet). - translate_c package (2.0.0) replaces the deprecated addTranslateC. default_init is set to keep 0.16's zero-initialized struct fields. Build: - b.pathFromRoot/build_root/args/sysroot are gone: resolve paths from b.root, use addPassthruArgs, declare configure-time file and directory dependencies, and poison the configure cache for the git version. - Drop curl config values the template never used (now an error). - Drop the 0.16 zip-fetch workaround from the Makefile and CI. - -Doptimize=ReleaseFast -> -Doptimize=fast. Language and std: - @intFromEnum/@enumFromInt -> @backingInt/@fromBackingInt. - Struct-of-arrays @typeInfo (field_names, field_types, decl_names, param_types, error_names). - `a ** n` removed: @splat for arrays, string.repeat for strings. - errdefer captures removed: split WebDriver.run and fulfillRedirect. - @hasDecl only sees pub decls: make StyleManager's Spec.finish pub, otherwise it silently stops running. - SafeAllocator replaces DebugAllocator; the test runner now has to initialize std.testing.allocator_instance itself. - std.fmt.allocPrint/bufPrint -> Allocator.print/std.mem.print, dupeZ -> dupeSentinel, std.builtin -> std.lang, builtin.os -> builtin.target.os, zon.parse arena API, BufferFirstAllocator, and the remaining renames (getLastOrNull, bit_set, ascii.find*, meta.Int). --- .github/actions/install/action.yml | 8 - .github/actions/v8-snapshot/action.yml | 2 +- .github/workflows/agent-regression.yml | 2 +- .github/workflows/e2e-integration-test.yml | 2 +- .github/workflows/e2e-test.yml | 2 +- .github/workflows/orderfile.yml | 2 +- .github/workflows/release.yml | 8 +- .github/workflows/wpt.yml | 2 +- AGENTS.md | 2 +- Dockerfile | 4 +- Makefile | 19 +- README.md | 2 +- build.zig | 168 ++++++++---------- build.zig.zon | 22 ++- orderfile/README.md | 2 +- orderfile/mark_hot_sections.zig | 2 +- orderfile/tools/regen.sh | 2 +- src/Arena.zig | 4 +- src/Config.zig | 38 ++-- src/Metrics.zig | 4 +- src/Notification.zig | 7 +- src/RecyclingAllocator.zig | 2 +- src/Regex.zig | 10 +- src/SemanticTree.zig | 2 +- src/Sighandler.zig | 2 +- src/TestWSServer.zig | 2 +- src/agent/Agent.zig | 31 ++-- src/agent/Conversation.zig | 2 +- src/agent/Spinner.zig | 6 +- src/agent/Terminal.zig | 6 +- src/agent/auth/auth.zig | 2 +- src/agent/auth/codex.zig | 18 +- src/agent/auth/models_dev.zig | 2 +- src/agent/md_term.zig | 25 +-- src/agent/picker.zig | 4 +- src/agent/prompt_assist.zig | 18 +- src/agent/save.zig | 2 +- src/agent/settings.zig | 53 +++--- src/agent/welcome.zig | 2 +- src/browser/EventManagerBase.zig | 2 +- src/browser/Factory.zig | 25 +-- src/browser/Frame.zig | 18 +- src/browser/Mime.zig | 15 +- src/browser/Page.zig | 2 +- src/browser/ScriptManager.zig | 2 +- src/browser/SelectorPath.zig | 10 +- src/browser/StyleManager.zig | 14 +- src/browser/URL.zig | 39 ++-- src/browser/clutter.zig | 2 +- src/browser/data_url.zig | 2 +- src/browser/frame/framing.zig | 2 +- src/browser/frame/node_factory.zig | 4 +- src/browser/image_dimensions.zig | 2 +- src/browser/interactive.zig | 2 +- src/browser/js/Caller.zig | 50 +++--- src/browser/js/Context.zig | 6 +- src/browser/js/Function.zig | 10 +- src/browser/js/Isolate.zig | 2 +- src/browser/js/Local.zig | 70 ++++---- src/browser/js/Module.zig | 2 +- src/browser/js/Promise.zig | 2 +- src/browser/js/Snapshot.zig | 43 +++-- src/browser/js/TaggedOpaque.zig | 2 +- src/browser/js/TryCatch.zig | 8 +- src/browser/js/Value.zig | 4 +- src/browser/js/bridge.zig | 28 +-- src/browser/markdown.zig | 3 +- src/browser/parser/Parser.zig | 4 +- src/browser/pdf.zig | 16 +- src/browser/referrer.zig | 9 +- src/browser/screenshot.zig | 14 +- src/browser/tools.zig | 92 +++++----- src/browser/webapi/Blob.zig | 4 +- src/browser/webapi/CSS.zig | 4 +- src/browser/webapi/Console.zig | 2 +- src/browser/webapi/DOMException.zig | 4 +- src/browser/webapi/DOMMatrix.zig | 2 +- src/browser/webapi/DOMMatrixReadOnly.zig | 6 +- src/browser/webapi/Document.zig | 4 +- src/browser/webapi/Element.zig | 2 +- src/browser/webapi/Event.zig | 54 +++--- src/browser/webapi/EventCounts.zig | 2 +- src/browser/webapi/FileReader.zig | 8 +- src/browser/webapi/History.zig | 4 +- src/browser/webapi/Location.zig | 2 +- src/browser/webapi/Navigator.zig | 2 +- src/browser/webapi/NavigatorUAData.zig | 6 +- src/browser/webapi/PerformanceObserver.zig | 10 +- src/browser/webapi/Sanitizer.zig | 4 +- .../webapi/ServiceWorkerGlobalScope.zig | 2 +- src/browser/webapi/SharedWorker.zig | 2 +- .../webapi/SharedWorkerGlobalScope.zig | 2 +- src/browser/webapi/SubtleCrypto.zig | 4 +- src/browser/webapi/URL.zig | 2 +- src/browser/webapi/URLPattern.zig | 14 +- src/browser/webapi/WebDriver.zig | 14 +- src/browser/webapi/cache/Cache.zig | 6 +- .../webapi/canvas/WebGLRenderingContext.zig | 20 +-- src/browser/webapi/canvas/context2d.zig | 2 +- src/browser/webapi/collections/iterator.zig | 8 +- src/browser/webapi/collections/node_live.zig | 2 +- src/browser/webapi/css/CSSRule.zig | 2 +- .../webapi/css/CSSStyleDeclaration.zig | 2 +- src/browser/webapi/element/DOMStringMap.zig | 2 +- src/browser/webapi/element/Html.zig | 4 +- src/browser/webapi/element/html/Image.zig | 4 +- src/browser/webapi/element/html/Input.zig | 20 +-- src/browser/webapi/element/html/Media.zig | 22 +-- src/browser/webapi/element/html/Track.zig | 8 +- src/browser/webapi/element/reflection.zig | 2 +- src/browser/webapi/event/KeyboardEvent.zig | 16 +- .../geolocation/GeolocationPositionError.zig | 8 +- src/browser/webapi/global_event_handlers.zig | 10 +- src/browser/webapi/navigation/Navigation.zig | 10 +- src/browser/webapi/net/EventSource.zig | 10 +- src/browser/webapi/net/Fetch.zig | 4 +- src/browser/webapi/net/FormData.zig | 6 +- src/browser/webapi/net/Request.zig | 8 +- src/browser/webapi/net/Response.zig | 2 +- src/browser/webapi/net/WebSocket.zig | 14 +- src/browser/webapi/net/XMLHttpRequest.zig | 18 +- src/browser/webapi/net/body_init.zig | 4 +- src/browser/webapi/sanitizer_defaults.zig | 2 +- src/browser/webapi/selector/List.zig | 8 +- src/browser/webapi/selector/Parser.zig | 2 +- src/browser/webapi/storage/Cookie.zig | 17 +- src/browser/webapi/storage/idb/Engine.zig | 24 +-- src/browser/webapi/storage/idb/Manager.zig | 3 +- src/browser/webapi/storage/storage.zig | 8 +- src/browser/webapi/svg/Angle.zig | 4 +- src/browser/webapi/svg/AnimatedNumber.zig | 2 +- src/browser/webapi/svg/Length.zig | 4 +- .../webapi/svg/PreserveAspectRatio.zig | 2 +- src/browser/xpath/result.zig | 4 +- src/cli.zig | 92 +++++----- src/cookies.zig | 2 +- src/core_dump.zig | 4 +- src/crash_handler.zig | 48 ++--- src/datetime.zig | 32 ++-- src/lightpanda.zig | 2 +- src/log.zig | 48 ++--- src/main.zig | 6 +- src/malloc_tuning.zig | 2 +- src/mcp/HttpServer.zig | 4 +- src/mcp/Server.zig | 2 +- src/mcp/Transport.zig | 2 +- src/mcp/protocol.zig | 2 +- src/mcp/tools.zig | 52 +++--- src/network/CorsGate.zig | 4 +- src/network/CorsStore.zig | 6 +- src/network/HttpClient.zig | 44 +++-- src/network/RateLimiter.zig | 4 +- src/network/RobotsGate.zig | 2 +- src/network/WebBotAuth.zig | 18 +- src/network/adblock/AdBlocker.zig | 5 +- src/network/adblock/Engine.zig | 3 +- src/network/adblock/HostnameTrie.zig | 5 +- src/network/cache/Cache.zig | 2 +- src/network/cache/SqliteCache.zig | 3 +- src/network/http.zig | 17 +- src/script/Runtime.zig | 8 +- src/script/Schema.zig | 10 +- src/script/command.zig | 8 +- src/server/Link.zig | 19 +- src/server/Server.zig | 87 ++++----- src/server/WS.zig | 7 +- src/server/bidi/browsing_context.zig | 4 +- src/server/bidi/http_command.zig | 28 ++- src/server/bidi/input.zig | 4 +- src/server/bidi/remote_value.zig | 4 +- src/server/bidi/script.zig | 2 +- src/server/bidi/session.zig | 15 +- src/server/bidi/testing.zig | 2 +- src/server/cdp/AXNode.zig | 6 +- src/server/cdp/CDP.zig | 2 +- src/server/cdp/Node.zig | 2 +- src/server/cdp/domains/fetch.zig | 5 +- src/server/cdp/domains/input.zig | 2 +- src/server/cdp/domains/lp.zig | 2 +- src/server/cdp/domains/network.zig | 8 +- src/server/cdp/domains/page.zig | 14 +- src/server/cdp/domains/runtime.zig | 5 +- src/server/cdp/domains/target.zig | 2 +- src/server/cdp/id.zig | 12 +- src/server/cdp/testing.zig | 3 +- src/server/http.zig | 19 +- src/string.zig | 35 +++- src/sys/libcurl.zig | 12 +- src/sys/net.zig | 4 +- src/telemetry/lightpanda.zig | 6 +- src/test_runner.zig | 25 +-- src/testing.zig | 39 ++-- 192 files changed, 1124 insertions(+), 1141 deletions(-) diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index ad5df1b5e..631db155a 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -41,14 +41,6 @@ runs: # Zig version used from the `minimum_zig_version` field in build.zig.zon - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 - # Zig 0.16 writes fetched .zip archives to /tmp but no longer - # creates that directory, so zip dependencies (sqlite amalgamation) fail - # with "failed to create temporary zip file: FileNotFound" on the fresh - # cache dir setup-zig points ZIG_GLOBAL_CACHE_DIR at. - - name: Zig 0.16 zip-fetch workaround - shell: bash - run: mkdir -p "${ZIG_GLOBAL_CACHE_DIR:-$HOME/.cache/zig}/tmp" - # Rust Toolchain for src/rust (html5ever bridge, renderer) - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master with: diff --git a/.github/actions/v8-snapshot/action.yml b/.github/actions/v8-snapshot/action.yml index f6c2ab732..53a0c8b2a 100644 --- a/.github/actions/v8-snapshot/action.yml +++ b/.github/actions/v8-snapshot/action.yml @@ -44,4 +44,4 @@ runs: - name: v8 snapshot shell: bash if: hashFiles('src/snapshot.bin') == '' - run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin + run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=fast snapshot_creator -- src/snapshot.bin diff --git a/.github/workflows/agent-regression.yml b/.github/workflows/agent-regression.yml index a3f689548..a6cf22521 100644 --- a/.github/workflows/agent-regression.yml +++ b/.github/workflows/agent-regression.yml @@ -28,7 +28,7 @@ jobs: - uses: ./.github/actions/v8-snapshot - name: zig build release - run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 + run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=fast -Dcpu=x86_64 - name: upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/e2e-integration-test.yml b/.github/workflows/e2e-integration-test.yml index 5a52a23cd..3d0067332 100644 --- a/.github/workflows/e2e-integration-test.yml +++ b/.github/workflows/e2e-integration-test.yml @@ -27,7 +27,7 @@ jobs: - uses: ./.github/actions/install - name: zig build release - run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 + run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=fast -Dcpu=x86_64 - name: upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 9c06217a1..fe309b801 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -53,7 +53,7 @@ jobs: - uses: ./.github/actions/v8-snapshot - name: zig build release - run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dorderfile=orderfile/lightpanda.ld -Dcpu=x86_64 + run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=fast -Dorderfile=orderfile/lightpanda.ld -Dcpu=x86_64 - name: upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/orderfile.yml b/.github/workflows/orderfile.yml index e12814505..83fae4e8a 100644 --- a/.github/workflows/orderfile.yml +++ b/.github/workflows/orderfile.yml @@ -56,7 +56,7 @@ jobs: - uses: ./.github/actions/orderfile id: orderfile with: - build-args: -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=x86_64 + build-args: -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=fast -Dcpu=x86_64 - name: open a pull request env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 67d83551a..93fe84757 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,10 +58,10 @@ jobs: arch: ${{env.ARCH}} - name: v8 snapshot - run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin + run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=fast snapshot_creator -- src/snapshot.bin - name: zig build - run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dorderfile=orderfile/lightpanda.ld ${{ matrix.cpu_flag }} ${{ env.VERSION_FLAG }} + run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=fast -Dorderfile=orderfile/lightpanda.ld ${{ matrix.cpu_flag }} ${{ env.VERSION_FLAG }} - name: Rename binary run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} @@ -118,10 +118,10 @@ jobs: arch: ${{env.ARCH}} - name: v8 snapshot - run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin + run: zig build -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=fast snapshot_creator -- src/snapshot.bin - name: zig build - run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast ${{ env.VERSION_FLAG }} + run: zig build -Dsnapshot_path=../../snapshot.bin -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=fast ${{ env.VERSION_FLAG }} - name: Rename binary run: mv zig-out/bin/lightpanda lightpanda-${{ env.ARCH }}-${{ env.OS }} diff --git a/.github/workflows/wpt.yml b/.github/workflows/wpt.yml index 3a1f75c7f..0ec1db6c5 100644 --- a/.github/workflows/wpt.yml +++ b/.github/workflows/wpt.yml @@ -37,7 +37,7 @@ jobs: arch: ${{env.ARCH}} - name: zig build release - run: zig build -Dwpt_extensions -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=ReleaseFast -Dcpu=generic + run: zig build -Dwpt_extensions -Dprebuilt_v8_path=v8/libc_v8.a -Doptimize=fast -Dcpu=generic - name: upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/AGENTS.md b/AGENTS.md index f5f688947..0a90cfa15 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for how to open a pull request (CLA, dev Run `make download-v8` once first: it fetches the prebuilt V8 archive into `.lp-cache/`, which `build.zig` picks up automatically. Without it every build compiles V8 from source (10+ minutes). -The C and Rust dependencies are built in ReleaseFast whatever `-Doptimize` is, so debug and release builds share them. Pass `ZIGFLAGS=-Ddebug_deps` to step into a dependency with a debugger. +The C and Rust dependencies are built with `-Doptimize=fast` whatever `-Doptimize` is, so debug and release builds share them. Pass `ZIGFLAGS=-Ddebug_deps` to step into a dependency with a debugger. ```bash make test # Run all tests diff --git a/Dockerfile b/Dockerfile index a66fa0419..822aebd89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,12 +52,12 @@ RUN case $TARGETPLATFORM in \ mv libc_v8.a v8/libc_v8.a # build v8 snapshot -RUN zig build -Doptimize=ReleaseFast \ +RUN zig build -Doptimize=fast \ -Dprebuilt_v8_path=v8/libc_v8.a \ snapshot_creator -- src/snapshot.bin # build release -RUN zig build -Doptimize=ReleaseFast \ +RUN zig build -Doptimize=fast \ -Dsnapshot_path=../../snapshot.bin \ -Dprebuilt_v8_path=v8/libc_v8.a \ -Dorderfile=orderfile/lightpanda.ld diff --git a/Makefile b/Makefile index 16a4f8f70..1894cb0cd 100644 --- a/Makefile +++ b/Makefile @@ -77,14 +77,7 @@ help: # $(ZIG) commands # ------------ -.PHONY: build build-v8-snapshot build-dev download-v8 run run-release test bench data end2end clean zig-cache-tmp - -# Zig 0.16.0 doesn't create /tmp before fetching .zip deps. -# https://codeberg.org/ziglang/zig/issues/31964 -zig-cache-tmp: - @dir=$$($(ZIG) env | sed -n 's/^ *\.global_cache_dir = "\(.*\)",$$/\1/p'); \ - test -n "$$dir" || { printf "\033[31mCould not read global_cache_dir from '$(ZIG) env'\033[0m\n"; exit 1; }; \ - mkdir -p "$$dir/tmp" +.PHONY: build build-v8-snapshot build-dev download-v8 run run-release test bench data end2end clean ## Download the prebuilt V8 libraries (skips the 10+ min source build) download-v8: @@ -105,19 +98,19 @@ ifeq ($(OS)_$(ARCH),linux_x86_64) endif ## Build v8 snapshot -build-v8-snapshot: zig-cache-tmp +build-v8-snapshot: @printf "\033[36mBuilding v8 snapshot (release safe)...\033[0m\n" - @$(ZIG) build $(ZIGFLAGS) -Doptimize=ReleaseFast snapshot_creator -- src/snapshot.bin || (printf "\033[31mBuild ERROR\033[0m\n"; exit 1;) + @$(ZIG) build $(ZIGFLAGS) -Doptimize=fast snapshot_creator -- src/snapshot.bin || (printf "\033[31mBuild ERROR\033[0m\n"; exit 1;) @printf "\033[32mBuild OK\033[0m\n" ## Build in release-fast mode build: build-v8-snapshot @printf "\033[36mBuilding (release fast)...\033[0m\n" - @$(ZIG) build $(ZIGFLAGS) -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin || (printf "\033[31mBuild ERROR\033[0m\n"; exit 1;) + @$(ZIG) build $(ZIGFLAGS) -Doptimize=fast -Dsnapshot_path=../../snapshot.bin || (printf "\033[31mBuild ERROR\033[0m\n"; exit 1;) @printf "\033[32mBuild OK\033[0m\n" ## Build in debug mode -build-dev: zig-cache-tmp +build-dev: @printf "\033[36mBuilding (debug)...\033[0m\n" @$(ZIG) build $(ZIGFLAGS) || (printf "\033[31mBuild ERROR\033[0m\n"; exit 1;) @printf "\033[32mBuild OK\033[0m\n" @@ -132,7 +125,7 @@ run-debug: build-dev @printf "\033[36mRunning...\033[0m\n" @./zig-out/bin/lightpanda || (printf "\033[31mRun ERROR\033[0m\n"; exit 1;) -test: zig-cache-tmp +test: TEST_FILTER="$(or $(F),$(TEST_FILTER))" $(ZIG) build $(ZIGFLAGS) test -freference-trace ## Run demo/runner end to end tests diff --git a/README.md b/README.md index 461ae20ea..777a22649 100644 --- a/README.md +++ b/README.md @@ -458,7 +458,7 @@ zig build -Dwpt_extensions run -- fetch \ it's useful to build in `releaseFast` mode to make tests faster. ``` -zig build -Dwpt_extensions -Doptimize=ReleaseFast run -- serve ... +zig build -Dwpt_extensions -Doptimize=fast run -- serve ... ``` ## Contributing diff --git a/build.zig b/build.zig index 4eb495f8a..8677c590d 100644 --- a/build.zig +++ b/build.zig @@ -18,6 +18,7 @@ const std = @import("std"); const builtin = @import("builtin"); +const Translator = @import("translate_c").Translator; const lightpanda_version = std.SemanticVersion.parse(@import("build.zig.zon").version) catch unreachable; const min_zig_version = std.SemanticVersion.parse(@import("build.zig.zon").minimum_zig_version) catch unreachable; @@ -44,18 +45,18 @@ pub fn build(b: *Build) !void { const prebuilt_v8_path_option = b.option([]const u8, "prebuilt_v8_path", "Path to a prebuilt libc_v8.a or libc_v8.so"); const dev_fast = b.option(bool, "dev_fast", "Linux debug builds: shared V8 + self-hosted backend. Implies -Dshared_v8, -Duse_llvm=false and a bundled-CRT target") orelse - (builtin.os.tag == .linux and builtin.cpu.arch == .x86_64 and - optimize == .Debug and requested_target.query.isNative() and + (builtin.target.os.tag == .linux and builtin.target.cpu.arch == .x86_64 and + optimize == .debug and requested_target.query.isNative() and !enable_tsan and !enable_asan and (prebuilt_v8_path_option == null or std.mem.endsWith(u8, prebuilt_v8_path_option.?, ".so"))); if (dev_fast) { - if (builtin.os.tag != .linux) { - std.debug.print("-Ddev_fast is Linux-only (host is {s})\n", .{@tagName(builtin.os.tag)}); + if (builtin.target.os.tag != .linux) { + std.debug.print("-Ddev_fast is Linux-only (host is {s})\n", .{@tagName(builtin.target.os.tag)}); return error.DevFastUnsupportedHost; } - if (optimize != .Debug) { - std.debug.print("-Ddev_fast is Debug-only (optimize is {s})\n", .{@tagName(optimize)}); + if (optimize != .debug) { + std.debug.print("-Ddev_fast is debug-only (optimize is {s})\n", .{@tagName(optimize)}); return error.DevFastRequiresDebug; } if (!requested_target.query.isNative()) { @@ -75,10 +76,10 @@ pub fn build(b: *Build) !void { // Dependencies never follow -Doptimize, and they build for the requested // target rather than the dev_fast bundled-CRT query, so debug and release // builds share one set of dependency objects in the cache. - const debug_deps = b.option(bool, "debug_deps", "Build the C and Rust dependencies in Debug instead of ReleaseFast") orelse false; + const debug_deps = b.option(bool, "debug_deps", "Build the C and Rust dependencies in debug instead of fast") orelse false; const deps: Deps = .{ .target = requested_target, - .optimize = if (debug_deps) .Debug else .ReleaseFast, + .optimize = if (debug_deps) .debug else .fast, }; // Without an explicit -Dprebuilt_v8_path, pick up whatever `make @@ -118,7 +119,7 @@ pub fn build(b: *Build) !void { const fmt_step = b.step("fmt", "Check code formatting"); const fmt = b.addFmt(.{ - .paths = &.{ "src", "build.zig", "build.zig.zon" }, + .paths = b.pathList(&.{ "src", "build.zig", "build.zig.zon" }), .check = true, }); fmt_step.dependOn(&fmt.step); @@ -167,9 +168,7 @@ pub fn build(b: *Build) !void { b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe); - if (b.args) |args| { - run_cmd.addArgs(args); - } + run_cmd.addPassthruArgs(); const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); @@ -185,9 +184,7 @@ pub fn build(b: *Build) !void { extras_step.dependOn(&b.addInstallArtifact(exe, .{}).step); const run_cmd = b.addRunArtifact(exe); - if (b.args) |args| { - run_cmd.addArgs(args); - } + run_cmd.addPassthruArgs(); const run_step = b.step("snapshot_creator", "Generate a v8 snapshot"); run_step.dependOn(&run_cmd.step); } @@ -197,7 +194,7 @@ pub fn build(b: *Build) !void { const exe = addExe(b, exe_config, "lightpanda-skills", "skills_check", "src/main_skills.zig"); const run_cmd = b.addRunArtifact(exe); - const out_dir = run_cmd.addOutputDirectoryArg("skills"); + const out_dir = run_cmd.addOutputDirectoryArg2("skills", .{}); const install = b.addInstallDirectory(.{ .source_dir = out_dir, .install_dir = .prefix, @@ -222,14 +219,14 @@ pub fn build(b: *Build) !void { const Deps = struct { target: Build.ResolvedTarget, - optimize: std.builtin.OptimizeMode, + optimize: std.lang.Optimize, }; const ExeConfig = struct { check: *Build.Step, lightpanda_module: *Build.Module, target: Build.ResolvedTarget, - optimize: std.builtin.OptimizeMode, + optimize: std.lang.Optimize, use_llvm: bool, orderfile: ?[]const u8, sanitize_c: ?std.zig.SanitizeC, @@ -255,7 +252,7 @@ fn addExe(b: *Build, config: ExeConfig, name: []const u8, check_name: []const u8 if (config.orderfile) |path| { // Per-function/per-datum sections exist only so the orderfile script // can place individual hot functions; the self-hosted backend used by - // Debug builds does not support them on the C libraries, so they are + // debug builds does not support them on the C libraries, so they are // gated on the orderfile being set (release/LLVM only). exe.link_function_sections = true; exe.link_data_sections = true; @@ -282,9 +279,11 @@ fn devFastGlibcVersion(b: *Build) std.SemanticVersion { /// it cannot drift from CI (the Makefile reads the same source of truth). fn findPrebuiltV8(b: *Build, target: Build.ResolvedTarget, dev_fast: bool) ?[]const u8 { const io = b.graph.io; + const action_path = ".github/actions/install/action.yml"; + b.dependOnFileContents(b.path(action_path)); const action = std.Io.Dir.cwd().readFileAlloc( io, - b.pathFromRoot(".github/actions/install/action.yml"), + rootPath(b, action_path), b.allocator, .limited(64 * 1024), ) catch return null; @@ -294,7 +293,7 @@ fn findPrebuiltV8(b: *Build, target: Build.ResolvedTarget, dev_fast: bool) ?[]co return null; } - const cache_dir = b.pathFromRoot(".lp-cache"); + const cache_dir = ".lp-cache"; // The .so must keep the name the exe's DT_NEEDED records; the archive // name encodes V8 version, os and arch. const path = if (dev_fast) @@ -307,7 +306,8 @@ fn findPrebuiltV8(b: *Build, target: Build.ResolvedTarget, dev_fast: bool) ?[]co @tagName(target.result.cpu.arch), }) }); }; - std.Io.Dir.cwd().access(io, path, .{}) catch { + b.dependOnFileMetadata(b.path(path)); + std.Io.Dir.cwd().access(io, rootPath(b, path), .{}) catch { std.debug.print("No prebuilt V8 at {s}; using the V8 source-build path. `make download-v8` fetches the prebuilt.\n", .{path}); return null; }; @@ -315,6 +315,10 @@ fn findPrebuiltV8(b: *Build, target: Build.ResolvedTarget, dev_fast: bool) ?[]co return path; } +fn rootPath(b: *Build, sub_path: []const u8) []const u8 { + return b.root.joinString(b.allocator, sub_path) catch @panic("OOM"); +} + /// Returns the quoted `default:` value of a top-level `key` in the install /// action's yaml. fn actionDefault(action: []const u8, key: []const u8) ?[]const u8 { @@ -346,18 +350,18 @@ fn markHotSections(b: *Build, archive: Build.LazyPath) Build.LazyPath { .root_module = b.createModule(.{ .root_source_file = b.path("orderfile/mark_hot_sections.zig"), .target = b.graph.host, - .optimize = .ReleaseSafe, + .optimize = .safe, }), }); const run = b.addRunArtifact(tool); run.addFileArg(archive); run.addFileArg(b.path("orderfile/v8.txt")); - return run.addOutputFileArg("libc_v8.a"); + return run.addOutputFileArg2("libc_v8.a", .{}); } /// Per-function/per-datum sections let the -Dorderfile linker script place /// individual hot functions. Only enabled for orderfile (release/LLVM) builds: -/// the self-hosted backend used by Debug builds fails to link the C libraries +/// the self-hosted backend used by debug builds fails to link the C libraries /// with them. fn sectionize(lib: *Build.Step.Compile, enabled: bool) *Build.Step.Compile { if (enabled) { @@ -384,7 +388,7 @@ fn linkV8( .is_tsan = is_tsan, .inspector_subtype = false, .v8_enable_sandbox = is_tsan, - .cache_root = b.pathFromRoot(".lp-cache"), + .cache_root = rootPath(b, ".lp-cache"), .prebuilt_v8_path = prebuilt_v8_path, .shared_v8 = shared_v8, }); @@ -393,7 +397,7 @@ fn linkV8( fn linkRust(b: *Build, mod: *Build.Module, deps: Deps) void { // Cargo's "dev" profile writes to target/debug. - const profile, const out_subdir = if (deps.optimize == .Debug) .{ "dev", "debug" } else .{ "release", "release" }; + const profile, const out_subdir = if (deps.optimize == .debug) .{ "dev", "debug" } else .{ "release", "release" }; // One cargo workspace, one staticlib (src/rust/Cargo.toml explains why). const exec_cargo = b.addSystemCommand(&.{ @@ -412,7 +416,7 @@ fn linkRust(b: *Build, mod: *Build.Module, deps: Deps) void { _ = exec_cargo.captureStdErr(.{}); // TODO: We can prefer `--artifact-dir` once it become stable. - const out_dir = exec_cargo.addPrefixedOutputDirectoryArg("--target-dir=", "rust"); + const out_dir = exec_cargo.addOutputDirectoryArg2("rust", .{ .prefix = "--target-dir=" }); const rust_step = b.step("rust", "Build the Rust staticlib (requires cargo)"); rust_step.dependOn(&exec_cargo.step); @@ -425,14 +429,18 @@ fn linkRust(b: *Build, mod: *Build.Module, deps: Deps) void { /// input of `run`, skipping the `skip_dir` subtree at any depth. fn addDirInputs(b: *Build, run: *Build.Step.Run, root: []const u8, skip_dir: []const u8) !void { const io = b.graph.io; - var dir = try b.build_root.handle.openDir(io, root, .{ .iterate = true }); + var dir = try std.Io.Dir.cwd().openDir(io, rootPath(b, root), .{ .iterate = true }); defer dir.close(io); + b.dependOnDirectoryContents(b.path(root)); var walker = try dir.walk(b.allocator); defer walker.deinit(); while (try walker.next(io)) |entry| { switch (entry.kind) { - .directory => if (std.mem.eql(u8, entry.basename, skip_dir)) walker.leave(io), + .directory => if (std.mem.eql(u8, entry.basename, skip_dir)) + walker.leave(io) + else + b.dependOnDirectoryContents(b.path(b.pathJoin(&.{ root, entry.path }))), .file => run.addFileInput(b.path(b.pathJoin(&.{ root, entry.path }))), else => {}, } @@ -440,14 +448,12 @@ fn addDirInputs(b: *Build, run: *Build.Step.Run, root: []const u8, skip_dir: []c } fn linkSqlite(b: *Build, mod: *Build.Module, deps: Deps, enable_csan: ?std.zig.SanitizeC, is_tsan: bool, section: bool) void { - const dep = b.dependency("sqlite3", .{ - .target = deps.target, - .optimize = deps.optimize, - }); + const dep = b.dependency("sqlite3", .{}); - const lib = sectionize(dep.artifact("sqlite3"), section); - lib.root_module.sanitize_c = enable_csan; - lib.root_module.sanitize_thread = is_tsan; + const lib_mod = cLibModule(b, deps.target, deps.optimize, is_tsan); + lib_mod.sanitize_c = enable_csan; + lib_mod.addCSourceFile(.{ .file = dep.path("sqlite3.c") }); + const lib = sectionize(b.addLibrary(.{ .name = "sqlite3", .root_module = lib_mod }), section); const macros = [_]struct { []const u8, []const u8 }{ .{ "SQLITE_DEFAULT_FILE_PERMISSIONS", "0600" }, @@ -489,12 +495,7 @@ fn linkSqlite(b: *Build, mod: *Build.Module, deps: Deps, enable_csan: ?std.zig.S mod.linkLibrary(lib); - const translate_c = b.addTranslateC(.{ - .root_source_file = lib.getEmittedIncludeTree().path(b, "sqlite3.h"), - .target = mod.resolved_target.?, - .optimize = mod.optimize.?, - }); - mod.addImport("sqlite3", translate_c.createModule()); + mod.addImport("sqlite3", translateC(b, mod, dep.path("sqlite3.h")).mod); } fn linkPcre2(b: *Build, mod: *Build.Module, deps: Deps, enable_csan: ?std.zig.SanitizeC, is_tsan: bool, section: bool) void { @@ -509,13 +510,9 @@ fn linkPcre2(b: *Build, mod: *Build.Module, deps: Deps, enable_csan: ?std.zig.Sa lib.root_module.sanitize_thread = is_tsan; mod.linkLibrary(lib); - const translate_c = b.addTranslateC(.{ - .root_source_file = lib.getEmittedIncludeTree().path(b, "pcre2.h"), - .target = mod.resolved_target.?, - .optimize = mod.optimize.?, - }); - translate_c.defineCMacro("PCRE2_CODE_UNIT_WIDTH", "8"); - mod.addImport("pcre2", translate_c.createModule()); + const translator = translateC(b, mod, lib.getEmittedIncludeTree().path(b, "pcre2.h")); + translator.defineCMacro("PCRE2_CODE_UNIT_WIDTH", "8"); + mod.addImport("pcre2", translator.mod); } fn linkCurl(b: *Build, mod: *Build.Module, deps: Deps, is_tsan: bool, section: bool) void { @@ -523,13 +520,9 @@ fn linkCurl(b: *Build, mod: *Build.Module, deps: Deps, is_tsan: bool, section: b mod.linkLibrary(curl); const dep = b.dependency("curl", .{}); - const translate_c = b.addTranslateC(.{ - .root_source_file = dep.path("include/curl/curl.h"), - .target = mod.resolved_target.?, - .optimize = mod.optimize.?, - }); - translate_c.addIncludePath(dep.path("include")); - mod.addImport("curl", translate_c.createModule()); + const translator = translateC(b, mod, dep.path("include/curl/curl.h")); + translator.addIncludePath(dep.path("include")); + mod.addImport("curl", translator.mod); const zlib = buildZlib(b, deps.target, deps.optimize, is_tsan, section); curl.root_module.linkLibrary(zlib); @@ -545,9 +538,7 @@ fn linkCurl(b: *Build, mod: *Build.Module, deps: Deps, is_tsan: bool, section: b if (deps.target.result.os.tag == .macos) { // needed for proxying on mac - const framework_path = if (b.sysroot) |sysroot| - b.pathJoin(&.{ sysroot, "System/Library/Frameworks" }) - else if (b.graph.environ_map.get("SDKROOT")) |sdk_root| + const framework_path = if (b.graph.environ_map.get("SDKROOT")) |sdk_root| b.pathJoin(&.{ sdk_root, "System/Library/Frameworks" }) else "/System/Library/Frameworks"; @@ -557,7 +548,7 @@ fn linkCurl(b: *Build, mod: *Build.Module, deps: Deps, is_tsan: bool, section: b } } -fn cLibModule(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, is_tsan: bool) *Build.Module { +fn cLibModule(b: *Build, target: Build.ResolvedTarget, optimize: std.lang.Optimize, is_tsan: bool) *Build.Module { return b.createModule(.{ .target = target, .optimize = optimize, @@ -566,7 +557,7 @@ fn cLibModule(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.Opt }); } -fn buildZlib(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, is_tsan: bool, section: bool) *Build.Step.Compile { +fn buildZlib(b: *Build, target: Build.ResolvedTarget, optimize: std.lang.Optimize, is_tsan: bool, section: bool) *Build.Step.Compile { const dep = b.dependency("zlib", .{}); const mod = cLibModule(b, target, optimize, is_tsan); @@ -592,7 +583,7 @@ fn buildZlib(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.Opti return lib; } -fn buildBrotli(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, is_tsan: bool, section: bool) [3]*Build.Step.Compile { +fn buildBrotli(b: *Build, target: Build.ResolvedTarget, optimize: std.lang.Optimize, is_tsan: bool, section: bool) [3]*Build.Step.Compile { const dep = b.dependency("brotli", .{}); const mod = cLibModule(b, target, optimize, is_tsan); @@ -634,7 +625,7 @@ fn buildBrotli(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.Op return .{ brotlicmn, brotlidec, brotlienc }; } -fn buildBoringSsl(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, section: bool) [2]*Build.Step.Compile { +fn buildBoringSsl(b: *Build, target: Build.ResolvedTarget, optimize: std.lang.Optimize, section: bool) [2]*Build.Step.Compile { const dep = b.dependency("boringssl-zig", .{ .target = target, .optimize = optimize, @@ -650,7 +641,7 @@ fn buildBoringSsl(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin return .{ ssl, crypto }; } -fn buildNghttp2(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, is_tsan: bool, section: bool) *Build.Step.Compile { +fn buildNghttp2(b: *Build, target: Build.ResolvedTarget, optimize: std.lang.Optimize, is_tsan: bool, section: bool) *Build.Step.Compile { const dep = b.dependency("nghttp2", .{}); const mod = cLibModule(b, target, optimize, is_tsan); @@ -696,7 +687,7 @@ fn buildNghttp2(b: *Build, target: Build.ResolvedTarget, optimize: std.builtin.O fn buildCurl( b: *Build, target: Build.ResolvedTarget, - optimize: std.builtin.OptimizeMode, + optimize: std.lang.Optimize, is_tsan: bool, section: bool, ) *Build.Step.Compile { @@ -721,7 +712,7 @@ fn buildCurl( const byte_size = struct { fn it(b2: *std.Build, target2: Build.ResolvedTarget, name: []const u8, comptime ctype: std.Target.CType) []const u8 { - return b2.fmt("#define SIZEOF_{s} {d}", .{ name, target2.result.cTypeByteSize(ctype) }); + return b2.fmt("#define SIZEOF_{s} {d}", .{ name, target2.result.cTypeByteSize(ctype).? }); } }.it; @@ -731,7 +722,6 @@ fn buildCurl( .USE_NGHTTP2 = true, .USE_OPENSSL = true, - .OPENSSL_IS_BORINGSSL = true, .CURL_BORINGSSL_VERSION = null, .CURL_CA_PATH = null, .CURL_CA_BUNDLE = null, @@ -751,11 +741,9 @@ fn buildCurl( .CURL_DISABLE_LDAP = true, .CURL_DISABLE_LDAPS = true, .CURL_DISABLE_MQTT = true, - .CURL_DISABLE_NTLM = true, .CURL_DISABLE_PROGRESS_METER = true, .CURL_DISABLE_POP3 = true, .CURL_DISABLE_RTSP = true, - .CURL_DISABLE_SMB = true, .CURL_DISABLE_SMTP = true, .CURL_DISABLE_TELNET = true, .CURL_DISABLE_TFTP = true, @@ -777,7 +765,6 @@ fn buildCurl( .SIZEOF_INT_CODE = byte_size(b, target, "INT", .int), .SIZEOF_LONG_CODE = byte_size(b, target, "LONG", .long), - .SIZEOF_LONG_LONG_CODE = byte_size(b, target, "LONG_LONG", .longlong), .SIZEOF_OFF_T_CODE = byte_size(b, target, "OFF_T", .longlong), .SIZEOF_CURL_OFF_T_CODE = byte_size(b, target, "CURL_OFF_T", .longlong), @@ -805,8 +792,6 @@ fn buildCurl( .HAVE_PWD_H = !is_windows, .HAVE_STDATOMIC_H = true, .HAVE_STDBOOL_H = true, - .HAVE_STDDEF_H = true, - .HAVE_STDINT_H = true, .HAVE_STRINGS_H = true, .HAVE_STROPTS_H = false, .HAVE_SYS_EVENTFD_H = is_linux or is_freebsd or is_netbsd, @@ -835,18 +820,15 @@ fn buildCurl( .HAVE_BUILTIN_AVAILABLE = true, .HAVE_CLOCK_GETTIME_MONOTONIC = !is_darwin and !is_windows, .HAVE_CLOCK_GETTIME_MONOTONIC_RAW = is_linux, - .HAVE_FILE_OFFSET_BITS = true, .HAVE_GETEUID = !is_windows, .HAVE_GETPPID = !is_windows, .HAVE_GETTIMEOFDAY = true, .HAVE_GLIBC_STRERROR_R = is_gnu, .HAVE_GMTIME_R = !is_windows, .HAVE_LOCALTIME_R = !is_windows, - .HAVE_LONGLONG = !is_windows, .HAVE_MACH_ABSOLUTE_TIME = is_darwin, .HAVE_MEMRCHR = !is_darwin and !is_windows, .HAVE_POSIX_STRERROR_R = !is_gnu and !is_windows, - .HAVE_PTHREAD_H = !is_windows, .HAVE_THREADS_POSIX = !is_windows, .HAVE_SETLOCALE = true, .HAVE_SETRLIMIT = !is_windows, @@ -854,12 +836,8 @@ fn buildCurl( .HAVE_SIGINTERRUPT = !is_windows, .HAVE_SIGNAL = true, .HAVE_SIGSETJMP = !is_windows, - .HAVE_SIZEOF_SA_FAMILY_T = false, - .HAVE_SIZEOF_SUSECONDS_T = false, - .HAVE_SNPRINTF = true, .HAVE_STRCASECMP = !is_windows, .HAVE_STRCMPI = false, - .HAVE_STRDUP = true, .HAVE_STRERROR_R = !is_windows, .HAVE_STRICMP = false, .HAVE_STRUCT_TIMEVAL = true, @@ -867,8 +845,6 @@ fn buildCurl( .HAVE_UTIME = true, .HAVE_UTIMES = !is_windows, .HAVE_WRITABLE_ARGV = !is_windows, - .HAVE__SETMODE = is_windows, - .USE_THREADS_POSIX = !is_windows, .USE_RESOLV_THREADED = !is_windows, // filesystem, network @@ -885,16 +861,12 @@ fn buildCurl( .HAVE_FSETXATTR = is_darwin or is_linux or is_netbsd, .HAVE_FSETXATTR_5 = is_linux or is_netbsd, .HAVE_FSETXATTR_6 = is_darwin, - .HAVE_FTRUNCATE = true, .HAVE_GETADDRINFO = true, .HAVE_GETADDRINFO_THREADSAFE = is_linux or is_freebsd or is_netbsd, .HAVE_GETHOSTBYNAME_R = is_linux or is_freebsd, .HAVE_GETHOSTBYNAME_R_3 = false, - .HAVE_GETHOSTBYNAME_R_3_REENTRANT = false, .HAVE_GETHOSTBYNAME_R_5 = false, - .HAVE_GETHOSTBYNAME_R_5_REENTRANT = false, .HAVE_GETHOSTBYNAME_R_6 = is_linux, - .HAVE_GETHOSTBYNAME_R_6_REENTRANT = is_linux, .HAVE_GETHOSTNAME = true, .HAVE_GETIFADDRS = if (is_windows) false else !is_android or target.result.os.versionRange().linux.android >= 24, .HAVE_GETPASS_R = is_netbsd, @@ -910,7 +882,6 @@ fn buildCurl( .HAVE_IOCTLSOCKET_FIONBIO = is_windows, .HAVE_IOCTL_FIONBIO = !is_windows, .HAVE_IOCTL_SIOCGIFADDR = !is_windows, - .HAVE_MSG_NOSIGNAL = !is_windows, .HAVE_OPENDIR = true, .HAVE_PIPE = !is_windows, .HAVE_PIPE2 = is_linux or is_freebsd or is_netbsd or is_openbsd, @@ -922,9 +893,7 @@ fn buildCurl( .HAVE_SEND = true, .HAVE_SENDMMSG = !is_darwin and !is_windows, .HAVE_SENDMSG = !is_windows, - .HAVE_SETMODE = !is_linux, .HAVE_SETSOCKOPT_SO_NONBLOCK = false, - .HAVE_SOCKADDR_IN6_SIN6_ADDR = !is_windows, .HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID = true, .HAVE_SOCKET = true, .HAVE_SOCKETPAIR = !is_windows, @@ -1022,12 +991,17 @@ fn linkIsocline(b: *Build, mod: *Build.Module) void { .file = dep.path("src/isocline.c"), }); - const translate_c = b.addTranslateC(.{ - .root_source_file = dep.path("include/isocline.h"), + mod.addImport("isocline", translateC(b, mod, dep.path("include/isocline.h")).mod); +} + +fn translateC(b: *Build, mod: *Build.Module, header: Build.LazyPath) Translator { + return .init(b.dependency("translate_c", .{}), .{ + .c_source_file = header, .target = mod.resolved_target.?, .optimize = mod.optimize.?, + // Zig 0.16's built-in translate-c defaulted to this. + .default_init = true, }); - mod.addImport("isocline", translate_c.createModule()); } /// Resolves the semantic version of the build. @@ -1072,10 +1046,14 @@ fn resolveVersion(b: *std.Build) std.SemanticVersion { } fn runGit(b: *std.Build, args: []const []const u8) ![]const u8 { - var code: u8 = undefined; + // HEAD moves without touching any file the cache could track cheaply. + b.graph.poisonCache(); const command = try std.mem.concat(b.allocator, []const u8, &.{ - &.{ "git", "-C", b.pathFromRoot(".") }, + &.{ "git", "-C", rootPath(b, ".") }, args, }); - return b.runAllowFail(command, &code, .ignore); + return switch (b.runFallible(command, .{ .stderr_behavior = .ignore })) { + .success => |stdout| stdout, + else => error.GitFailed, + }; } diff --git a/build.zig.zon b/build.zig.zon index 8fa11a202..3bf2fac20 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,11 +2,11 @@ .name = .browser, .version = "1.1.0-dev", .fingerprint = 0xda130f3af836cea0, // Changing this has security and trust implications. - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/200123d4a5eee5136aa96a3933c651cc3d2c069f.tar.gz", - .hash = "v8-0.0.0-xddH67dJAwAdG6Puk7KzLsWsEzP8kqxLDfS-6tpkKS5B", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/10f6a11e81fd4809988a24e3f5b3d5d4fd3a73fe.tar.gz", + .hash = "v8-0.0.0-xddH6w1QAwDcuJPtc73VXElNBsgPgrRBqQR__jWhy_C4", }, // .v8 = .{ .path = "../zig-v8-fork" }, .brotli = .{ @@ -32,20 +32,24 @@ .hash = "N-V-__8AALJXUwHr71AwttzhEqqYTvFb_jz0EQ5Ou3OWpHBM", }, .sqlite3 = .{ - .url = "https://github.com/allyourcodebase/sqlite3/archive/7a615f5af79009cd733e3480658586d9b0d28b35.tar.gz", - .hash = "sqlite3-3.53.2-DMxLWuAOAAA_Px0arJOIOaP4AKEu5prbsQgPMA35W1zz", + .url = "https://sqlite.org/2026/sqlite-amalgamation-3530200.zip", + .hash = "N-V-__8AALc_rgC04POe18O0LKFRm4ouQpLtL3riscEmWp-Q", }, .zenai = .{ - .url = "git+https://github.com/lightpanda-io/zenai.git#b3386fde741d3d9f76674e4706ac1db27923c855", - .hash = "zenai-0.0.0-iOY_VJEhBwBj9qGuHOtw1TdQw0aiIbOZhXlybkSqhXKs", + .url = "git+https://github.com/lightpanda-io/zenai.git#63df800afc438c0e90603270a4696689eb339ccc", + .hash = "zenai-0.0.0-iOY_VEuBBwAanO_h2hbjC9Up37-a4pjYlViKeOYJLQjS", }, .isocline = .{ .url = "git+https://github.com/arrufat/isocline#4a99434bee4a5ed04c1639514224d48cd55e1405", .hash = "N-V-__8AAHxtEwB16xj2Xz-zx_uklGdTP5C2-GHXDoSzah-8", }, .pcre2 = .{ - .url = "https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.49/pcre2-10.49.tar.gz", - .hash = "pcre2-10.49.0-IZ6r68cregBKN199ndY6AaKNyKJpNmnQVSm8-h0gp4sj", + .url = "git+https://github.com/PCRE2Project/pcre2#9ad4a2e956876747a5022bf04614c7e39e71d4ec", + .hash = "pcre2-10.50.0-DEV-IZ6r6yDTrgCmI3jcBntPIdZLWLhNORGZZSi8hxQkc11x", + }, + .translate_c = .{ + .url = "git+https://codeberg.org/ziglang/translate-c?ref=2.0.0#0da7a16c3235b935b82421646076e0657cda21f6", + .hash = "translate_c-2.0.0-Q_BUWltOBwA0229IE8wwSxxGzOxyRso3hvcb1nN8oJX_", }, }, .paths = .{""}, diff --git a/orderfile/README.md b/orderfile/README.md index 7a61991e7..de72ba60e 100644 --- a/orderfile/README.md +++ b/orderfile/README.md @@ -59,7 +59,7 @@ below), with no change in run duration. ```bash # root for /sys/kernel/debug/fault_around_bytes; ../demo checked out with # `npm install` done; node, go, python3 and binutils on the PATH. -orderfile/tools/regen.sh -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin -Dcpu=x86_64 +orderfile/tools/regen.sh -Doptimize=fast -Dsnapshot_path=../../snapshot.bin -Dcpu=x86_64 ``` The script diff --git a/orderfile/mark_hot_sections.zig b/orderfile/mark_hot_sections.zig index 59bd5df07..8fa61020a 100644 --- a/orderfile/mark_hot_sections.zig +++ b/orderfile/mark_hot_sections.zig @@ -123,7 +123,7 @@ fn rewriteArchive(gpa: Allocator, archive: []const u8, hot: *const HotSet, out: out.appendSliceAssumeCapacity("!\n"); for (members.items) |m| { var header = m.header.*; - _ = try std.fmt.bufPrint(header[48..58], "{d:<10}", .{m.body.len}); + _ = try std.mem.print(header[48..58], "{d:<10}", .{m.body.len}); out.appendSliceAssumeCapacity(&header); const start = out.items.len; out.appendSliceAssumeCapacity(m.body); diff --git a/orderfile/tools/regen.sh b/orderfile/tools/regen.sh index 741372022..1f9f3d9d6 100755 --- a/orderfile/tools/regen.sh +++ b/orderfile/tools/regen.sh @@ -4,7 +4,7 @@ # # usage: orderfile/tools/regen.sh [zig build args...] # The build args are those of the release build minus -Dorderfile, e.g. -# -Doptimize=ReleaseFast -Dsnapshot_path=../../snapshot.bin -Dcpu=x86_64 +# -Doptimize=fast -Dsnapshot_path=../../snapshot.bin -Dcpu=x86_64 # # Needs root (sudo) for /sys/kernel/debug/fault_around_bytes, a checkout of # lightpanda-io/demo (DEMO_DIR, npm install done), node, go, python3 and diff --git a/src/Arena.zig b/src/Arena.zig index 96b288025..006d01307 100644 --- a/src/Arena.zig +++ b/src/Arena.zig @@ -109,8 +109,8 @@ pub fn dupe(self: *Arena, comptime T: type, m: []const T) ![]T { return self.allocator().dupe(T, m); } -pub fn dupeZ(self: *Arena, comptime T: type, m: []const T) ![:0]T { - return self.allocator().dupeZ(T, m); +pub fn dupeSentinel(self: *Arena, comptime T: type, m: []const T, comptime sentinel: T) ![:sentinel]T { + return self.allocator().dupeSentinel(T, m, sentinel); } // Arena is being released. Account goes back to 0 (everything is being released) diff --git a/src/Config.zig b/src/Config.zig index 0bb03abdb..2b2b8a402 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -358,7 +358,7 @@ pub const AgentVerbosity = enum { high, pub fn atLeast(self: AgentVerbosity, min: AgentVerbosity) bool { - return @intFromEnum(self) >= @intFromEnum(min); + return @backingInt(self) >= @backingInt(min); } }; @@ -1038,7 +1038,7 @@ pub const HttpHeaders = struct { languages: []const []const u8, pub fn init(allocator: Allocator, value: []const u8) !AcceptLanguage { - const header = try allocator.dupeZ(u8, value); + const header = try allocator.dupeSentinel(u8, value, 0); errdefer allocator.free(header); var languages: std.ArrayList([]const u8) = .empty; @@ -1067,15 +1067,15 @@ pub const HttpHeaders = struct { pub fn init(allocator: Allocator, config: *const Config) !HttpHeaders { const user_agent: [:0]const u8 = if (config.userAgent()) |ua| - try allocator.dupeZ(u8, ua) + try allocator.dupeSentinel(u8, ua, 0) else if (config.userAgentSuffix()) |suffix| - try std.fmt.allocPrintSentinel(allocator, "{s} {s}", .{ user_agent_base, suffix }, 0) + try allocator.printSentinel("{s} {s}", .{ user_agent_base, suffix }, 0) else user_agent_base; errdefer if (config.userAgent() != null or config.userAgentSuffix() != null) allocator.free(user_agent); const proxy_bearer_header: ?[:0]const u8 = if (config.proxyBearerToken()) |token| - try std.fmt.allocPrintSentinel(allocator, "Proxy-Authorization: Bearer {s}", .{token}, 0) + try allocator.printSentinel("Proxy-Authorization: Bearer {s}", .{token}, 0) else null; errdefer if (proxy_bearer_header) |hdr| allocator.free(hdr); @@ -1133,7 +1133,7 @@ pub fn printUsageAndExit(self: *const Config, allocator: Allocator, help_for: Ru \\{s} \\ , .{Help.general}); - break :text try std.fmt.allocPrint(allocator, template, .{exec_name}); + break :text try allocator.print(template, .{exec_name}); }, inline .fetch, .serve, .mcp, .agent, .run => |tag| text: { const template = comptimePrint( @@ -1142,11 +1142,11 @@ pub fn printUsageAndExit(self: *const Config, allocator: Allocator, help_for: Ru \\{s} \\ , .{ @field(Help, @tagName(tag)), Help.common_options }); - break :text try std.fmt.allocPrint(allocator, template, .{ exec_name, info_or_warn, pretty_or_logfmt }); + break :text try allocator.print(template, .{ exec_name, info_or_warn, pretty_or_logfmt }); }, .version => text: { const template = Help.version ++ "\n"; - break :text try std.fmt.allocPrint(allocator, template, .{exec_name}); + break :text try allocator.print(template, .{exec_name}); }, }; defer allocator.free(text); @@ -1228,8 +1228,8 @@ pub fn parseArgs(allocator: Allocator, proc_args: std.process.Args) !Config { } // run's fields are a strict subset of Agent's (compile error otherwise). var agent_opts: Agent = .{}; - inline for (@typeInfo(@TypeOf(run)).@"struct".fields) |f| { - @field(agent_opts, f.name) = @field(run, f.name); + inline for (@typeInfo(@TypeOf(run)).@"struct".field_names) |field_name| { + @field(agent_opts, field_name) = @field(run, field_name); } command = .{ .agent = agent_opts }; } @@ -1428,7 +1428,7 @@ test "Config: validateLocale" { try std.testing.expectError(error.InvalidSubtag, validateLocale("en-U")); try std.testing.expectError(error.InvalidSubtag, validateLocale("en-US-x-toolongsub")); try std.testing.expectError(error.InvalidSubtag, validateLocale("en-U$")); - try std.testing.expectError(error.TooLong, validateLocale("en-" ++ "a" ** 40)); + try std.testing.expectError(error.TooLong, validateLocale("en-" ++ string.repeat("a", 40))); } test "Config: validateTimezone" { @@ -1438,7 +1438,7 @@ test "Config: validateTimezone" { try std.testing.expectError(error.Empty, validateTimezone("")); try std.testing.expectError(error.InvalidCharacter, validateTimezone("Europe/ Paris")); try std.testing.expectError(error.InvalidCharacter, validateTimezone("UTC\n")); - try std.testing.expectError(error.TooLong, validateTimezone("a" ** 65)); + try std.testing.expectError(error.TooLong, validateTimezone(string.repeat("a", 65))); } test "Config: HttpHeaders.acceptLanguageFor" { @@ -1634,7 +1634,7 @@ pub fn validateUserAgent(ua: []const u8) !void { } } - if (std.ascii.indexOfIgnoreCase(ua, "mozilla") != null) { + if (std.ascii.findIgnoreCase(ua, "mozilla") != null) { return error.Reserved; } } @@ -1645,7 +1645,7 @@ fn localeValidator(allocator: Allocator, args: *std.process.Args.Iterator, field log.fatal(.app, "invalid option value", .{ .arg = "--locale", .value = str, .err = err, .hint = "must be a BCP 47 tag such as en-US, de or zh-Hant-TW" }); return error.InvalidArgument; }; - field.* = try allocator.dupeZ(u8, str); + field.* = try allocator.dupeSentinel(u8, str, 0); } fn timezoneValidator(allocator: Allocator, args: *std.process.Args.Iterator, field: *?[:0]const u8) !void { @@ -1654,7 +1654,7 @@ fn timezoneValidator(allocator: Allocator, args: *std.process.Args.Iterator, fie log.fatal(.app, "invalid option value", .{ .arg = "--timezone", .value = str, .err = err, .hint = "must be an IANA time zone such as Europe/Paris or UTC" }); return error.InvalidArgument; }; - field.* = try allocator.dupeZ(u8, str); + field.* = try allocator.dupeSentinel(u8, str, 0); } /// A BCP 47 tag restricted to what ICU and the Accept-Language derivation @@ -1708,8 +1708,8 @@ pub const tagNames = cli.tagNames; /// `` ghost-text hint built from the same enum's tag names. pub fn tagHint(comptime E: type) []const u8 { var s: []const u8 = "<"; - for (@typeInfo(E).@"enum".fields, 0..) |f, i| { - s = s ++ (if (i == 0) f.name else "|" ++ f.name); + for (@typeInfo(E).@"enum".field_names, 0..) |field_name, i| { + s = s ++ (if (i == 0) field_name else "|" ++ field_name); } return s ++ ">"; } @@ -1717,8 +1717,8 @@ pub fn tagHint(comptime E: type) []const u8 { /// JSON array `["a","b","c"]` representation of the enum tag names. pub fn tagJsonArray(comptime E: type) []const u8 { var s: []const u8 = "["; - for (@typeInfo(E).@"enum".fields, 0..) |f, i| { - s = s ++ (if (i == 0) "\"" else ",\"") ++ f.name ++ "\""; + for (@typeInfo(E).@"enum".field_names, 0..) |field_name, i| { + s = s ++ (if (i == 0) "\"" else ",\"") ++ field_name ++ "\""; } return s ++ "]"; } diff --git a/src/Metrics.zig b/src/Metrics.zig index c92278d6f..5fb5cdc89 100644 --- a/src/Metrics.zig +++ b/src/Metrics.zig @@ -151,8 +151,8 @@ fn _write(self: *const Metrics, writer: *std.Io.Writer) !void { "# TYPE build_info gauge\nbuild_info{{version=\"{s}\"}} 1\n", .{lp.build_config.version}, ); - inline for (@typeInfo(Metrics).@"struct".fields) |f| { - try @field(self, f.name).write(f.name, @field(help, f.name), writer); + inline for (@typeInfo(Metrics).@"struct".field_names) |field_name| { + try @field(self, field_name).write(field_name, @field(help, field_name), writer); } } diff --git a/src/Notification.zig b/src/Notification.zig index ab43e73ab..f13e8deb2 100644 --- a/src/Notification.zig +++ b/src/Notification.zig @@ -484,12 +484,7 @@ pub fn dispatch(self: *Notification, comptime event: EventType, data: ArgType(ev // Given an event type enum, returns the type of arg the event emits pub fn ArgType(comptime event: Notification.EventType) type { - inline for (std.meta.fields(Notification.Events)) |f| { - if (std.mem.eql(u8, f.name, @tagName(event))) { - return f.type; - } - } - unreachable; + return @FieldType(Notification.Events, @tagName(event)); } // Given an event type enum, returns the listening function type diff --git a/src/RecyclingAllocator.zig b/src/RecyclingAllocator.zig index 663d8c10b..9f40cf121 100644 --- a/src/RecyclingAllocator.zig +++ b/src/RecyclingAllocator.zig @@ -43,7 +43,7 @@ free_lists: std.array_hash_map.Custom(Class, ?[*]u8, Class.Context, false) = .em // class, e.g. a 34-byte string and a 40-byte node recycle each other. const Class = packed struct(u64) { alignment: Alignment, - size: std.meta.Int(.unsigned, 64 - @bitSizeOf(Alignment)), + size: @Int(.unsigned, 64 - @bitSizeOf(Alignment)), fn of(len: usize, alignment: Alignment) Class { const class_alignment = Alignment.max(alignment, .of(usize)); diff --git a/src/Regex.zig b/src/Regex.zig index a5c567f60..f37798b05 100644 --- a/src/Regex.zig +++ b/src/Regex.zig @@ -25,6 +25,7 @@ const std = @import("std"); const pcre2 = @import("pcre2"); +const repeat = @import("string.zig").repeat; const Allocator = std.mem.Allocator; @@ -171,8 +172,9 @@ const MATCH_SCRATCH = 24 * 1024; /// Whether the pattern matches anywhere in `text`, as `RegExp.test` would /// answer. A match that hits the backtracking limits counts as no match. pub fn matches(self: Regex, text: []const u8) bool { - var scratch = std.heap.stackFallback(MATCH_SCRATCH, self.context.allocator); - var allocator = scratch.get(); + var scratch_buf: [MATCH_SCRATCH]u8 = undefined; + var scratch: std.heap.BufferFirstAllocator = .init(&scratch_buf, self.context.allocator); + var allocator = scratch.allocator(); const general = pcre2.pcre2_general_context_create_8(Context.cMalloc, Context.cFree, &allocator) orelse return false; defer pcre2.pcre2_general_context_free_8(general); @@ -236,9 +238,9 @@ test "Regex: invalid patterns are errors, runaway ones no match" { // the caller. const runaway = try context.compile("^(a+)+$", .{ .case_insensitive = true }, null); defer runaway.deinit(); - const subject = "a" ** 64 ++ "b"; + const subject = repeat("a", 64) ++ "b"; try testing.expect(!runaway.matches(subject)); - try testing.expect(runaway.matches("a" ** 64)); + try testing.expect(runaway.matches(repeat("a", 64))); } test "Regex: dot_all and multiline follow the JavaScript flags" { diff --git a/src/SemanticTree.zig b/src/SemanticTree.zig index 575337cd2..76cb4b07f 100644 --- a/src/SemanticTree.zig +++ b/src/SemanticTree.zig @@ -385,7 +385,7 @@ const JsonVisitor = struct { try self.jw.beginObject(); try self.jw.objectField("nodeId"); - try self.jw.write(try std.fmt.allocPrint(self.tree.arena, "{d}", .{data.id})); + try self.jw.write(try self.tree.arena.print("{d}", .{data.id})); try self.jw.objectField("backendDOMNodeId"); try self.jw.write(data.id); diff --git a/src/Sighandler.zig b/src/Sighandler.zig index c6638efbe..aabeef9d2 100644 --- a/src/Sighandler.zig +++ b/src/Sighandler.zig @@ -129,7 +129,7 @@ fn sighandle(self: *SigHandler) noreturn { std.process.exit(1); } - switch (@as(std.posix.SIG, @enumFromInt(sig))) { + switch (@as(std.posix.SIG, @fromBackingInt(@intCast(sig)))) { .INT, .TERM => { self.mutex.lockUncancelable(lp.io); if (self.attempt > 1 and !self.no_hard_exit) { diff --git a/src/TestWSServer.zig b/src/TestWSServer.zig index ed24c380a..9f889ce60 100644 --- a/src/TestWSServer.zig +++ b/src/TestWSServer.zig @@ -145,7 +145,7 @@ fn handleClient(client: posix.socket_t) void { // Send upgrade response var resp_buf: [256]u8 = undefined; - const resp = std.fmt.bufPrint(&resp_buf, "HTTP/1.1 101 Switching Protocols\r\n" ++ + const resp = std.mem.print(&resp_buf, "HTTP/1.1 101 Switching Protocols\r\n" ++ "Upgrade: websocket\r\n" ++ "Connection: Upgrade\r\n" ++ "Sec-WebSocket-Accept: {s}\r\n\r\n", .{accept_key}) catch return; diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 15b0a76b5..a605c13c3 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -55,8 +55,8 @@ const UserError = error{ }; pub fn isUserError(err: anyerror) bool { - inline for (@typeInfo(UserError).error_set.?) |e| { - if (err == @field(anyerror, e.name)) return true; + inline for (@typeInfo(UserError).error_set.error_names.?) |name| { + if (err == @field(anyerror, name)) return true; } return false; } @@ -194,7 +194,7 @@ api_error_buf: [512]u8 = undefined, api_error_detail: ?[]const u8 = null, pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent { - var providers_buf: [@typeInfo(Config.AiProvider).@"enum".fields.len]Candidate = undefined; + var providers_buf: [@typeInfo(Config.AiProvider).@"enum".field_names.len]Candidate = undefined; const found_providers = settings.availableProviders(&providers_buf); const available_providers = try allocator.alloc([]const u8, found_providers.len); for (found_providers, 0..) |f, i| { @@ -233,8 +233,9 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Agent // Load remembered selection up front so a saved null provider can flip the // REPL into basic mode before resolution. Pure script runs need nothing. - const remembered: ?settings.Remembered = if (will_repl or is_one_shot) settings.loadRemembered(allocator) else null; - defer if (remembered) |r| std.zon.parse.free(allocator, r); + var remembered_arena: std.heap.ArenaAllocator = .init(allocator); + defer remembered_arena.deinit(); + const remembered: ?settings.Remembered = if (will_repl or is_one_shot) settings.loadRemembered(allocator, remembered_arena.allocator()) else null; // A remembered null provider means the user disabled the LLM via // `/provider null`; honor it for the REPL only (one-shot --task and script @@ -645,7 +646,7 @@ fn runRepl(self: *Agent) void { self.terminal.printError("{s}", .{switch (err) { error.OutOfMemory => "out of memory", error.FrameNotLoaded => "no page loaded — run /goto first (Esc exits JS mode)", - else => std.fmt.allocPrint(aa, "evaluate failed: {s}", .{@errorName(err)}) catch "evaluate failed", + else => aa.print("evaluate failed: {s}", .{@errorName(err)}) catch "evaluate failed", }}); continue :repl; }; @@ -699,7 +700,7 @@ fn runRepl(self: *Agent) void { .comment => continue :repl, .llm => |lc| { var label_buf: [32]u8 = undefined; - const label = std.fmt.bufPrint(&label_buf, "/{s}", .{@tagName(lc)}) catch "/?"; + const label = std.mem.print(&label_buf, "/{s}", .{@tagName(lc)}) catch "/?"; if (!self.requireLlm(label)) continue :repl; _ = self.runTurn(.{ .prompt = lc.prompt(), .record_comment = line, .capture_for_save = true, .label = label }); }, @@ -1008,7 +1009,7 @@ fn subscriptionLogin(self: *Agent, desc: *const auth.Descriptor) ?auth.Session { fn promptStoredSubscription(self: *Agent, desc: *const auth.Descriptor, stored: auth.Session) ?auth.Session { var session = stored; var header_buf: [128]u8 = undefined; - const header = std.fmt.bufPrint(&header_buf, "Already logged in with your {s}. Pick:", .{desc.label}) catch + const header = std.mem.print(&header_buf, "Already logged in with your {s}. Pick:", .{desc.label}) catch "Already logged in. Pick:"; const idx = picker.promptNumberedChoice(header, &.{ "keep — use the stored login", @@ -1167,7 +1168,7 @@ fn handleSave(self: *Agent, arena: std.mem.Allocator, rest: []const u8) void { fn promptSaveMode(self: *Agent, path: []const u8) ?save.Mode { var header_buf: [256]u8 = undefined; - const header = std.fmt.bufPrint(&header_buf, "{s} already exists. Pick save mode:", .{path}) catch + const header = std.mem.print(&header_buf, "{s} already exists. Pick save mode:", .{path}) catch "File already exists. Pick save mode:"; const with_llm = self.ai_client != null; const modes: []const save.Mode = if (with_llm) @@ -1507,7 +1508,7 @@ fn runCommand(self: *Agent, arena: std.mem.Allocator, tc: Command.ToolCall) brow .text = switch (err) { error.OutOfMemory => "out of memory", error.FrameNotLoaded => "no page loaded — run /goto first", - else => std.fmt.allocPrint(arena, "{s} failed: {s}", .{ tc.name(), browser_tools.errorMessage(err) }) catch "tool failed", + else => arena.print("{s} failed: {s}", .{ tc.name(), browser_tools.errorMessage(err) }) catch "tool failed", }, .is_error = true, }; @@ -1634,7 +1635,7 @@ fn recordSlashToolCall( const tool_calls = try ma.alloc(zenai.provider.ToolCall, 1); tool_calls[0] = .{ - .id = try std.fmt.allocPrint(ma, "lp-slash-{d}", .{self.synthetic_tool_call_id}), + .id = try ma.print("lp-slash-{d}", .{self.synthetic_tool_call_id}), .name = try ma.dupe(u8, tool_name), .arguments = if (args) |v| try zenai.json.dupeValue(ma, v) else null, }; @@ -1685,9 +1686,9 @@ fn formatApiError(self: *Agent, client: zenai.provider.Client, err: anyerror) [] else ""; if (e.message) |m| { - if (std.fmt.bufPrint(&self.api_error_buf, "HTTP {d} — {s}{s}", .{ status, m, hint })) |s| return s else |_| {} + if (std.mem.print(&self.api_error_buf, "HTTP {d} — {s}{s}", .{ status, m, hint })) |s| return s else |_| {} } - return std.fmt.bufPrint(&self.api_error_buf, "HTTP {d}{s}", .{ status, hint }) catch @errorName(err); + return std.mem.print(&self.api_error_buf, "HTTP {d}{s}", .{ status, hint }) catch @errorName(err); } /// Returned text lives in `conversation.arena`, valid only until the next prune. @@ -1932,7 +1933,7 @@ fn capToolOutput(allocator: std.mem.Allocator, tool_name: []const u8, output: [] if (output.len <= cap) return output; const prefix = string.truncateUtf8(output, cap); var suffix_buf: [128]u8 = undefined; - const suffix = std.fmt.bufPrint(&suffix_buf, "\n...[truncated, original {d} bytes — re-read scoped (selector/backendNodeId)]", .{output.len}) catch return prefix; + const suffix = std.mem.print(&suffix_buf, "\n...[truncated, original {d} bytes — re-read scoped (selector/backendNodeId)]", .{output.len}) catch return prefix; return std.mem.concat(allocator, u8, &.{ prefix, suffix }) catch prefix; } @@ -1954,7 +1955,7 @@ fn handleToolCall(ctx: *anyopaque, allocator: std.mem.Allocator, tool_name: []co var selector: ?[]const u8 = null; const outcome = self.toolOutcome(allocator, tool_name, arguments, &selector) catch |err| zenai.provider.Client.ToolHandler.Result{ - .content = std.fmt.allocPrint(allocator, "Error: {s}", .{browser_tools.errorMessage(err)}) catch "Error: tool execution failed", + .content = allocator.print("Error: {s}", .{browser_tools.errorMessage(err)}) catch "Error: tool execution failed", .is_error = true, }; if (self.capturing_for_save) { diff --git a/src/agent/Conversation.zig b/src/agent/Conversation.zig index 5b4a8e3c4..c851a3db3 100644 --- a/src/agent/Conversation.zig +++ b/src/agent/Conversation.zig @@ -151,7 +151,7 @@ test "expireImages keeps the newest images and annotates the rest" { const image = [_]zenai.provider.ContentPart{.{ .image = .{ .data = "AAAA", .mime_type = "image/png" } }}; for (0..4) |n| { const results = try a.alloc(zenai.provider.ToolResult, 1); - results[0] = .{ .id = "c", .name = "screenshot", .content = try std.fmt.allocPrint(a, "shot {d}", .{n}), .parts = &image }; + results[0] = .{ .id = "c", .name = "screenshot", .content = try a.print("shot {d}", .{n}), .parts = &image }; try conv.messages.append(std.testing.allocator, .{ .role = .tool, .tool_results = results }); } diff --git a/src/agent/Spinner.zig b/src/agent/Spinner.zig index 500b55cbc..70cfde678 100644 --- a/src/agent/Spinner.zig +++ b/src/agent/Spinner.zig @@ -140,7 +140,7 @@ pub fn stop(self: *Spinner) void { const elapsed_s = @as(f64, @floatFromInt(elapsed_ms)) / @as(f64, std.time.ms_per_s); var buf: [frame_buf_bytes]u8 = undefined; - const summary = std.fmt.bufPrint( + const summary = std.mem.print( &buf, "\r" ++ clear_eol ++ ansi.dim ++ "[agent: worked for {d:.1}s · {d} tool call{s}]" ++ ansi.reset ++ "\n", .{ elapsed_s, self.tool_calls, if (self.tool_calls == 1) "" else "s" }, @@ -275,7 +275,7 @@ fn renderLocked(self: *Spinner) void { const glyph = braille[self.frame % braille.len]; const written = switch (self.state) { .idle => return, - .thinking => std.fmt.bufPrint( + .thinking => std.mem.print( &buf, "\r" ++ ansi.yellow ++ "{s}" ++ ansi.reset ++ " " ++ ansi.dim ++ "[agent: thinking]" ++ ansi.reset ++ clear_eol, .{glyph}, @@ -297,7 +297,7 @@ fn renderLocked(self: *Spinner) void { const cap = @min(max_args_cells, room); const cut = truncToCells(all_args, cap); const suffix: []const u8 = if (cut < all_args.len) ellipsis else ""; - break :blk std.fmt.bufPrint( + break :blk std.mem.print( &buf, "\r" ++ ansi.yellow ++ "{s}" ++ ansi.reset ++ " " ++ ansi.dim ++ "[{s}{s} {s}{s}]" ++ ansi.reset ++ clear_eol, .{ glyph, prefix, name, all_args[0..cut], suffix }, diff --git a/src/agent/Terminal.zig b/src/agent/Terminal.zig index 6dcd2a295..d2669e2e4 100644 --- a/src/agent/Terminal.zig +++ b/src/agent/Terminal.zig @@ -141,7 +141,7 @@ fn emitStderr(self: *Terminal, bytes: []const u8) void { fn formatBulletLine(arena: std.mem.Allocator, name: []const u8, args: []const u8, ok: bool) ![]const u8 { const bullet_color = if (ok) ansi.green else ansi.red; - return std.fmt.allocPrint(arena, bullet_line_fmt, .{ bullet_color, ansi.reset, ansi.dim, name, ansi.reset, args }); + return arena.print(bullet_line_fmt, .{ bullet_color, ansi.reset, ansi.dim, name, ansi.reset, args }); } pub fn setIdleCallback(fun: ?*const c.ic_idle_fun_t, arg: ?*anyopaque) void { @@ -307,7 +307,7 @@ pub fn printToolOutcome(self: *Terminal, name: []const u8, text: []const u8, is_ pub fn printScriptDone(self: *Terminal, name: []const u8, args: []const u8) void { if (!self.spinner.isEnabled()) return; var buf: [256]u8 = undefined; - const line = std.fmt.bufPrint( + const line = std.mem.print( &buf, ansi.green ++ "●" ++ ansi.reset ++ " " ++ ansi.dim ++ "[{s} {s}]" ++ ansi.reset ++ "\n", .{ name, args }, @@ -354,7 +354,7 @@ pub fn printWarning(self: *Terminal, comptime fmt: []const u8, args: anytype) vo fn printSeverity(self: *Terminal, color: []const u8, label: []const u8, comptime fmt: []const u8, args: anytype) void { if (self.repl_arena) |*a| { defer _ = a.reset(.retain_capacity); - const bytes = std.fmt.allocPrint(a.allocator(), "{s}●{s} " ++ fmt ++ "\n", .{ color, ansi.reset } ++ args) catch return; + const bytes = a.allocator().print("{s}●{s} " ++ fmt ++ "\n", .{ color, ansi.reset } ++ args) catch return; return self.emitStderr(bytes); } std.debug.print("{s}{s}{s}: " ++ fmt ++ "{s}\n", .{ ansi.bold, color, label } ++ args ++ .{ansi.reset}); diff --git a/src/agent/auth/auth.zig b/src/agent/auth/auth.zig index 074a4ad50..de06eb296 100644 --- a/src/agent/auth/auth.zig +++ b/src/agent/auth/auth.zig @@ -50,7 +50,7 @@ pub const TokenSet = struct { account_id: ?[]const u8 = null, pub fn dup(allocator: std.mem.Allocator, access: []const u8, refresh: []const u8, expires_at_ms: i64, account_id: ?[]const u8) !TokenSet { - const a = try allocator.dupeZ(u8, access); + const a = try allocator.dupeSentinel(u8, access, 0); errdefer allocator.free(a); const r = try allocator.dupe(u8, refresh); errdefer allocator.free(r); diff --git a/src/agent/auth/codex.zig b/src/agent/auth/codex.zig index cdee87474..c69f4f3db 100644 --- a/src/agent/auth/codex.zig +++ b/src/agent/auth/codex.zig @@ -108,13 +108,13 @@ fn parseTokenResponse(allocator: std.mem.Allocator, body: []const u8) !auth.Toke } fn refreshBody(arena: std.mem.Allocator, refresh_token: []const u8) ![]u8 { - return std.fmt.allocPrint(arena, "grant_type=refresh_token&client_id=" ++ client_id ++ "&refresh_token={s}", .{ + return arena.print("grant_type=refresh_token&client_id=" ++ client_id ++ "&refresh_token={s}", .{ try lp.URL.percentEncodeSegment(arena, refresh_token, .component), }); } fn exchangeBody(arena: std.mem.Allocator, code: []const u8, code_verifier: []const u8) ![]u8 { - return std.fmt.allocPrint(arena, "grant_type=authorization_code&client_id=" ++ client_id ++ + return arena.print("grant_type=authorization_code&client_id=" ++ client_id ++ "&redirect_uri=" ++ device_redirect_uri ++ "&code={s}&code_verifier={s}", .{ try lp.URL.percentEncodeSegment(arena, code, .component), try lp.URL.percentEncodeSegment(arena, code_verifier, .component), @@ -155,7 +155,7 @@ fn deviceLogin(allocator: std.mem.Allocator, interrupt: ?*zenai.http.Interrupt) const code_res = try post(a, interrupt, device_code_url, "application/json", "{\"client_id\":\"" ++ client_id ++ "\"}"); if (code_res.status != .ok) { - log.warn(.app, "codex device-code failed", .{ .status = @intFromEnum(code_res.status), .body = code_res.body }); + log.warn(.app, "codex device-code failed", .{ .status = @backingInt(code_res.status), .body = code_res.body }); return error.DeviceCodeRequestFailed; } const dc = try std.json.parseFromSliceLeaky(DeviceCode, a, code_res.body, .{ .ignore_unknown_fields = true }); @@ -166,7 +166,7 @@ fn deviceLogin(allocator: std.mem.Allocator, interrupt: ?*zenai.http.Interrupt) .{ verify_url, dc.user_code }, ); - const poll_body = try std.fmt.allocPrint(a, "{f}", .{std.json.fmt( + const poll_body = try a.print("{f}", .{std.json.fmt( .{ .device_auth_id = dc.device_auth_id, .user_code = dc.user_code }, .{}, )}); @@ -178,7 +178,7 @@ fn deviceLogin(allocator: std.mem.Allocator, interrupt: ?*zenai.http.Interrupt) // Still pending — the user hasn't finished authorizing. .forbidden, .not_found => continue, else => { - log.warn(.app, "codex device-auth poll failed", .{ .status = @intFromEnum(res.status), .body = res.body }); + log.warn(.app, "codex device-auth poll failed", .{ .status = @backingInt(res.status), .body = res.body }); return error.DeviceAuthFailed; }, } @@ -187,7 +187,7 @@ fn deviceLogin(allocator: std.mem.Allocator, interrupt: ?*zenai.http.Interrupt) const exchange = try exchangeBody(a, dt.authorization_code, dt.code_verifier); const tok_res = try post(a, interrupt, token_url, "application/x-www-form-urlencoded", exchange); if (tok_res.status != .ok) { - log.warn(.app, "codex token exchange failed", .{ .status = @intFromEnum(tok_res.status), .body = tok_res.body }); + log.warn(.app, "codex token exchange failed", .{ .status = @backingInt(tok_res.status), .body = tok_res.body }); return error.TokenExchangeFailed; } return parseTokenResponse(allocator, tok_res.body); @@ -200,7 +200,7 @@ fn refreshGrant(allocator: std.mem.Allocator, refresh_token: []const u8) !auth.T const body = try refreshBody(a, refresh_token); const res = try post(a, null, token_url, "application/x-www-form-urlencoded", body); if (res.status != .ok) { - log.warn(.app, "codex token refresh failed", .{ .status = @intFromEnum(res.status), .body = res.body }); + log.warn(.app, "codex token refresh failed", .{ .status = @backingInt(res.status), .body = res.body }); return error.RefreshFailed; } return parseTokenResponse(allocator, res.body); @@ -233,7 +233,7 @@ fn makeJwt(arena: std.mem.Allocator, payload_json: []const u8) ![]const u8 { const enc = std.base64.url_safe_no_pad.Encoder; const p = try arena.alloc(u8, enc.calcSize(payload_json.len)); _ = enc.encode(p, payload_json); - return std.fmt.allocPrint(arena, "aGVhZGVy.{s}.c2ln", .{p}); + return arena.print("aGVhZGVy.{s}.c2ln", .{p}); } test "accountIdFromJwt: top-level chatgpt_account_id" { @@ -281,7 +281,7 @@ test "parseTokenResponse derives account id and absolute expiry" { defer arena.deinit(); const a = arena.allocator(); const jwt = try makeJwt(a, "{\"chatgpt_account_id\":\"acct-x\"}"); - const body = try std.fmt.allocPrint(a, "{{\"access_token\":\"acc\",\"refresh_token\":\"ref\",\"id_token\":\"{s}\",\"expires_in\":3600}}", .{jwt}); + const body = try a.print("{{\"access_token\":\"acc\",\"refresh_token\":\"ref\",\"id_token\":\"{s}\",\"expires_in\":3600}}", .{jwt}); const tokens = try parseTokenResponse(std.testing.allocator, body); defer tokens.deinit(std.testing.allocator); try std.testing.expectEqualStrings("acc", tokens.access_token); diff --git a/src/agent/auth/models_dev.zig b/src/agent/auth/models_dev.zig index 9e3221b96..12d6fb4d1 100644 --- a/src/agent/auth/models_dev.zig +++ b/src/agent/auth/models_dev.zig @@ -51,7 +51,7 @@ const Cache = struct { }; fn cachePath(arena: std.mem.Allocator, app_dir: []const u8, provider_id: []const u8) ![]const u8 { - const name = try std.fmt.allocPrint(arena, "models-dev-{s}.json", .{provider_id}); + const name = try arena.print("models-dev-{s}.json", .{provider_id}); return std.fs.path.join(arena, &.{ app_dir, name }); } diff --git a/src/agent/md_term.zig b/src/agent/md_term.zig index 239db1029..e59534a81 100644 --- a/src/agent/md_term.zig +++ b/src/agent/md_term.zig @@ -19,6 +19,7 @@ const std = @import("std"); const ansi = @import("ansi.zig"); const js_highlight = @import("js_highlight.zig"); +const repeat = @import("../string.zig").repeat; /// Render markdown `src` as ANSI-styled terminal output to `w`. Tables are /// aligned straight from `src`, so unlike `Stream` there is no size limit. @@ -470,7 +471,7 @@ fn renderLine(w: *std.Io.Writer, line: []const u8, js: ?*js_highlight.State) !vo // Dashed, unlike the solid fence rules, so adjacent ones read differently. if (isHorizontalRule(trimmed)) { - try styled(w, "┄" ** rule_width, ansi.dim); + try styled(w, repeat("┄", rule_width), ansi.dim); return; } @@ -729,8 +730,8 @@ test "md_term: nested inline styles" { ); } -const open_rule = "\x1b[2m╭" ++ "─" ** 23 ++ "\x1b[0m"; -const close_rule = "\x1b[2m╰" ++ "─" ** 23 ++ "\x1b[0m"; +const open_rule = "\x1b[2m╭" ++ repeat("─", 23) ++ "\x1b[0m"; +const close_rule = "\x1b[2m╰" ++ repeat("─", 23) ++ "\x1b[0m"; test "md_term: fenced code block is highlighted as JavaScript" { try expectRender( @@ -743,13 +744,13 @@ test "md_term: fenced code block is highlighted as JavaScript" { test "md_term: fence rules carry the language tag" { try expectRender( - "\x1b[2m╭─ js " ++ "─" ** 18 ++ "\x1b[0m\n\nx\n\n" ++ close_rule, + "\x1b[2m╭─ js " ++ repeat("─", 18) ++ "\x1b[0m\n\nx\n\n" ++ close_rule, "```js\nx\n```", ); // An overlong info string doesn't fit the rule and is dropped. try expectRender( open_rule ++ "\n\nx\n\n" ++ close_rule, - "```" ++ "x" ** 20 ++ "\nx\n```", + "```" ++ repeat("x", 20) ++ "\nx\n```", ); } @@ -791,8 +792,8 @@ test "md_term: blockquote" { } test "md_term: horizontal rule" { - try expectRender("\x1b[2m" ++ "┄" ** 24 ++ "\x1b[0m", "---"); - try expectRender("\x1b[2m" ++ "┄" ** 24 ++ "\x1b[0m", "***"); + try expectRender("\x1b[2m" ++ repeat("┄", 24) ++ "\x1b[0m", "---"); + try expectRender("\x1b[2m" ++ repeat("┄", 24) ++ "\x1b[0m", "***"); try expectRender("---x", "---x"); } @@ -828,7 +829,7 @@ test "md_term: pipe rows without a separator pass through" { try expectRender("| a |\n", "| a |\n"); } -const big_table = "| A | B |\n|-|-|\n" ++ ("| " ++ "a" ** 16 ++ " | " ++ "b" ** 16 ++ " |\n") ** 500; +const big_table = "| A | B |\n|-|-|\n" ++ repeat("| " ++ repeat("a", 16) ++ " | " ++ repeat("b", 16) ++ " |\n", 500); test "md_term: batch aligns tables beyond the stream table buffer" { try testing.expect(big_table.len > table_buf_len); @@ -839,8 +840,8 @@ test "md_term: batch aligns tables beyond the stream table buffer" { const pipe = "\x1b[2m│\x1b[0m"; try testing.expectEqual(0, std.mem.count(u8, out, "| a")); - try testing.expectEqual(500, std.mem.count(u8, out, pipe ++ " " ++ "a" ** 16 ++ " " ++ pipe)); - try testing.expect(std.mem.indexOf(u8, out, "\x1b[2m├" ++ "─" ** 18 ++ "┼" ++ "─" ** 18 ++ "┤\x1b[0m\n") != null); + try testing.expectEqual(500, std.mem.count(u8, out, pipe ++ " " ++ repeat("a", 16) ++ " " ++ pipe)); + try testing.expect(std.mem.indexOf(u8, out, "\x1b[2m├" ++ repeat("─", 18) ++ "┼" ++ repeat("─", 18) ++ "┤\x1b[0m\n") != null); } test "md_term: stream falls back to raw rows past its table buffer" { @@ -853,8 +854,8 @@ test "md_term: stream falls back to raw rows past its table buffer" { } test "md_term: overwide table falls back to verbatim rows" { - const header = "|a" ** 17 ++ "|"; - const sep = "|-" ** 17 ++ "|"; + const header = repeat("|a", 17) ++ "|"; + const sep = repeat("|-", 17) ++ "|"; try expectRender( "\x1b[1m" ++ header ++ "\x1b[0m\n\x1b[2m" ++ sep ++ "\x1b[0m\n", header ++ "\n" ++ sep, diff --git a/src/agent/picker.zig b/src/agent/picker.zig index 79879b32a..097fb3d75 100644 --- a/src/agent/picker.zig +++ b/src/agent/picker.zig @@ -118,8 +118,8 @@ const RawTerminal = struct { raw.lflag.ICANON = false; raw.lflag.IEXTEN = false; raw.lflag.ISIG = false; - raw.cc[@intFromEnum(std.c.V.MIN)] = 0; - raw.cc[@intFromEnum(std.c.V.TIME)] = 1; + raw.cc[@backingInt(std.c.V.MIN)] = 0; + raw.cc[@backingInt(std.c.V.TIME)] = 1; std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, raw) catch return error.NotInteractive; // Under `ansi.kitty_disambiguate` (pushed by `Terminal.readLine`), // cursor keys arrive as CSI-u the byte reader can't parse; push the diff --git a/src/agent/prompt_assist.zig b/src/agent/prompt_assist.zig index eaa5be3be..6b200b9b8 100644 --- a/src/agent/prompt_assist.zig +++ b/src/agent/prompt_assist.zig @@ -72,13 +72,13 @@ const kind_styles = blk: { const n = std.enums.values(js_highlight.Kind).len; var arr: [n]?[:0]const u8 = @splat(null); for (styles) |s| for (s.kinds) |kind| { - if (arr[@intFromEnum(kind)] != null) @compileError("kind styled twice: " ++ @tagName(kind)); - arr[@intFromEnum(kind)] = s.name; + if (arr[@backingInt(kind)] != null) @compileError("kind styled twice: " ++ @tagName(kind)); + arr[@backingInt(kind)] = s.name; }; var out: [n][:0]const u8 = undefined; for (arr, 0..) |name, i| { out[i] = name orelse @compileError("js_highlight.Kind with no ps-* style: " ++ - @tagName(@as(js_highlight.Kind, @enumFromInt(i)))); + @tagName(@as(js_highlight.Kind, @fromBackingInt(@intCast(i))))); } break :blk out; }; @@ -169,7 +169,7 @@ fn addPrefixedCompletion( partial: []const u8, ) void { if (!std.ascii.startsWithIgnoreCase(name, partial)) return; - const text = std.fmt.bufPrintZ(buf, "{s}{s}{s}", .{ prefix, name, suffix }) catch return; + const text = std.mem.printSentinel(buf, "{s}{s}{s}", .{ prefix, name, suffix }, 0) catch return; _ = c.ic_add_completion_prim(cenv, text.ptr, null, null, @intCast(input.len), 0); } @@ -408,7 +408,7 @@ fn addPathCompletions( var name_buf: [completion_buf_len]u8 = undefined; while (matches.next()) |m| { const suffix: []const u8 = if (m.is_dir) "/" else ""; - const full = std.fmt.bufPrint(&name_buf, "{s}{s}", .{ matches.dir_part, m.name }) catch continue; + const full = std.mem.print(&name_buf, "{s}{s}", .{ matches.dir_part, m.name }) catch continue; addPrefixedCompletion(cenv, buf, input, prefix, full, suffix, body); } } @@ -580,7 +580,7 @@ fn ghostPathFirstMatch(body: []const u8) [*c]const u8 { defer matches.deinit(); const m = matches.next() orelse return null; const suffix: []const u8 = if (m.is_dir) "/" else ""; - const text = std.fmt.bufPrintZ(&hint_buf, "{s}{s}", .{ m.name[matches.base.len..], suffix }) catch return null; + const text = std.mem.printSentinel(&hint_buf, "{s}{s}", .{ m.name[matches.base.len..], suffix }, 0) catch return null; return text.ptr; } @@ -589,7 +589,7 @@ fn ghostPathFirstMatch(body: []const u8) [*c]const u8 { fn ghostFirstMatch(names: []const []const u8, body: []const u8, lead: []const u8) [*c]const u8 { for (names) |v| { if (!std.ascii.startsWithIgnoreCase(v, body)) continue; - const text = std.fmt.bufPrintZ(&hint_buf, "{s}{s}", .{ lead, v[body.len..] }) catch return null; + const text = std.mem.printSentinel(&hint_buf, "{s}{s}", .{ lead, v[body.len..] }, 0) catch return null; return text.ptr; } return null; @@ -621,7 +621,7 @@ fn renderSchemaHint(schema: *const Schema, body: []const u8, ends_ws: bool) [*c] for (schema.hints) |slot| { if (a.isUsed(slot.name)) continue; if (!std.ascii.startsWithIgnoreCase(slot.name, pk)) continue; - const text = std.fmt.bufPrintZ(&hint_buf, "{s}=…", .{slot.name[pk.len..]}) catch return null; + const text = std.mem.printSentinel(&hint_buf, "{s}=…", .{slot.name[pk.len..]}, 0) catch return null; return text.ptr; } return null; @@ -725,7 +725,7 @@ const IcSink = struct { henv: ?*c.ic_highlight_env_t, pub fn emit(self: IcSink, start: usize, len: usize, kind: js_highlight.Kind) void { - c.ic_highlight(self.henv, @intCast(start), @intCast(len), kind_styles[@intFromEnum(kind)].ptr); + c.ic_highlight(self.henv, @intCast(start), @intCast(len), kind_styles[@backingInt(kind)].ptr); } }; diff --git a/src/agent/save.zig b/src/agent/save.zig index d2f212009..ccbf988a7 100644 --- a/src/agent/save.zig +++ b/src/agent/save.zig @@ -72,7 +72,7 @@ pub fn randomFilename(arena: std.mem.Allocator) ![]const u8 { var n_bytes: [8]u8 = undefined; lp.io.random(&n_bytes); const n = std.mem.readInt(u64, &n_bytes, .little); - const path = try std.fmt.allocPrint(arena, "session-{x}.js", .{n}); + const path = try arena.print("session-{x}.js", .{n}); if (!(try fileExists(path))) return path; } return error.NameCollision; diff --git a/src/agent/settings.zig b/src/agent/settings.zig index 817d6ca78..3594b71e4 100644 --- a/src/agent/settings.zig +++ b/src/agent/settings.zig @@ -126,7 +126,7 @@ pub fn gcloudAccessToken(allocator: std.mem.Allocator) ![:0]const u8 { std.debug.print("`gcloud auth print-access-token` failed:\n{s}", .{result.stderr}); return error.GcloudTokenFailed; } - return allocator.dupeZ(u8, token); + return allocator.dupeSentinel(u8, token, 0); } /// True when a non-Ollama provider key is available (flag, remembered, or @@ -271,24 +271,24 @@ pub const Remembered = struct { search_engine: ?lp.tools.SearchEngine = null, }; -pub fn loadRemembered(allocator: std.mem.Allocator) ?Remembered { - const data = std.Io.Dir.cwd().readFileAllocOptions(lp.io, remembered_path, allocator, .limited(1024), .of(u8), 0) catch return null; - defer allocator.free(data); - return parseRemembered(allocator, data); +/// The result's strings live in `arena`. +pub fn loadRemembered(gpa: std.mem.Allocator, arena: std.mem.Allocator) ?Remembered { + const data = std.Io.Dir.cwd().readFileAllocOptions(lp.io, remembered_path, gpa, .limited(1024), .of(u8), 0) catch return null; + defer gpa.free(data); + return parseRemembered(gpa, arena, data); } -fn parseRemembered(allocator: std.mem.Allocator, data: [:0]const u8) ?Remembered { - // A real Diagnostics, not null: a type-check failure allocates an owned - // error note that leaks unless a Diagnostics owns it to free on deinit. - var diag: std.zon.parse.Diagnostics = .{}; - defer diag.deinit(allocator); - const remembered = std.zon.parse.fromSliceAlloc(Remembered, allocator, data, &diag, .{}) catch return null; +fn parseRemembered(gpa: std.mem.Allocator, arena: std.mem.Allocator, data: [:0]const u8) ?Remembered { + var diag: std.zon.parse.Diagnostics = undefined; + const remembered = std.zon.parse.fromSlice(Remembered, .{ + .gpa = gpa, + .arena = arena, + .source = data, + .diagnostics = &diag, + }) catch return null; // An empty model is corrupt only when a provider is set; a null provider // (LLM disabled) legitimately has no model to remember. - if (remembered.provider != null and remembered.model.len == 0) { - std.zon.parse.free(allocator, remembered); - return null; - } + if (remembered.provider != null and remembered.model.len == 0) return null; return remembered; } @@ -423,15 +423,16 @@ pub fn reconcileModel( const testing = @import("../testing.zig"); -test "parseRemembered: invalid enum is rejected without leaking" { - // A bad enum builds an owned error note; the leak detector fails here if - // the Diagnostics doesn't free it. - try testing.expect(parseRemembered(testing.allocator, ".{ .provider = .not_a_provider, .model = \"x\" }") == null); +test "parseRemembered: invalid enum is rejected" { + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + try testing.expect(parseRemembered(testing.allocator, arena.allocator(), ".{ .provider = .not_a_provider, .model = \"x\" }") == null); } test "parseRemembered: valid file round-trips" { - const remembered = parseRemembered(testing.allocator, ".{ .provider = null, .model = \"some-model\" }").?; - defer std.zon.parse.free(testing.allocator, remembered); + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const remembered = parseRemembered(testing.allocator, arena.allocator(), ".{ .provider = null, .model = \"some-model\" }").?; try testing.expect(remembered.provider == null); try testing.expectString("some-model", remembered.model); // Absent `stream` is null so pre-streaming files still fall back to the default. @@ -439,14 +440,16 @@ test "parseRemembered: valid file round-trips" { } test "parseRemembered: stream field round-trips" { - const remembered = parseRemembered(testing.allocator, ".{ .model = \"m\", .stream = false }").?; - defer std.zon.parse.free(testing.allocator, remembered); + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const remembered = parseRemembered(testing.allocator, arena.allocator(), ".{ .model = \"m\", .stream = false }").?; try testing.expect(remembered.stream == false); } test "parseRemembered: search_engine field round-trips" { - const remembered = parseRemembered(testing.allocator, ".{ .model = \"m\", .search_engine = .brave }").?; - defer std.zon.parse.free(testing.allocator, remembered); + var arena: std.heap.ArenaAllocator = .init(testing.allocator); + defer arena.deinit(); + const remembered = parseRemembered(testing.allocator, arena.allocator(), ".{ .model = \"m\", .search_engine = .brave }").?; try testing.expect(remembered.search_engine == .brave); } diff --git a/src/agent/welcome.zig b/src/agent/welcome.zig index 517f045e5..27754d1cc 100644 --- a/src/agent/welcome.zig +++ b/src/agent/welcome.zig @@ -83,7 +83,7 @@ comptime { /// hints beside it, vertically centered. `llm_active` picks the tagline. pub fn print(llm_active: bool) void { var version_buf: [192]u8 = undefined; - const version: []const u8 = std.fmt.bufPrint(&version_buf, ansi.dim ++ "{s}" ++ ansi.reset, .{lp.build_config.version}) catch ""; + const version: []const u8 = std.mem.print(&version_buf, ansi.dim ++ "{s}" ++ ansi.reset, .{lp.build_config.version}) catch ""; var lines: [9][]const u8 = undefined; var n: usize = 0; diff --git a/src/browser/EventManagerBase.zig b/src/browser/EventManagerBase.zig index f15d26646..1e0ce977c 100644 --- a/src/browser/EventManagerBase.zig +++ b/src/browser/EventManagerBase.zig @@ -476,7 +476,7 @@ pub const Listener = struct { }; }, .string => |string| { - const str = try arena.dupeZ(u8, string.str()); + const str = try arena.dupeSentinel(u8, string.str(), 0); local.eval(str, null) catch |err| { if (err == error.ExecutionTerminated) { return error.ExecutionTerminated; diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index dd3280c8b..cedcc6eda 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -479,21 +479,14 @@ pub fn chained(self: *Factory, values: anytype) !*ChainedLeaf(@TypeOf(values)) { } pub fn chainedWithAllocator(allocator: Allocator, values: anytype) !*ChainedLeaf(@TypeOf(values)) { - const fields = @typeInfo(@TypeOf(values)).@"struct".fields; - const types = comptime blk: { - var types: [fields.len]type = undefined; - for (fields, 0..) |f, i| { - types[i] = f.type; - } - break :blk types; - }; + const types = @typeInfo(@TypeOf(values)).@"struct".field_types; comptime { for (types[1..], 0..) |T, i| { assert(reflect.Proto(T).? == types[i]); } } - const chain = try PrototypeChain(&types).allocate(allocator); + const chain = try PrototypeChain(types).allocate(allocator); inline for (0..types.len) |i| { const ptr = chain.get(i); ptr.* = values[i]; @@ -505,8 +498,8 @@ pub fn chainedWithAllocator(allocator: Allocator, values: anytype) !*ChainedLeaf } fn ChainedLeaf(comptime Values: type) type { - const fields = @typeInfo(Values).@"struct".fields; - return fields[fields.len - 1].type; + const field_types = @typeInfo(Values).@"struct".field_types; + return field_types[field_types.len - 1]; } pub fn document(self: *Factory, child: anytype) !*@TypeOf(child) { @@ -684,8 +677,8 @@ fn typeInit(comptime Parent: type, value: anytype) Parent.Type { } fn subtypeTag(comptime Parent: type, comptime V: type) Parent.Type { - for (@typeInfo(Parent.Type).@"enum".fields) |f| { - const tag: Parent.Type = @enumFromInt(f.value); + for (@typeInfo(Parent.Type).@"enum".field_values) |field_value| { + const tag: Parent.Type = @fromBackingInt(field_value); if (Parent.Subtype(tag) == V) return tag; } @compileError(@typeName(V) ++ " is not a subtype of " ++ @typeName(Parent)); @@ -699,9 +692,9 @@ fn subtypeTag(comptime Parent: type, comptime V: type) Parent.Type { // This only works because we never have a union with a field S and another // field *S. fn unionFieldName(comptime T: type, comptime V: type) []const u8 { - inline for (@typeInfo(T).@"union".fields) |field| { - if (reflect.Struct(field.type) == reflect.Struct(V)) { - return field.name; + inline for (@typeInfo(T).@"union".field_names, @typeInfo(T).@"union".field_types) |field_name, field_type| { + if (reflect.Struct(field_type) == reflect.Struct(V)) { + return field_name; } } @compileError(@typeName(V) ++ " is not a valid type for " ++ @typeName(T) ++ ".type"); diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 11d70286c..ea6073350 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -696,7 +696,7 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo else if (is_srcdoc) "about:srcdoc" else - try self.arena.dupeZ(u8, request_url); + try self.arena.dupeSentinel(u8, request_url, 0); // even though about:blank navigations may share the same _data_, we // have to do this to make sure window.location is at a unique _address_. @@ -825,7 +825,7 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo self.url = blk: { if (URL.isCompleteHTTPUrl(request_url)) { - break :blk try self.arena.dupeZ(u8, request_url); + break :blk try self.arena.dupeSentinel(u8, request_url, 0); } break :blk try std.mem.concatWithSentinel(self.arena, u8, &.{ "http://", request_url }, 0); }; @@ -836,7 +836,7 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo .reason = opts.reason, .method = opts.method, .body = if (opts.body) |b| try self.arena.dupe(u8, b) else null, - .header = if (opts.header) |h| try self.arena.dupeZ(u8, h) else null, + .header = if (opts.header) |h| try self.arena.dupeSentinel(u8, h, 0) else null, }; const transfer = try http_client.newRequest(.{ @@ -947,7 +947,7 @@ pub fn scheduleNavigation(self: *Frame, request_url: []const u8, opts: NavigateO fn scheduleNavigationWithArena(originator: *Frame, arena: *lp.Arena, request_url: []const u8, opts: NavigateOpts, nt: Navigation) !void { const resolved_url, const is_about_something = blk: { if (URL.isCompleteHTTPUrl(request_url)) { - break :blk .{ try arena.dupeZ(u8, request_url), false }; + break :blk .{ try arena.dupeSentinel(u8, request_url, 0), false }; } if (std.mem.eql(u8, request_url, "about:blank")) { @@ -1012,7 +1012,7 @@ fn scheduleNavigationWithArena(originator: *Frame, arena: *lp.Arena, request_url const is_fragment_navigation = !std.mem.eql(u8, target.url, resolved_url) and URL.eqlDocument(target.url, resolved_url); if (!opts.force and is_fragment_navigation) { const old_url = target.url; - target.url = try target.arena.dupeZ(u8, resolved_url); + target.url = try target.arena.dupeSentinel(u8, resolved_url, 0); const location = try Location.init(target.url, target); location.acquireRef(); @@ -1061,7 +1061,7 @@ fn scheduleNavigationWithArena(originator: *Frame, arena: *lp.Arena, request_url // that from its owner. Only a top-level navigation's initiator is another // document. if (nav_opts.initiator_url == null and target.parent == null and std.mem.startsWith(u8, referrer_source, "http")) { - nav_opts.initiator_url = .{ .url = try arena.dupeZ(u8, referrer_source) }; + nav_opts.initiator_url = .{ .url = try arena.dupeSentinel(u8, referrer_source, 0) }; } if (nav_opts.initiator_origin == null) { if (originator.origin) |o| { @@ -1468,7 +1468,7 @@ fn frameHeaderDoneCallback(transfer: *HttpClient.Transfer) !HttpClient.Transfer. const response_url = transfer.req.url; if (std.mem.eql(u8, response_url, self.url) == false) { // would be different than self.url in the case of a redirect - self.url = try self.arena.dupeZ(u8, response_url); + self.url = try self.arena.dupeSentinel(u8, response_url, 0); self.origin = try URL.getOrigin(self.arena, self.url); } @@ -3868,10 +3868,10 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For opts.body = buf.written(); opts.header = switch (encoding) { .urlencode => "Content-Type: application/x-www-form-urlencoded", - .formdata => |b| try std.fmt.allocPrintSentinel(arena.allocator(), "Content-Type: multipart/form-data; boundary={s}", .{b}, 0), + .formdata => |b| try arena.allocator().printSentinel("Content-Type: multipart/form-data; boundary={s}", .{b}, 0), // Per WHATWG HTML §4.10.21.6, text/plain submissions include the form's // resolved encoding (accept-charset or document charset). - .plaintext => try std.fmt.allocPrintSentinel(arena.allocator(), "Content-Type: text/plain; charset={s}", .{charset}, 0), + .plaintext => try arena.allocator().printSentinel("Content-Type: text/plain; charset={s}", .{charset}, 0), }; } else { action = try URL.concatQueryString(arena.allocator(), action, buf.written()); diff --git a/src/browser/Mime.zig b/src/browser/Mime.zig index 9ca9ae385..2300b803f 100644 --- a/src/browser/Mime.zig +++ b/src/browser/Mime.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const repeat = @import("../string.zig").repeat; const Allocator = std.mem.Allocator; const Mime = @This(); @@ -28,9 +29,15 @@ charset_len: usize = default_charset_len, is_default_charset: bool = true, /// String "UTF-8" continued by null characters. -const default_charset = .{ 'U', 'T', 'F', '-', '8' } ++ .{0} ** 36; +const default_charset = charsetBuf("UTF-8"); const default_charset_len = 5; +fn charsetBuf(comptime name: []const u8) [41]u8 { + var buf: [41]u8 = @splat(0); + @memcpy(buf[0..name.len], name); + return buf; +} + /// Mime with unknown Content-Type, empty params and empty charset. pub const unknown = Mime{ .content_type = .{ .unknown = {} } }; @@ -376,7 +383,7 @@ pub fn sniff(body: []const u8) ?Mime { // UTF-16 big-endian BOM return .{ .content_type = .{ .text_plain = {} }, - .charset = .{ 'U', 'T', 'F', '-', '1', '6', 'B', 'E' } ++ .{0} ** 33, + .charset = comptime charsetBuf("UTF-16BE"), .charset_len = 8, .is_default_charset = false, }; @@ -385,7 +392,7 @@ pub fn sniff(body: []const u8) ?Mime { // UTF-16 little-endian BOM return .{ .content_type = .{ .text_plain = {} }, - .charset = .{ 'U', 'T', 'F', '-', '1', '6', 'L', 'E' } ++ .{0} ** 33, + .charset = comptime charsetBuf("UTF-16LE"), .charset_len = 8, .is_default_charset = false, }; @@ -1017,7 +1024,7 @@ test "Mime: parse charset (WHATWG parameter semantics)" { try expect(.{ .content_type = .{ .text_html = {} }, .charset = "UTF-8" }, "text/html;charset =gbk"); // A long preceding parameter doesn't hide a later charset. - try expect(.{ .content_type = .{ .text_html = {} }, .charset = "gbk" }, "text/html;" ++ ("a" ** 130) ++ "=x;charset=gbk"); + try expect(.{ .content_type = .{ .text_html = {} }, .charset = "gbk" }, "text/html;" ++ (repeat("a", 130)) ++ "=x;charset=gbk"); } test "Mime: isHTML" { diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 63c165473..9e7f20d6c 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -406,7 +406,7 @@ pub fn createBlobUrl(self: *Page, blob: *Blob, origin: ?[]const u8, creator_fram var uuid: [36]u8 = undefined; @import("../id.zig").uuidv4(&uuid); - const url = try std.fmt.allocPrint(self.frame_arena, "blob:{s}/{s}", .{ origin orelse "null", uuid }); + const url = try self.frame_arena.print("blob:{s}/{s}", .{ origin orelse "null", uuid }); try self.blob_urls.put(self.frame_arena, url, .{ .blob = blob, .creator = creator_frame_id }); blob.acquireRef(); return url; diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 824c7c2d4..a492cc730 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -141,7 +141,7 @@ pub fn preloadScript(self: *ScriptManager, element: ?*Element.Html, url: []const const arena = try frame.getArena(.small, "SM.preloadScript"); errdefer arena.release(); - const owned_url = try arena.dupeZ(u8, url); + const owned_url = try arena.dupeSentinel(u8, url, 0); const script = try arena.create(Script); script.* = .{ diff --git a/src/browser/SelectorPath.zig b/src/browser/SelectorPath.zig index c5713cd51..a8b8c75bd 100644 --- a/src/browser/SelectorPath.zig +++ b/src/browser/SelectorPath.zig @@ -63,7 +63,7 @@ fn buildGreedy(self: SelectorPath, target: *Element) !?[]const u8 { var el = target.parentElement(); while (el) |ancestor| { el = ancestor.parentElement(); - const trial = try std.fmt.allocPrint(self.arena, "{s} {s}", .{ try self.localSegment(ancestor), candidate }); + const trial = try self.arena.print("{s} {s}", .{ try self.localSegment(ancestor), candidate }); const trial_count = self.matchCount(trial); if (trial_count != 0 and trial_count < count) { candidate = trial; @@ -92,7 +92,7 @@ fn buildStrictPath(self: SelectorPath, target: *Element) !?[]const u8 { fn localSegment(self: SelectorPath, el: *Element) ![]const u8 { if (el.getId()) |id| { if (id.len != 0) { - const id_sel = try std.fmt.allocPrint(self.arena, "#{s}", .{try CSS.escape(id, self.frame)}); + const id_sel = try self.arena.print("#{s}", .{try CSS.escape(id, self.frame)}); if (self.isFirstMatch(el, id_sel)) return id_sel; } } @@ -101,7 +101,7 @@ fn localSegment(self: SelectorPath, el: *Element) ![]const u8 { if (!self.siblingMatches(el, base)) return base; if (try self.hasSegment(el, base)) |sel| return sel; - if (nthOfType(el)) |n| return try std.fmt.allocPrint(self.arena, "{s}:nth-of-type({d})", .{ base, n }); + if (nthOfType(el)) |n| return try self.arena.print("{s}:nth-of-type({d})", .{ base, n }); return base; } @@ -118,7 +118,7 @@ fn hasSegment(self: SelectorPath, el: *Element, base: []const u8) !?[]const u8 { try self.enqueueChildren(&queue, d); const desc = (try self.descriptor(d)) orelse continue; - const candidate = try std.fmt.allocPrint(self.arena, "{s}:has({s})", .{ base, desc }); + const candidate = try self.arena.print("{s}:has({s})", .{ base, desc }); if (self.isFirstMatch(el, candidate)) return candidate; } return null; @@ -150,7 +150,7 @@ fn qualifyByAttrs(self: SelectorPath, base: []const u8, el: *Element, comptime a inline for (attrs) |attr| { if (el.getAttributeSafe(comptime .wrap(attr))) |value| { if (value.len != 0 and isPlainAttrValue(value)) { - sel = try std.fmt.allocPrint(self.arena, "{s}[{s}=\"{s}\"]", .{ sel, attr, value }); + sel = try self.arena.print("{s}[{s}=\"{s}\"]", .{ sel, attr, value }); added = true; } } diff --git a/src/browser/StyleManager.zig b/src/browser/StyleManager.zig index 853a08c7a..834ba50b8 100644 --- a/src/browser/StyleManager.zig +++ b/src/browser/StyleManager.zig @@ -312,7 +312,7 @@ fn registerLayerPath(self: *StyleManager, build_arena: Allocator, parent: u16, d fn internAnonymousLayer(self: *StyleManager, build_arena: Allocator, parent: u16) Allocator.Error!u16 { const id = self.next_anon_layer; // \x00{d} isn't a valid layer name, so this can't conflict - const name = try std.fmt.allocPrint(build_arena, "\x00{d}", .{id}); + const name = try build_arena.print("\x00{d}", .{id}); self.next_anon_layer = id + 1; return self.internLayer(build_arena, parent, name); } @@ -321,7 +321,7 @@ fn internLayer(self: *StyleManager, build_arena: Allocator, parent: u16, name: [ const path = if (parent == NO_LAYER) try build_arena.dupe(u8, name) else - try std.fmt.allocPrint(build_arena, "{s}.{s}", .{ self.layers.items[parent].path, name }); + try build_arena.print("{s}.{s}", .{ self.layers.items[parent].path, name }); const gop = try self.layer_ids.getOrPut(build_arena, path); if (gop.found_existing) { @@ -738,7 +738,7 @@ fn Group(comptime Spec: type) type { const Declared = Spec.Declared; const Computed = Spec.Computed; const Field = std.meta.FieldEnum(Declared); - const fields = std.meta.fieldNames(Declared); + const fields = @typeInfo(Declared).@"struct".field_names; comptime { // compute copies each declared value into its Computed namesake @@ -1026,7 +1026,7 @@ pub fn ruleInserted(self: *StyleManager, sheet: *CSSStyleSheet, rule: *CSSRule) fn appendable(self: *const StyleManager, sheet: *CSSStyleSheet, rule: *CSSRule) bool { const rules = sheet._css_rules orelse return false; - if (rules._rules.getLastOrNull() != rule) { + if (rules._rules.last() != rule) { return false; } const sheets = self.frame.document._style_sheets orelse return false; @@ -1282,7 +1282,7 @@ const Visibility = struct { // element — per CSS Cascade §6.1 any normal-origin author rule beats UA // origin regardless of specificity, so `.x { display: flex }` on a // `