Skip to content

fix(check): atomic values are copy-locked — reject every by-value copy (#1399) - #1430

Closed
Ljunqiang wants to merge 1 commit into
limitlessprojects:mainfrom
Ljunqiang:fix/issue-1399-atomic-value-copy
Closed

Ljunqiang wants to merge 1 commit into
limitlessprojects:mainfrom
Ljunqiang:fix/issue-1399-atomic-value-copy

Conversation

@Ljunqiang

Copy link
Copy Markdown

Fixes #1399.

Bug

TypeShape::Atomic (atomic.bool, atomic.int32, atomic.int64, atomic.uint32, atomic.uint64, atomic.uintptr) was sendable-by-type (model.rs::is_sendable_by_type) and const-copyable (shape_is_const_sendable), so every by-value copy passed hyb check --deny-managed with 0 diagnostics:

  • decl copies (other := hits), whole-value assignments (hits = z), compound RMWs (hits += 1 / hits++) — statement level;
  • by-value task/spawn parameters, ch <- hits, and move of a container holding one — boundary level.

On the Go side these types embed sync/atomic.noCopy: go vet (copylocks) rejects every one of them, and a copy taken while another task mutates through &hits is an unsynchronized data race (go run -race, exit 66 — both races in the issue).

Fix (one-directional: VALUES never copy; the pointer route stays)

  • model.rs::is_sendable_by_type: Atomic removed from the by-type class; Pointer(Atomic) kept (the OQ6 mediated route — task worker(&hits, ...) still accepts, corpus fix(hy-check): new(T) returns a pointer *T, not a primitive — fixes #16 #17 unchanged).
  • sendability.rs: Atomic removed from shape_is_const_sendable; new explicit Atomic reject arm in classify_move_ident (bare move); Atomic joins the unmovable leaves in shape_holds_unmovable_leaf (so move of a holder struct rejects too).
  • emit_guard.rs: new statement-level guard check_atomic_value_copy on decl-copy/assign values and compound/inc-dec targets — emits the new HYB-E2011 (registered in diagnostics.json: category ownership-sendability, rule O4, tier0 A1c).
  • Boundary copies flow through the existing sendability family: task → HYB-E3001, ch <- hits → HYB-E3002, move → HYB-E3001.

Legal routes pinned as controls: &hits spawn, hits.add(1) / hits.load(), pointer copies (q := p on *atomic.int64).

Regression pins

  • hy-check/tests/atomic_value_copy.rs: 9 tests (3 statement forms, 3 boundary forms, 3 legal-route controls).
  • tests/checker-corpus/atomic-value-copy-reject.hy (REJECT A1c, first diagnostic HYB-E2011); census 126 → 127 (ACCEPT 38 · AM 2 · REJECT 87); VERDICTS.md + README registration appended.

Verification

  • cargo test -p hy-check / -p hy-syntax: green (incl. the 9 new tests, parse_all asserts 127).
  • tools/corpus_harness.py: 127/127, tripwire not fired (corpus fix(hy-check): new(T) returns a pointer *T, not a primitive — fixes #16 #17 &hits positive control still ACCEPT).
  • tools/differential_loop.py: accept-side 40/40 (build+run), reject-side 87/87.
  • Negative validation: verdict flipped to ACCEPT → harness 126/127; restored → 127/127.
  • Issue repro on this branch: repro_atomic_val_race.hy → HYB-E3001; local-copy repro → HYB-E2011 ×2 + HYB-E3002; holder move repro → HYB-E3001.

Note: sibling PR #1428 (issue #1398, slot-loan flush) also moves the census numbers (126 → 127 with an ACCEPT pin). Whichever lands second rebases and renumbers to 128.

limitlessprojects#1399)

TypeShape::Atomic was sendable-by-type (model.rs) and const-copyable
(shape_is_const_sendable), so every by-value copy of a scalar sync/atomic
value passed `hyb check --deny-managed` with 0 diagnostics: decl copies
(`other := hits`), whole-value assignments (`hits = z`), compound
read-modify-writes (`hits += 1` / `hits++`), by-value task/spawn/send
operands, and `move` of a container holding one. The Go types embed
sync/atomic.noCopy, so `go vet` (copylocks) rejects every copy, and a
copy racing a `&hits` task is a real data race (go run -race, exit 66).

The rule is one-directional: an atomic VALUE never copies by value; the
pointer route (`&hits`, OQ6) and atomic methods stay legal.

- model.rs::is_sendable_by_type: Atomic removed from the by-type class;
  Pointer(Atomic) kept (OQ6).
- sendability.rs: Atomic removed from shape_is_const_sendable; new
  explicit Atomic reject arm in classify_move_ident; Atomic joins the
  unmovable leaves in shape_holds_unmovable_leaf (holder structs).
- emit_guard.rs: new HYB-E2011 statement-level guard on decl-copy and
  assign values, plus compound/inc-dec targets; registered in
  diagnostics.json (category ownership-sendability, rule O4, A1c).

Pinned by hy-check/tests/atomic_value_copy.rs (9 tests: 3 statement
forms, 3 boundary forms, 3 legal-route controls) and the corpus fixture
atomic-value-copy-reject (REJECT A1c, first diagnostic HYB-E2011);
census 126 -> 127 (ACCEPT 38 / AM 2 / REJECT 87). Verified: harness
127/127, differential accept 40/40 + reject 87/87, negative flip
126/127 restored, cargo test hy-check / hy-syntax green, corpus limitlessprojects#17
(the `&hits` positive control) unchanged.
@limitlessprojects

Copy link
Copy Markdown
Owner

Intake triage — queued for deep verification, not landed this round. The fix direction is correct: atomic values ARE copy-locked in Go (sync/atomic wrappers are noCopy-adjacent and by-value copies are vet-flagged), and moving them from "sendable by value" to "reference-only" in the O4.6 deep-sendability table is the right call per #1399.

What holds the landing here is scale + shared surfaces: 14 files including GUARANTEES.md, README.md, Makefile, ci.yml, diagnostics.json, parse_all.rs (the corpus-census-coupled surfaces — every one of those has a machine-checked claim against it, see tools/check_corpus_census.py) plus a corpus fixture. That's a whole-battery review with census reconciliation, not a drive-by merge, and there are several same-surface PRs in today's queue — merge-order matters on these files.

Verification plan (next round): (1) repro #1399's by-value atomic copy on current main, (2) your probe matrix on the PR tree, (3) over-reach controls — &atomic_binding send must STAY accepted (your diff's doc comment says it does; I'll pin it), managed-tainted class 6 unchanged, (4) census + diagnostics.json structural sort check (E-code ordering), (5) full battery. If everything holds, this lands as-is with a census alignment commit.

Nothing blocking from a design standpoint — this is process honesty about the surface size, not a quality concern. Thanks @Ljunqiang.

Ljunqiang pushed a commit to Ljunqiang/Go-Hybrid that referenced this pull request Oct 24, 2026
…nd comparisons (fixes limitlessprojects#922)

Root cause: check_binary_operands compared numeric FAMILIES, not concrete
kinds — for `< <= > >=` and `+` the mismatch term was
`lk.is_numeric() != rk.is_numeric()` (int and float64 are both numeric, so
never mismatched), and `- * /` only checked kind membership. `x + f`,
`x - f`, `x * f`, `x / f`, `x < f`, `f <= x` checked clean while go build
refused each with `mismatched types int and float64` (GUARANTEES Row 13).

Fix: a new Collector::numeric_adapt_conflict routes cross-kind operand
pairs through Go's only implicit numeric conversion — untyped-constant
adaptation — and emits HYB-E1002 when it cannot bridge them:
  - `1 + 2.5` (both untyped) and `f + 1` / `1 + f` (int const -> float64)
    keep building, including untyped `const` identifiers (`f + k`);
  - `x + f`, `f + x`, `x - f`, `x < f`, `f <= x`, and the untyped float
    const against a typed int (`x + 2.5`) are rejected;
  - `Define.untyped_const` (from LocalDecl.ty) distinguishes `const k = 1`
    (untyped) from `const k int = 1` (typed like a variable) for fn-local
    consts — the annotation was recorded but never consulted.

Validation: new hy-check/tests/binop_operand_types_issue_922.rs 24/24
(15 reject faces + 9 accept controls); corpus pin
binop-mismatched-numeric-reject.hy (REJECT BUILDABLE + HYB-E1002), census
126 -> 127 on all 7 surfaces; harness 127/127, tripwire not fired;
negative flip: baseline upstream accepts the pin (126/127, pin FAILs) and
restores to 127/127; differential 40/40 accept + 87/87 reject;
cargo test --workspace green except the pre-existing local-CRLF
spec_t3_strings failure (reproduces on clean HEAD).

Harvest credit: stale companion rickymulya PR limitlessprojects#923 (bot:stale-base +
bot:conflict) proposed the same family of rules with literal-only
helpers; this take supersedes it with const-ident support and the
untyped_const annotation check.

Coordination: limitlessprojects#1428, limitlessprojects#1430 and limitlessprojects#1447 also bump census 126 -> 127 — the
PR that lands last rebases the count to 128+. The numeric_adapt_conflict
doc carries a dedupe NOTE against limitlessprojects#1447's is_untyped_constant (the
kinds_conflict / PR limitlessprojects#620 duplicate-helper precedent) for whoever lands
second.
@limitlessprojects

Copy link
Copy Markdown
Owner

Queue-reset sweep: this repo now runs a rolling conveyor, and open PRs
outside the active review window are being closed as not planned to keep the
queue honest — a stale queue wastes every contributor's time, including yours.

This is not a verdict on the work. If this change still matters:

  1. rebase against current main (main has landed a lot since this PR opened),
  2. confirm the diff still applies and the affected verdicts still hold,
  3. reopen — the triage board picks reopened PRs up as fresh arrivals.

Wildcard holds (verdict window 2026-11-01) and Pareto land candidates are
unaffected. Thanks for the effort — high-volume repos only stay reviewable
with a queue that actually drains.

Ljunqiang pushed a commit to Ljunqiang/Go-Hybrid that referenced this pull request Oct 24, 2026
…nd comparisons (fixes limitlessprojects#922)

Root cause: check_binary_operands compared numeric FAMILIES, not concrete
kinds — for `< <= > >=` and `+` the mismatch term was
`lk.is_numeric() != rk.is_numeric()` (int and float64 are both numeric, so
never mismatched), and `- * /` only checked kind membership. `x + f`,
`x - f`, `x * f`, `x / f`, `x < f`, `f <= x` checked clean while go build
refused each with `mismatched types int and float64` (GUARANTEES Row 13).

Fix: a new Collector::numeric_adapt_conflict routes cross-kind operand
pairs through Go's only implicit numeric conversion — untyped-constant
adaptation — and emits HYB-E1002 when it cannot bridge them:
  - `1 + 2.5` (both untyped) and `f + 1` / `1 + f` (int const -> float64)
    keep building, including untyped `const` identifiers (`f + k`);
  - `x + f`, `f + x`, `x - f`, `x < f`, `f <= x`, and the untyped float
    const against a typed int (`x + 2.5`) are rejected;
  - `Define.untyped_const` (from LocalDecl.ty) distinguishes `const k = 1`
    (untyped) from `const k int = 1` (typed like a variable) for fn-local
    consts — the annotation was recorded but never consulted.

Validation: new hy-check/tests/binop_operand_types_issue_922.rs 24/24
(15 reject faces + 9 accept controls); corpus pin
binop-mismatched-numeric-reject.hy (REJECT BUILDABLE + HYB-E1002), census
126 -> 127 on all 7 surfaces; harness 127/127, tripwire not fired;
negative flip: baseline upstream accepts the pin (126/127, pin FAILs) and
restores to 127/127; differential 40/40 accept + 87/87 reject;
cargo test --workspace green except the pre-existing local-CRLF
spec_t3_strings failure (reproduces on clean HEAD).

Harvest credit: stale companion rickymulya PR limitlessprojects#923 (bot:stale-base +
bot:conflict) proposed the same family of rules with literal-only
helpers; this take supersedes it with const-ident support and the
untyped_const annotation check.

Coordination: limitlessprojects#1428, limitlessprojects#1430 and limitlessprojects#1447 also bump census 126 -> 127 — the
PR that lands last rebases the count to 128+. The numeric_adapt_conflict
doc carries a dedupe NOTE against limitlessprojects#1447's is_untyped_constant (the
kinds_conflict / PR limitlessprojects#620 duplicate-helper precedent) for whoever lands
second.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:land-candidate Shepherd: fresh, gate-clean, verifiable (set from classify())

Projects

None yet

2 participants