refactor(variant): pass add_var a named var_fields struct (#136) - #190
Merged
Conversation
TimD1
force-pushed
the
136_td_harden-add-var
branch
from
August 5, 2026 19:26
cb2b224 to
b1acddb
Compare
add_var took 27 positional parameters, 17 of them defaulted, so inserting a parameter anywhere but the end silently rebound every argument after it. #116 had already shipped that bug once: a caller's supercluster argument became rec_idx and compiled clean under -Wall -Wextra. Every field now travels in a var_fields aggregate, matched by designator instead of position, with the 12 per-haplotype fields nested as hap[HAPS] so a hap1/hap2 transposition is no longer expressible. Members with no default are required: omitting one is a build failure under the newly added -Werror=missing-field-initializers (GCC; clang does not diagnose it). Call sites name the type -- add_var(var_fields{...}) rather than add_var({...}) -- because GCC 13.3, the compiler on ubuntu-24.04 and so on CI, rejects a bare designated-initializer list as a function argument once any member is initialized from a non-constant expression. Naming the type sidesteps that and still enforces the required members. -std=c++20 does not help, so the C++17 standard level is unchanged. The four cluster.cpp merge sites hand-copied 13 parallel vectors each, which is the shape that produced the #116 miswiring; they now read one var_fields through the new ctgVariants::get_var accessor. Pure refactor: on the chr20 fixture every output is unchanged, the two files recording the invocation aside.
TimD1
force-pushed
the
136_td_harden-add-var
branch
from
August 5, 2026 19:43
b1acddb to
775aedb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Authorship: the content below was drafted by Claude Opus 5 (an AI coding agent) and
filed via
ghunder @TimD1-bot, a bot account operated by @TimD1. It reflects theagent's analysis, not a statement authored by @TimD1.
ctgVariants::add_vartook 27 positional parameters, 17 of them defaulted. Inserting a parameteranywhere but the end silently rebound every argument after it, and the compiler could not object
because the neighbouring types are interchangeable. #116 shipped that bug once already: a caller's
superclusterargument becamerec_idx, and it compiled clean under-Wall -Wextra.Every field now travels in a
var_fieldsaggregate, matched by designator instead of position.vars.add_var(var_fields{.pos = 500, .rlen = 2, .type = TYPE_CPX, .loc = BED_OUTSIDE, .ref = "AC", .alt = "GT", .orig_gt = GT_ALT1_ALT1, .gt_qual = 21, .var_qual = 22, .phase_set = 33, .rec_idx = 12, .alt_idx = 3, .ploidy = 2, .supercluster = 7, .calc_gt = GT_ALT1_REF, .hap = {{.errtype = ERRTYPE_FN, .sync_group = 4, ...}, {.errtype = ERRTYPE_TP, .sync_group = 5, ...}}});What this buys
Insertion is safe. A new field is a compile-safe no-op for every call site that does not set
it, so the per-variant vectors planned in #48, #47, and #46 can land without the rebinding hazard.
Transposition is inexpressible. The 12 per-haplotype parameters are nested as
hap[HAPS],indexed by
HAP1/HAP2, mirroring the storage layout. There is no longer a pair of adjacentsame-typed arguments to swap.
Required fields stay required. The 10 required members carry no default initializer and the 17
optional ones do, which is what makes
-Werror=missing-field-initializers(added to bothMakefiles) reject an omission:
Two limitations worth naming. This is GCC-only — clang does not diagnose it even with the flag
passed explicitly, so macOS developers rely on CI (ubuntu-24.04) for the check. And it means a
future compiler release that reports this warning somewhere new becomes a build failure for
source-installing users; scoping the flag to the single warning rather than blanket
-Werrorbounds that risk.
The hand-copies are gone. The four
cluster.cppmerge sites each copied 13 parallel vectorsby hand — the exact shape that produced the #116 miswiring. They now read one
var_fieldsthrougha new
ctgVariants::get_varaccessor:Why call sites name the type
Call sites write
add_var(var_fields{...})rather thanadd_var({...}). GCC 13.3.0 — the compileron ubuntu-24.04, and therefore on CI — rejects a bare designated-initializer list as a function
argument once any member is initialized from a non-constant expression:
Upstream GCC 13.4.0 accepts the identical file, so this is a patch-level compiler difference rather
than anything about the code. Measured on GCC 13.3.0:
add_var({...})into aconst ¶meteradd_var({...})into a by-value parameteradd_var(var_fields{...})add_var(var)-std=c++20does not help — the same error appears — so this is not a standard-level problemand the C++17 setting is unchanged, as is the README's GCC 9.1+ floor. Naming the type still
enforces the required members: omitting one through
var_fields{...}reports every missing member.The reason is recorded in
var_fields' own documentation so a later cleanup does not strip theprefix and break the build again.
Behavioral equivalence
The old
cluster.cppcalls stopped atploidy, leavingsupercluster,calc_gt, and theper-haplotype fields at their defaults, whereas
get_varcopies the source's actual values. Theseare equivalent because the merge runs from the
superclusterDataconstructor beforesupercluster(), and those fields are only ever written to the merged containers — the per-hapsources stay pristine.
add_varalso still clamps both quals tog.max_qual, which is idempotenton an already-clamped value.
Verified rather than assumed: on the chr20 fixture, every output file is unchanged against
dev.The only two files that differ are
parameters.tsvandsummary.vcf, each solely on the line thatrecords the invocation, because the two binaries were run from different paths; stripping those
lines leaves both identical, and
precision-recall-summary.tsvmatches metric for metric.Verification
pytestdev, apart from the two invocation-recording lines above.rlenfails the build on GCCOne tradeoff, unmeasured:
get_varcopiesref/altinto the struct andadd_varcopies againinto the vectors, so the merge path does one extra string copy per variant. The default build is
-g -pg -O1, so no performance claim is made in either direction; a move-aware overload wouldremove it if it ever matters.
Notes for review
var_descintest_helpers.his kept rather than folded intovar_fields, so tests can keepdescribing a variant with a single
qualand terse positional literals.fields now (
test_variant.cpp,test_harness.cpp,test_helpers.cpp).cluster.cppgenotype overridemutates a local instead of appearing in the initializer.
devafter fix(variant): clamp gt_qual to --max-qual in add_var (#135) #184 landed, which added agt_qualclamp insideadd_varand strengthened the quality tests; both are carried through here.Closes #136