Home | Manual | Static Analysis | Playground

Cake - C23 and Beyond

Cake C Compiler — Reference Manual

1. Introduction

Cake is a source-to-source C compiler that translates modern C (C99 through C2Y) into C89-compatible output, suitable for compilation by existing toolchains such as GCC and MSVC.

Cake integrates directly into existing build environments:

Where applicable, Cake uses the same command-line flags as GCC and MSVC to minimize friction when integrating into existing projects.


2. Getting Started

2.1 Platform Support

Cake runs on Windows (targeting MSVC), Linux (targeting GCC), and macOS (targeting Clang). Cake can also perform cross-compilation, as long as you provide the headers from the target platform.

2.2 Include Directories

Include directories are specified in cake.json, a configuration file kept next to the cake executable. It replaces the former cakeconf.h header.

Discovering system include paths manually:

On Windows (from a Visual Studio Developer Command Prompt):

echo %INCLUDE%

On Linux:

echo | gcc -E -Wp,-v -

On macOS:

echo | clang -v -E -

Sample cake.json for Linux:

{
  "include_dirs": [
    "/usr/lib/gcc/x86_64-linux-gnu/11/include",
    "/usr/local/include",
    "/usr/include/x86_64-linux-gnu",
    "/usr/include"
  ]
}

Sample cake.json for Windows:

{
  "include_dirs": [
    "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.38.33130/include",
    "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.38.33130/ATLMFC/include",
    "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Auxiliary/VS/include",
    "C:/Program Files (x86)/Windows Kits/10/include/10.0.22000.0/ucrt",
    "C:/Program Files (x86)/Windows Kits/10/include/10.0.22000.0/um",
    "C:/Program Files (x86)/Windows Kits/10/include/10.0.22000.0/shared",
    "C:/Program Files (x86)/Windows Kits/10/include/10.0.22000.0/winrt",
    "C:/Program Files (x86)/Windows Kits/10/include/10.0.22000.0/cppwinrt",
    "C:/Program Files (x86)/Windows Kits/NETFXSDK/4.8/include/um"
  ]
}

Sample cake.json for macOS:

{
  "include_dirs": [
    "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include",
    "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include",
    "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include"
  ]
}

Unlike the cakeconf.h it replaces, cake.json is data rather than a preprocessed header: it cannot use #ifdef to hold several platforms in one file, and it cannot #include another config. Each installation has its own cake.json, describing the machine it runs on. To add include directories for one project rather than the whole installation, use -I on the command line, or #pragma dir in the source itself.

The cake IDE reads and writes this same file, keeping its own global compiler options in it under a "compile" object. Both tools share one format, and -auto-config rewrites only "include_dirs", leaving anything else in the file untouched.

2.3 Cake's Own Headers and #include_next

Cake ships its own directory of annotated standard headers (stdlib.h, string.h, stdio.h, and the rest), carrying _Owner/_Opt and the other ownership annotations described in 11.9. This directory is added to the search path ahead of the real system include directories, so #include <stdlib.h> resolves to Cake's annotated header first.

Cake's header then pulls in the real one with #include_next <stdlib.h> - the same directive GCC and Clang use for this purpose - which continues the search from the next directory after Cake's own, i.e. the real system include path. This is how the annotated declarations and the platform's actual declarations both end up in scope: Cake's header supplies the contract, #include_next supplies everything else (types, other functions, platform-specific details) from the real header.

This directory - include next to the cake executable - is built in: Cake always searches it first, before every entry of cake.json's "include_dirs", so it is not listed in cake.json.

The -cake-headers option switches to a different mode: it defines the built-in macro CAKE_HEADERS, which each of Cake's headers checks to decide whether to declare everything itself - types, macros, and function prototypes - instead of deferring to #include_next. With -cake-headers, Cake's headers are entirely self-contained and the real system headers are never consulted at all, so compilation no longer depends on having a system toolchain or its include paths configured. This is how Cake compiles itself and runs its test suite portably; it is not meant for compiling ordinary programs, since a program built this way only sees the subset of the platform's declarations that Cake's own headers happen to provide.

2.4 System include paths auto-configuration

The -auto-config option fills in cake.json's "include_dirs" automatically by querying the active compiler environment:

cake -auto-config

On Windows this reads the INCLUDE variable, so it must be run from a Visual Studio Developer Command Prompt. On Linux and macOS it reads the search list reported by gcc -v -E / clang -v -E.


3. Invocation

3.1 Synopsis

cake [options] source1.c [source2.c ...]

3.2 Examples

cake source.c

Compiles source.c and writes the C89 output to ./<target>/source.c, where <target> is the platform Cake was built for (e.g. ./aarch64-apple-darwin-clang/source.c).

cake -target=i686-pc-windows-msvc source.c

Compiles source.c targeting MSVC x86 and writes output to ./i686-pc-windows-msvc/source.c.

cake file.c -o file.cc && cl file.cc

Compiles file.c to file.cc, then passes file.cc to the MSVC compiler.


4. Command-Line Options

4.1 Preprocessor Options

-I <dir>
Add <dir> to the list of directories searched for include files. Same as GCC and MSVC.

-D <macro>
Define a preprocessing symbol for the source file. Same as GCC and MSVC.

-E
Copy preprocessor output to standard output. Same as GCC and MSVC.

-H
Output a list of include files used during compilation. Equivalent to /showIncludes in MSVC and -H in GCC.

-dump-tokens
Output the token stream before preprocessing.

-preprocess-def-macro
Expand and preprocess #define macros after expansion.

4.2 Output Options

-o <name.c>
Set the output filename when compiling a single file. Same as GCC and MSVC.

-output-root=<dir>
Write each output to <dir>/<target>/<path relative to dir>, so a file always goes to the same place no matter which other files are passed. <dir> must be a parent folder of the input files; it is not a separate output folder. Files outside <dir> ignore it and go to <their dir>/<target>/. Without this option the root is the common directory of the files given. The IDE passes the project directory.

Example: cake -target=x86_64-pc-windows-msvc -output-root=project project/main.c project/src/util.c other/lib.c

project/                  <- -output-root
├── main.c
├── src/
│   └── util.c
└── x86_64-pc-windows-msvc/             <- generated
    ├── main.c
    └── src/
        └── util.c
other/                    <- outside project
├── lib.c
└── x86_64-pc-windows-msvc/             <- generated next to lib.c
    └── lib.c

Without -output-root, compiling only project/src/util.c would write project/src/x86_64-pc-windows-msvc/util.c.

Because the generated folder mirrors the source tree, a Makefile from project/ can be copied into project/x86_64-pc-windows-msvc/ and used unchanged. The output has no #include, so headers and -I are not needed. Files outside the root (like other/lib.c) need their paths adjusted, e.g. ../../other/x86_64-pc-windows-msvc/lib.c.

-no-output
Run all analysis passes but suppress file output.

