Series contents · Resources and Scheduling · 阅读中文版
“How did r7oops get past the register parser?”
“The parser successfully read 7.”
“What about the oops?”
“It didn’t ask.”
Many backend bugs arise not from a complete lack of checks, but from checks that answer a weaker question than intended. Parsing an integer proves only that the beginning of a string contains a parseable fragment, not that the whole field obeys the grammar. Splitting an immediate into high and low parts proves only that the formula looks plausible, not that an extra bit cannot appear at a boundary.
Register names form a complete language
Suppose our teaching assembly accepts r0 through r15. Valid input requires more than a number within range. The prefix must be correct, the numeric part must be nonempty, every character must be consumed, and the grammar must specify whether a plus sign, whitespace, or leading zeros are allowed.
Validation can be divided into five steps:
1 | Check the prefix |
The verified change replaces a permissive string-to-integer call with a conversion that reports both the parse endpoint and an error status, and explicitly requires consumption of the entire numeric suffix. The associated tests cover trailing garbage, whitespace, plus and minus signs, excessively long numbers, out-of-range indices, and aliases.
“Strict” should not mean “accept as little as possible.” Some aliases and numbers with leading zeros were already defined as valid, and the tests retain them. Strictness means implementing the established grammar completely, not opportunistically tightening every historical behavior.
Why failure may be deferred until binary export
Some generators retain register strings and instruction objects, parsing the strings only when encoding fields. Constructing an instruction can therefore succeed, with r7oops discovered only during export.
Deferring this check is not inherently wrong, but the interface must make clear that successful construction is not a final guarantee of encodability. The error boundary must cover export, and failed export should preserve the caller’s existing output rather than replacing valid old data with incomplete new data. The associated tests specifically check this behavior, complementing the status interfaces discussed in Error Boundaries.
To report an error earlier, construction could parse the string into a typed register. That entails interface migration and decisions about diagnostic ownership. Retaining strings may also serve readability or tool interoperability. The right validation layer depends on the overall design; one invalid input does not establish that all deferred encoding is undesirable.
Immediate splitting is vulnerable at sign boundaries
Consider a teaching instruction set with machine-word width W and low immediate width L, where the low part participates in addition as a signed value. We want to construct an unsigned bit pattern x:
1 | high = ((x + 2^(L-1)) >> L) masked_to_(W-L)_bits |
Why add a bias to the high part? When the highest bit of the low part is 1, that part is interpreted as negative, so the high part must compensate for sign extension. Why apply a mask as well? Near the upper limit of the full word, this compensation can push the high part beyond its field width.
Take the teaching example W=8, L=3, and x=255. The low three bits are 111, which represent −1 as a signed three-bit number. With the compensation and fixed-width wraparound, the high part becomes 0. The eight-bit result of 0 + (−1) is exactly 255. Treating the compensated high part as an unbounded positive integer and putting it directly into the field can cause an erroneous rejection or encode an extra bit.
The reviewed fix applies a field mask to the high part and checks the final execution result for boundary values. This implements finite-width semantics; it is not a license to discard high bits arbitrarily in mathematics.
Range checks must precede the first write
Another change moves the check for an immediate exceeding the supported width ahead of pseudo-instruction insertion. Previously, the control flow could record the high-level load intention first and then throw during expansion, leaving the actual and pseudo-instruction streams inconsistent after failure.
Checking earlier ensures that this class of invalid call changes neither output. It is a strong local guarantee: this parameter error occurs before side effects. It does not establish rollback for every generator failure, because other errors may occur after several valid instructions have already been emitted.
Cheap, decisive validation is often worth doing early in these interfaces. It reduces cleanup work and makes expectations for negative tests clearer. Some errors still depend on later context, however, and must be handled later. “Move all validation up front” does not solve those cases.
Why comparing golden bytes is not enough
The associated tests use two forms of verification. One compares boundary inputs against exact expected machine words. The other uses a small scalar execution model to compute the instructions’ results, checking that the loaded register equals the original bit pattern and repeating the check for further sampled values.
These answer different questions. Golden bytes detect unexpected encoding changes; the execution model checks whether expansion reconstructs the original value. If both derive from the same flawed algorithm, both may be wrong together, so a teaching experiment should use independent reference logic where possible.
This small model supports only the instruction subset it understands. It should not be called a complete device simulator. Defining its limits protects the conclusion better than giving it an ambitious name.
Enum values need “complete parsing” too
The same group of boundary changes also adds validation for the enumeration selecting a reduction statistic. A C++ enum object does not guarantee that its runtime value is one of the declared enumerators. Explicit casts, deserialization, or a damaged configuration can still introduce an unrecognized choice.
If a branch handles known kinds without a rejection path, an invalid value may fall through to a default algorithm, producing an apparently valid result with the wrong semantics. As with register parsing, looking like the right type is not the same as having a validated value.
Loop counts present a similar issue. A field may fit in a large integer type without being representable by the target scalar-load instruction. Validation belongs at an entry point where the feature is enabled, distinguishing unused configuration fields from parameters that will actually be encoded.
A practical way to choose boundaries
Boundary testing does not require blindly accumulating random numbers. Start by listing where the algorithm changes behavior: whether one short-immediate instruction can represent the value, where the low part changes from positive to negative, where the high part carries, and where the full word reaches its upper limit. Testing the value just before, at, and just after each transition often reveals more than uniform sampling.
The same approach works for strings. Start with a valid name, then separately add a trailing character, leading whitespace, a sign, an excessively long number, or a different prefix. Change one property at a time so that a failure points clearly to syntax validation, range validation, or the alias table. Combining several errors may prove rejection without showing which rule was responsible.
Also distinguish accepted input from canonical output. A register may accept several aliases while always printing in one standard form. This helps stabilize golden tests without implying that the other aliases should be rejected. Whether leading zeros are preserved is likewise a representation choice, not a difference in register identity.
Finally, express each test’s expectation in two parts: whether it should succeed or fail, and which output states it may change. Correctly rejecting an error while clearing an existing vector can still violate the caller’s contract. Boundary tests are most valuable when they constrain both results and side effects, rather than merely checking whether an exception was thrown.
Regression matrix and experiments not yet run
| Category | Boundary selection | What to verify |
|---|---|---|
| Register syntax | Empty suffix, trailing characters, signs, whitespace | The entire string matches the established grammar |
| Register range | Minimum, maximum, just out of range, extremely long numbers | No truncation or unintended acceptance |
| Immediate | Either side of the low part’s sign transition | Correct high-part compensation and low-part sign |
| Full-word upper limit | Maximum bit pattern and nearby values | The high-part field does not overflow |
| Invalid immediate | Values exceeding the supported width | Actual and pseudo outputs remain unchanged |
| Enum | Integers outside the known set | Explicit failure rather than a default fallback |
Some of these directions already have tests in the commits. A further experiment could exhaustively enumerate every bit pattern at teaching widths W=8 or W=12, checking that interpretation after expansion equals the original value. For register strings, insertion, deletion, and substitution mutations of valid strings could check the accepted and rejected sets.
Performance claims should remain limited. Strict parsing may avoid exception-conversion costs, but any effect on compilation time depends on call frequency and the input distribution. Its most direct benefit here is stopping invalid input at the right boundary, reducing mysterious encoding errors far from the cause.
“Parsing succeeded” is meaningful only after success has been defined. For a compiler, understanding the prefix and ignoring the rest is rarely a form of tolerance that should reach the device.
Back to series contents · Previous · Next
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !