12 KiB
12 KiB
- Always read the full README.md before doing anything
- Build commands:
cmake --build ./build_releasecmake --build ./build_debug
- Never use
ninjadirectly: it bypasses cmake's configuration and invalidates the build cache - Always try the release build first before building with the debug version
- Use the debug build only when it is useful to obtain a clear stack trace with symbols, inspect names, place breakpoints, or test a small case interactively
- The debug build is very slow, so use it only on small fast tests such as operation validations, not on network validations
Core engineering philosophy
- Clean architecture matters as much as making the immediate test pass
- Prefer fixes that preserve clear ownership boundaries, explicit invariants, and simple dataflow
- Do not stack compensating fixes on top of earlier mistakes. If the current approach is becoming messy, stop and explain why
- A correct fix should usually make the responsible producer, resolver, verifier, or lowering own the behavior directly
- Avoid late repair passes, defensive cleanup, or broad rewrites when a cleaner owner-side fix is possible
- Do not hide an upstream modeling bug by normalizing it later in the pipeline. Fix the producer when the producer owns the invariant
- Prefer patterns/rewrites for local IR canonicalization. Use module walks only when pass-level structural analysis genuinely requires them
- Prefer compact, structured designs over long case-by-case implementations
Think before coding
- State assumptions explicitly before implementing when they affect the design
- If multiple interpretations exist, present them instead of silently choosing one
- If a simpler approach exists, say so and prefer it unless there is a clear reason not to
- If something is unclear, stop, name what is confusing, and ask
- If the requested or obvious approach would make the architecture worse, push back and propose a cleaner alternative
Code changes
- Keep changes minimal and localized to the relevant parts of the code
- Preserve the existing naming conventions and coding style used in the surrounding code
- Keep code easy to read, well organized, and suitable for future extensibility
- A function must not exceed roughly 200/250 lines. If a change pushes a function beyond that, extract focused helpers
- Prefer clear naming and structure over comments. Add comments only when they materially improve clarity
- Do not rename symbols, move files, or restructure modules unless that is necessary for the requested change
- Avoid duplicate ad-hoc logic. If the same concept appears in multiple places, consider whether it deserves a shared helper/API
- When adding a helper or API, ask:
- Could this be useful to another component now
- Is another component already implementing the same idea differently
- Is this likely to be needed by a future adjacent component
- What is the narrowest useful abstraction
- What is the correct ownership level for this API
- If a shared API is justified, place it at the lowest clean layer that can be used by all relevant consumers without creating dependency cycles or leaking policy across layers
- If an existing component should use a newly introduced shared API, refactor that component in the same patch when doing so is directly related and reduces duplication
- Do not create broad frameworks just because a helper might someday be useful. Shared APIs should encode a real reusable concept, not speculative generality
- If the reusable abstraction is plausible but not clearly needed yet, keep the code local and mention the possible future extraction separately
Avoid case-listing designs
- Avoid solving problems with large chains of
if/else, switches, or repeated special cases that enumerate every possible situation - Long case listings tend to overfit the current tests, grow the codebase, and hide the underlying abstraction
- When you see a growing list of special cases, stop and look for the shared concept, data model, interface, or normalization step that would make the cases collapse
- Prefer table-driven logic, traits/interfaces, small reusable predicates, structured dispatch, or producer-side normalization when they express the invariant more directly
- A few explicit cases are fine when the domain is genuinely small and closed
- If the list is likely to grow, refactor toward a cleaner and more compact design instead of adding another branch
- When keeping a case list is the pragmatic choice, explain why the domain is closed or why a broader abstraction would be premature
Ownership and invariants
Before implementing, identify the owner of the behavior:
- A producer should emit IR/data that satisfies the contract of the next stage
- A lowering should make representation changes explicit and semantically correct
- A resolver should resolve existing structure without silently changing semantics
- A verifier should reject invalid states with bounded, actionable diagnostics
- Codegen should assume verified invariants and fail clearly if they are violated
When fixing a bug:
- State the invariant that was violated
- State which component should own that invariant
- Fix that component directly
- Avoid fixes that merely mask the violation later in the pipeline
- Add or preserve verification if the invariant is important enough to regress
Refactor and API policy
You may propose or implement a refactor when:
- the local fix would duplicate logic
- the local fix would violate a layer boundary
- the bug exists because responsibility is assigned to the wrong component
- multiple components already implement ad-hoc variants of the same concept
- a shared helper/API would make the code smaller, clearer, and easier to maintain
- existing callers can be migrated cleanly without broad churn
- the current implementation is turning into a long list of special cases instead of a structured solution
When proposing or implementing a refactor:
- Explain what responsibility is being moved or shared
- Justify why the new location is the right ownership level
- Keep the API narrow and named after the concept or invariant it represents
- Migrate directly related existing users when that improves compactness and consistency
- Separate changes required for correctness from optional cleanup
- Avoid unrelated renames, formatting changes, or module moves
- Do not expand a justified refactor beyond directly related callers
Do not refactor when:
- the issue is truly local and a local fix is clearer
- the abstraction would have only one user and no clear adjacent use
- the abstraction would mix policies from different layers
- the refactor would affect unrelated behavior
- the refactor is mainly aesthetic
Working style
- Infer style and conventions from the existing code before introducing new patterns
- When several implementation options are possible, prefer the simplest one that fits the current architecture and minimizes churn
- Push back when the requested or obvious fix would make the architecture worse
- If a cleaner fix requires a small refactor or shared helper/API, propose it explicitly and justify it
- Avoid broad refactors unless explicitly requested or clearly necessary for correctness and maintainability
- When tests fail, bucket failures by likely root cause and separate patch-related failures from pre-existing or out-of-scope failures
Simplicity first
- Minimum code that solves the problem cleanly. Nothing speculative
- No features beyond what was asked
- No error handling for impossible scenarios
- If you write 200 lines and it could be 50, rewrite it
- Ask: “Would a senior engineer say this is overcomplicated?” If yes, simplify
- Prefer direct, explicit code over generic machinery unless the generic machinery clearly reduces duplication and preserves boundaries
Fallbacks and defaults
- Avoid silent fallback behavior when the semantic category is unknown
- Do not treat “unknown” as “safe” unless the codebase already defines that convention
- If a value cannot be classified, either preserve the existing behavior deliberately or fail with a clear diagnostic
- When adding a fallback, state why it is semantically valid and what invariant makes it safe
Surgical changes
- Touch only what you must
- Clean up only the mess introduced by your own change
- Do not “improve” adjacent code, comments, or formatting
- Match existing style, even if you would personally do it differently
- If you notice unrelated dead code, bad abstractions, or fragile design, mention it separately. Do not delete or rewrite it unless asked
- When your changes create orphans, remove imports, variables, functions, or files made unused by your change
- Every changed line should trace directly to the requested fix, a required cleanup, or a justified reuse/refactor decision
Diagnostics and verification
- Use existing bounded diagnostic mechanisms for pass-level verification or codegen failures
- Do not emit unbounded repeated diagnostics from loops or parallel workers
- Diagnostics should identify the violated invariant and the relevant value/op when useful
- Verifiers should reject invalid states, not repair them
- Codegen should not compensate for invalid IR/data unless codegen is the owner of that invariant
- Do not make failing tests pass by weakening verifiers, assertions, or diagnostics unless the check itself is proven wrong
- If a check is too strict, explain the valid case it rejects and update the invariant accordingly
- Prefer fixing invalid IR/data producers over relaxing consumers
- If adding diagnostics only for debugging, remove them or cap them before finalizing
Temporary debugging code
- Temporary diagnostics, dumps, assertions, and debug-only helpers must be removed or intentionally converted into bounded permanent diagnostics before finalizing
- If debug instrumentation remains, explain why it is useful as permanent infrastructure
- Do not leave noisy validation output behind
Performance awareness
- Avoid algorithmic regressions in compiler passes, especially repeated full-module walks, repeated expensive analyses, or per-op recomputation inside nested loops
- If a change adds a walk, cache, analysis, or structural traversal, justify why it is needed
- For hot paths, prefer preserving existing asymptotic behavior unless a better structure is part of the requested change
- If performance may change, mention the expected impact and suggest a targeted timing check
Goal-driven execution
For multi-step tasks, state a brief plan:
- [Step] → verify: [check]
- [Step] → verify: [check]
- [Step] → verify: [check]
Define success criteria before implementing:
- For bug fixes, success means reproducing or identifying the failure, fixing the responsible owner, and verifying the targeted case
- For refactors, success means preserving behavior while making ownership, reuse, or structure cleaner
- For validation changes, success means checking both valid and invalid cases when applicable
Transform tasks into verifiable goals:
- “Fix the bug” → identify the invariant, reproduce the failure, fix the owner, verify the targeted case
- “Add validation” → write or identify tests for invalid inputs, then make them pass/fail as expected
- “Refactor X” → preserve behavior before and after, then run relevant tests
Final self-review
Before reporting completion, check:
- Did I fix the owner of the invariant rather than masking the issue downstream
- Did I avoid broad case lists and ad-hoc special handling
- Did I introduce a helper/API only at the right ownership level
- Did I migrate directly related duplicate logic when doing so improves compactness
- Did I avoid weakening verifiers or assertions unnecessarily
- Did I remove temporary debugging code or make it bounded and intentional
- Did I avoid unrelated formatting, renames, or cleanup
- Did I consider performance impact for added walks, analyses, caches, or repeated computations
- Did I run the required build/test commands
- Did I clearly report remaining failures or risks
When reporting back:
- Say what changed
- Say what was verified
- Say what remains
- When showing code in chat, make it easy to copy-paste into the codebase
- Keep outputs focused on the changed parts
- List bad practices, fragile assumptions, or cleaner alternatives separately
- If a change is intentionally pragmatic rather than architecturally ideal, say so and explain the tradeoff