-line-directives
Emit #line directives in the generated C89 output to preserve source location information.

-sarif
Generate SARIF diagnostic output files. Compatible with the Microsoft SARIF Viewer extension for Visual Studio.

-sarif-path <dir>
Specify the directory for SARIF output. Typical Visual Studio External Tools invocation:
-w11 -msvc-output -no-output -sarif -sarif-path "$(SolutionDir).sarif" $(ItemPath)

-msvc-output
Format diagnostic output to be compatible with the Visual Studio IDE error parser. Same as -fdiagnostics-format=msvc plus -fdiagnostics-color=never.

-fdiagnostics-format=<format>
Select how the position of each diagnostic is printed. Valid formats:

Format Output
gcc (default) file.c:1:2: warning 10: message
msvc file.c(1,2): warning 10: message
ide file.c:1:2: warning 10: message

Both shapes are understood by Visual Studio and by Visual Studio Code. The file being compiled is printed by name only; files reached through #include are printed with their full path.

-fdiagnostics-color=never
Disable ANSI color codes in diagnostic output. Same as GCC.

4.3 Diagnostic Options

Option Effect
-w<number> enable warning number <number>, e.g. -w2. See the Warnings Reference
-wd<number> disable warning number <number>, e.g. -wd2
-wall / -Wall enable all warnings

Disabled by default
Most warnings are on unless -wd<number> turns them off, but a few are off until asked for:

Number Warning
2 unused variable
6 unused function parameter
11 style
33 nullable pointer flow check
35 nullable pointer flow check
83 parameter set but not used
84 variable set but not used

-Werror
Report every enabled warning as an error. Notes are not affected, and warnings that are disabled stay disabled. Because they become errors, warnings coming from included headers are no longer suppressed, and any occurrence makes the compilation fail.

Per-line suppression with lint comments
Suppress a diagnostic on one line with a trailing lint comment listing its number(s): //lint 35, // lint 35, or /* lint 81 */. An unnecessary suppression is flagged with warning 59.

-ignore-lint
Treat lint comments as plain comments: they suppress nothing and are not checked by warning 59. Useful to see every diagnostic a file would report.

4.4 Target Options

-target=<name>
Set the compilation target. Controls integer sizes, alignment, and the style of generated C89 output.

Available targets:

Name Platform Output compiler
default the platform Cake itself was built for (same as omitting -target) —
x86_64-linux-gnu-gcc Linux x86-64 GCC
aarch64-linux-gnu-gcc Linux aarch64 (e.g. Raspberry Pi) GCC
arm-linux-gnueabihf-gcc Linux 32-bit ARM (e.g. Raspberry Pi 1/2) GCC
i686-pc-windows-msvc Windows x86 MSVC
x86_64-pc-windows-msvc Windows x64 MSVC
aarch64-apple-darwin-clang macOS arm64 (Apple Silicon) Clang
x86_64-w64-mingw32-tcc Windows x64 TCC
x86_64-linux-gnu-tcc Linux x86-64 TCC
aarch64-apple-darwin-tcc macOS arm64 (Apple Silicon) TCC
propeller-catalina Catalina Catalina C compiler

-auto-config
Generate cake.json with the include directories of the current system.

4.5 Analysis Options

-flow
Run Cake's built-in flow analysis pass, including null checks (same as #pragma flow enable).

