Skip to content

Sema: add zig_lazy parameter qualifier for inline calls#22

Closed
Jarred-Sumner wants to merge 5 commits into
upgrade-0.15.2from
jarred/lazy
Closed

Sema: add zig_lazy parameter qualifier for inline calls#22
Jarred-Sumner wants to merge 5 commits into
upgrade-0.15.2from
jarred/lazy

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Adds a zig_lazy parameter qualifier. A zig_lazy parameter's argument expression is captured unevaluated and only analyzed on first reference inside the inlined body. If that reference is behind a comptime-dead branch, the expression is never analyzed at all — side effects, type instantiation, and @compileError all disappear.

pub inline fn assert(zig_lazy ok: bool) void {
    if (comptime !runtime_safety) return;   // ok never evaluated
    if (!ok) unreachable;
}

pub inline fn log(comptime fmt: []const u8, zig_lazy args: anytype) void {
    if (comptime !enabled) return;          // args never evaluated
    realPrint(fmt, args);
}

Semantics

  • The argument is evaluated at most once, on first reference, with AIR emitted into the caller's block (so first-use inside a runtime if still hoists evaluation before the inline body — laziness is comptime-only).
  • If the call is not actually inlined (e.g. callconv(.auto) in Debug builds), zig_lazy is a no-op and the argument is evaluated eagerly.
  • Comptime call memoization is disabled for functions with zig_lazy params.
  • zig_lazy is a real keyword; lazy remains a valid identifier.

Implementation

Mirrors the noalias plumbing: a lazy_bits: u32 mask is packed into FuncFancy trailing data and surfaced via Zir.FnInfo. analyzeCall skips analyzeArg for marked params on inline calls and stashes a LazyArg thunk (caller code/inst_map/block + arg index) on Sema. resolveInst falls through to resolveLazyArg on the first lookup, which swaps to caller context, runs analyzeArg, and memoizes the result in the callee's inst_map.

Tests

test/behavior/lazy_param.zig — 19 cases covering elision, evaluation, anytype tuples, mixed-type masks, position independence, evaluate-once, first-use ordering, nested inline, runtime-branch hoisting, log-style runtime tuples, and eager fallback for non-inline callconv.

A zig_lazy parameter's argument expression is captured unevaluated and
only analyzed on first reference inside the inlined body, so arguments
behind comptime-dead branches (assert/log/trace gated by build flags)
are guaranteed to generate no code. Degrades to ordinary eager
evaluation when the call is not inlined.
@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5a39df22-2094-4e1a-814c-a66c8d75d23b

📥 Commits

Reviewing files that changed from the base of the PR and between 5b90660 and 771d1d7.

📒 Files selected for processing (3)
  • lib/docs/wasm/html_render.zig
  • tools/docgen.zig
  • tools/doctest.zig

Walkthrough

Adds a new zig_lazy parameter qualifier across the compiler: lexer/tokenizer, parser, AST rendering, lowering (lazy_bits), ZIR encoding/decoding, inlining-aware semantic analysis for deferred argument evaluation, source-span and printing updates, and a comprehensive test suite.

Changes

