I spent some time earlier in my holiday seeing what the newest frontier models
could actually do (Fable 5, and Codex 5.6 for the review rounds), and pointed
them at something I already cared about.
Back in #58 I argued quickr should be stricter with sizes: that
NA should be disallowed in declare(type())
so dimensions could be checked, and that
conformability should be verified before compiling
via conform_elementwise() / conform_matrix(). In that same comment I also
flagged what was wrong with it — a strict compile-time check errors on valid
inputs, e.g. dims(A) = x_len__ against dims(B) = y_len__ + 1 where
y_len__ = x_len__ - 1.
Working through the corner cases changed my mind, and the runtime guard is what
came out of it: statically wrong shapes are still compile errors, but dimensions
that can't be proven statically get one size() comparison before the BLAS call
rather than being rejected. So NA dims stay legal — just always treated as
unverified, never as equal — and crossprod() stops refusing programs that are
perfectly fine at run time. That, together with the type-promotion and
assignment/subscript work, is what I'm happiest about here.
The rest of this issue is the overview of the 15 PRs. Together they make
compiled functions honor one contract:
quick(f)(x) either returns exactly what f(x) returns — same values,
same typeof(), same shape — or raises an error. Never a third thing.
Today the answers to "what type does this result have?" and "are these
operands conformable?" live in several places that disagree, and several of
the gaps produce silent wrong answers: mixed-mode arithmetic returning the
wrong type, recycling that was accepted but never emitted, BLAS calls
proceeding on unverified shapes, complex operands read as reals. The series
fixes the bugs first, then adds the tests and structure that keep them fixed,
then documents the resulting semantics.
Each PR has its own issue, stands on its own, and lands in order. 11 of the 15
change behavior (1–7, 10–12, 15); the other 4 are tests (8), a refactor (9),
generated-code cleanups (13) and documentation (14).
How to review this
All 15 PRs are open at once so you can see the whole shape, but GitHub can
only diff a fork branch against main, so PR n also displays PRs 1..n−1's
commits until they merge. Each PR body opens with a note naming its parent and
the commits that are new to it. Reviewing in order (1 → 15) is the only order
in which the diffs read cleanly.
The branches live in a fork with no restrictions on them — push to them,
amend them, rebase them, or reorder them as you see fit. If you merge PR 1,
I rebase the rest onto the new main in one command and the remaining diffs
shrink accordingly. If you'd rather have fewer, larger PRs, say so and I'll
re-cut them.
Two things worth knowing before you start:
- PR 9's branch name is stale.
conformability/09-operator-table
originally consolidated the 17 elementwise operators into a descriptor
table, which PR 15 then moved back out of. That round trip has been
re-cut away: the table is never built, PR 9 ships only the shared
conformability helpers, and PR 15 ships only the dedup. The branch keeps
its name because renaming it would break the PR; the titles are accurate.
- Nothing here is load-bearing on my judgment alone. The "Decisions
embedded" section below lists every judgment call, each cheap to un-make.
The series
| # |
Issue |
PR |
Branch |
What |
| 1 |
#122 |
#137 |
conformability/01-side-effects-and-crashes |
repeated side effects, na.rm=, three crashes |
| 2 |
#123 |
#138 |
conformability/02-literal-kinds |
wrong-kind Fortran literals |
| 3 |
#124 |
#139 |
conformability/03-type-promotion |
R's promotion lattice, applied |
| 4 |
#125 |
#140 |
conformability/04-ifelse-t-diag |
ifelse()/t()/diag() result types |
| 5 |
#126 |
#141 |
conformability/05-subscript-validation |
subscript validation, OpenMP cancellation |
| 6 |
#127 |
#142 |
conformability/06-recycling |
no more silent partial recycling |
| 7 |
#128 |
#143 |
conformability/07-blas-guards |
one policy for linear-algebra shapes |
| 8 |
#129 |
#144 |
conformability/08-conformability-grid |
the contract, tested as a grid |
| 9 |
#130 |
#145 |
conformability/09-operator-table |
the shared conformability helpers (neutral) |
| 10 |
#131 |
#146 |
conformability/10-solve-square |
solve() requires a square matrix |
| 11 |
#132 |
#147 |
conformability/11-short-circuit |
&&/` |
| 12 |
#133 |
#148 |
conformability/12-refusal-diagnostics |
clean not-supported errors |
| 13 |
#134 |
#149 |
conformability/13-codegen-cleanups |
generated-code cleanups |
| 14 |
#135 |
#150 |
conformability/14-semantics-vignette |
the semantics, documented (docs) |
| 15 |
#136 |
#151 |
conformability/15-maintainability |
dedup and remaining correctness gaps |
1. Small fixes the later PRs build on
floor()/ceiling() and runif() bounds evaluated their argument
expressions more than once — visibly wrong with impure arguments, e.g. two
different random draws from one expression. na.rm = in reductions was
silently ignored rather than rejected. Plus three small crash/diagnostic
fixes. Nothing here touches the type system; it removes hazards the later
diffs would otherwise sit on.
2. Wrong-kind Fortran literals in casts
Three one-line silent-wrong-answer fixes in emitted Fortran. TRUE / FALSE
did integer division on undefined behavior instead of returning Inf, because
1_c_double is an integer literal of kind 8, not a double. Integer %/%
lost precision above 2^24 through a real cast, and Re() dropped its kind.
Spelling fixes only, no policy.
3. R's promotion lattice, applied
The compiler computed R's mode lattice (logical < integer < double < complex) and then ignored it: result modes came from the first non-scalar
operand, so x + 0.5 on integer x silently truncated and runif(1) * 3L
was declared integer. Every combining operation now routes through one
promotion-and-cast path. One new refusal: reassignment that would narrow a
variable's type is a compile error, since Fortran can't re-type a variable
where R would promote.
4. ifelse(), t() and diag() result types
ifelse() with mixed-mode branches emitted invalid Fortran — merge()
requires same-typed branches — and its result shape was never checked against
test. t() and diag() forced double where R preserves the input type
(diag(1:3) is integer). All three now match R, and ifelse() enforces its
branch-shape contract: compile error when provably wrong, runtime check when
lengths are symbolic.
5. Subscript validation, and OpenMP loops that actually cancel
Negative and zero subscripts compiled to out-of-bounds reads; they are now
compile errors, since their R meaning (exclusion, dropping) produces
value-dependent shapes a static compiler can't represent. Assignment
subscripts, which bypassed validation entirely, get the same checks, and
literal subscripts are checked against known extents. Ships with the fix that
makes errors raised inside OpenMP loops cancel the loop, which needs
OMP_CANCELLATION set before the runtime initializes.
6. No more silent partial recycling
Elementwise ops accepted operands of different lengths whenever one length
divided the other, but no recycling was ever emitted — the generated loop just
read out of bounds. Mismatched known lengths are now compile errors,
unverifiable lengths get a runtime check, and the two forms that do work
(scalar broadcast, vector spanning a matrix's rows) are kept. Also follows R's
split for 1×1-matrix operands: recycled in arithmetic, rejected in
comparisons, where R errors too.
7. One policy for linear-algebra shapes
BLAS/LAPACK lowerings reacted three different ways to dimensions they couldn't
verify: warn-and-proceed, hard compile error (crossprod), or — when both
operands had NA dims — no check at all. Mismatches surfaced as garbage reads
or a cryptic DGEMM error code -10. Now: statically wrong shapes are compile
errors, unverifiable ones get a runtime check before the call, and the
compile-time warning is retired.
8. The contract, tested as a grid
New combinatorial test file: modes × shapes (scalar, two vector lengths,
matrix, 1×1 matrix, symbolic) × operators in both operand orders, plus c(),
multi-arg reductions and ifelse(), with plain R as the oracle. Compile cost
is contained by packing all nine mode pairs per cell into one compiled
function: ~1 min by default, full grid behind QUICKR_FULL_GRID=1. Its first
full run found a real pre-existing bug, which is the argument for having it.
9. The shared conformability helpers (behavior-neutral)
The conformability policy was spelled three times — the BLAS emitter, the
elementwise emitter, and an inline copy inside ifelse() whose private
verdict helper had already drifted (it let a statically zero-length branch
through). They converge on one guard_conformable_dims().
check_conformable() was dims_match() in list form and both call sites
now say so; the real-domain floor spelling shared by floor() and double
%/% becomes real_floor_expr(). Evidence of neutrality: zero snapshot
churn and the full grid passes identically. See the branch-name note
above.
10. solve() requires a square matrix
solve(a, b) with a rectangular a fell through to a least-squares dgels
call, returning qr.solve()'s answer where R raises 'a' (3 x 2) must be square. Statically rectangular systems are now compile errors; symbolic
squareness is checked at run time before dgesv. qr.solve() keeps its
least-squares behavior — that is its meaning in R — and the now-dead
rectangular tail of the lowering is deleted.
11. && and || match R's scalar semantics
They compiled exactly like &/|: elementwise over vectors, returning
answers where R errors, with no short-circuit guarantee. Operands must now be
length-1 logicals, and the right operand is evaluated only when the left side
doesn't decide — lowered to a conditional when it could error or have side
effects, kept as infix when provably pure. while conditions get the matching
fix, so while (i <= n && x[i] > 0) no longer reads out of bounds.
12. Clean not-supported errors
Refusals that were correct but unreadable: character declarations died in the
code generator with an S7 object dump; complex order comparisons and complex
%% died with raw gfortran output. All are now clean compile errors, the
complex ones using R's own message text. One behavior change: complex operands
in linear algebra are refused, where they previously flowed into the real
BLAS/LAPACK routines and returned a plausible wrong real answer
(complex %*% complex gave the dot product of the real parts).
13. Generated-code cleanups
Everything whose fix necessarily churns snapshots, landed together once PR 9's
zero-churn gate is past: matrix(scalar, m, n) against a rank-2 operand
broadcasts natively instead of materializing an O(m·n) temporary (claimed dims
still checked), and the generated C extern signature no longer ends every
line in a space — 280 lines on main do today. Also pays off one wart this
series created: the hoist helper PR 1 added over-hoists literals, which this
fixes properly instead of PR 1's local workaround. Churn is mechanical.
14. The semantics, documented (docs only)
New vignette quickr-semantics.Rmd — the package's first — stating the
contract, the result-type table, the three-verdict shape policy, and every
deliberate divergence from R: no NA, no partial recycling, zero-length
rules, fixed variable types, complex being elementwise-only, the
subscript-bounds contract, ifelse() rules, and eager closure arguments. All
chunks are eval = FALSE with verified pasted output, so it adds nothing to
check time and needs no Fortran toolchain to build.
15. Dedup and the remaining correctness gaps
Two separable parts. Deduplication of idioms that existed as N verbatim
copies (behavior-neutral, generated Fortran byte-identical): the
BLAS/LAPACK output-resolution and info-guard boilerplate, the
hoist-materialization idiom, the read/write subscript lowering, the
cbind/rbind twins, and the six comparison handlers, which were
byte-identical apart from the operator symbol. Then the correctness gaps
the earlier passes deferred: nested masked reductions crashed,
reassignment and BLAS output reuse ignored extents, diag(x) ignored R's
length-1 identity form, plus two diagnostics that were inverted and two
pieces of R semantics simply unimplemented (c(<matrix>),
as.vector()).
Decisions embedded in the stack
Flagging the judgment calls so they can be un-made cheaply:
- Recycling rejection and the narrowing-reassignment error — the two
original behavior breaks, both with NEWS entries.
- Rectangular
solve() removed (NEWS entry): it answered where R errors,
the exact class of divergence this series hunts. I introduced it in the
original linear-algebra PR; qr.solve() is R's spelling for least squares
and keeps working.
&&/|| scalar + short-circuit (NEWS entry): vector operands were
silently accepted elementwise; they now error, matching R.
- Runtime guards instead of warnings for unverifiable BLAS shapes, with
crossprod's hard compile error relaxed to the same guard.
- Scalar runtime subscripts (
x[i]) stay unchecked. Per-element bounds
checks in hot loops defeat the point of compiling, and this matches
Fortran's own contract; it's documented in the vignette. An opt-in
quickr.bounds_check option is a possible future escape hatch. Literal and
statically-known out-of-range subscripts — reads and writes — are compile
errors, which cost nothing at run time. This is the one item I'd most like
your explicit call on.
- Complex linear algebra is refused rather than implemented (NEWS entry):
supporting it means the z* BLAS/LAPACK families and complex destination
inference. Refusing is the honest translation until there's demand. Same
call for complex type joins in c(), ifelse() and binds.
ifelse() with scalar test and array branches is a compile error — R
shapes the result like test, which isn't representable statically.
- The operator layout (PRs 9 and 15) is structural. The correctness PRs
stand without either.
- Grid compile budget in CI: default tier ~1 min, full grid behind
QUICKR_FULL_GRID=1. Say the word if even the default is too much for
routine CI.
Known loose ends (deliberately not in this series)
- Under the same bounds contract as
x[i]: symbolic range subscripts check
only their lower bound against the extent at run time, and integer-vector
subscripts (x[idx]) are value-unchecked.
- Complex widening in the shared cast path, so
c(complex, double) could
return complex as R does — a feature, deliberately not built; the compile
errors are documented instead.
- A set of candidate dead-code deletions is held back, since some of it may
have a future purpose only you can judge. Happy to open a separate PR.
x <- numeric(n) followed by a scalar assignment intended as a fill now
needs x[] <- 0-style code, a consequence of the per-axis reassignment
check in PR 15.
Test status
At the tip of the stack, on main as of 16aa2c5: 14284 passing, 0
failures, 0 warnings, 0 skips with QUICKR_FULL_GRID=1, and no
uncommitted snapshot drift. Each behavior change carries a regression test;
the behavior-neutral PRs are gated on byte-identical generated code rather
than on new tests.
I spent some time earlier in my holiday seeing what the newest frontier models
could actually do (Fable 5, and Codex 5.6 for the review rounds), and pointed
them at something I already cared about.
Back in #58 I argued quickr should be stricter with sizes: that
NAshould be disallowed indeclare(type())so dimensions could be checked, and that
conformability should be verified before compiling
via
conform_elementwise()/conform_matrix(). In that same comment I alsoflagged what was wrong with it — a strict compile-time check errors on valid
inputs, e.g.
dims(A) = x_len__againstdims(B) = y_len__ + 1wherey_len__ = x_len__ - 1.Working through the corner cases changed my mind, and the runtime guard is what
came out of it: statically wrong shapes are still compile errors, but dimensions
that can't be proven statically get one
size()comparison before the BLAS callrather than being rejected. So
NAdims stay legal — just always treated asunverified, never as equal — and
crossprod()stops refusing programs that areperfectly fine at run time. That, together with the type-promotion and
assignment/subscript work, is what I'm happiest about here.
The rest of this issue is the overview of the 15 PRs. Together they make
compiled functions honor one contract:
Today the answers to "what type does this result have?" and "are these
operands conformable?" live in several places that disagree, and several of
the gaps produce silent wrong answers: mixed-mode arithmetic returning the
wrong type, recycling that was accepted but never emitted, BLAS calls
proceeding on unverified shapes, complex operands read as reals. The series
fixes the bugs first, then adds the tests and structure that keep them fixed,
then documents the resulting semantics.
Each PR has its own issue, stands on its own, and lands in order. 11 of the 15
change behavior (1–7, 10–12, 15); the other 4 are tests (8), a refactor (9),
generated-code cleanups (13) and documentation (14).
How to review this
All 15 PRs are open at once so you can see the whole shape, but GitHub can
only diff a fork branch against
main, so PR n also displays PRs 1..n−1'scommits until they merge. Each PR body opens with a note naming its parent and
the commits that are new to it. Reviewing in order (1 → 15) is the only order
in which the diffs read cleanly.
The branches live in a fork with no restrictions on them — push to them,
amend them, rebase them, or reorder them as you see fit. If you merge PR 1,
I rebase the rest onto the new
mainin one command and the remaining diffsshrink accordingly. If you'd rather have fewer, larger PRs, say so and I'll
re-cut them.
Two things worth knowing before you start:
conformability/09-operator-tableoriginally consolidated the 17 elementwise operators into a descriptor
table, which PR 15 then moved back out of. That round trip has been
re-cut away: the table is never built, PR 9 ships only the shared
conformability helpers, and PR 15 ships only the dedup. The branch keeps
its name because renaming it would break the PR; the titles are accurate.
embedded" section below lists every judgment call, each cheap to un-make.
The series
conformability/01-side-effects-and-crashesna.rm=, three crashesconformability/02-literal-kindsconformability/03-type-promotionconformability/04-ifelse-t-diagifelse()/t()/diag()result typesconformability/05-subscript-validationconformability/06-recyclingconformability/07-blas-guardsconformability/08-conformability-gridconformability/09-operator-tableconformability/10-solve-squaresolve()requires a square matrixconformability/11-short-circuit&&/`conformability/12-refusal-diagnosticsconformability/13-codegen-cleanupsconformability/14-semantics-vignetteconformability/15-maintainability1. Small fixes the later PRs build on
floor()/ceiling()andrunif()bounds evaluated their argumentexpressions more than once — visibly wrong with impure arguments, e.g. two
different random draws from one expression.
na.rm =in reductions wassilently ignored rather than rejected. Plus three small crash/diagnostic
fixes. Nothing here touches the type system; it removes hazards the later
diffs would otherwise sit on.
2. Wrong-kind Fortran literals in casts
Three one-line silent-wrong-answer fixes in emitted Fortran.
TRUE / FALSEdid integer division on undefined behavior instead of returning
Inf, because1_c_doubleis an integer literal of kind 8, not a double. Integer%/%lost precision above 2^24 through a
realcast, andRe()dropped its kind.Spelling fixes only, no policy.
3. R's promotion lattice, applied
The compiler computed R's mode lattice (
logical < integer < double < complex) and then ignored it: result modes came from the first non-scalaroperand, so
x + 0.5on integerxsilently truncated andrunif(1) * 3Lwas declared integer. Every combining operation now routes through one
promotion-and-cast path. One new refusal: reassignment that would narrow a
variable's type is a compile error, since Fortran can't re-type a variable
where R would promote.
4.
ifelse(),t()anddiag()result typesifelse()with mixed-mode branches emitted invalid Fortran —merge()requires same-typed branches — and its result shape was never checked against
test.t()anddiag()forced double where R preserves the input type(
diag(1:3)is integer). All three now match R, andifelse()enforces itsbranch-shape contract: compile error when provably wrong, runtime check when
lengths are symbolic.
5. Subscript validation, and OpenMP loops that actually cancel
Negative and zero subscripts compiled to out-of-bounds reads; they are now
compile errors, since their R meaning (exclusion, dropping) produces
value-dependent shapes a static compiler can't represent. Assignment
subscripts, which bypassed validation entirely, get the same checks, and
literal subscripts are checked against known extents. Ships with the fix that
makes errors raised inside OpenMP loops cancel the loop, which needs
OMP_CANCELLATIONset before the runtime initializes.6. No more silent partial recycling
Elementwise ops accepted operands of different lengths whenever one length
divided the other, but no recycling was ever emitted — the generated loop just
read out of bounds. Mismatched known lengths are now compile errors,
unverifiable lengths get a runtime check, and the two forms that do work
(scalar broadcast, vector spanning a matrix's rows) are kept. Also follows R's
split for 1×1-matrix operands: recycled in arithmetic, rejected in
comparisons, where R errors too.
7. One policy for linear-algebra shapes
BLAS/LAPACK lowerings reacted three different ways to dimensions they couldn't
verify: warn-and-proceed, hard compile error (
crossprod), or — when bothoperands had
NAdims — no check at all. Mismatches surfaced as garbage readsor a cryptic
DGEMM error code -10. Now: statically wrong shapes are compileerrors, unverifiable ones get a runtime check before the call, and the
compile-time warning is retired.
8. The contract, tested as a grid
New combinatorial test file: modes × shapes (scalar, two vector lengths,
matrix, 1×1 matrix, symbolic) × operators in both operand orders, plus
c(),multi-arg reductions and
ifelse(), with plain R as the oracle. Compile costis contained by packing all nine mode pairs per cell into one compiled
function: ~1 min by default, full grid behind
QUICKR_FULL_GRID=1. Its firstfull run found a real pre-existing bug, which is the argument for having it.
9. The shared conformability helpers (behavior-neutral)
The conformability policy was spelled three times — the BLAS emitter, the
elementwise emitter, and an inline copy inside
ifelse()whose privateverdict helper had already drifted (it let a statically zero-length branch
through). They converge on one
guard_conformable_dims().check_conformable()wasdims_match()in list form and both call sitesnow say so; the real-domain floor spelling shared by
floor()and double%/%becomesreal_floor_expr(). Evidence of neutrality: zero snapshotchurn and the full grid passes identically. See the branch-name note
above.
10.
solve()requires a square matrixsolve(a, b)with a rectangularafell through to a least-squares dgelscall, returning
qr.solve()'s answer where R raises'a' (3 x 2) must be square. Statically rectangular systems are now compile errors; symbolicsquareness is checked at run time before dgesv.
qr.solve()keeps itsleast-squares behavior — that is its meaning in R — and the now-dead
rectangular tail of the lowering is deleted.
11.
&&and||match R's scalar semanticsThey compiled exactly like
&/|: elementwise over vectors, returninganswers where R errors, with no short-circuit guarantee. Operands must now be
length-1 logicals, and the right operand is evaluated only when the left side
doesn't decide — lowered to a conditional when it could error or have side
effects, kept as infix when provably pure.
whileconditions get the matchingfix, so
while (i <= n && x[i] > 0)no longer reads out of bounds.12. Clean not-supported errors
Refusals that were correct but unreadable: character declarations died in the
code generator with an S7 object dump; complex order comparisons and complex
%%died with raw gfortran output. All are now clean compile errors, thecomplex ones using R's own message text. One behavior change: complex operands
in linear algebra are refused, where they previously flowed into the real
BLAS/LAPACK routines and returned a plausible wrong real answer
(
complex %*% complexgave the dot product of the real parts).13. Generated-code cleanups
Everything whose fix necessarily churns snapshots, landed together once PR 9's
zero-churn gate is past:
matrix(scalar, m, n)against a rank-2 operandbroadcasts natively instead of materializing an O(m·n) temporary (claimed dims
still checked), and the generated C
externsignature no longer ends everyline in a space — 280 lines on
maindo today. Also pays off one wart thisseries created: the hoist helper PR 1 added over-hoists literals, which this
fixes properly instead of PR 1's local workaround. Churn is mechanical.
14. The semantics, documented (docs only)
New vignette
quickr-semantics.Rmd— the package's first — stating thecontract, the result-type table, the three-verdict shape policy, and every
deliberate divergence from R: no
NA, no partial recycling, zero-lengthrules, fixed variable types, complex being elementwise-only, the
subscript-bounds contract,
ifelse()rules, and eager closure arguments. Allchunks are
eval = FALSEwith verified pasted output, so it adds nothing tocheck time and needs no Fortran toolchain to build.
15. Dedup and the remaining correctness gaps
Two separable parts. Deduplication of idioms that existed as N verbatim
copies (behavior-neutral, generated Fortran byte-identical): the
BLAS/LAPACK output-resolution and info-guard boilerplate, the
hoist-materialization idiom, the read/write subscript lowering, the
cbind/rbind twins, and the six comparison handlers, which were
byte-identical apart from the operator symbol. Then the correctness gaps
the earlier passes deferred: nested masked reductions crashed,
reassignment and BLAS output reuse ignored extents,
diag(x)ignored R'slength-1 identity form, plus two diagnostics that were inverted and two
pieces of R semantics simply unimplemented (
c(<matrix>),as.vector()).Decisions embedded in the stack
Flagging the judgment calls so they can be un-made cheaply:
original behavior breaks, both with NEWS entries.
solve()removed (NEWS entry): it answered where R errors,the exact class of divergence this series hunts. I introduced it in the
original linear-algebra PR;
qr.solve()is R's spelling for least squaresand keeps working.
&&/||scalar + short-circuit (NEWS entry): vector operands weresilently accepted elementwise; they now error, matching R.
crossprod's hard compile error relaxed to the same guard.x[i]) stay unchecked. Per-element boundschecks in hot loops defeat the point of compiling, and this matches
Fortran's own contract; it's documented in the vignette. An opt-in
quickr.bounds_checkoption is a possible future escape hatch. Literal andstatically-known out-of-range subscripts — reads and writes — are compile
errors, which cost nothing at run time. This is the one item I'd most like
your explicit call on.
supporting it means the z* BLAS/LAPACK families and complex destination
inference. Refusing is the honest translation until there's demand. Same
call for complex type joins in
c(),ifelse()and binds.ifelse()with scalartestand array branches is a compile error — Rshapes the result like
test, which isn't representable statically.stand without either.
QUICKR_FULL_GRID=1. Say the word if even the default is too much forroutine CI.
Known loose ends (deliberately not in this series)
x[i]: symbolic range subscripts checkonly their lower bound against the extent at run time, and integer-vector
subscripts (
x[idx]) are value-unchecked.c(complex, double)couldreturn complex as R does — a feature, deliberately not built; the compile
errors are documented instead.
have a future purpose only you can judge. Happy to open a separate PR.
x <- numeric(n)followed by a scalar assignment intended as a fill nowneeds
x[] <- 0-style code, a consequence of the per-axis reassignmentcheck in PR 15.
Test status
At the tip of the stack, on
mainas of16aa2c5: 14284 passing, 0failures, 0 warnings, 0 skips with
QUICKR_FULL_GRID=1, and nouncommitted snapshot drift. Each behavior change carries a regression test;
the behavior-neutral PRs are gated on byte-identical generated code rather
than on new tests.