Skip to content

Prism v1.1.5

Choose a tag to compare

@dawnlarsson dawnlarsson released this 27 Jul 18:33
· 41 commits to main since this release

Prism 1.1.5 fixes 200+ bugs since v1.1.4, Prism gets faster ~2.02x end-to-end warm cache; 1.65x on the stress TU with the cache disabled.

New: preprocessed-output cache, prism check <analyzer>, @file response files, stdin input.

Four fixes address bugs that corrupt a build: a uint16 scope-cap that dropped the scope tree past 32,768 scopes with no diagnostic; orelse reaching the C backend intact inside a case label; a braceless defer declaration that copied every following statement into the cleanup; and orelse on a volatile or _Atomic object reading it up to three times.

Performance

Preprocessed-output cache. cc -E was 39–99% of wall time (8.19 of 8.28 ms on a no-include file; 103.8 of 262.8 ms on test.c). Prism now caches the preprocessor's output and skips the spawn when nothing it read changed.

The preprocess phase drops 10–25x on a hit, scaling with how much output has to be read back: 6.5 → 0.4 ms for a 57 KB entry, 29.0 → 1.2 ms for 328 KB, 46.2 → 1.8 ms for 1.2 MB. End-to-end that is 2.02x v1.1.4 on a warm cache (70.9 → 35.0 ms, best-of-3 over four suite files).

Transpiler. Gains are workload-dependent, and the cache accounts for most of the end-to-end figure above. With PRISM_NO_PP_CACHE=1, on a 2,500-function defer/orelse/-fbounds-check stress TU it is 1.65x (70.9 → 43.0 ms); across four ordinary suite files 1.07x, ranging 1.20x on test.torture.c down to 0.94x on test.defer.c. Earlier notes reported ~65% on the phase timings of a stress TU; that holds for that shape, not as a general figure.

On Arch/9950X, GCC -O2 -g -DNDEBUG, CPU 8 pinned, 80-run perf stat batches: the 6,091,701-byte procedural TU moves 77.66 → 70.32 ms task-clock (−9.45%), cycles −10.26%, instructions −10.36%. The 1,109,463-byte self-host TU moves 15.94 → 15.05 ms (−5.58%). Outputs byte-identical.

Bug Fixes

defer

  • Scope-cap uint16 truncation: a TU with more than 32,768 scopes lost the rest of the scope tree with no error and mis-lowered file-scope declarations (b007d1e)
  • A declaration as a braceless body scanned past its own semicolon and pasted every statement up to the block close into the deferred code, so they ran a second time at scope exit. The prior test only checked the output contained int t = 0, so it never saw the duplication (16bba07)
  • Missing-semicolon detection inspected only the body's first token: defer break; was rejected, defer f() break; was accepted and emitted as f() break; with the keyword stripped, so the backend error pointed at code the user never wrote (unreleased)
  • Labeled break/continue unwound the wrong scope, including GNU __label__ (f6bff02)
  • Mid-expression defer after a comma or in return (x=1, defer g();, return defer g(), 0;) leaked the keyword in release builds; reject_defer_in_expr_context was PRISM_DEBUG-only (78f9c8a)
  • Expression-context splices through sizeof, !, ~, &, casts and ?: (86c20fc)
  • Scope/block depth confusion over-unwound defers across goto boundaries (c0a4935)
  • Braceless bodies under name shadows mis-parsed; defer;, for-init and ++ forms had to stay identifiers (06bb332)
  • Braceless control flow in defer/orelse bodies broke brace injection (fc098bb)
  • { defer } brace-init false reject (86c20fc)
  • typeof(defer) (fc3716d)
  • defer _Pragma (c0a4935)
  • File-scope defer was not rejected (5732201)
  • in_defer_emit thread-local was not reset after a pparse_error longjmp skipped the restore (a661b6b)
  • Attribute labels in a defer chain (a661b6b)
  • Unevaluated sizeof defer was not a no-op (f2cf4aa)
  • Zero-init-off orelse inside a defer (6bf4f84)
  • Braceless defer die(); missed __builtin_unreachable() injection: the synthesized ; skipped the noreturn probe (78f9c8a)