Cohort / File(s) Summary
Tokenizer & Parser
lib/std/zig/tokenizer.zig, lib/std/zig/Parse.zig
Register "zig_lazy" as a keyword/token (Token.Tag.keyword_zig_lazy) and accept .keyword_zig_lazy as an optional parameter qualifier alongside comptime/noalias.
AST & Rendering
lib/std/zig/Ast.zig, lib/std/zig/Ast/Render.zig
AST iterator and renderer treat .keyword_zig_lazy like existing param qualifiers when scanning/consuming tokens and when formatting parameter declarations (spacing/advancement).
Lowering & ZIR encoding
lib/std/zig/AstGen.zig, lib/std/zig/Zir.zig
Lowering gathers a lazy_bits: u32 bitset (32-param limit) into function payloads; ZIR adds has_any_lazy bit, stores/decodes trailing lazy_bits, and FnInfo gains a lazy_bits field. Review bitfield layout, reserved padding, and trailing-word offsets.
Semantic analysis (inlining)
src/Sema.zig
Adds deferred evaluation support: public LazyArg struct and sema.lazy_args storage; resolveInst can call resolveLazyArg to analyze lazy args on first use. Error APIs updated to return CompileError!. Memoization, guards for deferred .none, and debug-var emission adjusted.
Source spans & ZIR printing
src/Zcu.zig, src/print_zir.zig
Parameter span computation treats zig_lazy as an extendable prefix for param spans. ZIR text printer reads lazy_bits when has_any_lazy and prints lazy=0b... metadata when nonzero.
Tests
test/behavior.zig, test/behavior/lazy_param.zig
Adds behavior/lazy_param.zig and imports it into behavior tests. New tests exercise lazy-arg elision, evaluation order, single-evaluation semantics, nesting/inline interactions, hoisting, callconv effects, and various edge cases.
Docs/HTML rendering & tooling
lib/docs/wasm/html_render.zig, tools/docgen.zig, tools/doctest.zig
Syntax-highlighting and HTML/token rendering updated to emit .keyword_zig_lazy as a keyword (tok-kw).
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding the zig_lazy parameter qualifier for inline calls, which is the primary feature introduced across all modified files.
Description check ✅ Passed The description is directly related to the changeset, providing clear context about the zig_lazy parameter qualifier, its semantics, implementation approach, and test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Sema.zig`:
- Around line 249-251: The inst_map field in Sema (currently declared as
inst_map: InstMap) should not store a by-value snapshot because
analyzeArg/analyzeCall can grow/reallocate the caller map; change it to hold a
mutable reference to the caller map instead (e.g., make the field type a pointer
or reference to InstMap) so pointer/length updates propagate back to
old_inst_map when restoring caller context; update any construction sites that
populate Sema.inst_map and uses in analyzeCall/analyzeArg to pass and
dereference the mutable reference accordingly so the caller's InstMap is mutated
in place rather than copied.
- Around line 7932-7938: The pass currently skips parameters when
sema.inst_map.get(inst) returns null, which silently drops debug info for
lazily-forced params; change the logic so that when inst_map.get(inst) is null
you attempt to force/resolve the lazy arg (call resolveLazyArg or the
appropriate resolver) and then re-check sema.inst_map.get(inst) before
continuing, and only after that skip; once air_inst is available call
sema.addDbgVar(&child_block, air_inst, .dbg_arg_inline, param_name) as before.
If a resolver function is not available in this scope, instead record the inst
in a deferred list and emit dbg vars after body analysis (i.e., run this
emission after resolveLazyArg populates inst_map). Ensure you reference
sema.inst_map.get, resolveLazyArg, sema.addDbgVar, dbg_arg_inline and
child_block when making the change.
- Around line 2040-2047: The code currently clears the caller's lazy context by
setting sema.lazy_args = .empty before analyzing the lazy expression; instead
assign the callee's lazy-argument context (the field on the lazy record, e.g.
lazy.args or lazy.lazy_args) to sema.lazy_args so nested inline/lazy lookups
work, and keep the existing defer that restores cur_lazy_args; update the
assignment of sema.lazy_args (the line setting it to .empty) to use the lazy
record's lazy-args field and ensure the defer continues to restore cur_lazy_args
alongside cur_code and cur_inst_map.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6b924721-6572-40b6-99a6-0ab13062bed9

📥 Commits

Reviewing files that changed from the base of the PR and between 24475de and 165bd5d.

📒 Files selected for processing (11)
  • lib/std/zig/Ast.zig
  • lib/std/zig/Ast/Render.zig
  • lib/std/zig/AstGen.zig
  • lib/std/zig/Parse.zig
  • lib/std/zig/Zir.zig
  • lib/std/zig/tokenizer.zig
  • src/Sema.zig
  • src/Zcu.zig
  • src/print_zir.zig
  • test/behavior.zig
  • test/behavior/lazy_param.zig

Comment thread src/Sema.zig Outdated
Comment thread src/Sema.zig
Comment thread src/Sema.zig

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/behavior/lazy_param.zig`:
- Around line 338-341: Add a regression to verify disabled comptime memoization
by updating the "zig_lazy: comptime call with elided arg" test to perform two
identical comptime invocations of the same zig_lazy helper (e.g., middleLazy)
that use a side-effecting counter: declare a comptime-visible
counter/side-effect, call middleLazy(comptime-side-effecting-arg,
`@compileError`("dead"), 2) twice, and assert the counter increments twice and the
returned results are correct; ensure the new assertions reference the existing
middleLazy/zig_lazy function names so the test verifies that the second comptime
call is not served from cache.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e4d10e39-3fda-46e3-8999-9ba29d26c5ab

📥 Commits

Reviewing files that changed from the base of the PR and between 165bd5d and 51e6a05.

📒 Files selected for processing (2)
  • src/Sema.zig
  • test/behavior/lazy_param.zig

Comment on lines +338 to +341
test "zig_lazy: comptime call with elided arg" {
const r = comptime middleLazy(1, @compileError("dead"), 2);
try expect(r == 3);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add a regression for disabled comptime memoization.

This only proves elision for a single comptime call. The PR also changes zig_lazy functions to bypass comptime call memoization, so it would be worth adding two identical comptime invocations with a counter/side effect to ensure they are not served from cache.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/behavior/lazy_param.zig` around lines 338 - 341, Add a regression to
verify disabled comptime memoization by updating the "zig_lazy: comptime call
with elided arg" test to perform two identical comptime invocations of the same
zig_lazy helper (e.g., middleLazy) that use a side-effecting counter: declare a
comptime-visible counter/side-effect, call
middleLazy(comptime-side-effecting-arg, `@compileError`("dead"), 2) twice, and
assert the counter increments twice and the returned results are correct; ensure
the new assertions reference the existing middleLazy/zig_lazy function names so
the test verifies that the second comptime call is not served from cache.

@Jarred-Sumner Jarred-Sumner deleted the jarred/lazy branch April 27, 2026 01:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant