Skip to content

fix(examples): make the file example compile and run honestly - #2

Merged
hellerve merged 1 commit into
masterfrom
claude/fix-file-example
Aug 18, 2026
Merged

fix(examples): make the file example compile and run honestly#2
hellerve merged 1 commit into
masterfrom
claude/fix-file-example

Conversation

@carpentry-agent

Copy link
Copy Markdown

examples/file.carp is the repo's only example, and it did not compile:

I found an unresolved generic type `(Fn [(Ref (Result r54 String) t59)] String)`
for the expression `str` at line 619, column 10 in 'core/String.carp'

File.read dispatches through the generic read-from-file interface, so the bare (File.read &f 2) leaves the Result's success type unresolved and there is no str to 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 example with File.open, i.e. the default a+ mode. Writes in append mode always go to the end, so on a second run the file already held hi, the write made it hihi, 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:

--- a+ run 1 ---   (Success @"hi")   0000000   h   i
--- a+ run 2 ---   (Success @"hi")   0000000   h   i   h   i
--- a+ run 3 ---   (Success @"hi")   0000000   h   i   h   i   h   i

open-with … "w+" truncates, so each run writes, rewinds, and reads back its own hi no matter what earlier runs left behind. The example still leaves a two-byte example file 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.0 is current and is what master points at today (both 433f9db).

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 temporary Result that dies before println* sees it. Same one-line shape of fix. Happy to drop that hunk if you would rather keep this PR to examples/.

Verification

Everything below was run, not just built, from the repo root:

  • before: carp -x examples/file.carp → the codegen error above
  • after: carp -x examples/file.carp(Success @"hi"), exit 0; three consecutive runs give the same output and leave example at 2 bytes
  • the corrected README snippet compiles and runs → (Success @"") (nothing is written to the file in that one)
  • CI gates re-run locally on the same file set CI uses (./main.carp): carp -b main.carp, angler, carp-fmt --check all clean

Why CI never caught this

The Build library step only builds main.carp, and the Lint / Format check steps explicitly exclude ./examples/*. Nothing in CI ever loads the example, so it can rot silently — as it did. A carp -x examples/*.carp step (or at least carp -b) would close that gap. I have not touched .github/ because the app has no workflows permission; 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-fmt does not know using-do's shape and wants each argument on its own line, which puts the lone f binding on a line by itself. Since examples/* 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.

`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.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@hellerve
hellerve merged commit 5e94cf6 into master Aug 18, 2026
2 checks passed
@hellerve
hellerve deleted the claude/fix-file-example branch August 18, 2026 06:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant