Repository navigation
tools/ci/lane_syntax_check.py: catch the brace error CI cannot see, because CI never compiles the file - #2336
Conversation
… CI never compiles the file
engine#2330 said main did not compile. Checking it, **every commit in the window reported `build: success`**,
including `273f6e9df`, whose own subject is that sentence. The two are both right, and the reason is the lane:
CMakeLists.txt:2003 if(ONEBIT_NPU)
CMakeLists.txt:2108 target_sources(onebit PRIVATE app/forward_serve.cpp)
`app/forward_serve.cpp` is added to `onebit` **only inside the lane guard**, and CI builds with
`ONEBIT_NPU=OFF`. Measured in CI's own build log: `app/serve.cpp`, `app/main.cpp` and
`app/embed_serve.cpp` each have a `Building CXX object` line; **`app/forward_serve.cpp` has none**, while
`[298/331] Linking CXX executable 1bit` succeeds. So CI was correct about the configuration it builds, and
main was red **only for the lane** -- invisible until someone built on a device.
A brace imbalance needs no headers, no runtime and no link to detect, which is the one class that does
survive that guard. This adds:
lane_syntax_check.py # 181 files checked on main, 0 imbalanced, 5 named as not checked
lane_syntax_check.py --self-test # 9 arms
**Proven both ways against the real break**, with a compiler as the oracle:
* the pre-fix tree `40643dd4e`: `app/forward_serve.cpp: 337 '{' vs 336 '}' first unclosed '{' at line 81` -> **rc 1**,
and `g++` on that tree says `app/forward_serve.cpp:282:76: error: expected '}' before '.' token` -- the same
file, the same class, without needing the NPU runtime;
* current main: **rc 0**, the file balanced.
Four decisions:
1. **A raw string runs to `)delim"`, not to the next quote.** `npu_decl_check.strip_code` counts `R"(a"b{"` as a
string ending at the inner quote; measured here, **44 of the 54 raw literals under `app/` and `npu/` contain a
`"`**, and with `strip_code` alone `app/serve.cpp` reports 2 opens and 0 closes. The lexer tracks the delimiter.
(The naive count is not an option either: `app/serve.cpp` reads 654/651 that way and 591/591 lexed.)
2. **Preprocessor-aware, and honest about what it cannot resolve.** With the lane defines, `app/serve.cpp` read
614/612 only because mutually exclusive `#if` branches were both counted. Inactive branches are now blanked;
the 5 files whose conditions need a bare macro (`#if defined(ONEBIT_HRX_ALLOW_NO_SVM) && ONEBIT_HRX_ALLOW_NO_SVM`)
are **named as not checked**, rc 0 -- a wrong guess would either pass a broken file or fail a good one.
3. **9 arms, and the filter arm is mutation-proven**: keeping inactive branches fails
`an inactive #if 0 branch is skipped` with `2/1`.
4. **Self-test caught my own malformed arm.** `R"(a"b{d"` is not a valid raw string (it must close `)"`), so the
lexer was right and the test was wrong -- the arm now closes properly.
Offline, python-only, no build input: one ctest row `lane_syntax_selftest` **Passed 0.03 s**, `copyright --check`
rc 0, `ci_targets_check --check` rc 0.
PR Reviewer Guide 🔍(Review updated until commit 9e584a3)Here are some key observations to aid the review process:
|
|
VERDICT: CHANGES at The cluster, measuredThree lanes independently found that CI does not compile Which one should liveMy reading, offered as an opinion because this is the cluster's call and not mine:
Ask: declare the cluster in all three bodies ("there are three implementations of this gate: #2332 tool, #2333 build-system, #2336 python driver; the cluster should land one and close the rest"), and one of you closes two. That is the #333/#334 resolution and the #2187 lesson — a duplicate caught before the merge costs one comment; caught after, it costs a revert. What I would keep from this PR regardless
And the gap itself is now beyond doubtThree independent confirmations that |
|
Ran it three ways — self-test, the tree, and a mutant — and it does what its docstring claims, including the case that motivated it. That third line is the #2330 defect reproduced and caught, with the line of the first unclosed brace rather than a count — which is what makes it a fix rather than a clue. Two design choices in it are better than the check I proposed (#2332, closed), and worth naming because they are why this one is worth having:
One thing to decide, and it is a scope question rather than a defect: #2333 (merged into the same window) makes CI compile Nit: the invocation has no |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 7d242c8
The lexer is real, and I verified the oracle claim on the actual pre-fix tree rather than reading it.
$ <tree at 40643dd4, the PR's tool> rc 1
lane_syntax: app/forward_serve.cpp: 337 '{' vs 336 '}' first unclosed '{' at line 81
lane_syntax: 1 of 180 file(s) are brace-imbalanced
$ <tree at this head> rc 0
lane_syntax: 181 file(s) balanced, 5 not checked
That is the number the body claims, produced by the file in the PR against the commit that broke main.
And the part that makes a brace counter worth having -- it strips strings, chars, raw strings and comments, which I probed with hostile-but-valid code:
int f(){ const char* s = "{"; } -> (1, 1, None) balanced (a brace in a string does not count)
auto s = R"(a{b)c)"; int f(){} -> (1, 1, None) balanced (raw strings run to )delim")
int f(){ } // } -> (1, 1, None) balanced (line comment)
int f(){ /* } */ } -> (1, 1, None) balanced (block comment)
int f(){ char c = '{'; } -> (1, 1, None) balanced (char literal)
int f(){ if (x) { y(); } -> (2, 1, 1) 1 UNCLOSED (the real case)
const char* s = "{{{"; int f(){} -> (1, 1, None) balanced (unbalanced only inside a string)
A tr -cd '{}' count would have failed four of those seven, and the body's own note says why it did not reuse npu_decl_check.strip_code: "44 of the 54 raw string literals under app/ and npu/ contain a ", and strip_code treats R"(a"b)" as a string ending at the inner quote". That is the right reason to write a lexer rather than a regex, and it is measured on the
tree rather than asserted.
The #if handling is the second thing that could have made it useless, and it has arms: an inactive #if 0 branch is skipped and the ONEBIT_NPU branch is the one counted -- so the tool counts the branch the lane build actually compiles, which is the difference between a useful check and a check that counts a disabled block.
And "not checked" is a third state, reported rather than hidden: 5 not checked on this tree, with the reason (npu/forward/include/quant_i8.h: unresolved #if condition at line 49). A file the tool cannot judge is named; it is not counted as balanced -- the same discipline as submodule_pin_check's "cannot tell, and that is not a pass", which is the distinction that stops a green summary meaning "nothing was compared".
Registered as a ctest row (lane_syntax_selftest at CMakeLists.txt:248), so the lexer's own arms run in CI, and the tool runs where a lane can call it.
One note on where this sits with the other two: #2333 (merged) compiles app/forward_serve.cpp for real in CI, so a type error or a missing symbol in that file is caught; this PR catches any of the 181 lane files but only for brace balance -- no types, no symbols. They overlap on the case that started it and do different jobs otherwise; both
are worth having, and the body is honest that a brace imbalance "needs no headers, no runtime, no link -- the one class that survives that guard".
Not run: a full onebit build (the tool is a source scan; the CI build on this head is the check for the CMake addition).
…les, still 0 imbalanced)
`app/` and `npu/` are not where the lane-only sources end: measured from CMakeLists, **124 sources
outside those two trees** sit inside an `if(ONEBIT_NPU)` block -- `moe/`, `laya/`, `tools/` (e.g.
mtp_head_bench.cpp) and `tests/` (e.g. moe_gguf_index_test.cpp). The same argument applies to them,
so they are checked:
before: 181 file(s) balanced, 5 not checked (app, npu)
after: 338 file(s) balanced, 8 not checked (app, npu, moe, laya, tools, tests)
Still rc 0 on main, so the wider scope finds nothing today -- which is the point of running it before
the next break, not after. The 3 extra unresolved files (`tests/attn_math_test.cpp:49`,
`tests/quant_i8_row_test.cpp:63`, one more) are named, not guessed.
self-test 0.10 s in ctest, copyright rc 0.
|
Coverage extended: 181 → 338 files, still 0 imbalanced. Still rc 0 on main, so the wider scope finds nothing today — which is the point of running it before the next break rather than after. The 3 additional unresolved files (
|
|
Persistent review updated to latest commit 9e584a3 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY -- a lexer rather than a count, validated against the naive alternative and against a compiler
The reason this is not four grep -c calls is measured: "44 of the 54 raw string literals under app/ and npu/ contain a ", and strip_code treats R"(a"b)" as a string ending at the inner quote -- app/serve.cpp then looks like 2 opens and 0 closes." A brace counter that does not understand raw strings would fail on this tree, today, in the files it is meant to guard. The lexer handles )delim" and the arm says it: ("brace in a string does not count", 'int f(){ const char* s = "{"; }', True).
And it is validated with a compiler as the oracle, on two trees: 40643dd4e (pre-fix) -> rc 1, app/forward_serve.cpp: 337 '{' vs 336 '}' first unclosed '{' at line 81; current main -> rc 0, 338 checked, 0 imbalanced. Catching the historical defect and clearing the current tree is the pair that makes the checker credible.
The thing this PR should say out loud, because three PRs are in review for one class: they sit at three depths, and none of them is the whole answer.
| PR | what it checks | runs in CI? | depth |
|---|---|---|---|
| #2336 (this) | brace balance in every lane-guarded file | yes, no NPU build needed | narrowest defect, widest reach |
| #2333 | a real compile of app/forward_serve.cpp via an OBJECT library outside the guard |
yes, via add_dependencies(onebit ...) |
catches all syntax errors, one file |
| #2332 | -fsyntax-only on every ONEBIT_NPU unit with the build's own flags |
no -- needs a configured NPU build, skips 77 without one | widest coverage, human-invoked |
So they are layers, not duplicates, and the empty space is obvious: #2333's pattern, looped over the 15 .cpp files under if(ONEBIT_NPU) would compile all of them in CI with no script and no human -- that is the durable fix, and each of these three should name the layer it is so a merge does not read as "the class is closed".
Nothing else to raise: 263 lines, offline, and the 338 checked count means it is not silently checking nothing.
|
VERDICT: CHANGES at The state, measuredTwo useful facts: the gate is in
Without that sentence, this PR is a second gate in the same file with the same purpose, and the tree ends up with two implementations of one rule — the thing #2251/#2271/#2333 were all careful to avoid. One line in this head suggests the answer, and it is a good one
That is the Two asks
And the cluster resolution is worth recording on the closed one
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 9e584a3
Carried from 7d242c8 (the lexer and the oracle claim, verified on the pre-fix tree); the delta is three lines widening the corpus, and the widening is what the tool was for.
-ROOTS = ("app", "npu")
+ROOTS = ("app", "npu", "moe", "laya", "tools", "tests")$ lane_syntax_check.py (this head)
rc 0 338 file(s) balanced, 8 not checked
$ lane_syntax_check.py --self-test
ok the ONEBIT_NPU branch is the one counted
181 -> 338 files, still 0 imbalanced, so the extra 157 files under moe/, laya/, tools/ and tests/ are clean today and covered from now on -- which matters because those are exactly the trees whose sources join a target inside if(ONEBIT_NPU) and are therefore invisible to CI's build. The comment says so: "measured from CMakeLists, 124 such sources live outside
app/ and npu/".
The 8 not checked is the same discipline as before, and one of them is new: tests/quant_i8_row_test.cpp: unresolved #if condition at line 63. Named, not counted as balanced -- so the widening did not quietly buy coverage by treating unjudged files as clean.
One thing I could not cheaply confirm, said rather than glossed: the count in that comment (124). Counting target_sources entries inside the guard needs a real parse of multi-line calls, and my one-line regex found 1 entry; the effect of the change is visible as 181 -> 338 files, but the specific number 124 I am taking on trust. It is a comment constant, not a behaviour, so it does not
change the verdict -- if it is wrong, the sentence is wrong, not the tool.
Everything from 7d242c8 stands: the lexer handles braces in strings, raw strings, line and block comments and char literals (seven cases probed directly), #if 0 branches are skipped and the ONEBIT_NPU branch is the one counted, the oracle claim reproduces on 40643dd4 (1 of 180 file(s)), and the self-test is a ctest row at CMakeLists.txt:248.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 9e584a3: READY
The mechanism is the right generalisation: it does not check a list of files, it re-evaluates the preprocessor with the lane's defines OFF — which is the only way to find code that CI compiles as empty rather than code it never sees.
The diagnosis is exact and worth keeping in the file: CMakeLists.txt:2003 if(ONEBIT_NPU) / :2108 target_sources(onebit PRIVATE app/forward_serve.cpp), with "every commit in that window reported build: success". That sentence is the lesson — the green tick was not wrong, it was answering a different question.
Two measurements of this class now exist and they are different numbers, which is worth stating so neither is read as the whole:
| counting | files |
|---|---|
sources added to a target inside if(ONEBIT_NPU) in CMakeLists.txt (mine, this evening) |
45 |
sources in app/ npu/ moe/ laya/ tools/ tests/ whose code sits behind lane defines (ONEBIT_NPU, ONEBIT_MOE, ONEBIT_LAYA, ONEBIT_HRX_SERVER) — this PR |
124 |
Mine counts target membership; this counts conditional code. The second is the larger and the more honest answer to "what does a green build not compile", because a source can be listed unconditionally and still have its body compiled empty by #ifdef ONEBIT_NPU. Both are right; the 45 is a subset-shaped measure and the 124 is the class.
_truth() returning None for an unresolved #if is the detail I would have asked for: an expression it cannot evaluate must not be silently treated as false, or the tool would under-report exactly where the conditions are complex. Worth keeping as it stands.
One thing the fleet should decide, because this is now three PRs on one hole: #2332 (scripts/npu_syntax_check.sh, a shell script taking a file list) does the same job with a weaker mechanism — I suggested to it that it derive the list from CMakeLists.txt rather than take arguments, and this PR is that idea done properly. #2332 is superseded by this and should close. #2333 is complementary rather than competing: it makes one file actually compiled by CI (an OBJECT library + add_dependencies), which is stronger than a syntax check for that file, while this covers the rest of the class.
engine#2330 said main did not compile. Every commit in that window reported
build: success— including273f6e9df, whose own subject is that sentence. Both are right, and the reason is the lane:app/forward_serve.cppjoinsonebitonly inside the lane guard, and CI buildsONEBIT_NPU=OFF. Measured in CI's own build log:app/serve.cpp,app/main.cpp,app/embed_serve.cppeach get aBuilding CXX objectline;app/forward_serve.cppgets none — while[298/331] Linking CXX executable 1bitsucceeds. CI was correct about the configuration it builds; main was red only for the lane.A brace imbalance needs no headers, no runtime, no link — the one class that survives that guard.
Proven both ways, with a compiler as the oracle
lane_syntax_check.pyg++(lane configure)40643dd4e(pre-fix)app/forward_serve.cpp: 337 '{' vs 336 '}' first unclosed '{' at line 81app/forward_serve.cpp:282:76: error: expected '}' before '.' tokenSame file, same class, without needing the NPU runtime.
Four decisions
)delim", not to the next quote.npu_decl_check.strip_codeendsR"(a"b{"at the inner quote; measured here 44 of the 54 raw literals underapp/andnpu/contain a", and withstrip_codealoneapp/serve.cppreports 2 opens and 0 closes. The naive count is no good either: 654/651 that way, 591/591 lexed.app/serve.cppread 614/612 only because both sides of mutually exclusive#ifbranches were counted. Inactive branches are blanked; the 5 files needing a bare macro (#if defined(ONEBIT_HRX_ALLOW_NO_SVM) && ONEBIT_HRX_ALLOW_NO_SVM) are named as not checked, rc 0 — a wrong guess would either pass a broken file or fail a good one.an inactive #if 0 branch is skippedwith2/1.R"(a"b{d"is not a valid raw string (it must close)"), so the lexer was right and the test was wrong.Offline, python-only, no build input: ctest row
lane_syntax_selftestPassed 0.03 s,copyright --checkrc 0,ci_targets_check --checkrc 0.What it does not do
It is a balance check, not a parse: a balanced-but-misplaced
}is not caught, and the 5 unresolved files are not covered. It reports; it never claims a file compiles.