fix(examples): make the file example compile and run honestly - #2
Conversation
`File.read` dispatches through the generic `read-from-file` interface, so `(File.read &f 2)` left the Result's success type unresolved and codegen died with `I found an unresolved generic type (Fn [(Ref (Result r54 String) t59)] String) for the expression str`. Annotate the call the way the file library's own docs and tests do. The example also opened `example` with the default `a+` mode, so each run appended another "hi" and the read reported the *first* run's write, not this one's; the file grew by two bytes forever. `w+` truncates, so a run shows what it claims to show regardless of what earlier runs left behind. Pin the dependency to file@0.3.0. Every other dependency load across the org pins a release; `@master` can change what the example means without a commit here. The README's `using` snippet fails to compile for the same reason, and also took a reference to a temporary Result that dies before it is printed. Both snippets were verified by running them.
There was a problem hiding this comment.
Build & Tests
test (ubuntu-latest) and test (macos-latest) both green at dde0f83e, the current head (read off the check-runs API, not just gh pr checks). Merge-base is 54f908bc = current origin/master, so nothing is measured against a drifted tree. Note for anyone scanning: this repo defaults to master, not main.
The gate that matters here is not CI, since CI never loads the example — so I ran it.
Findings
Nothing blocking. Every factual claim in the body reproduced, including the two that are easy to assert and hard to check.
The compile error and the fix. carp -x examples/file.carp with master's file gives exactly the reported unresolved generic type (Fn [(Ref (Result r54 String) t59)] String) for the expression str; with this branch's file it prints (Success @"hi") and exits 0.
The append-mode trap is real, and it is the interesting half of this PR. I isolated it — annotation fixed, File.open (i.e. a+) kept — so the mode is the only variable:
a+ run 1: (Success @"hi") file: 2 bytes h i
a+ run 2: (Success @"hi") file: 4 bytes h i h i
a+ run 3: (Success @"hi") file: 6 bytes h i h i h i
Your table, byte for byte: the output never changes because the rewound 2-byte read keeps reporting run 1's write, while the file grows forever. With open-with "w+" I ran it three times and got (Success @"hi") with example steady at 2 bytes each time. That is the difference between an example that looks right and one that is right, and it is only visible if you run the thing twice — which is the argument for the CI gate better than any prose.
The README snippet. Same shape, checked both ways. master's &(File.read-all &f) dies in the same generic-str resolution; the branch's annotated version prints (Success @"hi") against a pre-populated file. (You reported (Success @""), which is the same snippet against an absent/empty example — consistent, not a discrepancy.)
The pin. file@0.3.0 and file's master are both 433f9db, so the pin changes nothing today and freezes what the example means. After this lands, grep across all 47 clones finds no remaining unpinned carpentry-org load in any .carp file; the only @master left anywhere is a README line in lens.carp, which has an open human PR on it and is not yours to touch.
The CI gap is exactly as described. .github/workflows/ci.yml builds only main.carp, and Lint and Format check both pass -not -path './examples/*' to find. Nothing in CI ever loads examples/, so it rotted silently and would rot again. Filing the gate for a human instead of pushing it is right — the App has no workflows permission.
One thing to add to that gate proposal. You disclosed that the example is not carp-fmt-clean before or after; it is also not angler-clean, and that half is not in the body. Fresh binaries (both newer than their repos' HEADs):
carp-fmt --check examples/file.carp -> would be reformatted
angler examples/file.carp -> examples/file.carp:8:16: [unsafe-result-unwrap]
Result.unsafe-from-success can crash on Error; use match or from-success instead
Both are pre-existing — angler flags master's (Result.unsafe-from-success (File.open "example")) identically — and neither is reachable by CI today. It only matters for the proposed gate: a carp -x examples/*.carp step is safe to add as-is, but dropping the -not -path './examples/*' exclusions from Lint and Format would redden this repo on day one. Worth saying in the issue so the human sizing it knows it is a compile gate, not a lint gate.
Not a finding, just noted. The README still opens with File.open (a+) while the example now uses w+. That snippet only reads, so append mode cannot bite it — but a reader who copies it and adds a write inherits the trap this PR just removed from the example next door. A one-word change if you think it is worth the consistency; genuinely optional.
Verdict: merge
Small, verified end to end, and it fixes more than it advertises: the compile error was the visible half, the a+ growth was the half that made the example lie about its own output, and both reproduce here exactly as reported. The dependency pin and the README hunk belong in the same change — the snippet is the first thing a reader copies and it was broken the same way. The CI-gap writeup is the right disposition given the missing workflows permission; add the angler half above when it gets filed.
examples/file.carpis the repo's only example, and it did not compile:File.readdispatches through the genericread-from-fileinterface, so the bare(File.read &f 2)leaves theResult's success type unresolved and there is nostrto print it with. The file library's own docs and tests annotate the call site ((the (Result String String) (File.read &f n))), so the example now does too.While in there, three more things:
The example was not honest about what it demonstrated. It opened
examplewithFile.open, i.e. the defaulta+mode. Writes in append mode always go to the end, so on a second run the file already heldhi, the write made ithihi, and the rewound 2-byte read reported the first run's write rather than this one's — while the file grew by two bytes on every run, forever:open-with … "w+"truncates, so each run writes, rewinds, and reads back its ownhino matter what earlier runs left behind. The example still leaves a two-byteexamplefile in the cwd, but its output no longer depends on it.The dependency is pinned. Line 2 loaded
carpentry-org/file.git@master, a moving target that can change what the example means without a commit here; every other dependency load across the org pins a release.file@0.3.0is current and is whatmasterpoints at today (both433f9db).The README snippet had the same bug. It is the first thing a reader copies, and it fails to compile for the identical reason — plus its
&(File.read-all &f)takes a reference to a temporaryResultthat dies beforeprintln*sees it. Same one-line shape of fix. Happy to drop that hunk if you would rather keep this PR toexamples/.Verification
Everything below was run, not just built, from the repo root:
carp -x examples/file.carp→ the codegen error abovecarp -x examples/file.carp→(Success @"hi"), exit 0; three consecutive runs give the same output and leaveexampleat 2 bytes(Success @"")(nothing is written to the file in that one)./main.carp):carp -b main.carp,angler,carp-fmt --checkall cleanWhy CI never caught this
The
Build librarystep only buildsmain.carp, and theLint/Format checksteps explicitly exclude./examples/*. Nothing in CI ever loads the example, so it can rot silently — as it did. Acarp -x examples/*.carpstep (or at leastcarp -b) would close that gap. I have not touched.github/because the app has noworkflowspermission; if you want the gate, it needs a human commit.Note that the example is not
carp-fmt-clean, both before and after this change:carp-fmtdoes not knowusing-do's shape and wants each argument on its own line, which puts the lonefbinding on a line by itself. Sinceexamples/*is deliberately excluded from the format check, I left the hand layout alone rather than churn it.Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.