djs: exit 1 when compile fails - #1577
Conversation
`compile` printed the parse error and then returned `pure(0)`, so a failed compile was indistinguishable from a successful one by exit status. The same branch also carries `transpile`'s `file not found` and `circular dependency` errors, so none of the failure modes were detectable from a script or CI. Return `pure(1)` instead, matching the argument-count check above it, and document the exit-code contract on `compile`. The output file was already (correctly) not written in this case. The `undefined:undefined:undefined` location printed for metadata-less errors is untouched here and moves to `fjs/djs/todo/parse-error-location-format.md`. Changelog: - **BREAKING CHANGES:** `djs`: `fjs compile` exits with code `1` instead of `0` when the input fails to parse or cannot be read Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATUpzeifLPfhNZegwbJPVC
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
functionalscript | 91e7ddd | Commit Preview URL Branch Preview URL |
Aug 15 2026, 01:36 AM |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATUpzeifLPfhNZegwbJPVC
fjs compile to exit with code 1 on parse errors
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Approving. Draft is not a blocker. Baseline is origin/main = 104bfa34 (types/array: factor the emptiness guard out of head and tail (#1574)).
Exit codes, measured by driving the real entry point
Not via virtual — by running node fjs/module.mjs compile … in both trees and reading $?. fjs/effects/node/module.mjs:381 does process.exit(await runEffect(p)), so compile's return value is the process exit status.
| invocation | 104bfa34 |
91e7ddd5 |
|---|---|---|
| no args / one arg | 1 | 1 |
valid input → .mjs |
0 | 0 |
valid input → .json |
0 | 0 |
parse error (export default @) |
0 | 1 |
| input file missing | 0 | 1 |
That is precisely the todo's complaint: the arg-count branch already returned pure(1) and the transpile-error branch was the odd one out. Both failure paths now agree with it, and neither success path moved. The output file is not written on either failure in either tree, so the only change is the status.
The new proof assertions aren't decorative
Two-sided mutation, running fjs/djs/proof.f.mjs:
| mutant | on 91e7ddd5 |
on 104bfa34 |
|---|---|---|
revert the branch to pure(0) |
dies — fileNotFound, parseError |
(is main) |
keep pure(1) but also writeUtf8File(outputFileName, …) before erroring |
dies — both cases | survives, 5/5 pass |
The second is the one worth calling out: it returns the right exit code and clobbers state, which is the shape that has slipped through before. The added assertEq(state.root['output.f.js'], undefined) is what catches it, and main's version of the same cases does not — so those two lines are a real strengthening, not padding.
**BREAKING CHANGES:** is the right call
fjs compile is a documented CLI command (README.md:56, fjs/README.md:39), and a shell doing fjs compile in out && next-step previously ran next-step on a parse error and now will not. That is observable to scripts in the way #1520's specifier change was, not merely a representation detail like #1524 — so it earns the prefix, and it has it. No in-repo consumer depends on the old code: nothing in .github/, package.json or any script invokes fjs compile (the only script-shaped occurrence is a proposal inside fjs/ci/todo/667-ci-self-test-script.md).
One nit on wording: the entry says "cannot be read or fails to parse", but the branch also covers circular dependency (transpiler/module.f.mjs:80), which now exits 1 as well. "fails to parse" arguably covers it; worth a word if you want the entry exhaustive.
Bookkeeping and gates
Deleting fjs/djs/todo/compile-error-exit-code.md and splitting the cosmetic half into parse-error-location-format.md is the right disposition — the undefined:undefined:undefined prefix does still reproduce at this head (fjs compile nope.f.mjs out.mjs → undefined:undefined:undefined - error: file not found), and the new todo's ## <slug>. <Title> heading matches its siblings in that directory. Nothing referenced the deleted file.
tsc --noEmit→ 0;npm run prepackfrom a clean tree → 0.npm test: 2725 pass / 0 fail, identical to main — expected, since the PR strengthens existing cases rather than adding any.- Broken-link sets identical to main's (diff empty).
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
compileinfjs/djs/module.f.mjsprinted the error and then returnedpure(0), so a failed compile was indistinguishable from a successful one byexit status — the message text was the only failure signal, and scripts and CI
could not detect the failure at all. The argument-count check directly above it
already returned
pure(1), so the branch was the odd one out.That branch carries every
transpilefailure, not just parse errors:file not foundandcircular dependencyreach it too, so none of the failure modes weredetectable. It now returns
pure(1). The output file was already (correctly)not written in this case, so nothing else about the failure path changes.
compilegains JSDoc stating the exit-code contract —0only once the outputfile is written,
1on every failure — sincetodo/README.mdasks that adeleted issue's design decision be captured in JSDoc or a
README.mdfirst. Theproofs for
fileNotFoundandparseErrornow assertcode === 1and, inaddition, that no output file was produced.
The one thing left behind: a metadata-less
ParseErrorstill printsundefined:undefined:undefinedas its location, the cosmetic half of theoriginal issue. Fixing it means either suppressing the prefix or giving those
errors real metadata — a design choice worth its own pull request rather than a
drive-by here, so it moves to
fjs/djs/todo/parse-error-location-format.mdalong with the
circular dependencycase, which has the samemetadata: null.Verified with
npx tsc(clean),fjs t(2789 pass, 0 fail), and the issue'sown repro, where a parse error and a missing input now both exit
1while asuccessful compile still exits
0and writes its output.Changelog:
djs:fjs compileexits with code1instead of0when the input cannot be read or fails to parse, so a failed compile is
detectable from the exit status
🤖 Generated with Claude Code
https://claude.ai/code/session_01ATUpzeifLPfhNZegwbJPVC