Skip to content

Call site

Attribute validation errors back to the call that dragged them in.

Inlining is lossy in exactly the way that matters for diagnostics. InlinePass splices a callee's body into the caller and the invoke disappears, but the spliced statements keep the callee's source info. If one of them then fails validation, the error names a file the user never opened::

Gemini Logical Validation:
  - Non-constant iterable in for loop is not supported ...
      File ".../gemini/logical/stdlib/post_processing.py", line 12, col 65
    │  return logical.default_post_processing(qbs)

and the excerpt is a chimera. ValidationError.attach overwrites source.lineno_begin with the entry method's and resolves the source lines against the entry's py_func, so the reader gets the callee's filename against the caller's line numbers and the caller's code -- line 12 above is in the caller, not in post_processing.py. When the callee's line number falls past the end of the caller, which is the usual case, the excerpt comes out empty and the diagnostic is a bare filename. That is bloqade-internal#449.

The lowering is not wrong to reject the program: default_post_processing loops over range(1, len(register)), which only aggressive_unroll=True can flatten. What is missing is which call introduced the offending code and what to do about it. So InlineOrigins records the call sites before inlining destroys them, and afterwards folds them back into the messages and re-points the excerpt at the kernel the statement actually came from.

A statement is identified with the kernel it was written in by (source.file, source.lineno_begin) -- filename plus that kernel's offset into it. File alone is not enough: a helper defined a few lines above the kernel that calls it is the common case, and it shares the caller's filename while needing exactly the same attribution.

Which call to blame comes from the static call graph: every method reachable from the entry inherits the call site of the first hop that reaches it, breadth-first so the shallowest -- the one written in the user's own kernel -- wins. That is coarse enough to survive nested inlining and precise enough to name the one call the user can change.

CallSite dataclass

CallSite(callee: str, file: str | None, lineno: int)

Where the validated kernel calls into another one.

InlineOrigins dataclass

InlineOrigins(
    callees: dict[
        tuple[str, int], tuple[CallSite, Method]
    ] = dict(),
    offsets: dict[int, int] = dict(),
)

Where each inlined statement came from, and which call brought it in.

Used across three points in run_pass, in order::

origins = InlineOrigins.collect(mt)   # before the inliner runs
origins.snapshot(mt)                  # before the validation suite runs
origins.annotate(mt, suite.validate(mt)).raise_if_invalid()

The middle step exists because attach mutates node.source in place -- ValidationError.source is the statement's SourceInfo object, not a copy -- so the statement's true line offset has to be read before the suite touches it.

callees class-attribute instance-attribute

callees: dict[tuple[str, int], tuple[CallSite, Method]] = (
    field(default_factory=dict)
)

(file, line offset) -> the kernel defined there and the call reaching it.

offsets class-attribute instance-attribute

offsets: dict[int, int] = field(default_factory=dict)

id(stmt) -> the statement's own line offset, taken pre-attach.

annotate

annotate(
    method: Method,
    result: ValidationResult,
    *,
    aggressive_unroll: bool = False
) -> ValidationResult

Explain every error that came from inlined code, and fix its excerpt.

Returns result so it can be chained onto validate.

Every such error names the call that brought it in. UNROLL_HELP is added only where it can help: the error is an UnrollFixableError and the kernel was not already built with aggressive_unroll=True. Anything else -- a non-Clifford gate, a gate after the terminal measurement, or a loop that stays dynamic even under unrolling -- is just as wrong unrolled, so the advice would only mislead.

offsets is what decides whether an error is about inlined code: snapshot only records statements that came from some other kernel, so a hit means the statement was spliced in and a miss means the user wrote it where the error says.

The note goes into the error's message rather than its help, because ValidationResult._format_errors always prints the message but only prints help alongside a source caret -- which is precisely what fails to render for an inlined statement.

