Releases: takeiteasy/cccc
Release list
v0.2.4
Added
- New
-Wint-conversionwarning (part of-Wall) for an implicit
integer↔pointer conversion with no cast —warn_implicit_conversion()
(src/type.c) had branches for pointer↔pointer, integer↔integer and
float conversions, but no branch matched an int/pointer pair, so e.g.
const char *p = 'a';compiled silently and only failed later, at
runtime, if the resulting garbage pointer was dereferenced. Covers
assignment/scalar initialization,return, and prototyped call
arguments; suppressed for the null pointer constant0. Does not cover
file-scope/global initializers, which take a separate constant-evaluation
path.
Fixed
-
A referenced
externglobal variable that is never defined anywhere
resolved silently instead of erroring, and — more seriously — redeclaring
the same global (across anexterndeclaration and its definition,
within one translation unit or across separate ones) could silently read
the wrong global's value or the wrong constant offset entirely (#957).
Global variable references compile to a data-segment offset baked
straight into theObjat codegen time (gen_addr,src/codegen.c),
but every declaration of a global created its ownObj
(new_gvar/global_variable(),src/parse.c) with nothing reconciling
them — codegen's allocation loop gave each one its own slot, and
cc_link_progs(src/linker.c) canonicalized definitions across
translation units by name but never propagated the resulting offset onto
the declaration-onlyObjs it dropped from the merged list. Concretely,
before this fix:extern int g; int f(void){return g;} int g=42;read 0
instead of 42 fromf();int g=42; extern int g;likewise read 0; and
linking two files where one declaredextern int g;and the other
definedint g=42; int pad=7;silently readpad's value (7) instead of
g's (42), with the result depending on file order. Fixed by
canonicalizing every redeclaration of a global variable within a
translation unit onto a singleObj(merge_global_decl()in
src/parse.c, keyed by a new per-TUglobal_decl_map) and propagating
each canonical global's offset onto the aliasObjscc_link_progs
drops (global_aliasesarray, populated insrc/linker.c, applied in
src/codegen.c'sgen()right after the data-segment allocation loop).
A referenced-but-never-defined global is now a hardundefined global: <name>compile error, mirroring the existingundefined function: %s
check (suppressed, not deferred, under-c/--link, since there is no
name-based data relocation mechanism for globals);sizeof()of an
undefined extern global still compiles, since the reference is only
counted where codegen actually materializes an address. Two full
definitions of the same global (int g=1; int g=2;) are now also a
redefinition of '<name>'error, matching the pre-existing cross-TU
check — previously silently accepted, with the second initializer
winning.environ(used byposix_spawnpin the POSIX test suite) is
now exposed via the same host-global accessor macro pattern aserrno
andstdin/stdout/stderr(include/unistd.h,
src/stdlib/posix.c), since it would otherwise have started hitting the
new undefined-global error as an inert, host-disconnected guest global.
Seetests/test_extern_global_undefined.c,
tests/suites/test_suite_global_canonicalization.c,
tests/test_cross_tu_global_offset.c/_reversed.c, and
tests/test_global_redefinition.c. -
tests/failures/was silently excluded from test discovery, so every
test inside it — 41 files — never ran —discover_tests()
(tools/testing/discovery.py) filtered out any path with afailures
component, but nothing generated or consumed that exclusion elsewhere; it
quietly turned the directory into dead weight, including real regression
coverage for tickets #1, #78, #172, #194, #195, #357, #884. The exclusion
is removed and every file audited individually: 33 tests moved into
tests//tests/macros/(12 already correct as-is; 3 that needed the
__builtin_quotediagnostic fix below; 3 memory-tagging tests that needed
CCCC_FLAGS: --memory-tagging+EXPECT_RUNTIME_ERROR, since without the
flag they were passing by accident on reused-but-unvalidated memory
content; 14 error-recovery/_BitInttests that were already correctly
rejected at compile time but had never been markedEXPECT_COMPILE_ERROR;
1extern-symbol test additionally markedCCCC_C4_SKIP, matching
test_bytecode_link_unresolved.c's existing #565 rationale; 1 typedef
test rewritten from a bare non-42 return to the exit-42 assertion
protocol). 7 files deleted as invalid C predating the current test
conventions (a misunderstanding of the declaration-comma vs.
comma-operator distinction, and##at the start of a__VA_OPT__
argument, both rejected by GCC/Clang too) or an incomplete scratch
fragment with nomain(). 1 file (an unreferencedexternglobal that
resolves silently instead of erroring) deleted pending a separate ticket,
since fixing it is a VM-level design question, not a test fix. Also added
tests/test_va_opt_basic.c, since the audit found__VA_OPT__had no
surviving coverage anywhere in the suite. -
error()(no source location) never printed the "N error(s)
generated." summary thaterror_tok()produces, and never incremented
vm->error_count— threeQuote()validation diagnostics in
quote_scan_and_rewrite()/quote_substitute()/quote_core()
(src/reflection.c) used the location-lesserror(), so the test
runner'shas_compile_errorcheck (tools/testing/runner.py, which keys
off that summary text) couldn't distinguish "correctly rejected at compile
time" from "compiled fine and the program itself returned a non-42 exit
code" — the test harness misclassified three correct
EXPECT_COMPILE_ERRORtests as failures. Converted toerror_tok()with
the offending token, which also gives these diagnostics a real source
location instead of none. -
Quote()/QuoteN()templates no longer drop statements after the
first one — an unbraced multi-statement template like
Quote("if (!$1) $1 = f($2); return $1;", a, b)parsed only the leading
if(quote_core()insrc/reflection.ccalledcc_parse_stmt()once
and never inspected the leftover tokens), silently discarding the
returnand leaving the generated function falling through with an
undefined return value — a silent-miscompile-shaped footgun with no
warning or error. An unbraced multi-statement template is now
transparently wrapped in braces and parsed as a block, exactly like the
already-safeQuote("{ ... }")form; any template that still leaves
tokens unparsed after that (e.g. trailing garbage on an expression
template) is now a compile error instead of a silent drop. (#955) -
RunCustom's vendored shell now performs POSIX-correct quote removal,
backslash escaping, and$VAR/${VAR}expansion — the lexer
(src/build_shell.c) previously only recognized a quote as the very
first character of a word, and even then returned its interior
unprocessed: an embedded quote (pre'mid'post), a backslash escape, or a
delimiter inside quotes (a";"b) all passed through with the quote
characters still attached instead of being stripped. ARunCustom
command whose value needed embedded quotes — e.g.cccc -c=generated ... -DSOME_MACRO='"literal"'— therefore handed the child a
multi-character character-constant instead of a string literal, silently
converted to a pointer and dereferenced (SIGSEGV in the child process,
not the outer--buildprocess, which correctly reported the step's
failure and non-zero exit). The word reader now performs real quote
removal and backslash escaping per POSIX, plus$VAR/${VAR}expansion
from the process environment as a single non-re-split, non-globbed
literal chunk. (#954) -
The comptime declaration index no longer mis-names a declaration whose
segment contains a fixed-size array inside an anonymous struct/union body,
or a leading C23 attribute —segment_declarator_name()(src/macros.c)
finds a declaration's declared name by scanning forward for the first
depth-0[array-dimension group, but tracked[/]depth without
tracking brace depth. A member array inside an anonymous struct/union body
declared in the same statement as its own declarator (e.g.typedef struct { char n[32]; } A;) put that member's[at apparent depth 0, so the
declaration was indexed under the member's name (n) instead of its own
(A); a leading attribute ([[deprecated]] int dx;) hit the same[path
with no preceding token, so the declaration was never indexed at all.
Either way, something later needing the real name as a typename (e.g. using
it as another struct's member type) failed to resolve it and misparsed
with an unrelatedexpected ','. The scan now tracks brace depth (only
treating a[as an array dimension at brace depth 0) and skips a leading
[[ ... ]]attribute-specifier-seq as a unit. (#951)
v0.2.3
Added
CHKNT's null-terminator guard now coversfloat/double,struct/
union, and wide_BitInt/_Decimalntarraypointees — previously
only integer and pointer pointees were guarded (a deliberate v1
exclusion, #923). Investigating the exclusion for a decision pass turned
up an actual hole:_BitInt(128)passed the oldis_integer()gate and
set the guard flag, but its store lowers through codegen's memcpy branch,
which returned before the oldCHKNT-only emission site — so the flag
was set and nothing was ever checked.float/doublenow reuseCHKNT
itself (their value's raw bits are transferred into an integer register
first). A new opcode,CHKNTZ, guards the memcpy-lowered pointees
(struct/union, wide_BitInt,_Decimal) that never pass through a
single value register: it scans the source bytes for any non-zero byte
before the underlyingmemcpyruns, so the terminator slot is never
actually clobbered when it traps.long doublestays unguarded on
purpose — its widened terminator slot is 16 bytes but the actual store is
an 8-byte flat-doubleFSTR, so no opcode inspects its full stored
representation.CHKNTZonly guards a whole-object store through the
pointer itself; a member-wise write into the same slot (tbl[n].a = 1;)
is a known, separately-tracked gap (#950). See
SAFETY.md § Checked Pointers and
VM.md § Safety Opcodes (#939)
Fixed
node_has_side_effects()now sees through a ternary's branches —
ND_COND(thecond ? then : elsternary) stores its two branches in
->then/->els, separately from->lhs/->rhs, and the side-effect
check that gates checked-pointer bounds declarations
(resolve_bounds_tokens()) and member object-expression instrumentation
(compute_checked_bounds(), #921/#945/#947) never recursed into them —
socount(c ? i++ : 3)was wrongly accepted, andi++would have run on
every checked access instead of never. Now rejected at the declaration,
same ascount(i++). See
SAFETY.md § Checked Pointers (#949)- GNU elvis (
a ?: b) no longer forces a compiler temp when the
condition is a plain, cheaply re-readable operand —a ?: balways
desugared totmp = a, tmp ? tmp : b, whoseND_ASSIGNmade a pure
elvis bounds expression likecount(n ?: 8)fail the check above even
though it has no side effects. Whenais anND_VAR/ND_NUMand not
volatile/_Atomic, the desugar now buildsa ? clone(a) : bdirectly
instead, which reads as side-effect-free; every other condition shape
keeps the original temp-based desugar. See
SAFETY.md § Checked Pointers (#949)
Changed
- Checked-pointer bounds propagation and assignment-time bounds
implication also evaluate a member object expression once per
assignment —q = arr[k].p;(bounds propagation) andarr[k].p = src;
(assignment-time bounds implication) used to re-evaluatek's indexing
arithmetic 2-3 times while building that one assignment's bounds, the
same duplication #945 already fixed for a direct per-access check. Both
passes now share #945's hoist-into-a-temp treatment via a second temp
allocator, needed because they run after their function's own local-list
snapshot. Pure performance cleanup — same checks, same traps, no
user-visible behavior change. See
SAFETY.md § Checked Pointers (#947)
Documented
- Why a side-effecting member object expression stays uninstrumented —
f()->p[i]is declined by checked-pointer bounds checking (no check
emitted) because the hoist introduced by #945/#947 rewrites the bounds
expressions, not the access itself:f()->p[i]'s own access still calls
f()once regardless, so emitting a check would call it again just to
build the hoisted object-expression temp — an extra evaluation of a
side-effecting expression that a--checked-pointersbuild must never
introduce over a default build. See
SAFETY.md § Checked Pointers (#948)
v0.2.2
Added
-
Path-sensitive checked-pointer bounds propagation — a propagation
candidate that mixes checked-rooted and non-checked-rooted assignments
(int *q = malloc(...); if (c) q = p; q[i];) is no longer poisoned to
"never checked" for the whole function; it's classified "OPT" and its
snapshot temps are refreshed at every assignment, rooted or not (a
non-rooted store writes an explicit invalid sentinel instead of skipping
the refresh), plus seeded with the sentinel at function entry. A new
opcode,CHKRO, checks the snapshot but no-ops on the sentinel, soq[i]
is enforced exactly on the paths whereqactually holds a checked-rooted
value at runtime — decided per executed path with no CFG/join/fixpoint
analysis at all. Candidate registration also now covers an uninitialized
declaration (int *q;), which previously never became a candidate.
checked_prop_optionalpropagates transitively through a #941 chain, so a
candidate chained from an OPT source is itself OPT even when its own
single store is unconditionally rooted. A candidate whose every assignment
is checked-rooted ("FULL") is completely unaffected — sameCHKR, same
codegen as before. See
SAFETY.md § Checked Pointers (#942) -
Chained checked-pointer bounds propagation — a local that is itself
only propagated (never declared checked) can now act as a propagation
source for a further candidate:int *q = p + 2; int *r = q + 1; int *s = r + 1;now enforcess[i]too, not justq[i], chaining to arbitrary
depth. Decided by iterating #919's whole-function eligibility rule to a
fixpoint, seeded from declared-checked sources only and growing round over
round, so an unrooted cycle (q = r + 1; r = q + 1;) never self-validates.
A self-rooted reassignment (q = q + 1;) is now treated as neutral rather
than poisoningq, matching the existingq++/q += kbehavior. See
SAFETY.md § Checked Pointers (#941)
Fixed
CHKNTnow covers read-modify-write through an[[cccc::ntarray]]
terminator slot —s[n] += 1,s[n]++,s[n]--(and the_Atomic
compare-and-swap RMW form) previously bypassed #923's null-terminator
guard: the read-modify-write desugar's synthesized store never carried the
checked-pointer boundsCHKNTkeys off, so a non-null RMW into the
terminator slot silently corrupted the invariant even though the
equivalent direct assignment already trapped. See
SAFETY.md § Checked Pointers (#937)
Changed
- Struct/union member checked-pointer bounds evaluate their object
expression once per access, not once per bound —arr[k].p[i]used to
re-evaluatek's indexing arithmetic 2-4 times per checked access (once
forlo, once or twice forhidepending on the bounds form), since
building each bound re-cloned the member access's object expression
(arr[k]) from scratch. A non-trivial object expression (a runtime
index; not a bare local or plain member chain, which already cost
nothing to re-clone) is now hoisted into a single compiler-generated
temp shared by every bound of that access. Pure performance cleanup —
same checks, same traps, no user-visible behavior change. See
SAFETY.md § Checked Pointers (#945)
v0.2.1
Added
- Checked-pointer bounds propagation across assignment —
int *q = p + k;now checksq[i]against a snapshot ofp's own absolute bounds
taken at the assignment, instead ofq(an ordinary unchecked pointer)
getting no check at all. Sound under arbitrary control flow with no
dataflow/join analysis: a local propagates only if its declaration and
every subsequent assignment to it are checked-rooted, andq++/q += k
preserve the snapshot since it's an absolute range. Composes with
struct-member bounds below. See SAFETY.md § Checked
Pointers (#919) - Checked-pointer bounds on struct/union members — a member's
count()/
byte_count()/bounds()may now name a sibling member (struct S { int n; int * [[cccc::array, cccc::count(n)]] p; };), resolved relative to
whichever instance is actually accessed (s.p[i],sp->p[i],(&s)->p[i],
(*sp).p[i]all reach the same member-relative base). Previously a
compile error. See SAFETY.md § Checked
Pointers (#921) CHKNT: null-terminator guard for[[cccc::ntarray]]— under
--checked-pointers, a store of a non-zero value into anntarray+
count(n)pointer's widened terminator slot now traps. The presence half
of the invariant (verifying a terminator actually exists somewhere in the
declared range) is deliberately not enforced —count(n)on a Checked C
_Nt_array_ptris a lower bound, not an assertion of terminator presence,
so a scan-based check would false-positive on conforming code and would
itself require reading past the declared bound. See
SAFETY.md § Checked Pointers (#923)
v0.2.0
Changed
-G/--emit-generatedfolded into-c=generated— "serialize the
runtime TU + macro-generated objects to C" is now a third-c=FMTtarget
alongsidenativeandbytecode(aliases:gen,g), instead of a
standalone flag with its own-osemantics.-Gis removed outright, no
deprecated alias.-c=generatedfollows the same default-filename
convention as the other two targets:./a.gen.cwhen-ois omitted
(previously-Gfell back to stdout).--emit-onlyand--attr-target
are unchanged in name and semantics; they now apply to-c=generated
(#936)- Bare
-c/--compilenow defaults tonative(wasbytecode);
-c=bytecode/bc/c4is the explicit spelling for the old default. Both
nativeandbytecodenow matchcc/clang/gcc'sa.outconvention:
no-owrites./a.out/./a.c4respectively (previouslybytecodefell
back to stdout andnativehard-errored without-o) (#932)
Added
--emit-cccc— preserves CCCC dialect syntax ([[cccc::...]]
attributes, cccc-only#includes, checked-pointer qualifiers) in
-E/-m/-c=generated/-c=nativeoutput instead of stripping it to
portable C. With-c=native, disables thecc/clang/gccPATH search
in favor of an explicitCCCC_NATIVE_CCthat understands the dialect
(#933)--test-run[=LEVEL]— runs the program once under CCCC's VM safety
instrumentation (maxby default, ornone/basic/standard/max/
0/1/2/3like--safety=) and only proceeds to compile if that run
succeeds (no crash, VM-detected safety violation, or hang; exit code is
not checked). Implies-c=nativewhen no-cis given. Runs in a forked
child so the smoke test's post-execution VM state never leaks into a
saved-c=bytecodeartifact (#934)
Fixed
usage()(src/main.c) audit: dropped the long-dead$publish
reference;--inline-limit's documented default corrected (20, not 256);
-s/--std's documented default corrected (gnu23, notgnu17), added
the already-supportedc89/c90/gnu89/gnu90spellings, and replaced
the stale "affects predefined macros only" note (--stdnow also gates
tokenizer/preprocessor features); three CLI error messages that named
-M(--memory-leak-detection) as an output mode instead of-m
(--dump-expanded) corrected;--trap-fp-divzeromoved out of "FFI
Safety Options" (it has nothing to do with FFI) into "Optimization";
documented the previously-missing--link,--url-cache-dir/
--url-cache-clear, and-I/-D/-U's long-form aliases
v0.1.3
Changed
-c=native/-m/-G: a VM-only safety/debug flag (--checked-pointers,
--bounds-checks,-g/--debug, etc.) used to be a hard compile-time
error under-c=nativeand a silent no-op under-m/-G. Both now warn
and continue instead — these flags are genuinely inert in modes that hand
off to the hostccor serialize plain C, not a real conflict — naming
every ignored flag in the message.#pragma cccc config(...)keys are
likewise silently dropped under-c=native; that now emits a
-Wignored-featuresdiagnostic (-Wall) at the pragma site instead of
staying quiet (#924)
Fixed
-Gserializer: reflection API file-scope anonymous globals built via
CompoundLiteral/InitArray/InitStruct(reflect_new_anon_gvar()'s
other two call sites, besidesMakeStringLiteralwhich #925 already
covered) no longer fall through as an undefined.L..Nreference —
rename_anon_globals()now runs under-Gtoo, and the-Gemit path
forward-declares macro-generated globals ahead of any generated function
body that references them-c=native/-m/-Gserializer: pointer arithmetic whose result type is
an array (e.g. a reflectionMakeSubscripton an array-typed anon
global) no longer casts to the array type itself ((int [3])..., invalid
C) — casts to pointer-to-element insteadtools/testing/c4.py: the c4 round-trip skip check tested for-M
(--memory-leak-detection) instead of-m(--dump-expanded) when
deciding whether a test's output mode was round-trip-incompatible — a test
combining-mwith the c4 suite silently mis-saved serialized C source as
a.c4bytecode file instead of being skipped, then failed to reload
v0.1.2
Fixed
-c=native/-mserializer: anonymous globals (new_anon_gvar's.L..N
name) are no longer treated as opaque string literals across the board —
static locals and compound literals ((int[]){...},&(struct S){...})
now get a real, valid-C identifier and a properstaticdefinition
instead of an unreferenceable dotted name. Previously such a reference
either emitted invalid C (a dotted identifier the host compiler rejects)
or, worse, silently aliased the wrong data — e.g. astatic struct S
local compiled fine but read back as garbage bytes off a bogus
string-literal global, the same severity class as #918's defect C-c=native/-mserializer: a function's hoisted local-variable
declarations no longer collide when the same name is reused in sibling or
nested blocks (e.g. twofor (int i = ...)loops in one function, or an
inner block shadowing a parameter's own name) — renamed on collision
instead of emitting a duplicate declaration; a parameter itself is never
renamed, since its signature is already committed to output by the time
the collision check runs-c=native/-mserializer: a declaration-formforloop init
(for (int i = 0; ...)) is no longer dropped as an unsupported
expression — the loop variable is now actually initialized
v0.1.1
Added
- CHKT3 shadow permutation for
qsort/bsearch— element movement
through these libc callbacks is now tracked so out-of-bounds/type-mixing
writes from a comparator or the reorder itself are still caught (see
man/SAFETY.md) %n-awareprintfclassification + CHKT3 shadow page reclamation —
%nwrite targets are classified against the shadow map, and shadow
pages are reclaimed on sweep instead of growing unbounded (see
man/SAFETY.md)
Fixed
- CHKT3 FFI shadow backstop now clears global buffers too, not just heap
allocations, closing a gap where a global passed through FFI kept a
stale shadow entry - Discarded-value loads (
(void)*p) no longer useREG_ZEROas the load
address, which could fault or silently no-op the bounds check depending
on codegen path reflection.hno longer declares a duplicateVirtualMachinetypedef
Changed
- Public headers (
building.h,reflection.h,testing.h) and the
internalsrc/cccc.hfully migrated to pure Doxygen doc comments;
WARN_AS_ERRORenabled now that header coverage is complete - CI:
devel/mainintegration and release branches retired —trunkis
now the only long-lived branch (see this file's Branching section in
CLAUDE.md); Doxygen docs now publish to GitHub Pages via
.github/workflows/ci.ymlon every push totrunk, and are no longer
built as part of the sr.ht suite
v0.1.0
First release. There is no prior version to diff against, so this entry
summarizes the feature surface rather than a set of changes.
Added
- Compile-time macros —
[[cccc::macro]]/__attribute__((macro))/@macro
functions that run during compilation, with quasi-quoting, hygienic
reflection, and AST construction helpers (seeman/MACROS.md) - Native compilation pipeline —
-c=nativehands CCCC-preprocessed C to
a real system compiler (CCCC_NATIVE_CC,cc,clang, orgcc) for a
production build with no VM overhead - Register-based bytecode VM — 32 integer + 32 floating-point registers,
a portable instruction set, and a built-in interpreter (seeman/VM.md) - Memory safety suite — four preset levels (
-0through-3) covering
use-after-free, buffer overflows, dangling pointers, uninitialized reads,
integer overflow, CFI, and more (seeman/SAFETY.md) - Interactive debugger — breakpoints, watchpoints, register/memory
inspection, and a source map export API (seeman/TOOLING.md) - Interactive REPL —
-r/--repl, incremental compilation, multi-line
continuation, session commands - Bytecode optimizer —
-O[N]/--optimize[=N]levels 1-4 with
individually toggleable passes (seeman/OPTIMIZATION.md) - URL includes —
#include <https://...>, optional (CCCC_HAS_CURL=1) - Decimal floating-point —
_Decimal32/64/128via the Intel BID library,
optional (CCCC_HAS_DECIMAL=1) - Built-in test framework —
[[cccc::test]],Assert*macros, TAP output
(seeman/TESTING.md) - Attribute support — GNU
__attribute__, C23[[...]],@name
shorthand (seeman/COVERAGE.md) - JSON reflection output —
--ffi-declsdumps declarations for FFI
wrapper generation - VM heap — intercepting allocator on by default at every safety level
--version— prints version, git describe, host triple, and enabled
build features- Release build mode —
./cccc --build build.c --build-target=release
(host-O2 -g -DNDEBUG), distinct from CCCC's own guest-side
--optimize/safety levels - Supported platforms: macOS and Linux, aarch64 and x86_64