Real fixes I've landed (or am landing) in tools I actually use. Smallest common thread: find a genuine correctness bug in a large unfamiliar codebase, trace it, and fix it the way the maintainers would.
| # | Project | Language | What I fixed | PR | Status |
|---|---|---|---|---|---|
| 1 | emscripten ⭐27k | C / JS | Added the desynchronized WebGL context attribute |
#27163 | ✅ Merged |
| 2 | vyperlang/vyper ⭐5k | Python | Codegen: raise instead of silently returning None in safe_pow() |
#5134 | ✅ Merged |
| 3 | rust-lang/rust-analyzer ⭐16k | Rust | Guarded an index panic in flycheck handling | #22634 | 🟢 Approved, about to merge |
| 4 | denoland/deno ⭐100k+ | Rust | Fixed a broken-pipe panic in deno lint --rules |
#35479 | 🔄 In progress |
PR: emscripten-core/emscripten#27163 · Issue: #8406
I picked issue #8406, "Support the desynchronized (low-latency) canvas
attribute," in emscripten-core/emscripten. The browser getContext() API has
supported a desynchronized WebGL attribute for years, it lets the canvas skip
the browser's normal compositing/double-buffering for lower latency, which matters
for things like drawing apps and emulators. emscripten's C API just didn't expose
it, so anyone compiling to WebGL through emscripten had no way to ask for it.
A few reasons it stood out:
- The scope is clear and self-contained. It's one boolean flag threaded through an
existing, well-established plumbing path (C struct -> struct-info offsets -> JS
library -> the
getContext()call), with a matching read-back path. No new subsystem, no API redesign. - It's a real capability gap, not a style nit. The feature exists in every modern browser; emscripten was simply missing the pass-through.
- emscripten is a heavily used toolchain (27k stars) and the change touches an interesting seam: handwritten C headers, a JSON struct-info file that generates byte offsets, and a JS "library" that the compiler stitches into the output. Good place to learn how that layering fits together.
- I wanted to do an end-to-end feature on a large C/JS codebase: header, codegen glue, docs, and a browser test that actually verifies the round-trip.
My understanding of the gap: EmscriptenWebGLContextAttributes (in
system/include/emscripten/html5_webgl.h) had no desynchronized field, and the
JS context-creation code in src/lib/libhtml5_webgl.js never passed
desynchronized into getContext(). So even if a user wanted a low-latency
context there was no field to set. The plan was to add the field, register it in
the struct-info so offsets generate correctly, set it on the JS attributes object,
read it back in emscripten_webgl_get_context_attributes(), document it, and prove
it round-trips with a browser test.
- Clone and set up emscripten:
git clone https://github.com/emscripten-core/emscripten.git && cd emscripten- install/activate a matching LLVM via emsdk so
emccworks.
- Open
system/include/emscripten/html5_webgl.hand look atstruct EmscriptenWebGLContextAttributes. There is nodesynchronizedmember, so C code has no field to request a desynchronized context. - Open
src/lib/libhtml5_webgl.jsand find where the attributes object is built forgetContext()(the object withalpha,depth,powerPreference,failIfMajorPerformanceCaveat, ...). There is nodesynchronizedkey, so the flag is never forwarded to the browser even if it existed in the struct. - Confirm the read-back path in the same file (the
emscripten_webgl_get_context_attributeswrite-out) has nodesynchronizedentry, so there's nothing to round-trip. - Net effect: a user who wants the documented browser behavior (lower-latency canvas) cannot get it through emscripten's WebGL API at all.
- Working branch on my fork:
fix/8406-desynchronized-canvas - Upstream issue: emscripten-core/emscripten#8406
- Add
bool desynchronizedtoEmscriptenWebGLContextAttributesinhtml5_webgl.h, next to the other booleans. - Register the new field in
src/struct_info.jsonso the generated offset tables (struct_info_generated*.json) pick it up and theC_STRUCTSmacros resolve. - In
src/lib/libhtml5_webgl.js: pass'desynchronized'into thegetContext()attributes object, and write it back in the get-attributes path so it round-trips. - Document the field in
site/source/docs/api_reference/html5.h.rst. - Add a browser test that sets
desynchronized = true, creates the context, reads the attributes back, and asserts the round-trip. - Use the real web attribute name
desynchronized(not alowLatencyalias), since the oldest Chrome emscripten supports (85) already shipsdesynchronized.
The core change is small and follows the existing pattern for every other context
attribute. The interesting wrinkles were all around it: keeping the generated
struct-info in sync, deciding the attribute name, handling OffscreenCanvas, and
rebaselining the code-size expectation tests (adding a field changes the generated
JS, so the byte-exact codesize snapshots move).
- Pull request: emscripten-core/emscripten#27163
- Branch:
fix/8406-desynchronized-canvas - Header —
system/include/emscripten/html5_webgl.h:bool renderViaOffscreenBackBuffer; bool desynchronized;
- Struct info —
src/struct_info.json(so offsets generate into the*_generated*files andC_STRUCTS.EmscriptenWebGLContextAttributes.desynchronizedresolves):"renderViaOffscreenBackBuffer", "desynchronized"
- JS glue —
src/lib/libhtml5_webgl.js, forward it intogetContext()and write it back on read:'desynchronized': !!HEAP8[attributes + {{{ C_STRUCTS.EmscriptenWebGLContextAttributes.desynchronized }}}],
{{{ makeSetValue('a', C_STRUCTS.EmscriptenWebGLContextAttributes.desynchronized, 't.desynchronized', 'i8') }}};
- Docs — added a
desynchronizedentry to theEmscriptenWebGLContextAttributesreference insite/source/docs/api_reference/html5.h.rst.
- Round-trip browser test in
test/browser/html5_webgl.c: setattrs.desynchronized = true, create the context, callemscripten_webgl_get_context_attributes(), and assert the value came back as expected. Guarded by a compile-timeEXPECT_DESYNCHRONIZED(-1= skip). - Wired into
test/test_browser.py: expect1on Chrome (which honors it) and0on browsers that report it back as false, and only on a normal canvas — notOffscreenCanvas, which doesn't honor the attribute (is_chrome()gate; skip when extra args select OffscreenCanvas). - Rebaselined the
codesize/ minimal-runtime code-size expectation JSONs that shifted because the generated WebGL JS now includes the extra attribute (test_codesize_hello_dylink_all, thehello_webgl*minimal-runtime snapshots).
- Pull request: emscripten-core/emscripten#27163
- Summary of what I contributed: Closes #8406. Adds the
desynchronizedWebGL context attribute end to end: the C struct field, the generated struct-info offsets, the JS pass-through intogetContext()and the read-back, reference docs, and a browser round-trip test. Lets emscripten programs opt into a lower-latency canvas, matching the browser API. - Feedback received and addressed:
- Naming: discussion about whether to also expose the legacy
lowLatencyname. Resolved by exposing only the real attributedesynchronized, since the oldest supported Chrome (85) already uses it; dropped thelowLatencymention from the docs too. - OffscreenCanvas: the round-trip assert failed there because OffscreenCanvas
doesn't honor
desynchronized. Gated the check to run only on a normal canvas. - Code size: the byte-exact
codesizeexpectation tests flagged the change, and they kept drifting against emscripten's tip-of-tree LLVM. Rebaselined the affected snapshots and re-synced withupstream/mainuntil CI was green; the maintainer helped land it through the moving baseline.
- Naming: discussion about whether to also expose the legacy
- Status: Merged into
main.
PR: vyperlang/vyper#5134 · Issue: #5026
This was my CodePath open-source deliverable, documented end to end below.
I picked issue #5026, "safe_pow() silently returns None for two-variable exponentiation instead of raising TypeCheckFailure," in vyperlang/vyper. It's a small, contained bug in the compiler's code-generation path, written in Python, which lines up well with what I already do. I work mostly in Python, and I'm comfortable reading and modifying logic-heavy code, so tracing a value through a compiler's codegen feels approachable to me.
A few reasons it stood out:
- The scope is clear. It's about replacing a silent
Nonereturn with an explicit compiler error, using an exception class the file already imports. There's no new feature, no API change, and no design work needed. - It's a genuine correctness problem. The
elsebranch returnsNone, and the caller passes that straight into IR construction, so a bypassed guard upstream would cause a cryptic downstream crash instead of a clean, actionable error. - Vyper is a notable, actively maintained project (5.2k stars) and the main smart-contract language for Ethereum, which makes it a great place to learn how a real-world compiler is organized and how a team handles contributions.
- Personally, I want to get better at jumping into a large unfamiliar codebase, following a value from one function down to its caller, and reusing what's already there instead of reinventing it.
After reading through the issue, here's my understanding of the problem. When both
the base and the exponent of a ** expression are runtime variables, safe_pow()
in vyper/codegen/arithmetic.py hits an else branch that just returns None.
The caller in expr.py feeds that None directly into IR construction, so instead
of a clear compiler error you'd get a confusing downstream crash if the type guard
upstream were ever bypassed. The branch is currently unreachable by design, so this
is defensive hardening. My plan is to replace the silent return with an explicit
raise TypeCheckFailure(...) with a clear message (the class is already imported
and used elsewhere in the file), and to check whether the newer venom codegen
backend has the same pattern so I can keep the two consistent.
- Clone the repo and set up the dev environment:
git clone https://github.com/vyperlang/vyper.git && cd vyperpython -m venv .venv && source .venv/Scripts/activate(Windows) orsource .venv/bin/activate(macOS/Linux)pip install -e .
- Open
vyper/codegen/arithmetic.pyand findsafe_pow(x, y). Look at the finalelsebranch (when neither the base nor the exponent is a compile-time constant). It runs a barereturn, which silently returnsNone. - Open
vyper/codegen/expr.pyand find thePowhandler (around line 447). It feedssafe_pow(...)'s return value straight intoIRnode.from_list(...)with noNonecheck, so aNonehere propagates into IR construction. - Confirm the branch is currently guarded by the front end. In a Python shell:
This is rejected with
from vyper import compile_code compile_code("@external\ndef g(a: uint256, b: uint256) -> uint256:\n return a ** b", output_formats=["bytecode"])
InvalidOperationbefore codegen, which is why theelsebranch is unreachable today. - To see the latent bug directly, temporarily relax that front-end guard and
recompile
a ** b. Instead of a clean compiler error you get a cryptic crash from theNoneflowing into IR construction. That is exactly the problem the issue describes: a silentNoneinstead of a clear error.
- My working branch on my fork: https://github.com/UnknownHacker1/vyper/tree/fix/5026-safe-pow-raise-typecheckfailure
- Upstream issue: vyperlang/vyper#5026
- Replace the silent
returninsafe_pow()'s finalelsebranch with an explicit raise, so the impossible state fails loudly instead of returningNone. - Match existing codebase conventions: raise a panic-style exception and mark the
unreachable branch with
# pragma: nocover, consistent with the defensive raise already a few lines above and with the venom backend'ssafe_pow. - No new feature, no API change, and no behavior change for valid programs.
- Verify: existing exponent tests pass, valid
**still compiles,a ** bis still rejected at the front end, and pre-commit (isort/black/flake8/mypy) is clean.
Implemented the fix in vyper/codegen/arithmetic.py. Replaced the bare return
in safe_pow()'s final else branch with a clear, loud failure for the
impossible state, and added a one-line comment noting the type checker guarantees
at least one literal operand. The approach evolved through maintainer review (see
Phase IV): it started as TypeCheckFailure, switched to CodegenPanic at the
reviewer's suggestion, and was then simplified to match the codebase's style.
- Pull request: vyperlang/vyper#5134
- Branch: https://github.com/UnknownHacker1/vyper/tree/fix/5026-safe-pow-raise-typecheckfailure
- Final change in
vyper/codegen/arithmetic.py:else: # pragma: nocover # type checker guarantees pow has at least one literal operand raise CodegenPanic("unreachable")
- Compile checks:
a ** 2(literal exponent) still compiles to identical bytecode;a ** b(both runtime) is still rejected at the front end withInvalidOperation, confirming theelsebranch stays unreachable. - Test suite:
pytest tests/functional/codegen/types/numbers/test_exponents.pypasses (23 passed). - Lint and types: ran the project's exact pre-commit hooks (isort, black, flake8, mypy) at their pinned versions, all pass.
- Confirmed no existing test referenced
safe_powor relied on the oldNonereturn.
- Pull request: vyperlang/vyper#5134
- Summary of what I contributed: Fixes #5026. In the classic codegen backend,
safe_pow()silently returnedNonefor the (currently unreachable) case where neither exponentiation operand is a compile-time constant. The PR raisesCodegenPanic("unreachable")instead, so the impossible state fails loudly rather than propagating aNoneinto IR construction. Marked# pragma: nocoversince the front end already rejects this case. - Feedback received and addressed:
- Reviewer (Sporarum) suggested raising
CodegenPanicinstead ofTypeCheckFailure, since the case is already caught by the type checker. Done. - Reviewer suggested a simpler comment and message and moving
# pragma: nocoveronto theelse:line. Applied their exact suggestion. - Reviewer asked me not to force-push the branch; switched to normal follow-up commits.
- Reviewer (Sporarum) suggested raising
- Status: Merged (approved by maintainer Sporarum and merged into
master).
PR: rust-lang/rust-analyzer#22634 · Issue: #21638
I picked issue #21638, a panic in rust-analyzer's flycheck (on-save check) handling. rust-analyzer is the Rust language server behind the VS Code / editor experience that basically every Rust developer uses, so a panic in its save-notification path is the kind of bug that silently kills people's diagnostics mid-session. The fix is a one-line defensive guard, which made it a clean way to work inside a large, real-world LSP codebase.
A few reasons it stood out:
- The scope is tightly bounded: an unchecked slice index that can panic on an empty list. The fix is to guard it. No behavior change for the normal case.
- It's a genuine robustness bug. A soft assertion right above it (
always!) only logs in release builds, it doesn't stop execution, so the very next line indexesworld.flycheck[0]and panics when the list is actually empty. - rust-analyzer is a prestigious, fast-moving rust-lang project. Good place to learn how an industrial LSP server is structured and how that team reviews.
- It fit a pattern I like: read the surrounding code, notice the existing idiom for "optional state," and reuse it instead of inventing something.
My understanding of the bug: in run_flycheck
(crates/rust-analyzer/src/handlers/notification.rs), under the "once" invocation
strategy, the code asserts there is exactly one flycheck handle and then indexes
world.flycheck[0] to restart it. The assertion is stdx::always!, which in
release is non-fatal, it logs and keeps going. So when world.flycheck is empty
(the condition the assert is meant to catch), execution falls straight through to
world.flycheck[0], which panics and takes down the handler.
- Clone and build rust-analyzer:
git clone https://github.com/rust-lang/rust-analyzer.git && cd rust-analyzercargo build(toolchain perrust-toolchain.toml).
- Open
crates/rust-analyzer/src/handlers/notification.rsand findrun_flycheck. Under the "once" invocation strategy you'll see:stdx::always!( world.flycheck.len() == 1, "should have exactly one flycheck handle when invocation strategy is once" ); let saved_file = vfs_path.as_path().map(ToOwned::to_owned); world.flycheck[0].restart_workspace(saved_file);
- Note that
stdx::always!is a soft assertion: on release builds it logs the message but does not return or abort. So ifworld.flycheckis empty, the guard does not actually stop the function. - The next statement,
world.flycheck[0], indexes an empty slice and panics. The panic is reached exactly in the state the assertion was warning about, an empty flycheck list when a save fires under the "once" strategy (as reported in #21638).
- Working branch on my fork:
fix/21638-flycheck-index-panic - Upstream issue: rust-lang/rust-analyzer#21638
- Replace the unchecked
world.flycheck[0]index with afirst()guard so an empty list is a safe no-op instead of a panic. - Keep the existing
always!assertion in place, it's still useful as a signal in debug builds that the "exactly one handle" invariant was violated. - No behavior change in the normal (one-handle) case; the guard only affects the previously-panicking empty case.
One-line, surgical change in run_flycheck. The codebase already uses
if let Some(..) = ...first() style guards for optional state elsewhere, so this
matches the existing idiom rather than introducing a new pattern. The soft
assertion stays so the invariant is still flagged in debug builds.
- Pull request: rust-lang/rust-analyzer#22634
- Branch:
fix/21638-flycheck-index-panic - Change in
crates/rust-analyzer/src/handlers/notification.rs:// before world.flycheck[0].restart_workspace(saved_file); // after if let Some(flycheck) = world.flycheck.first() { flycheck.restart_workspace(saved_file); }
cargo build/cargo checkclean.- Reasoned through both paths: with exactly one handle,
first()yields it and behavior is identical to the old index; with an empty list, the old code panicked and the new code is a no-op, which is the intended robust behavior. - No existing test depended on the panic; the change is a strict superset of the previous safe behavior.
- Pull request: rust-lang/rust-analyzer#22634
- Summary of what I contributed: Fixes rust-lang/rust-analyzer#21638. Guards the
world.flycheck[0]index inrun_flycheckwith afirst()check so an empty flycheck list under the "once" invocation strategy is a no-op instead of a panic. The pre-existingalways!soft assertion is kept for debug-build signal. - Feedback received and addressed:
- rustbot flagged the issue reference format; amended the commit to use the
canonical
Fixes rust-lang/rust-analyzer#21638so it links correctly. - Kept the change minimal per the project's preference for tightly-scoped fixes.
- rustbot flagged the issue reference format; amended the commit to use the
canonical
- Status: Approved on the rust-lang side, queued to merge.
PR: denoland/deno#35479 · Issue: #30248
I picked issue #30248: deno lint --rules panics with a broken-pipe error when its
output is piped into a program that closes the pipe early, like
deno lint --rules | head. It's a classic Unix papercut, a CLI that doesn't
tolerate a closed stdout, and deno is a 100k-star runtime, so it's a high-visibility
place to fix a real, reproducible bug. deno is also Rust, which I wanted more reps in.
A few reasons it stood out:
- The bug is concrete and trivially reproducible from the shell, and the fix has a clear, idiomatic shape.
- deno already had the right tool for this, a
write_to_stdout_ignore_sigpipehelper used elsewhere, so the fix is "use the existing pattern here too" rather than inventing error handling. - Touching a real CLI's output path on a huge project is good practice for writing a change that matches house style and passes a strict review/CI process.
My understanding of the bug: print_rules_list in cli/tools/lint/mod.rs writes
the rule listing directly with println! (and write_json_to_stdout(...).unwrap()
in the JSON branch). When stdout is a pipe that's already closed by the reader,
each println! hits a BrokenPipe error; println! panics on write failure, and
the .unwrap() in the JSON path panics too. So instead of exiting cleanly, the
command crashes with a panic.
- Build deno from source (
cargo build) or use a release binary. - Run the rules listing piped into a reader that closes early:
deno lint --rules | head -n 1 headreads one line and closes the pipe. The nextprintln!inprint_rules_listwrites to a closed stdout, hitsBrokenPipe, and panics instead of exiting cleanly. (The JSON path,deno lint --rules --json | head, panics the same way viawrite_json_to_stdout(...).unwrap().)
- Working branch on my fork:
fix/30248-lint-rules-broken-pipe - Upstream issue: denoland/deno#30248
- Stop writing directly with
println!/write_json_to_stdout().unwrap()inside the loop. - Build the entire listing (both the text and JSON branches) into a single
Stringbuffer usingwriteln!. - Write the buffer once through deno's existing
display::write_to_stdout_ignore_sigpipe, which treatsBrokenPipeasOk(()), the same helper deno already uses for pipe-safe stdout. - No change to the actual output content; only how it's written.
Converted print_rules_list to accumulate into a buffer and flush once. Added
use std::fmt::Write as _; for writeln! into a String, replaced every
println! / write_json_to_stdout(...).unwrap() with writeln!(output, ...), and
ended the function with a single pipe-safe write. This matches the pattern deno
already uses elsewhere for stdout that may be closed early.
- Pull request: denoland/deno#35479
- Branch:
fix/30248-lint-rules-broken-pipe - In
cli/tools/lint/mod.rs:use std::fmt::Write as _;
let mut output = String::new(); // ... build the listing with writeln!(output, ...) in both the json and // text branches instead of println! / write_json_to_stdout().unwrap() ... writeln!(output, "- {} {}", rule.code(), colors::green(enabled)).unwrap(); // ... // Write all at once and ignore broken pipe errors, e.g. when the output is // piped to a program that closes the pipe early like `head` (#30248). let _ = display::write_to_stdout_ignore_sigpipe(output.as_bytes());
- Manual repro before/after:
deno lint --rules | head -n 1panics on the old code and exits cleanly on the new code; same for--json | head. - Confirmed the visible output is byte-for-byte the same when not piped (the buffer
just batches what the
println!s used to emit). - Relying on deno's CI for the full lint/test matrix.
- Pull request: denoland/deno#35479
- Summary of what I contributed: Fixes #30248.
print_rules_listnow collects the rule listing into a single buffer and writes it throughwrite_to_stdout_ignore_sigpipe, sodeno lint --rules | head(and the--jsonvariant) exit cleanly instead of panicking on a broken pipe. - Status: Open, working through review and CI.