Search/Edit a JavaScript-Output Compiler and Its Build System, Testing Each Layer

Edit a JavaScript-Output Compiler and Its Build System, Testing Each Layer

Use when an AI changes a compiler monorepo that outputs JavaScript. It builds through the task runner, leaves frozen syntax-tree files alone, tests every layer it touches, carries new build options all the way through, and judges speed by measuring, not by linter advice.

To use this AI rule
## Build Through the Task Runner Without Skipping a Level Use the repository's task runner rather than invoking the underlying build tools by hand. The default target builds the platform toolchain; separate targets add the standard library, run the test suites, format the code, and check formatting without rewriting it. ### The targets build on each other Dependency installation runs automatically for any target that needs the JavaScript tooling. The default build produces the toolchain binaries — both the compiler executables and the separate build-system binary — and copies them into the platform-specific package directory. The standard-library target consumes those toolchain outputs to compile the runtime sources. Test targets reuse everything above them. ### Do not skip a level Because each target depends on the one below it, invoking a lower-level tool directly can leave stale binaries in place and produce results that do not match a clean build. When a build result looks impossible, run the clean target and rebuild from the top before investigating further. ## Leave the Frozen Syntax-Tree Definition Alone and Update the Mapping Modules Some files are deliberately frozen because external parser extensions depend on their exact shape. Never modify the frozen version of the syntax-tree definition. When the live syntax tree changes, update the two mapping modules that convert between the live and frozen representations instead, so extensions keep working while the main tree stays free to evolve. ### Compatibility with the original fork is not a constraint The compiler began as a fork of another compiler, but the project maintains its own syntax tree and may make breaking changes against the original. Decide by what serves this compilation target best, not by what the upstream project does. ### Avoid reserving new keywords Prefer an implementation that does not require a new reserved word, since reserving one breaks existing code that already uses the name. Treat new keywords as a last resort and look first for a syntax that reuses existing constructs. ## Fix the Cause Instead of Silencing Warnings or Using Placeholder Wildcards Never use a warning-suppression attribute to quiet the compiler. Fix the underlying issue instead — a suppressed warning hides exactly the class of mistake the warning exists to catch. ### Wildcards are a decision, not a shortcut Do not use wildcard patterns as placeholders for new language features that then get forgotten. Use one only when you are certain the value should be ignored in that specific case, and confirm that every new language feature is handled completely across all compiler layers. ### Discard results explicitly, and thread state through If a function is called only for its side effects, use the explicit ignore function rather than binding the result to a wildcard. Never discard a stateful result — plumb it through the call chain instead. ### Small conventions that keep patterns honest Do not add a trailing unit parameter to a function that already has a mandatory labeled argument; the label already prevents accidental partial application. Skip the trailing wildcard in record patterns when the warning it targets is disabled in the codebase. Watch for similarly named constructors across different intermediate representations, because the same variant name can mean different things in different compilation phases. ## Find Which Compiler Layer You Are Changing Before You Edit The pipeline turns source text into a surface syntax tree in the parser, applies frontend transformations, produces a typed tree in the type checker, lowers to a lambda-level intermediate representation, converts to a JavaScript-level intermediate representation, and finally emits code. Identify the layer you are editing before you touch anything. ### The layers map to directories The syntax directory owns parsing and surface syntax. The frontend directory owns syntax-tree transformations and foreign-function-interface processing. The type-checker directory owns type checking and typed-tree transformations. The core directory holds both the lambda-level and the JavaScript-level modules, distinguished by file prefix. Separate directories hold extended utilities and data structures, type-definition generation for the target ecosystem, and the language server and tooling. ### Changes usually cross layers A new language feature typically requires parser work, syntax-tree definition updates, type-checking support, lambda-level handling, JavaScript generation, and tests, in that order. Skipping one layer produces a feature that parses cleanly and then miscompiles. ## Debug by Printing the Intermediate Form at Each Stage The compiler binary accepts flags that print each stage: the preprocessed source, the parse tree after parsing, the typed tree after type checking, and the unoptimized lambda representation. Compare the dump at each stage against what you expect before you start reading compiler source. ### Route the symptom to the layer that owns it Output formatting problems belong to the pretty-printer module. Type errors belong to the type-checker modules. Wrong results that appear only after optimization belong to the lambda-level analysis passes. Malformed emitted code belongs to the JavaScript generation modules. ### Reproduce small Compile a single minimal file rather than the whole project while iterating. Add temporary printing in the lambda printer module when a representation needs inspecting, and remove that instrumentation before the change lands. ## Add Tests to Each Layer as Soon as You Change It Add tests as soon as you modify each compiler layer, not after all the changes are complete. Testing late hides which layer introduced a regression and turns a small bisect into a whole-change investigation. ### Cases that always need a test New language features and bug fixes always require tests. Modifying an analysis pass or changing JavaScript generation always requires tests. Always inspect the quality of the emitted JavaScript, not only its correctness. ### Cover the right suites Parser tests validate syntax. Integration tests cover end-to-end behavior. Unit tests cover individual compiler functions. Build tests cover error cases and edge cases, including single-file type errors and cross-module errors that need separate implementation and interface files. Round-trip, generated-type, analysis, and tooling suites each have their own target and should be run when the matching area changes. ## Update the Error and Warning Catalog in the Same Change The repository keeps a per-module catalog of every error and warning the compiler can emit, with each entry mapped either to a fixture or to a documented reason it is unreachable. The catalog is the primary tool for finding coverage gaps and dead-code removal candidates, so stale entries make both jobs harder. ### Update the catalog in the same change as the code When you add or remove an error variant, add or remove its row in the matching module section, set its status to covered, unreachable, or still to do, and either link the covering fixture or record why the variant cannot be reached. When you add or remove a fixture, update the affected row's fixture path and status so the catalog and the test suite never drift apart. ## Keep Compiling Fast and the Output JavaScript Readable The compiler is designed for fast feedback loops and has to scale to large codebases, and its generated output is read by humans. Both properties are review criteria, not afterthoughts. ### Keep the emitted code legible Avoid emitting meaningless generated symbols and keep the output readable. A change that produces correct but unreadable output is not finished. ### Measure before and after Consider the compilation-speed impact of every change, use the appropriate optimization passes at the lambda and JavaScript levels, and profile before and after any performance-motivated change instead of reasoning about it from the code. ## Match Naming Rules and Log User-Facing Changes in the Changelog Match the naming convention of the language you are writing in: the compiler's implementation language uses snake case, while sources written in the compiled language itself use camel case. Follow existing patterns in the codebase, prefer existing utility functions over reinventing them, comment complex algorithms and non-obvious logic, and preserve backward compatibility where it is reasonable to do so. ### Commits carry obligations Sign off commits in the form the project requires, include the appropriate tests with every change, and confirm the build passes before committing. ### Record user-facing changes Add a changelog entry for any user-facing change, whether a bug fix, a feature, or a breaking change. Put it under the matching section of the current unreleased version and end the line with a link to the contribution. Submissions are expected to include one. ## Add a New Build-System Option Everywhere It Travels The companion build system, written in a systems language, provides fast incremental builds and better error reporting. Its modules split cleanly: core data structures such as the build state and module records, compilation and compiler-argument generation, syntax-tree generation and parser arguments, package discovery and dependency resolution, the module dependency graph and cycle detection, artifact cleanup, build logging and error reporting, the command-line interface, project-configuration parsing, the file watcher, and the entry point. ### Add an option in every place it travels Declare the flag in both the build and the watch argument types and update the conversion between them. Extend the configuration parser for any new project-config field. Modify the relevant build modules. Then thread the parameter through the whole chain, from the entry point into the build function, into build initialization, and into the build state, and along the watch path as well. Add unit tests for the new behavior. ### Settle precedence and error handling up front Command-line flags override the project configuration file. Propagate errors through the project's standard error-wrapping result type rather than panicking, and use debug-level logging for development diagnostics. ## Run the Build System Directly, Pointed at the Local Compiler Run the build system's own toolchain commands from the repository root by pointing at its manifest file, the way continuous integration does, rather than changing directory into the subproject. The task runner also exposes shortcuts for building it and for running its integration tests. ### Point the development binary at the local toolchain Run directly and the binary will otherwise pick up an installed release. Export the environment variables for the compiler executable path and for the runtime path, both resolved to absolute paths inside the current checkout, before running it. Note that the development binary may be named after the package rather than after the tool, so check the produced file name instead of assuming. ### Turn up verbosity when behavior is unclear The build system accepts increasing verbosity flags for info-level and debug-level logging. That is how you see details such as which folders a watch session is actually watching. ### Tracing follows the standard specification Distributed tracing is configured entirely through the standard telemetry environment variables — collector endpoint, trace-specific endpoint override, exporter headers, service name, resource attributes, and log level — with no project-specific knobs. Setting an endpoint is what enables telemetry; with none set, tracing is a no-op. Emit events through the tracing macros rather than the plain logging macros when you want them to appear in traces, because the two use separate systems. ## Make CI Tests Wait for a Condition, Not a Fixed Sleep Continuous-integration runners are slower than development machines, which makes fixed sleeps fragile. Poll for the condition you actually care about, such as waiting for a file to appear, instead of sleeping for a guessed duration. ### Asynchronous shutdown is not completion A helper that signals a watcher to exit — for example by removing a lock file — only sends the signal; it does not wait for the process to terminate. Avoid triggering configuration-change events just before exiting, because the watcher may start a concurrent rebuild you did not expect. ### Do not hand-roll platform-specific text edits In-place stream editing behaves differently across operating systems, so use the repository's replacement and path-normalization helpers instead of raw editor invocations in shell tests. An individual integration script can be run alone by exporting the executable and runtime paths, sourcing the shared helper file, and invoking that one script, which is useful for iterating without running the full suite. ## Judge Linter Suggestions by Real Performance Linter suggestions are input, not verdicts. When a suggested refactor could hurt performance or clarity, judge the trade-off explicitly instead of applying the suggestion mechanically. ### Accept complexity only where it pays A parameter struct is not automatically better than several arguments; allow the lint locally for functions that legitimately need many parameters and where a struct would add only ceremony. A copy is acceptable when it is small, one-time, required by ownership rules, and outside a hot path — do not over-engineer around it. ### Optimize on evidence Profile before optimizing. Most performance concerns in a build system are negligible next to actual compilation time. Where efficiency does matter, keep parameter types consistent along a call chain instead of converting between owned and borrowed string types at every boundary, since those conversions add allocations for no benefit. Recompile only dirty modules, parallelize independent work, watch the memory footprint of the build state on large projects, and minimize file-system operations.