orelse

  • Reached the C backend intact inside a case label: case (x orelse 1): was accepted and the literal token passed through. Found by the tag-alphabet totality suite on its first run (16bba07)
  • On a volatile or _Atomic object, read the object up to three times; single-eval hoisting now covers atomic typedefs (ee1fc71)
  • typeof(int[N]) / _Atomic(int[N]) accepted because arrayness lived on the type rather than the declarator, emitting broken if (!a){…} (c0a4935)
  • Dimension holes in _Alignof, _Generic, _Static_assert and nested typeof (78f9c8a)
  • Return-expression leak: return (x) orelse 1 leaked because the backward walk crossed (x) to return and applied the return orelse; identifier exemption (78f9c8a)
  • static / extern / _Thread_local dimension orelse lowered to illegal static VLAs (17329c2)
  • _Atomic(int[n orelse …]) leaked: type-specifier dims were skipped and the type emitter never walked _Atomic( (17329c2)
  • .orelse orelse decl-init (06bb332)
  • Mid-chain empty orelse orelse; orelse() now allowed (86c20fc)
  • Mid-chain continue / block-form reject (f2cf4aa)
  • Struct-value and chain-after-control-flow rejects (b007d1e)
  • Keyword leak via static raw verbatim bypass
  • Raw string literal R"(...)" inside #if 0 desynchronized block-comment state
  • __attribute__ bracket-orelse queue desynchronization
  • init-cast VLA crash on variably-modified types
  • Bare comma split escaped braceless control flow; paren comma corruption in chained expressions
  • Preprocessor-conditional mangling in bracket orelse
  • Designator side effects, compound-literal VLA, and nested _Static_assert leak (f6bff02)
  • Non-ICE designator-dimension orelse; false control-flow diagnostic (06bb332)
  • Soft type-spelling orelse values (06bb332)
  • const _Atomic / volatile if-hoist; _Nonnull / _Nullable qualifier scoping (86c20fc)
  • typeof(_Atomic(int) orelse 0) leaked: _Atomic(T) was not treated as a type-specifier constructor
  • typedef int (*F)(int a[0 orelse 1]) ternary-lowered parameter dims; the declarator check jumped the (*F) group and typedef walks never called the prototype reject
  • GNU range designators [0 ... 2 orelse 3] ternary-destroyed the ... into illegal C
  • constexpr int a[0 orelse 1] hoisted a runtime temporary into a constexpr dimension
  • Empty-orelse emit leaks; return + defer subscript orelse (65b0ee7)
  • Paren-led bare orelse after a control statement (97a9fba, b20455e)
  • Multi-label and attribute bare-orelse peel (e03ee60)
  • Brace-init designator leak; _Atomic side-effect register; GNU attribute-between-dims FIFO (f2cf4aa)
  • typeof(enum) did not set is_enum (f2cf4aa)
  • enum E : typeof(T) { orelse } and enum E : _Atomic(int) { orelse } (c0a4935, 86c20fc)
  • asm symbolic [orelse] operand names (c0a4935)
  • Array variable orelse that can never trigger is now diagnosed rather than silently lowered

bounds-check

  • Local pointers, including qualified, typedef, pointer-to-array and raw forms, could shadow a tracked file-scope array without creating a typedef-table shadow, so g[i] inherited the outer array and was wrapped with sizeof(pointer)/sizeof(pointer[0])
  • Block-scope incomplete extern int g[] inherited the complete outer bounds, generating an invalid sizeof(g)
  • Decayed parameters wrapped against the file-scope array: int g[10]; f(int g[20]){ return g[i]; }, same for K&R and int *g params that skipped the shadow optimization
  • Wraps skipped when the index itself used orelse (a[i orelse 0]) because try_bracket_orelse ran before the subscript check in the emit loop; they compose now, with the ternary index inside __prism_bchk (78f9c8a)
  • Silent bypasses: (i)[tern], ((i))[tern], (2)[tern] (last emitted was )); *((T*)(a+i)) and *(((T*)(a+i))) (a grouping paren hid the cast+additive pair); bare idx[&arr[0]] without an outer paren; (*&(a))[i] and (*a)[j] (c0a4935, 78f9c8a)
  • p[a[i]] false-rejected as commutative idx[arr] despite the subscripted-index exemption (c0a4935)
  • Cast×subscript used naive sizeof(arr)/sizeof(arr[0]) instead of the cast element size
  • Cast-prefix unary & one-past ((void)&a[n], (int*)&a[n]) mistook the cast ) for a binary-& operand (78f9c8a)
  • (a+i)[0], 0[a+i], cast-deref and _Atomic·typeof dims (f6bff02)
  • One-past parenthesized bounds (aa1b961); paren-deref (b007d1e)
  • for/if/switch-init declarations never registered array bindings (missing wraps) or shadows (pointer init-stmt inheriting outer sizeof) (c0a4935)
  • Block-scope static/constexpr initializers wrapped g[0] into a non-ICE (c0a4935)
  • raw VLAs were still re-registered (c0a4935)
  • Pointer-arith *(a+i) bypass after the registry split; the lookup still consulted only the typedef table
  • typeof(fixed_array) bounds registration; VLA-of-pointers false wraps
  • emit_statements did not run bounds checks (e03ee60)
  • C89 __prism_bchk form (fc3716d)

