Prism 1.1.7 hardens transpilation, compiler invocation, output publication, cache correctness, and language-feature parsing.
The procedural regression suite grows by 838 cells to 46,334 platform-inclusive cells, faster test suit.
fixes
-
--prism-emitnow publishes output atomically. -
--prism-emitrejects an input file or hard link as its destination, preserves an old destination on failure, and rejects one explicit destination for multiple sources. -
Stdout emission uses the caller's descriptor on POSIX and reports write, flush, and close failures on every output path.
-
Generated
#linefilenames now escape quotes, controls, and trigraph-forming question marks. -
Raw stripping keeps directives on their own line.
-
Stray braces now diagnose safely instead of reaching emission in an invalid parser state.
-
Malformed file-scope
orelsenow diagnoses safely. -
Illegal bracket
orelsedimensions now diagnose safely. -
Declaration conditions now diagnose safely.
-
Statement-expression
gotos now diagnose safely. -
Parser arena shrinking is safe for library callers.
-
First-context allocation failure is safe for library callers.
-
Result-error allocation failure is safe for library callers.
-
Source-directive recovery is safe for library callers.
-
prism_reset()is safe for library callers after parser errors. -
prism_thread_cleanup()is safe for library callers. -
prism_free(NULL)is safe for library callers. -
File APIs reject directories and non-C inputs before invoking the compiler.
-
File APIs reject malformed feature-array counts safely.
-
Oversized preprocessor argv construction is rejected before unsafe allocation.
-
File APIs reject embedded-NUL input before unsafe processing.
-
File APIs reject UTF-16-looking
.iinput before unsafe processing. -
Compiler processes receive a fresh environment snapshot for every spawn.
-
Windows preserves Unicode environment values and rejects malformed UTF-8 conversion instead of silently changing it.
-
Stdin preprocessing normalizes
-x noneto C. -
Re-emitted API definitions preserve preprocessor order and last-definition-wins behavior.
-
Quoted custom compiler commands preserve embedded spaces and platform quote rules without shell parsing.
-
Response files now expand lazily, respect
checkand--boundaries, work as option operands, and support nesting and pipes. -
Response files support UTF-8 and Windows UTF-16 BOMs and handle malformed reads safely.
-
MSVC
/c,/Fe,/Fo,/FI,/D,/E,/EP,/P, stdin, and slash-option operands are modeled correctly. -
GNU
-f...flags no longer collide with MSVC dash spellings. -
Non-flattened direct system headers retain the original preprocessor stream when re-emission could change macro, pragma, include, or reinclusion ordering.
-
The preprocessor cache validates payload checksums, full file identity, working directory, Unicode environment inputs, and volatile header macros.
-
The cache bypasses dependency side effects, marker suppression, response files, specs files, volatile flags, and custom compiler wrappers.
-
Cache publishing uses unique temporary files, saturated limits, and safe unavailable-admin paths.
-
Braceless
defernow captures an entire control statement, includingif/else, loop,switch, anddo/whilebodies. -
Braced defer bodies validate embedded
breakandcontinue, preventing an invalid deferred escape from reaching emitted C. -
Bounds checking now rejects a conditional base derived from tracked arrays instead of using one branch's extent for another.
-
Auto-static no longer mistakes shadowable
trueorfalseidentifiers for constant initializers. -
Deferred-capture discovery excludes actual
orelseoperators. -
Deferred-capture discovery excludes
rawdeclaration prefixes and raw blocks. -
defer,orelse, andrawnow recognize literal backslash-LF and backslash-CRLF splices before soft-keyword matching. -
C trigraph translation now runs before splice handling, so a trigraph-derived backslash such as
??/followed by LF/CRLF also joins a Prism keyword correctly. -
Generated
#linedirectives now retain the physical source line after a splice instead of mapping the next token to the preceding line. -
A function that returns a function pointer now synthesizes its defer return-type typedef with the returned type's parameter list.
int (*f(void))(int)producedtypedef int (*T);, so the return temporary was anint *; GCC 16 and Clang 22 reject the resulting initializer andreturnoutright, and older compilers accepted them with incompatible-pointer warnings. Array-pointer returns were already correct;(params)was the one declarator suffix being dropped. -
A
returnvalue expression with no terminating;now diagnoses instead of copying the rest of the translation unit into the return temporary's initializer and emitting unbalanced C. -
Defer bodies, and the synthetic blocks spliced for
return,break,continue, andgoto, no longer land on a surviving preprocessor directive's line. A conforming preprocessor discards trailing tokens on a directive line, so with-fno-line-directivesa#pragmaimmediately before a scope exit silently deleted the cleanup, and in thereturncase thereturnstatement with it. Line directives happened to force the line break, which is why the failure only appeared with them disabled. -
Macro-hygiene wrap parentheses around a declaration initializer no longer hide an empty
orelseaction.int x = (f() orelse);was accepted and lowered tox = x ? x :;, while the unwrappedint x = f() orelse;was already rejected. -
An unterminated GNU
__label__declaration no longer duplicates every token from the declaration to end of input. The emitter wrote the tokens before discovering there was no terminator, then reported the statement as unhandled, and the caller emitted them a second time. -
--prism-emit=<file>publishes with the destination's previous mode when replacing one, and with0666 & ~umaskwhen creating one. Atomic publication is viamkstemp, which forces0600. -
Trigraph translation no longer runs twice.
cc -E(and a.iinput) has already completed translation phases 1 to 3 with the backend's trigraph policy, which is off by default for GCC, Clang and MSVC and removed outright in C23. Re-applying phase 1 to that stream rewrote literals the compiler had deliberately kept:const char *s = "what??!";compiled towhat|under Prism andwhat??!under the very compiler Prism was driving. Prism now matchesccexactly under-std=c99,-std=gnu99,-std=c11,-std=c2x,-trigraphsand the default. Raw source reaching the library API is unaffected: Prism is the only front end that text sees, so it still owns phases 1 and 2, and the spliced-keyword forms keep working. -
An
orelsefallback value with no terminating;now diagnoses instead of copying the rest of the enclosing block intoLHS = ( ... ). Same defect and same shape as fix 46, on the bare-assignment path. -
A
deferdirectly after a GNU__label__declaration is no longer rejected as expression context. Phase 1D handed on the__label__token itself as the previous token, so the following statement looked like a continuation of an expression; valid GNU C plus valid Prism was refused, and only when nothing else stood between the two. -
A
__label__declaration as a bracelessdeferbody now diagnoses. It carries no type tag, so the existing braceless-declaration check never saw it, and the body scan emitted a stray}. -
A bare
orelsewhose Phase 1 recipe has no recorded assignment=no longer dereferences a null token. Found by UBSan in the fuzz target. -
Extension elimination is enforced at emission instead of assumed. A token Phase 1 classified as the keyword, whose feature is enabled and which is not inside a
raw { ... }block, must be consumed by a lowering path; reaching raw emission now diagnoses. Three retired-regression cells were locking in the opposite:defer { int x = 5 }with no;had the rest of the file swallowed into the deferred body and pasted back, so the output declaredtask_btwice and contained a livedefer; and an attribute betweenifand its(leftp = get() orelse 0;in the generated C. All three inputs are rejected by a C compiler on their own merits, and all three previously produced output no compiler could use. The check costs one predictable branch on an already-loaded tag word. -
Library calls no longer inherit the line-directive spelling from an earlier call.
use_linemarkersselects GCC's# N "f"linemarker over C99's#line N "f"; only the CLI ever assigned it, and no library entry point reset it, so once anything in a thread set it every laterprism_transpile_sourcesilently emitted the other spelling. Found by sharding the suite: a shard that ran the internal-platform recipe next to the feature matrix lost#linefrom all 64 of its line-directive cells. -
Every
posix_spawnsite retries the transient failures. Four of the five had no retry at all -- including the preprocessor spawn -- and the one that did coveredEAGAINandETXTBSYbut notENOMEM, which is what macOS returns when the machine is briefly short of memory. -
--prism-emitto a destination that is not a regular file (/dev/null, a character device, a fifo) now streams the finished output instead of failing.renamecannot publish onto such a destination, and the fallback also covers a cross-deviceEXDEVwhen the sibling temporary had to fall back to$TMPDIR. The destination is still opened only after translation and verification succeed. -
The Windows build links again. Fix 58's retry helper called
nanosleep, which has no MSVC import. The call had existed for a long time insidespawn_command, whose every caller is POSIX-only, so the linker discarded the function whole and the symbol never had to resolve; giving the retry four more call sites that are live on Windows turned it intounresolved external symbol nanosleep. The sleep is nowSleepon Win32 andnanosleepelsewhere, and the helper's parameters match windows.c'sposix_spawnpshim rather than the POSIX prototype, since a non-const argument converts to POSIX's const one but not the reverse. -
windows.cincludes<basetsd.h>in lowercase. The MSVC SDK is case-insensitive so both spellings work there, but mingw-w64 ships the lowercase name and a case-sensitive host could not resolve<BaseTsd.h>. This is what had made the_WIN32source path uncheckable anywhere except Windows CI, which is why fix 60 reached a push. -
The suite's two infrastructure counters are declared outside the POSIX guard.
infra_retriesandslow_clock_skipsare read by the summary inmain(), which is compiled on every platform, but sat beside the POSIX-only code that increments them; MSVC reported fourundeclared identifiererrors. Same shape as fix 60 -- code added under one platform's assumptions and never built under the other. -
Bounds checking covers
int a[static N]. C11 6.7.6.3p7 promises the argument points at at least N elements, and that promise is the only extent an array parameter has -- it decays to a pointer, so the usualsizeof(a)/sizeof(a[0])would measure the pointer against the element. The bound is the literal N.[static const N]and[const static N]are both recognised; an array parameter without the promise, and a plain pointer parameter, stay unchecked as before. -
Bounds checking covers pointer-to-array dereference.
int (*p)[4]carries its extent in the type, and*pandp[0]denote the same object, so(*p)[i],p[0][i]and(*(p))[i]all bound againstsizeof(p[0])/sizeof(p[0][0]). The pointer hop itself stays unchecked -- nothing says how many arraysppoints at -- sop[i]is untouched, and a base built from a pointer-to-array in any other shape is left alone rather than diagnosed. -
Header-bearing preprocessor-cache entries are enabled. Recording each header's identity was never the missing piece; knowing whether it is still the file the next preprocess would choose was. An entry now also records the backend's include search path, taken from
-E -vwith the same flags, and each dependency's spelling as the preprocessor reported it. A file appearing ahead of a header in the search path moves that directory's mtime; a symlink retargeted anywhere along the way changes what the spelling resolves to. Either one is a miss. Failing to determine the search path means the entry is not published, so the fallback is exactly the previous behaviour.PP_CACHE_MAGICadvances toPRISMPPC4; older entries are ignored. -
Macro operands of surviving
#pragmadirectives are preserved. No preprocessor expands a pragma's operands, so flattening -- which hands the preprocessed stream through and re-emits no source defines at all -- left#pragma pack(push, PK)in the output withPKdefined nowhere. A define named by a surviving pragma is now kept. Where it goes depends on where it was: a define ahead of the first include is hoisted with the rest, and one that follows a header is emitted in place, immediately before the pragma that needs it, because a define written after a header may be completing or shadowing what that header set up and must not be lifted above it. Defines no pragma names are still dropped. Preserving the define makes the question of whether a given backend expands pragma operands moot: it now does whatever it would have done with the original source. -
-fno-orelseleaves the wordorelsealone. Phase 1D annotated and then diagnosed the declaration-initializer form without consulting the feature bit, soint j = w || orelse;was rejected with the feature off, where the word is an ordinary identifier and an undeclared one is the backend's complaint to make in its own words. It also meant Prism could not reparse its own output whenever an input used the name as a plain identifier -- the reparse oracle runs with every transformation disabled. -
An expression walk that reaches
}without its;diagnoses instead of consuming the closing brace. This was the last of the four expression walks still missing the bound that fixes 46 and 52 added to the others; found by the fuzz corpus. -
typeof-based zero-init at file scope no longer indexes a negative function slot. Zero-init lowers to a runtime statement -- a byte loop or amemset-- which has no meaning outside a function, and the emitter reached for the enclosing function before checking there was one. -
A spawn the machine refused is reported as such instead of as a preprocessing error. The library returned
PRISM_ERR_IOwith "Preprocessing failed" whether the preprocessor had run and disagreed or the machine had declined to start it at all -- two conditions that need different responses, and only one of which is about C. The message now names the refusal and its errno. The retry budget behind it also grows from five attempts over 0.2s to nine over ~2.8s: a Mac with 14.9 GB paged out refused straight through the old window, and waiting longer costs nothing on a machine where the first attempt succeeds.