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 | |
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 | |
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 | |