-safety
Cake contracts are checked: _Owner, _View, _Dtor, _Out, _Opt... and pointers without _Opt are non-null. Also turns on -flow (same as #pragma safety enable). Without it the annotations are ignored, like empty macros.

-const-literal
Treat string literals as const char[] rather than char[].

-unused-extern-report Report mode (used by the IDE's Project > Report Unused): reports only what is defined and never used in the files given. No flow analysis, no output.

Warning Reported
57 static function not used in its file
94 macro not used in any of the files
95 external function not used in any of the files
96 enumerator not used in any of the files

94, 95 and 96 are reported at the end. Macros and enumerators are reported only when defined under the common directory of the files.

4.6 Style and Formatting Options

-style=<name>
Select the coding style checked by diagnostic 11 (style). Passing -style turns diagnostic 11 on as a note; -style=none turns it off. Valid names: none, cake, gnu, microsoft.

-format
Reformat the file's spacing, braces, else placement and indentation to match -style (defaults to cake) and print the result instead of compiling.

-format-lines=<first>:<last>
Restrict -format to the given line range.

The built-in styles:

cake gnu microsoft
Function-body { own line (Allman) own line (Allman) own line (Allman)
Control-flow { own line (Allman) own line (Allman) own line (Allman)
else new line new line new line
Pointer * next to the type: int* p next to the name: int *p next to the name: int *p
Indentation 4 spaces tabs 4 spaces
struct / enum names snake_case camelCase PascalCase
Function names snake_case camelCase PascalCase
Globals, locals, parameters, members snake_case snake_case PascalCase
Enumerators UPPERCASE UPPERCASE UPPERCASE
Spacing rules (below) on off off

In every style } must be on its own line. Indentation must be tabs only or spaces only, and with spaces a multiple of the indent width.

Spacing rules, checked when the style enables them:

Rule Example
one space after , f(a, b)
no space before ; return x;
one space between a keyword and ( if (x)
no space between a callee and ( f(x)
one space on each side of a binary operator a + b
one declarator per declaration int i; int j; rather than int i, j;

4.7 Find Definition

-find-definition <line> <col> <file> [<other files>...] Prints where the identifier at <line> <col> of <file> is defined (used by the IDE's F12). It is resolved with the scopes at that point, and the parser stops as soon as it is found. The result is note 93:

c:/project/util.h:1:5: note: declaration of 'f'
c:/project/util.c:4:5: note: definition of 'f'

For a prototype or extern, the definition is then searched in the other files. Macros are resolved by the preprocessor. <file> can be a header; if it does not compile alone, it is resolved in the first file that includes it.

4.8 Using cake inside Visual Studio

Use cake as Custom Build Tool for a specific file.c

Then you can run/debug normally.

It also can be used as external tool for static analysis:

4.9 Using cake inside Visual Studio Code

TODO


5. Output

5.1 C89 Backend

The current Cake backend generates C89-compatible source code. This output can be passed directly to any C89 compiler (MSVC, GCC, Clang, etc.) to produce an executable.

The generated output is a simplified, lowered form of the input. All modern C constructs are removed and replaced with equivalent C89 idioms. Specifically, the output contains:

It currently requires bit-fields, but they are planned for removal.

Anyone writing a new backend (or a tool that consumes Cake output) only needs to handle this subset.

5.2 Output Files

For a single input file:

cake c:\project\file1.c
c:\project\
├── file1.c
└── x86_64-pc-windows-msvc\
    └── file1.c

The output directory is named after the compilation target (see -target, section 4.4): x86_64-pc-windows-msvc, x86_64-linux-gnu-gcc, aarch64-apple-darwin-clang, etc. The examples below use x86_64-pc-windows-msvc.

Using the -o option (single input file only):

cake c:\project\file1.c -o file89.c
c:\project\
├── file1.c
└── file89.c

For multiple input files:

cake c:\project\file1.c c:\project\other\file2.c

Cake determines the common root path, in this case c:\project\, then recreates the directory structure under <target> within this common path.

c:\project\
├── file1.c
├── other\
│   └── file2.c
└── x86_64-pc-windows-msvc\
    ├── file1.c
    └── other\
        └── file2.c

5.5 Generated Names

All generated identifiers start with a double underscore. A backend or a consumer of the output should never rely on their exact numbering, only on the prefixes.

Prefix Meaning
__c<name> hoisted local static object or local function (__ccalls, __cadd)
__cfn hoisted function literal
__cake_func_<function> __func__ string of <function>
__L<n> label generated for switch, throw, catch
__a temporary for a switch controlling expression, a compound literal or a statement expression
__b temporary holding a return value while _Defer blocks run
__vm<n> captured size of a variably-modified type
__tag<n> tag given to an anonymous struct/union
__m<n> name given to an anonymous member
__cmemset, __cmemcpy runtime helpers (5.6)

Cake also emits comments (/*switch*/, /*case 1*/, /* break */, /*try*/, /*catch*/, /* throw */, /* default */) to make the lowered control flow readable. They carry no meaning.

5.6 Runtime Helpers

The output is self-contained: it never includes a header. When a construct needs library support, Cake emits a static helper with a prototype at the top of the file and a definition at the end.

Helper Emitted when
static void * __cmemset(void *dest, int ch, size_t count) an aggregate is zero-initialized ({}, {0}, partial initializers)
static void * __cmemcpy(void *dest, const void *src, size_t n) a char array is initialized from a string literal
static void __assert_fail(...) / static void __assert_rtn(...) assert() from the system header is used and the target's libc declares that symbol

size_t is spelled with the target's underlying type (unsigned long on LP64, unsigned long long on Windows x64, unsigned int on 32-bit targets).

Functions from the standard library that the program calls (printf, malloc, free, strlen, ...) are not helpers: Cake emits their prototypes, with const removed, and the C89 compiler links them from libc.

5.7 Compiling the Output

The generated file is a complete translation unit. It can be compiled with the target compiler with no extra flags:

gcc -w x86_64-linux-gnu-gcc/file1.c -o file1

Warnings are disabled here only because prototypes for libc functions are emitted without const (int puts(char *)), which some compilers flag when they also see the system header.


6. Language Standard Support

Standard reference documents:


7. C99 Features

7.1 restrict Pointers

restrict is parsed and type-checked but stripped from the generated C89 output.

void f(const char* restrict s);
int main() { f(""); }

7.2 Variably-Modified (VM) Types and Variable-Length Arrays

VM type pointer:

#include <stdlib.h>
#include <stdio.h>

int main() {
    int n = 2, m = 3;
    int (*p)[n][m] = malloc(sizeof *p);
    printf("%zu\n", sizeof(*p));
    free(p);
}

VLA as 2D function parameter:

#include <stdio.h>

void print_matrix(int rows, int cols, int m[rows][cols]) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++)
            printf("%d ", m[i][j]);
        printf("\n");
    }
}

int main(void) {
    int r = 2, c = 3;
    int m[r][c];
    for (int i = 0; i < r; i++)
        for (int j = 0; j < c; j++)
            m[i][j] = i * c + j;
    print_matrix(r, c, m);
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n683.htm

7.3 Flexible Array Members

#include <stdio.h>
#include <stdlib.h>

struct X {
    int count;
    double values[];   /* flexible array member */
};

int main() {
    int n = 3;
    struct X* p = malloc(sizeof(struct X) + n * sizeof(double));
    p->count = n;
    p->values[0] = 10.0;
    p->values[1] = 20.0;
    p->values[2] = 30.0;
    for (int i = 0; i < p->count; ++i)
        printf("%f\n", p->values[i]);
    free(p);
}

The size of a structure with a flexible array member is computed as if the member were omitted, except that additional trailing padding may be present.

7.4 static and Type Qualifiers in Array Declarators

The static keyword in array declarators is parsed and checked. Passing NULL, nullptr, or an array smaller than the declared minimum is a diagnostic.

#include <stdlib.h>

void F(int a[static 5]) {}

int main() {
    F(0);              /* error: null pointer */
    F(NULL);           /* error: null pointer */
    int a[] = {1, 2, 3};
    F(a);              /* error: fewer than 5 elements */
    int b[] = {1, 2, 3, 4, 5};
    F(b);              /* ok */
}

7.5 Hexadecimal Floating Constants

double d = 0x1p+1;

Cake has no way to emit a hexadecimal floating constant in C89 output, so it converts the literal to a decimal one:

double a = 0x1p+1;                    /* becomes  a = 2.0;    */
double b = 0x1.5555555555555p-2;      /* becomes  b = 0.33333333333333331; */
double c = 0x1p-1074;                 /* becomes  c = 4.9406564584124654e-324; */

For float and double the conversion is lossless: the decimal Cake writes is guaranteed to read back as the exact same value, bit for bit. The same holds for ordinary decimal constants, which are re-emitted in normalized form - and because the form chosen is the shortest one that round-trips, the output stays readable:

double g = 0.1;      /* stays   g = 0.1;    not 0.10000000000000001 */
float  f = 0.1f;     /* stays   f = 0.1f;   not 0.100000001490116119384765625 */
double h = 1.0/3.0;  /* becomes h = 0.3333333333333333; */

The printer is an implementation of Grisu2:

Florian Loitsch, Printing Floating-Point Numbers Quickly and Accurately with Integers, PLDI 2010. https://doi.org/10.1145/1806596.1806623 - paper (PDF)

Useful cross-references when reading cake_dtoa_shortest in src/object.c:

Grisu2 guarantees the round-trip but not minimality: for roughly one value in a thousand it emits one digit more than strictly necessary. It never emits a wrong one. Guaranteed-shortest output needs Grisu3 or Ryu, which both require a bignum fallback path.

The one case where precision can be lost is long double on targets where it is wider than 64 bits (the 80-bit x87 format gcc and clang use on x86). There Cake falls back to the host's snprintf, and the value has in any case already passed through the host's own long double. Do not rely on the exact value of a long double constant beyond double precision.

Infinity and NaN

Infinity and NaN have no literal form in C. When a constant expression folds to one of them, Cake emits the same construct the standard headers use - an overflowing product, cast to the wanted type:

double d = INFINITY;    /* becomes  d = ((float)(1e+300 * 1e+300)); */
double n = NAN;         /* becomes  n = ((float)((1e+300 * 1e+300) * 0.0)); */

7.6 Compound Literals

struct s { int i; };

int f(void) {
    struct s *p = 0, *q;
    int j = 0;
    again:
        q = p, p = &((struct s){ j++ });
    if (j < 2) goto again;
    return p == q && q->i == 1;
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n716.htm

7.7 Designated Initializers

int main() {
    int a[6] = {[4] = 29, [2] = 15};
    struct point { int x, y; };
    struct point p = { .y = 2, .x = 3 };
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n494.pdf

7.8 Declarations in for Loop Initializers

int main() {
    const int max = 10;
    for (int n = max - 1; n >= 0; n--) {
        /* ... */
    }
}

7.9 inline Functions

inline int sum(int a, int b) { return a + b; }

int main(void) {
    int r = sum(1, 2);
}

Cake does not perform function inlining itself; what it does with inline depends on whether the translation unit defines the function:

This rule is the same for every target; on MSVC an inline function with a body in the unit (the snprintf family in its stdio.h, for instance) is a definition and takes the first form.

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n709.htm

7.10 _Pragma Preprocessing Operator

#define LISTING(x) PRAGMA(listing on #x)
#define PRAGMA(x) _Pragma(#x)
LISTING(..listing.dir)

7.11 __func__ Predefined Identifier

#include <stdio.h>
int main() {
    printf("%s\n", __func__);
}

7.12 Variadic Macros

#include <stdio.h>
#define debug(...) fprintf(stderr, __VA_ARGS__)

int main() {
    debug("X = %d\n", 1);
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n707.htm

7.13 _Bool

int main(void) {
    _Bool b = 1;
}

7.14 Line Comments

C99 // line comments are implemented.

7.15 Complex and Imaginary Types

Not implemented.

7.16 Universal Character Names (\u, \U)

Not implemented.


8. C11 Features

8.1 _Static_assert / static_assert

int main() {
    _Static_assert(1 == 1, "error");
}

_Static_assert is aliased to static_assert in C23.

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1330.pdf

8.2 Anonymous Structures and Unions

struct v {
    union {
        struct { int i, j; };       /* anonymous struct */
        struct { long k, l; } w;
    };
    int m;
} v1;

int main() {
    v1.i = 2;    /* valid */
    v1.w.k = 5;  /* valid */
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1406.pdf

8.3 _Noreturn

_Noreturn void f(void) {
    abort();
}

_Noreturn became [[noreturn]] in C23.

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1478.htm

8.4 _Thread_local / _Atomic

_Thread_local is supported and maps to __declspec(thread) in MSVC output and __thread in GCC output.

_Atomic is supported, both as a qualifier (_Atomic int) and as a specifier (_Atomic(int)), and so is <stdatomic.h>.

_Atomic int counter;

int main(void)
{
    counter = 1;
    counter++;
    counter *= 3;
    int value = counter;
    return value;
}

Every operation on an _Atomic object becomes a call to a static helper, generated once per type:

The atomic_* functions of <stdatomic.h> call the same helpers. All operations are sequentially consistent.

The helpers use:

8.5 _Generic Type-Generic Expressions

#include <math.h>

#define cbrt(X) _Generic((X),    \
                  double: cbrtl, \
                  float: cbrtf,  \
                  default: cbrtl \
              )(X)

int main(void) { cbrt(1.0); }

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1441.htm

8.6 Unicode Character Constants (u'', U'')

int i  = U'ç';
int i2 = u'ç';

Note: Cake assumes source files are UTF-8 encoded.

8.7 UTF-8 String Literals (u8"...")

char* s1 = u8"maçã";
char* s2 = u8"maca";

Generated code (non-ASCII bytes become 3-digit octal escapes):

char* s1 = "ma\303\247\303\243";
char* s2 = "maca";

Note: Cake assumes source files are UTF-8 encoded.

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1488.htm

8.8 _Alignof / alignof

int main() {
    int align = alignof(int);
}

_Alignof became alignof in C23.

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1397.htm

8.9 _Alignas / alignas

Uses __declspec(align(n)) in MSVC output and __attribute__((aligned(n))) in GCC output.


9. C23 Features

9.1 Variably-Modified (VM) Types — Mandatory

C23 formally separates two concepts previously bundled in C99:

VLA objects (int a[n]) remain optional in C23. Implementations defining __STDC_NO_VLA__ do not support them.

Cake implements VLA, and it is allocated on the stack using alloca. The use of VLA is discouraged.

VM types (int (*p)[n]) are mandatory in all conforming C23 implementations. Cake supports VM type pointers and translates them to C89-compatible output.

/* VM type pointer - mandatory in C23 */
void foo(int n, double (*x)[n]) {
    (*x)[0] = 1.0;
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2778.pdf

9.2 static_assert — Single-Argument Form

int main(void) {
    static_assert(1 == 2);   /* no message argument required */
}

9.3 u8 Character Prefix

int main() {
    unsigned char c = u8'~';
}

Reference: https://open-std.org/JTC1/SC22/WG14/www/docs/n2418.pdf

9.4 No Function Declarations Without Prototypes

In C23, calling an undeclared function is a constraint violation:

int main() {
    func();   /* error in C23 */
}

Reference: https://www.open-std.org/JTC1/SC22/WG14/www/docs/n2841.htm

9.5 Unnamed Parameters in Function Definitions

int f(int);

int f(int) {   /* unnamed parameter is valid */
    return 0;
}

Note: TODO add placeholder name in the C89 output.

Reference: https://open-std.org/JTC1/SC22/WG14/www/docs/n2480.pdf

9.6 Digit Separators

int main() {
    int a = 1000'00;
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2626.pdf

9.7 Binary Literals

#define X  0b1010

int main() {
    int a = X;
    int b = 0B1010;
}

9.8 nullptr Constant

int main() {
    void* p          = nullptr;
    auto  p2         = nullptr;
    typeof(nullptr) p3 = nullptr;
}

Reference: https://open-std.org/JTC1/SC22/WG14/www/docs/n3042.htm

9.9 true and false as First-Class Keywords

int main() {
    bool b = true;
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2935.pdf

9.10 Empty Initializer {}

int main() {
    struct X { int i; } x = {};
    x = (struct X){};
}

Note Empty initializer can be used initialize VLAs

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2900.htm

9.11 auto Type Deduction

static auto a = 3.5;
auto p = &a;

double A[3] = { 0 };
auto pA = A;
auto qA = &A;

Reference: https://open-std.org/JTC1/SC22/WG14/www/docs/n3007.htm

9.12 typeof / typeof_unqual

#define SWAP(a, b) \
    do { typeof(a) temp = a; a = b; b = temp; } while (0)

int main() {
    int a = 1;
    typeof(a) b = 1;
    typeof(int*) p1, p2;
    SWAP(a, b);
}

Reference: https://open-std.org/JTC1/SC22/WG14/www/docs/n2927.htm

9.13 constexpr

#include <stdio.h>

constexpr int c     = 123;
constexpr int c2    = c + 1000;
constexpr double PI = 3.14;

int a[c];
static_assert(PI + 1 == 3.14 + 1.0);

int main() {
    constexpr char ch = 'a';
    printf("%f %c", PI, ch);
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3018.htm

9.14 Improved and enhancements to Enumerations (Typed Enums)

enum X : short { A };

int main() {
    enum X x = A;
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3030.htm

Improved enumerations Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3029.htm

//https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3029.htm

enum a {
    a0 = 0xFFFFFFFFFFFFFFFFULL
};

int main () {}

9.15 Attributes ([[...]])

Cake supports the C23 double-bracket attribute syntax. Recognized standard attributes:

[[fallthrough]] (Supported)
Warns (-w88) about a case/default label reached by falling through a statement that doesn't end with break/return/continue/goto. Write [[fallthrough]]; as the last statement before the label to mark it intentional; grouped empty labels (case 1: case 2:) never warn. The attribute itself must be immediately followed by a case/default label, or it is flagged as misplaced.

void g(void);
void h(void);

void f(int n) {
    switch (n) {
    case 1:
    case 2:          /* grouped labels, no warning */
        g();
        [[fallthrough]];
    case 3:
        h();
        break;
    case 4:
        g();          /* warning: unannotated fall-through */
    case 5:
        break;
    }
}

int main(void) {}

[[deprecated]] (Supported)
Emits a warning when the annotated entity is used. Compile with -w03.

[[deprecated]] void f2(void) {}
struct [[deprecated]] S { int a; };

int main(void) {
    struct S s;
    f2();         /* warning: deprecated */
}

[[maybe_unused]] (Supported)
Suppresses unused-variable or unused-parameter diagnostics.

void f([[maybe_unused]] int arg1, int arg2) {}

[[nodiscard]] (Supported — optional message argument not yet implemented)
Emits a warning when the return value of the annotated function is discarded.

struct [[nodiscard]] error_info { int error; };
struct error_info enable_safety(void);

void test(void) {
    enable_safety();   /* warning: return value discarded */
}

[[noreturn]] (Supported)
Replaces C11 _Noreturn.

[[unsequenced]] / [[reproducible]] (TODO)

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2335.pdf

9.16 __has_attribute / __has_include

#if __has_c_attribute(fallthrough)
#  warning Attribute supported
#endif

#if __has_include(<stdio.h>)
#  warning Header found
#endif

9.17 #warning Directive

int main() {
#warning my warning message
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2686.pdf

9.18 #embed Directive

#embed expands to the bytes of a resource as a comma-separated list of integer constants.

#include <stdio.h>

int main() {
    static const char file_txt[] = {
#embed "stdio.h" limit(64)
        , 0
    };
    printf("%s\n", file_txt);
}

The resource is searched like an #include: the "file" form looks in the directory of the current file first and then in the include directories; the <file> form looks only in the include directories.

The standard parameters are supported (also with the __limit__ spelling):

Parameter Effect
limit(N) at most N bytes; N is a constant expression evaluated as in #if (defined is not allowed, negative values are an error)
prefix(tokens) emitted before the bytes, only if the resource is not empty
suffix(tokens) emitted after the bytes, only if the resource is not empty
if_empty(tokens) emitted instead of the bytes when the resource is empty (or limit(0))

Each standard parameter can appear at most once; an unknown parameter is an error. vendor::parameter names are accepted and ignored.

static const char message[] = {
#embed "message.txt" suffix(, '\0') if_empty('n', 'o', 'n', 'e', '\0')
};

__has_embed(resource parameters) can be used in #if and evaluates to __STDC_EMBED_NOT_FOUND__ (0), __STDC_EMBED_FOUND__ (1) or __STDC_EMBED_EMPTY__ (2). It also evaluates to 0 when a parameter is not supported.

#if __has_embed("logo.png") == __STDC_EMBED_FOUND__
static const unsigned char logo[] = {
#embed "logo.png"
};
#endif

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3017.htm

9.19 #elifdef / #elifndef

#define Y

#ifdef X
#  define VERSION 1
#elifdef Y
#  define VERSION 2
#else
#  define VERSION 3
#endif

9.20 __VA_OPT__

#define F(...)    f(0 __VA_OPT__(,) __VA_ARGS__)
#define G(X, ...) f(0, X __VA_OPT__(,) __VA_ARGS__)

void f(int i, ...) {}

int main() {
    int a = 1, b = 2, c = 3;
    F(a, b, c);
    F();
    G(a, b, c);
    G(a);
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3033.htm

9.21 _BitInt(N) — Bit-Precise Integers

Supported for widths up to 64 bits (BITINT_MAXWIDTH is 64). Wider types are reported as an error.

typedef unsigned _BitInt(24) u24;

struct S {
    _BitInt(12) m;
    unsigned _BitInt(3) n : 2;
};

int main() {
    _BitInt(12) a = 100;
    unsigned _BitInt(12) b = 4095uwb;
    signed _BitInt(40) c = -1;
    u24 d = 0xFFFFFF;

    static_assert(sizeof(_BitInt(12)) == 2);
    static_assert(_Generic(a + a, _BitInt(12): 1, default: 2) == 1);

    constexpr unsigned _BitInt(12) w = (unsigned _BitInt(12)) 4096;
    static_assert(w == 0);
}

The front end follows C23:

In the output _BitInt(N) is lowered to the smallest standard integer type that holds N bits, which is also what sizeof and alignof report:

short a;
unsigned short b;
long c;
unsigned int d;

Since the lowered type is wider than N bits, the generated code wraps values to N bits itself, so runtime results match the C23 semantics: arithmetic that produces a _BitInt(N) value (+ - * << ~ and unary -) and every conversion into a _BitInt(N) (assignment, initialization, cast, argument passing, return, compound assignment, ++/--) is masked - and sign extended for the signed types:

unsigned _BitInt(12) a = x;   /* a = ((unsigned short)((unsigned long long)(x) & 0xfffULL)); */
_BitInt(12) b = x;            /* b = ((short)(((long long)((unsigned long long)(x) & 0xfffULL) ^ 0x800LL) - 0x800LL)); */

Widths that match the lowered type exactly (_BitInt(32), _BitInt(64)) need no mask, and a value that already has the target _BitInt type is not masked again.

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2763.pdf

9.22 Decimal Floating Types

Not implemented. (_Decimal32, _Decimal64, _Decimal128)

9.23 Improved Tag Compatibility

Two struct, union or enum definitions with the same tag and the same content are the same type, wherever they appear in the translation unit. For a struct or union this means the same members, in the same order, with the same names, types, bit-field widths and alignment; for an enum it means the same underlying type (if one is specified) and the same enumerators, in the same order, with the same names and values. Before C23 a second definition in the same scope was an error, and a definition in an inner scope was always a distinct type.

This makes it possible to define a type at each place it is used, for example from a macro, instead of requiring one typedef per instance:

#define VEC(T) struct vec_##T { T* data; int size; int capacity; }

/* struct vec_int is defined here, inside the parameter list (prototype scope) */
void push_int(VEC(int)* v, int x)
{
    v->data[v->size++] = x;
}

int main()
{
    int storage[4];
    VEC(int) v = { storage, 0, 4 };   /* same type as push_int's parameter */
    push_int(&v, 1);
}

This also applies to a struct defined inside a parameter list: the tag is not visible after the prototype, but a later definition with the same content is the same type (gcc 15 accepts this in C23 mode as well).

A redefinition in the same scope with different content is an error (2070), and definitions with different content in different scopes are distinct, incompatible types (950 for structs and unions, 40 for enums).

enum E : short { A = 1, B = 2 };
void f(enum E);

void g(void)
{
    enum E : short { A = 1, B = 2 };
    f(A);                             /* same type */
}

void h(void)
{
    enum E : short { A = 1, B = 3 };  /* different value: a new enum E */
    f(A);                             /* warning 40: incompatible types */
}

The generated code contains a single definition of the tag: an inner-scope or repeated definition that has the same content reuses the one already emitted instead of being renamed.

struct vec_int {
    int * data;
    int size;
    int capacity;
};

void push_int(struct vec_int * v, int x)
{
    v->data[v->size++] = x;
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3037.pdf

9.24 Compound Literals with Storage Specifier

void F(int* p) {}

int main() {
    F((static int[]){1, 2, 3, 0});
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3038.htm

9.25 va_start with One Argument

In C23 the second argument of va_start is optional, and a variadic function may have no named parameter: void f(...).

#include <stdarg.h>

int sum(int count, ...) {
    int result = 0;
    va_list args;
    va_start(args);  /* C23 */
    for (int i = 0; i < count; i++)
        result += va_arg(args, int);
    va_end(args);
    return result;
}

Before C23, the second argument is the last named parameter, so the conversion is always possible when the function has one. Cake generates:

__builtin_va_start(args, count);

Note: void f(...), with no named parameter, cannot be converted; the output keeps __builtin_c23_va_start(args), which needs a C23 compiler. The same happens when the last parameter is unnamed. Cake reports warning 126 in both cases.

Note: msvc targets are not supported yet: the va_start of the msvc headers requires two arguments, and the generated __builtin_va_start does not exist in msvc.

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2975.pdf


10. C2Y Features

These features are from the current C2Y working draft. Support status is noted for each.

10.1 Octal Literals with 0o / 0O Prefix

static_assert(0o52 == 052);
static_assert(0O52 == 42);

int main() {
    int i = 0o52;
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3353.htm

10.2 Case Range Expressions

#include <stdio.h>

void f(int n) {
    switch (n) {
        case 1 ... 10:
            printf("in range 1..10\n");
            break;
        default:
            break;
    }
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3370.htm

10.3 #def / #enddef Multi-Line Macro Blocks

This feature is implemented in Cake as an experimental extension while its inclusion in C2Y is evaluated.

/* Use -E to observe expansion */
#def foo(x)
    do {
        bar(x);
        baz(x);
    } while (0)
#enddef

foo(1)
foo(2)

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3524.txt

10.4 _Countof Operator

Returns the number of elements in the outermost dimension of an array type.

int main() {
    int a[7][3];
    static_assert(_Countof(a) == 7);
    static_assert(_Countof(int[2][3]) == 2);
}

Cake extension: _Countof is additionally defined for enum types, returning the number of enumerators. This is not part of C2Y.

enum E { A, B, C, D, E, F };
static_assert(_Countof(enum E) == 6);

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3369.pdf

10.5 _Defer Statement

_Defer executes a statement or block when the enclosing scope exits, in reverse order of appearance. It is designed for deterministic resource cleanup.

Key rules:

#include <stdlib.h>

int main() {
    void* buf = malloc(20);
    _Defer { free(buf); }
    return use_buffer(20, buf);
    /* buf is freed AFTER use_buffer returns */
}

Reverse execution order:

int main() {
    int r = 0;
    {
        _Defer {
            _Defer r *= 4;
            r *= 2;
            _Defer { r += 3; }
        }
        _Defer r += 1;
    }
    return r;   /* returns 20 */
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3734.pdf

10.6 if and switch Declarations

The controlling clause of if and switch (the selection-header) accepts three forms:

selection-header:
    expression
    declaration expression
    declaration-condition

declaration-condition:
    attribute-specifier-sequence(opt) declaration-specifiers declarator = initializer

The third form if (T D = X) is treated exactly as if (T D = X; D): the declared object is the controlling expression. The declared identifier is in scope in the secondary block and in the else branch, and goes out of scope after the statement.

#include <stdio.h>

int main()
{
   
   FILE* f0;
   if ( f0 = fopen("file.txt", "r"))
   {
     /*...*/
     fclose(f0);
   }
   
   if (FILE* f = fopen("file.txt", "r"); f)
   {
     /*...*/
     fclose(f);
   }

   if (FILE* f = fopen("file.txt", "r"))
   {    
     /*...*/
     fclose(f);
   }

   switch (int c = getchar())
   {
     case EOF: break;
     default: putchar(c); break;
   }
}

Constraints (diagnosed as errors):

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3580.htm

10.7 typename in _Generic

Allows type names as the controlling expression in _Generic.

int main() {
    const int* const p;
    static_assert(_Generic(p, const int*: 1));

    static_assert(_Generic(typeof(p), const int* const: 1));
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3260.pdf

10.8 __COUNTER__ Predefined Macro

/* Use -E to observe expansion */
#define X(Z) Z Z
X(__COUNTER__)   /* 0 0 */
X(__COUNTER__)   /* 1 1 */

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3457.htm

10.9 Local Functions

int main() {
    static int dup(int a) { return a * 2; }
    return dup(1);
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3678.pdf

10.10 Function Literals

#include <stdio.h>

int main() {
    printf("%d", (static int (void)){ return 1; }());
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3679.pdf

10.11 Statement Expressions

#include <stdio.h>

#define maxint(a, b) \
    ({ int _a = (a), _b = (b); _a > _b ? _a : _b; })

int main() {
    printf("%d", maxint(1, 2));
}

Reference: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3643.htm

10.12 Elvis Operator (?:)

The Elvis operator is a shorthand for the ternary operator where the middle operand is omitted. When the condition is truthy, it is returned; otherwise the right-hand operand is returned. The condition is evaluated exactly once.

a ?: b

is equivalent to a ? a : b, but a is evaluated only once.

#include <stdio.h>

int main() {
    int x = 0, y = 5;
    int r1 = x ?: y;   /* 5 - x is falsy */
    x = 3;
    int r2 = x ?: y;   /* 3 - x is truthy */
    printf("%d %d\n", r1, r2);
}

Pointer fallback — most common usage:

const char* display = get_name() ?: "unknown";

Side effects: when the condition has side effects, Cake introduces a temporary variable in the C89 output to guarantee single evaluation:

int r = i++ ?: b;

Generated C89 output:

int __v0;
__v0 = i++;
int r = __v0 ? __v0 : b;

Nesting associates right-to-left:

int a = 0, b = 0, c = 7;
int r = a ?: b ?: c;   /* 7 */

Reference: https://www.open-std.org/JTC1/SC22/WG14/www/docs/n3804.txt

In preprocessor expressions: the Elvis operator is also accepted in #if/#elif constant expressions:

#define VALUE 0

#if VALUE ?: 42
/* VALUE is falsy, so this branch is taken with value 42 */
#endif

10.13 static_assert as an Expression

In C2Y, static_assert is extended to work not only as a declaration but also as an expression operator with type void. This makes it possible to embed compile-time assertions directly inside expressions, which is especially useful when writing function-like macros.

Previously, adding a static check inside a macro required awkward workarounds such as negative-size arrays, struct-wrapped assertions, or GNU compound statement expressions. With this change, static_assert can be used naturally in combination with the comma operator.

Example — bounds-checked bit-shift macro:

#include <limits.h>

#define BIT(n) ( \
    static_assert(n >= 0),      \
    static_assert(n < UINT_WIDTH), \
    1U << (n) \
)

int main() {
    unsigned x = BIT(3);   /* ok */
    unsigned y = BIT(99);  /* compile-time error: assertion failed */
}

Disambiguation rule: A block item consisting solely of static_assert(...) followed by a semicolon is always treated as a static_assert declaration, preserving backward compatibility. static_assert is only treated as an expression when it appears in an expression context (e.g., as an operand of the comma operator, or as the controlling expression of _Generic).

void func() {
    static_assert(1);       /* declaration - behaviour unchanged */
    static_assert(1), 0;    /* expression - new in C2Y */
}

Using _Generic to produce an integer constant expression:

Because the comma operator and void are not permitted in integer constant expressions, the result of the comma-based form cannot be used where an integer constant expression is required (e.g., as an array size). A _Generic workaround can be used in those cases:


#define CHAR_BIT 8

#define BIT(type, n) \
    _Generic(static_assert(n >= 0 && n < sizeof(type) * CHAR_BIT), \
             void: (type)1 << (n))

int arr[BIT(unsigned int, 2)];   /* ok - integer constant expression */

Reference: https://open-std.org/jtc1/sc22/wg14/www/docs/n3715.pdf

Cake extensions static_debug, assert_state, override_state also works in the same way.

#pragma safety enable
int * _Opt f();
void func() {
    
    int* _Opt p = f();
    p = p ? (static_debug(p), p) : 0;
}

11. Cake Language Extensions

11.1 Built-in _Assert

In Cake, assert is a built-in statement rather than a macro because flow analysis need it even in release builds. The effect of assert(expression) is equivalent of if (!(expression)) exit(1);.

void list_push_back(struct list* list, struct item* _Owner p_item)
{
    if (list->head == NULL) {
        list->head = p_item;
    } else {
        _Assert(list->tail != nullptr);       /* narrows: tail is non-null below */
        _Assert(list->tail->next == nullptr); /* narrows: tail->next is null below */
        list->tail->next = p_item;
    }
    list->tail = p_item;
}

11.2 try / throw / catch

Cake provides a structured local-jump mechanism for error handling. try/catch is explicitly a local jump - it cannot propagate across function boundaries. This is by design.

extern int error;

int main() {
    try {
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 10; j++) {
                if (error) throw;
            }
        }
    }
    catch {
        /* handle error */
    }
}

Note The catch block is optional. throw transfers control to the end of the nearest enclosing try block.

Cake source code uses macros to emulate this feature.

/* emulation of cake try catch using macros */
#define try  
#define catch if (0) catch_label:
#define throw do { throw_break_point(); goto catch_label;}while (0)

11.3 Checked Expressions (! operator)

checked-expression:
    assignment-expression
    assignment-expression !

The ! postfix operator evaluates its operand. If the result compares equal to zero (or is a null pointer), control transfers to the nearest enclosing catch block. Otherwise the value is returned unchanged. Applicable to any scalar expression.

int f(void);
int* get_ptr(void);

int main() {
    try {
        int  i = f()!;
        int* p = get_ptr()!;
    }
    catch {
        /* one or more expressions evaluated to zero/null */
    }
}

Combined with ownership:

#pragma safety enable

void* _Owner _Opt malloc(unsigned long size);
void free(void* _Owner _Opt ptr);

int main() {
    try {
        void* _Owner p = malloc(1)!;
        free(p);
    }
    catch {
        /* malloc returned null */
    }
}

Note this is a very experimental feature

11.4 #pragma dir

Adds a path to the list of directories searched for include files. This pragma declares an include directory from inside a source file. System include directories come from cake.json instead (see 2.2), but #pragma dir remains available for project-specific paths.

#pragma dir "C:/Program Files (x86)/Windows Kits/10/include/10.0.22000.0/cppwinrt"
#pragma dir "./openssl/include"

11.5 #pragma pack

By default the compiler inserts padding bytes between struct members so that each member starts at an address that is a multiple of its natural alignment (4 for int, 8 for double, and so on), and pads the end of the struct so that arrays of it keep every element aligned. #pragma pack(n) limits that alignment to at most n bytes for every member of the structs declared while it is in effect. With pack(1) no padding is inserted at all, so the struct occupies exactly the sum of its members' sizes.

It is used to match a layout that is fixed by something other than the compiler: a file format, a network protocol, a hardware register block, or a struct shared with code compiled under different settings. The macOS SDK, for instance, wraps the Mach message structures in #pragma pack(push, 4). Accessing a misaligned member costs extra instructions on some CPUs and faults on others, so packing is for data interchange, not a general-purpose space saver.

The pragma is not part of the C standard, but GCC, Clang and MSVC all implement it with the same syntax, and Cake follows them: the pack value is honored when computing sizeof, _Alignof and offsetof of structs and unions, and a struct declared while a pack value is in effect keeps that value for its whole lifetime.

#pragma pack(push, n)  // save the current value, then set it to n
#pragma pack(push)     // save the current value
#pragma pack(pop)      // restore the last saved value
#pragma pack(n)        // set it to n
#pragma pack()         // reset to the default (no packing)
#pragma pack(push, 1)
struct packed { char c; int i; };
#pragma pack(pop)

struct normal { char c; int i; };

static_assert(sizeof(struct packed) == 5);
static_assert(offsetof(struct packed, i) == 1);
static_assert(_Alignof(struct packed) == 1);
static_assert(sizeof(struct normal) == 8);

In the generated code every struct declared under a pack value is wrapped in its own #pragma pack(push, n) / #pragma pack(pop), so the host compiler lays it out exactly as Cake did. The wrapper travels with the struct rather than with the original directive, because Cake emits only the structs a file uses, in dependency order rather than source order. Given

#pragma pack(push, 1)
typedef struct { char c; int i; } packed_t;
struct named { char c; int i; };
#pragma pack(pop)
struct outer { char c; struct named n; packed_t p; };

int main(void) { struct outer o = {0}; return o.c; }

Cake generates

#pragma pack(push, 1)
struct named {
    char c;
    int i;
};
#pragma pack(pop)

#pragma pack(push, 1)
struct __tag2 {
    char c;
    int i;
};
#pragma pack(pop)

struct outer {
    char c;
    struct named n;
    struct __tag2 p;
};

int main(void)
{
    struct outer o;
    __cmemset(&o, 0, 11);
    return o.c;
}

Reading the output:

Bit-fields follow the packed layout of the target compiler too. For GCC and Clang a bit-field goes at the next free bit and, while a pack value is in effect, may straddle a storage unit of its type; a zero-width bit-field still aligns to the natural alignment of its type. MSVC keeps its storage-unit model with the unit's alignment capped at n.

11.6 __attribute__((packed)) and __attribute__((aligned(n)))

The GCC/Clang attributes that change struct layout are honored the same way #pragma pack is: they take part in sizeof, _Alignof and offsetof, and they are carried into the generated code so the host compiler lays the struct out identically.

struct __attribute__((packed)) header { char tag; int length; };
struct record { char c; int i __attribute__((packed)); short s; };
struct __attribute__((aligned(16))) block { char c; };

static_assert(sizeof(struct header) == 5);
static_assert(_Alignof(struct header) == 1);
static_assert(offsetof(struct record, s) == 6);
static_assert(sizeof(struct record) == 8);
static_assert(sizeof(struct block) == 16);

In the output a packed struct is wrapped in #pragma pack(push, 1) / #pragma pack(pop), which every host compiler understands, including MSVC; aligned(n) on a struct is written as __attribute__((aligned(n))), or __declspec(align(n)) for the MSVC targets; packed on a member is written back as __attribute__((packed)). MSVC has no per-member equivalent, so for the MSVC targets Cake omits it and warns (warning 9) that the generated struct will not have the packed layout - use #pragma pack or pack the whole struct instead. The example above generates

#pragma pack(push, 1)
struct header {
    char tag;
    int length;
};
#pragma pack(pop)

struct record {
    char c;
    int i __attribute__((packed));
    short s;
};

struct __attribute__((aligned(16))) block {
    char c;
};

11.7 offsetof Operator

In Cake, offsetof is a built-in operator rather than a macro, following N3958 "Making offsetof a core language feature" (and GCC's __builtin_offsetof). This allows its use in constant expressions and avoids the undefined behavior associated with traditional macro implementations.

offsetof ( type-name , member-designator )

member-designator:
    identifier designator-list_opt

designator:
    [ expression ]
    . identifier

offsetof vs __builtin_offsetof

Cake recognizes two spellings of the operator, and they are the same thing:

Spelling Why it exists
offsetof The N3958 keyword. Works with no header included.
__builtin_offsetof GCC/Clang compatibility.

Both map to one token in the parser, so they share the same grammar, evaluation and diagnostics; everything described above applies to either.

This matters because of how system headers define the macro:

/* GCC / Clang <stddef.h> */
#define offsetof(TYPE, MEMBER) __builtin_offsetof (TYPE, MEMBER)

/* MSVC <stddef.h> */
#define offsetof(s, m) ((size_t)&(((s*)0)->m))

With <stddef.h> included, offsetof(T, m) is first expanded by the preprocessor and then parsed by Cake:

Without <stddef.h>, offsetof is simply the keyword. In every case the generated C89 code contains the computed byte offset (or, for a non-constant index, plain arithmetic), never a call to __builtin_offsetof, so the output does not depend on the target compiler providing a builtin.

Because offsetof is a keyword in every -std mode (C89 through C2Y), a program that declares its own identifier named offsetof is rejected. This is stricter than N3958, which reserves the name only in the new standard, but the name has been reserved by <stddef.h> since C89 so conforming code is never affected.

struct foo { int N; struct { int a; int b[4]; } in; char buf[]; };

static_assert(offsetof(struct foo, in.b[2]) == 16);

size_t buf_offsetof(int i)
{
    return offsetof(struct foo, buf[i]); /* not a constant expression */
}
#include <stdio.h>

struct S {
    char   m0;
    double m1;
    short  m2;
    char   m3;
};

int main() {
    printf("m0 offset = %zu\n", offsetof(struct S, m0));
    printf("m1 offset = %zu\n", offsetof(struct S, m1));
    printf("m2 offset = %zu\n", offsetof(struct S, m2));
    printf("m3 offset = %zu\n", offsetof(struct S, m3));
}

Cake also supports the traditional macro-based offsetof (as found in MSVC's <stddef.h> or in user code) by making an exception in the constant-expression rules: &(((T*)0)->member) is folded to the member offset at compile time instead of being rejected as a non-constant address.

#define offsetof(type, member) ((size_t)&(((type *)0)->member))

11.8 Type Traits

Cake provides compile-time type introspection functions that return boolean integer constant expressions:

_is_pointer(T) — true for pointers to objects or functions

_is_array(T) — true for array types

_is_function(T) — true for function types

int main()
{
  static_assert(_is_function(main));
  int a[10];
  static_assert(_is_array(a));
  int (*b)[2];
  static_assert(_is_pointer(b));
}

11.9 Object Lifetime Checks (Ownership)

Cake includes a static analysis system for tracking object ownership and lifetime, activated with #pragma safety enable or -safety.

_Owner — This pointer or variable is responsible for freeing the object.

_Opt — This pointer may be null (optional).

_View — This pointer borrows the object without owning it.

To annotate the standard library, Cake ships its own annotated headers ahead of the system ones on the include search path and chains to the real header with #include_next (2.3).

Full documentation: ownership.html

11.10 Pre-defined Macros

Cake defines the following macros:

#define __CAKE__          202311L
#define __STDC_VERSION__  202311L

12. GCC Compatibility

Cake recognizes the following GCC built-ins and extensions:

Additional GCC built-ins are declared in src/include/builtins/gcc_builtins.h.

Pre-defined GCC compatibility macros: https://gcc.gnu.org/onlinedocs/cpp/Predefined-Macros.html


13. MSVC Compatibility

Cake recognizes the following MSVC extensions:

Pre-defined MSVC compatibility macros: https://learn.microsoft.com/en-us/cpp/preprocessor/predefined-macros

See also: src/include/x86_msvc_macros.h, src/include/x64_msvc_macros.h


End of Cake C Compiler Reference Manual