zero-init

  • Empty and [0] aggregate memset (06bb332)
  • Const-subobject memset was undefined behaviour (aa1b961)
  • typeof(_BitInt(N))
  • sole-FAM and nested-empty {0}, plus register/const × memset rejects
  • for/if-init _Atomic and register rejects (86c20fc)
  • Member-side const-union false reject (86c20fc)
  • _Atomic aggregate and union/VLA const cases now diagnosed rather than silently mis-lowered
  • Soft-keyword bitfields (fc3716d)
  • SUE { scope tagging (ee1fc71)

auto-static / auto-unreachable

  • Auto-static promoted mutable typeof(int[N]); typeof-as-const applies to orelse temps only (c0a4935)
  • Auto-static missed leading cleanup / [[attr]], typedef-const, unbraced string initializers, C23 true/false, typeof(int[N]) (86c20fc)
  • Statement-final noreturn through casts, grouping, comma tails and call arguments ((void)(0, die());, die(), 1;, foo(die());) missed __builtin_unreachable(); detection required an immediate ; after the call's ) (78f9c8a)
  • Unevaluated operands falsely marked unreachable: sizeof die(); and no-paren sizeof +die() (78f9c8a)
  • Contextual noreturn/taint poisoning via soft-keyword callee, attribute name, asm __volatile__ goto, declaration shadow (aa1b961)
  • User TU definitions of exit/setjmp/… false-tainted and false-unreached
  • _Noreturn void a(), b(); tagged only the first declarator
  • asm soft-keyword taint (fc3716d)

raw

  • raw { … } still lowered defer/orelse or injected auto-static, bounds and unreachable
  • typedef int raw; raw raw x keyword strip (c0a4935)
  • Statement-form raw { … } suppress blocks (c0a4935)
  • raw on a VLA is still subject to the goto check, since jumping past a VLA bypasses stack allocation regardless of initialization
  • An all-raw declaration must emit identically to the same declaration with raw deleted and zero-init off. That differential failed 32/229 cells on first run and exposed __attribute__((unused)) const raw int v; emitting const const int v;, invalid C that Phase 1 accepted. The attribute prelude was bounded at the raw token even when raw sat inside the specifiers

tokenizer / preprocessor

  • Function-like macros were silently dropped from re-emitted defines while object-like ones survived: #define MAX(a,b) did not round-trip a transpile (unreleased)
  • .i input: //\ line splice, UTF-8 BOM, universal character names in identifiers (06bb332)
  • .i translation units were dropped entirely (fc3716d)
  • Digraph %: line markers (86c20fc); digraph/trigraph #define (f6bff02)
  • UTF-16 BOM now rejected with a diagnostic instead of mis-tokenized (b007d1e)
  • Sticky # N "a.h" 1 3 system-skip dropped the rest of a user TU
  • Hard type keywords used as declarator names (06bb332)
  • Parenthesized _Generic fold (06bb332); glibc _Generic default-fold declarations (5732201)
  • GNU nested-function is_func_body CFG classification (06bb332); nested-function verbatim emission (fc3716d)
  • goto "assigned-first" analysis accepted x = x + 1 (86c20fc)
  • goto into C23 if/switch init when the init-semicolon search returned NULL
  • Brace-unsafe walk could scan past type_end on large TUs (86c20fc)
  • Enum-walk underflow (65b0ee7)
  • Ill-formed while (decl), do decl, for (raw {…}), scalar for (raw int x = 1), while(defer 0), if(defer 0) in control parens

driver / CLI

  • @file response files treated a backslash as a path separator on Windows, eating C:\ paths (1d18a57)
  • Force-include re-injection; split -I under -fpreprocessed; bits/ re-include (fc3716d)
  • -M dependency routing (b007d1e)
  • -U_GNU_SOURCE ordering; clang #… 3 under no-flatten (f6bff02)
  • No-flatten #include path escaping
  • C++ mix driver preprocess/skip, C++ link, -x c temps, cross-compiler prefix (65b0ee7)
  • sos_ensure_do OOM dangling pointer (5732201); sos_* thread-local free (a661b6b)
  • Calling-convention return capture (6bf4f84)
  • Newline emission before declarations after statement boundaries (ee1fc71)
  • Control-paren scope tracking in emit_statements; for-init is_loop preservation (a661b6b); else/do paren-led forms and do at-statement-start (b20455e); GNU attribute control-paren tracking (e03ee60)

Windows / MSVC

  • memstream, handle inheritance, PATH, mkstemps, install (aa1b961)
  • build_clean_environ was used by windows.c before its declaration; MSVC infers int() and then rejects the real declaration with C2040. GCC 14+ would reject it too (unreleased)
  • win32_memstream_path removed; suite tests rewritten for DELETE_ON_CLOSE (06bb332)
  • mkstemps and noreturn shims (5732201)
  • CLI_PUSH under-allocation (65b0ee7)
  • __assume(0) now counted as an unreachable marker alongside __builtin_unreachable (17329c2)
  • Fake cl test used bash; Alpine has no /bin/bash (1d18a57)

assertions / harness

  • PRISM_DEBUG false assertions: balanced-group checks treated initializer braces and index brackets as parentheses, and the missing-declaration-recipe assertion forgot that Phase 1 excludes GNU nested-function locals from the outer CFG table (78f9c8a)
  • Harness accounting: a local total in test_win_env_scrubbing shadowed the runner's thread-local CHECK counter, so three checks incremented passed but not total. The invariant total == passed + failed is now itself a counted meta-test
  • Four leaked temp/result ownership paths found by LeakSanitizer; undefined _Bool feature-mask write; UB-based OOB "trap" oracles replaced with wrapper-presence and zero-survivor checks

Testing

104 generative tiers Prism test suit moves to more procedural tests to cover much wider set of edge cases.
suites test.autostatic.c, test.autounreach.c, test.golf.c, test.cert.c and test.raw.c were absorbed into generative tiers.

Code organization

C-language logic moved out of prism.c into parse.c, which is moving towards a reusable C-parsing library with Prism dialect support. parse.c 4,595 → 12,928 lines; prism.c 12,336 → 9,696.

Stats

  • 200+ bugs fixed since v1.1.4
  • 40 commits; 36 files changed, +37,133 / −17,479
  • 104 generative tiers, 3,820 CHECK sites (876 at v1.1.4)
  • Suite: 14,543+ platform-inclusive CHECK inventory, up from 6,035 in v1.1.4. Audited: 14,286/14,286 Darwin/Clang, 14,355/14,355 Arch/GCC 16, the latter also clean under ASan + UBSan + LeakSanitizer. 11,327/11,327 verified under GCC across 18 of 19 tiers. test.parse.c requires Clang, since it uses for (typedef double x;;), which C11 6.8.5.3 permits only for auto/register
  • Self-host: stage0 → stage1 → stage2 → stage3 transpile output byte-identical (1,058,598 bytes)
  • --prism-verify passes on prism.c, parse.c, and all 21 corpus sources
  • 2.02x end-to-end warm cache; 1.65x on the stress TU with the cache disabled