Source code in .venv/lib/python3.12/site-packages/bloqade/gemini/common/validation/call_site.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def annotate(
    self,
    method: ir.Method,
    result: ValidationResult,
    *,
    aggressive_unroll: bool = False,
) -> ValidationResult:
    """Explain every error that came from inlined code, and fix its excerpt.

    Returns ``result`` so it can be chained onto ``validate``.

    Every such error names the call that brought it in. ``UNROLL_HELP`` is
    added only where it can help: the error is an ``UnrollFixableError`` and
    the kernel was not already built with ``aggressive_unroll=True``.
    Anything else -- a non-Clifford gate, a gate after the terminal
    measurement, or a loop that stays dynamic even under unrolling -- is
    just as wrong unrolled, so the advice would only mislead.

    ``offsets`` is what decides whether an error is about inlined code:
    ``snapshot`` only records statements that came from some other kernel, so
    a hit means the statement was spliced in and a miss means the user wrote
    it where the error says.

    The note goes into the error's *message* rather than its ``help``,
    because ``ValidationResult._format_errors`` always prints the message but
    only prints ``help`` alongside a source caret -- which is precisely what
    fails to render for an inlined statement.
    """
    if not self.callees:
        return result

    for errors in result.errors.values():
        for err in errors:
            source = getattr(err, "source", None)
            if source is None or source.file is None:
                continue

            offset = self.offsets.get(id(err.node))
            if offset is None:
                continue

            found = self.callees.get((source.file, offset))
            if found is None:
                continue

            site, owner = found
            note = f"\n    (inlined from {site}"
            if not aggressive_unroll and isinstance(err, UnrollFixableError):
                note += f"; {UNROLL_HELP}"
            note += ")"
            if err.args and isinstance(err.args[0], str):
                err.args = (err.args[0] + note,) + err.args[1:]
            else:  # pragma: no cover - ValidationError always has a message
                err.args = (note,) + err.args

            _repoint(err, source, offset, owner)

    return result

collect classmethod

collect(method: Method) -> InlineOrigins

Record the call sites reachable from method.

Must run before inlining: the invokes it reads are gone afterwards.

The entry method itself is excluded. Code the user wrote in the kernel being compiled is not something a call introduced, and claiming otherwise would attach a misleading note to every ordinary error.

Source code in .venv/lib/python3.12/site-packages/bloqade/gemini/common/validation/call_site.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
@classmethod
def collect(cls, method: ir.Method) -> InlineOrigins:
    """Record the call sites reachable from ``method``.

    Must run **before** inlining: the invokes it reads are gone afterwards.

    The entry method itself is excluded. Code the user wrote in the kernel
    being compiled is not something a call introduced, and claiming otherwise
    would attach a misleading note to every ordinary error.
    """
    graph = CallGraph(method)
    origins = cls()

    # Direct invokes first, so each callee is credited to the call site
    # actually written in the entry method rather than to one further down.
    frontier: deque[tuple[ir.Method, CallSite]] = deque()
    seen: set[ir.Method] = set()
    for stmt in method.code.walk():
        trait = stmt.get_trait(ir.StaticCall)
        if trait is None or stmt.source is None:
            continue

        callee = trait.get_callee(stmt)
        if callee in seen:
            continue

        seen.add(callee)
        frontier.append(
            (
                callee,
                CallSite(
                    callee=callee.sym_name or "<lambda>",
                    file=stmt.source.file or method.file,
                    # `lineno` is relative to the enclosing kernel and
                    # `lineno_begin` is that kernel's offset into the file.
                    lineno=stmt.source.lineno + stmt.source.lineno_begin,
                ),
            )
        )

    entry = _key(method.file, method.lineno_begin)
    while frontier:
        reached, site = frontier.popleft()
        key = _key(reached.file, reached.lineno_begin)
        if key is not None and key != entry:
            origins.callees.setdefault(key, (site, reached))

        for callee in graph.callees(reached):
            if callee in seen:
                continue

            seen.add(callee)
            frontier.append((callee, site))

    return origins

snapshot

snapshot(method: Method) -> None

Record every inlined statement's line offset before attach runs.

Source code in .venv/lib/python3.12/site-packages/bloqade/gemini/common/validation/call_site.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def snapshot(self, method: ir.Method) -> None:
    """Record every inlined statement's line offset before ``attach`` runs."""
    if not self.callees:
        return

    entry = _key(method.file, method.lineno_begin)
    for stmt in method.code.walk():
        source = stmt.source
        if source is None:
            continue

        key = _key(source.file, source.lineno_begin)
        if key is None or key == entry:
            continue

        self.offsets[id(stmt)] = source.lineno_begin