Skip to content

The C3 Blog

C3 0.8.6 - Let's grab those libraries

C3 0.8.6 is now available. The two main features are the revamped library fetch functionality and a target for packaging full-source libraries. It also sees improvements to compilation speed and reduces the binary size of safe-mode binaries by one third. It also contains fixes from a systematic review of casts, vectors and calling conventions.

Libraries: packaging and fetching

C3 libraries are distributed as .c3l files. These are zip archives with C3 sources, headers, prebuilt binaries per target and a manifest.json. Until now, we used a stop-gap solution: downloads from the c3lang/vendor repository. While this worked, it had no support for versioning and created unnecessary friction as well as a single supported source. Packaging libraries was also a manual affair with multiple steps.

The flagship features of 0.8.6 are two new additions to deal with that: a target for building a library from source, a simple repository protocol, versions and checksums.

Creating a source library

There's a new target type, source-lib. It packs C3 source files into a .c3l, which can be shipped.

{
  "version": "0.1.0",
  "authors": [ "John Doe <[email protected]>" ],
  "sources": [ "src/**" ],
  "targets": {
    "my_lib": {
      "type": "source-lib"
    }
  }
}

c3c build my_lib writes my_lib.c3l with a generated manifest. A few files can also be packed without a project:

c3c source-lib src/my_lib.c3 src/extra.c3 -o my_lib

To go with this, a manifest without targets now works for every platform, which is what a library made only from source needs, and manifests may contain version, authors and langrev.

Fetching from a repository

c3c fetch adds the ability to download libraries from custom repositories, addressed as <namespace>/<name>[@<version>]:

c3c fetch-list raylib            # search
c3c fetch-info c3lang/raylib6    # versions, checksums, sizes
c3c fetch c3lang/raylib6         # download the latest version
c3c fetch c3lang/[email protected]   # or a specific one

Inside a project, the library is installed in the first dependency search path and its name is added to dependencies. Libraries are now unpacked into a <name>.c3l directory by default. Use --decompress=no to keep the zip.

A repository is deliberately basic: it only requires three paths that answer GET requests (index.json, info/<namespace>/<name> and download/<namespace>/<name>@<version>). That means any static file host works. The default repository is served directly from a branch of the vendor repository on GitHub. A version may give a url that the library is downloaded from instead, so the files themselves can live on GitHub releases. To use another repository, pass --fetch-url, or set fetch-url in project.json.

Every download is verified against the sha256 checksum in the repository, and you can also lock a dependency to an exact checksum, so that a library can't change under you even if the repository does:

c3c fetch c3lang/[email protected]:d96a68af8dc785ebbc2d7b9f90b024bb5f67829e1d7e890bf1aa5e3f5d794e5b

This builds on – and at the same time entirely replaces – vendor-fetch, which only supported pulling the latest version of a library from the vendor repository.

Project dependencies

dependencies may now name repository libraries, optionally with a version, checksum and repository URL: [<url>:]<namespace>/<name>[@<version>][:<sha256>]. The build itself only uses the name – the rest is for project fetch:

"dependencies": [ "c3lang/[email protected]", "c3lang/sqlite3" ]

c3c project fetch without arguments lists what is missing, and with arguments fetches exactly those libraries:

$ c3c project fetch
Missing dependencies, fetch them with 'c3c project fetch <library>...':
  raylib6
  sqlite3
$ c3c project fetch raylib6 sqlite3

(Note: That project fetch doesn't automatically fetch dependencies is a deliberate decision.)

Smaller binaries in safe mode

In safe mode, C3 checks array bounds, null dereferences, shifts, casts, unwraps and so on, and each check that can fail needs code to report the failure with a message, a file, a function and a line. In 0.8.5 each of those sites built its own message, and this adds up: in a large program there are tens of thousands of them.

In 0.8.6, the code for checks was optimized, and panic functions were created on demand and reused. The effect on executable size was quite substantial: binary sizes were reduced by 30% or more.

Changes to PanicFn and --panicfn

To support this, the panic function signature has changed. It now gets a short, always static summary, plus an optional format string and its arguments with the details:

alias PanicFn = fn void(String summary, String file, String function, int line, String fmt, any[] args);

If you have your own panic handler, it needs to be updated. resources/examples/panic_override.c3 shows how (try it in the playground).

--panicfn also works properly now (#3443). It names a function rather than a function pointer variable, and that function becomes the initial value of builtin::panic, so panics from the standard library and abort reach it too, not only the ones generated by the compiler.

Finally, the --panic-msg and --show-backtrace combinations were made consistent: with --panic-msg=no you still get the short summary with file, function and line, unless you also pass --show-backtrace=no, in which case a failed check just traps.

While the change to the panic function type is, strictly speaking, a break, note that changing the panic function did not quite work properly before this.

Stricter rules

Casts

For this release, we did a fairly thorough review of the cast rules as implemented. This unearthed a bunch of corner-case crashes and miscompilations – but also some conversions that were simply too permissive:

ulong x = -1;          // Error: out of range for 'ulong', just like 'uint x = -1' was
int[3] a;
uint[3] b = a;         // Error: same size signed <-> unsigned in arrays, slices and vectors
                       // now needs an explicit cast, just like 'int' to 'uint'
float16 h = some_bf;           // Error: 'bfloat' and 'float16' no longer convert implicitly
void* p = (void*)some_int128;  // Error: narrow it first, '(void*)(uptr)some_int128'
int i = (int)1e30;             // Error: a constant float that doesn't fit is now a compile error

In the other direction, an explicit cast from a slice to a pointer is now allowed for any pointer type, the same way as a cast between pointers.

else if needs braces

C3 has always required { } around the statement of an else, but else if accidentally didn't (#3473). Now it does:

if (x == 0)
{
    foo();
}
else if (x == 1) foo(); // Error: An 'else if' must use '{ }' even around a single statement.

Duplicate designated initializers

Initializing the same member twice is now an error rather than silently using the last value (#3579):

Foo f = { .a = 1, .b = 2, .a = 3 }; // Error: This element is assigned twice in the same initializer.

This only applies when the initializer is literally used twice. Other uses, such as struct splatting or using .$eval(...), are not affected.

Other compiler changes

Errors in generic code show where they were instantiated

An error inside generics that only shows up for certain parameters used to be hard to track down, because it didn't say which instantiation caused it (#3588). Now there's a note pointing to it:

 11:     self.size += by;
                      ^^
rect.c3:11:15: error: 'float' cannot implicitly be converted to 'int[<2>]', but you may use a cast.

 18: alias IntRect = Rectangle{int};
                     ^^^^^^^^^
rect.c3:18:17: note: Instantiated here.

Compiler statistics with --stats

--stats=summary prints where the time and memory went during compilation, --stats=full adds lists of the largest modules and functions, and --stats=json (or --stats-file) saves everything as JSON for tooling. This replaces the more ad-hoc --print-large-functions and the previous stats.

━━ SUMMARY ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 Wall time                   758.1 ms
 Throughput                  133.3 k physical lines/s
 Frontend                    274.9 ms, 36.3 % (initialization, parsing and analysis)
 Codegen wall / thread time  96.8 ms / 351.4 ms
 Effective parallelism       3.63Γ— of 8 threads, 24 tasks
 Arena memory                100.3 MB, upper bound for the peak, LLVM excluded
 Largest phase               Linking, 46.2 % of wall time
 Largest module              std.io, 27.0 % of summed module time

Project files: depends-on and environment variables

Targets can now depend on other targets, which are built first, in the listed order:

"targets": {
  "my_lib": { "type": "static-lib", "depends-on": [ "generate" ] },
  "my_app": { "type": "executable", "depends-on": [ "my_lib" ] }
}

Strings in project.json can use environment variables written as ${NAME}, and env / env-override set variables for the exec scripts (#3580):

{
  "env": [ "MODE=release" ],
  "targets": {
    "my_target": {
      "type": "executable",
      "cc": "${CC}",
      "linker-search-paths": [ "${SDK_PATH}/lib" ]
    }
  }
}

Smaller things

  • @asmname sets the exact symbol name, without the platform's C name mangling that @cname gets (such as the leading underscore on macOS). This is useful for linker-generated symbols, e.g. extern char start @asmname("section$start$__DATA$__mysect");. This is the same as the asm attribute in GCC.
  • LLVM IR (.ll) and bitcode (.bc) files can be passed to the compiler, just like .o files (#3339).
  • -gline-tables-only emits only line table debug info.
  • Selectors for @dynamic methods are now a stable 128-bit value computed from the method name, instead of the address of a symbol that the linker had to merge.
  • An empty infinite loop like for (;;) {} was compiled as a panic, which in optimized builds could break the code around the loop (#3630). It is now a real infinite loop, which is what embedded code waiting for an interrupt wants. The old check is available in safe mode with --infinite-loop-detect=yes.
  • Paths with non-ASCII characters now work on Windows, both for files and for processes started by the compiler (#3605, #3606).
  • An error in one of the compiler's worker threads, such as a failing C file, could crash the compiler (#3584).

The C backend

The experimental C backend from 0.8.5 has seen a lot of work: it's now about 3Γ— faster, prunes unused types and declarations from the generated C, supports $$sysclock and @asmname, and fixes many codegen differences with the LLVM backend. There's some work done for compiling the output with the MSVC compiler.

So the backend is still experimental, but progress is being made.

Standard Library Updates

Compression and hashing

std::compression and std::hash got a big round of work:

  • crc32 is much sped up, which also affects things like zip.
  • The deflate decompressor was rewritten as a table-driven decoder, giving 2–3Γ— faster decompression.
  • deflate, gzip and zip take an optional compression level, from deflate::MIN_LEVEL (fastest) to deflate::MAX_LEVEL (smallest). The default produces the same output as before, and incompressible data is now stored instead of expanded.
  • Many zip fixes: double close on errors, ZIP64 archives with a comment or exactly 65535 entries, CRC32 verification on extract, and more.
char[] packed = deflate::compress(mem, data, level: 9)!;

Regex

std::encoding::regex is faster, and integer overflows in the regex compiler were fixed (CVE-2026-107891). As part of this, the faults and flags were renamed to descriptive names, e.g. NOMATCH is now NO_MATCH and ICASE is IGNORE_CASE. NOSUB was removed.

Other additions

  • file::chmod and File.chmod, using FilePermission (#3536).
  • file::open_fd opens a File from a file descriptor.
  • List.push, insert_at and push_front are faster, and remove_item and compact are now single pass instead of quadratic. remove_first and remove_last take an unordered parameter.
  • Matrices gained perspective_infinite, perspective_reverse_z and perspective_infinite_reverse_z, as well as column, row, set_column, set_row and scale_factors.
  • math::sign supports floats.
  • XmlSerializer.add_raw writes unescaped text.
  • OpenBSD gets libc::termios support, plus pledge and unveil.

std::gfx

The new windowing module got a good amount of real-world use and fixes: vsync can now be turned off with OpenGL (#3577), Win32 supports shared OpenGL contexts and respects more pixel format hints, several X11 problems were fixed, as were hidden and transparent mouse cursors, key mappings and mouse wheel deltas on WASM.

Fixes

A selection of the fixes, the full list is in the change log below:

  • ABI: char/ushort arguments to C functions weren't zero/sign-extended on macOS arm64 (#3593), overaligned structs and HFA/HVA were passed incorrectly on Linux aarch64, and Win64 returned @simd vectors through memory instead of XMM0 (#3628).
  • Vectors: /, % and >> on unsigned integer vectors used signed operations, % on float vectors was wrong, v[^i] with a non-constant i wrote the wrong element, v.xy++ didn't change the vector, and the $$overflow_* builtins were lowered incorrectly for vectors.
  • Constant folding: int::min / -1, != on faults and on global addresses, and the sign of NaN were all folded incorrectly.
  • a += e on an optional a did not assign the fault when e was a fault.
  • Using a const or an enum associated value from another module that referred to a private symbol could fail to link.
  • Instantiating a generic with an anonymous inner struct or union crashed the compiler (#3631), and generic inference failed with optional expected types (#3612).
  • String.escape returned a truncated result once it outgrew its initial capacity (#3592).
  • A regression broke slices and pointers over 2 GB (#3595).
  • Loading several C3 dylibs on macOS regressed, and @finalizer now runs in the correct order.
  • Compiling with the standard library on OpenBSD could produce illegal instructions on some processors (#3563).
  • c3c project fetch could hang (#3562), and temporary directories were left behind by compile, fetch and the SDK fetch commands (#3553).

Upgrading

Most code will compile unchanged, but these changes may require updates:

  • else if with a single statement needs braces.
  • The stricter cast rules. If you relied on undocumented behaviour for negative constants to wider unsigned types, signed/unsigned array, slice and vector conversions and int128 to pointer, then you might need an explicit cast somewhere.
  • Assigning the same member twice in an initializer is an error.
  • Custom panic handlers must use the new PanicFn signature, and --panicfn now names a function.
  • --print-large-functions is gone, use --stats=full.
  • The regex faults and flags were renamed.
  • fetch unpacks libraries into a directory by default, and project fetch only fetches the libraries it is given.
  • for (;;) {} no longer panics.

Thank yous

As always, this release wouldn't have been possible without the C3 community. Thank you to everyone who contributed code, reported issues, tested, and discussed.

Special thanks to sponsors who went the extra mile: andromeda-fp, Josh Ring and konimarti.

PR contributors for this release

Stdlib:

andromeda-fp, Berat121, Manu Linares, Michael Pollind, Ricardo Tomasi.

Compiler & toolchain: Book-reader, Manu Linares, Michael Pollind, Tonis, yyxxryrx.

CI/Infrastructure: Manu Linares.

Change Log

Click for full change log

Changes / improvements

  • @asmname allows defining external names like @cname but without normal name mangling.
  • Improved GfxKey mappings.
  • Add the source-lib target type (and c3c source-lib command) to pack the C3 sources, C sources and headers of a project into a .c3l library.
  • .c3l manifests without targets now work for all platforms, and may contain version, authors and langrev.
  • fetch, vendor-fetch and project fetch now unpack libraries into a <name>.c3l directory by default. Use --decompress=no to keep them compressed.
  • fetch now verifies the downloaded library against the checksum (sha256:<hex>) of the registry, and refuses a library that doesn't match.
  • dependencies, in projects and in library manifests, may name a registry library as [<url>:]<namespace>/<name>[@<version>]. The build only uses the name.
  • fetch adds just the library name to dependencies.
  • project fetch only fetches the libraries it is given: c3c project fetch <library>.... Without any, it lists the missing ones. Each must be a dependency of the project, or of a library that is installed. A name uses the path of the dependency, or is looked up in the registry: the fetch-url project property or --fetch-url for the project's dependencies, the default registry for those of libraries.
  • A version in the registry's library info may have a url, which fetch downloads the library from instead of from the registry itself.
  • A registry library can be locked to a checksum: <namespace>/<name>@<version>:<sha256>. fetch refuses a library that doesn't match.
  • An else if now requires { } around a single statement, like else does. else if (x) foo(); is an error. #3473
  • Add depends-on to project targets, allowing them to depend on other targets.
  • LLVM IR (.ll) and bitcode (.bc) files can be added to the compilation, just like .o files #3339
  • project.json strings (except exec and type) expand environment variables written as ${NAME}. #3580
  • Add env and env-override to project.json, setting environment variables ("NAME=value") for the exec scripts. #3580
  • Add -gline-tables-only to emit line tables debug info only.
  • Disallow { .a = 1, .a = 1 }. #3579
  • Add better instantiation error output.
  • Implicitly converting a negative constant to a wider unsigned type, e.g. ulong x = -1;, is now correctly an error.
  • Improved the error when implicitly casting an enum to an integer, it now points to the explicit cast.
  • Casting a non-constant integer larger than a pointer, e.g. an int128, to a pointer is now an error. Cast it to uptr first.
  • Converting between signed and unsigned integers of the same size in arrays, slices and vectors, e.g. int[3] to uint[3], requires explicit casts.
  • float16 and bfloat no longer convert implicitly to each other, as both directions lose information.
  • An explicit cast of a slice to a pointer is now allowed for any pointer type, like a cast between pointers.
  • Selectors are now a stable 128 bit signature. The first 16 bytes are guaranteed unique.
  • Add --stats=<none|summary|full|json> for compiler statistics.
  • Removed --print-large-functions, --stats=full replaces it.
  • Compiler generated panics will now be emitted to the std::core::runtime::panics module.
  • PanicFn is changed to support both formatting and non-formatted messages.
  • --panic-msg=no only traps if --show-backtrace=no is also set, otherwise it gives the short message with file, function and line. --show-backtrace=no stops passing file, function and line.
  • --panicfn now names a function rather than a function pointer variable, and sets the initial value of builtin::panic through the new $$PANIC_FN, so stdlib panics and abort also reach it. #3443
  • Make panic on infinite loops opt-in with --infinite-loop-detect=yes – and is only added in safe mode. #3630

Fixes

  • Compressed .c3l libraries were not unpacked again when only files other than manifest.json had changed.
  • Fixed regression for MacOS loading multiple C3 dylibs. @finalizer now correctly works in descending order.
  • Fix miscompilation for OpenBSD on later processors. #3563
  • Polling a GfxEvent for a window and having events on other windows would silently discard the last event.
  • Setting no-border state during fullscreen on Win32 would not work properly.
  • X11 window destruction order was incorrect.
  • Win32 did not support sharing of OpenGL contexts.
  • Win32 OpenGL pixel format selection ignored several hints.
  • Added FAILED_TO_CREATE_WGL_CONTEXT for Win32 WGL format fails.
  • Win32 handle would leak on drag-and-drop when dnd was disabled.
  • X11 mode change did not change the stored mode.
  • X11 window name is now kept when recreating a window for opengl init.
  • Doc viewer collision between modules and declarations sharing the same path. #3572
  • Fixes to transparent mouse pointer.
  • Compiler crashes when using a value derived from a const as an initializer elsewhere. #3574
  • Improve OpenGL-handling of vsync.
  • Regression for emscripten, accidentally retainin unused panic functions. #3575.
  • Compiler hangs on c3c project fetch #3562
  • Temp dirs were left behind by c3c compile (including unpacked .c3l libraries), fetch-list, vendor-fetch and the SDK fetch commands, in particular after errors. #3553
  • Fix integer overflow vulnerabilities in regex compiler (CVE-2026-107891). #3585
  • Compiler crashes when emitting line-tables debug info. #3581
  • Do not pass PIE/PIC on Windows. #3586
  • Crashes in slice->vector and ptrvec->intvec conversions.
  • Implicit cast rules too loose for slice->arrptr.
  • Invalid lowering for (Bitstruct)-1 where the bitstruct is backed by an unsigned type.
  • Cast to any from typedef with an underlying inline pointer, would cause a crash.
  • Cast from constant bool[<2>] to float[<2>] would crash.
  • Padding would be incorrectly lowered in things like (char[<8>])(char[8])"abc".
  • Const casting a const slice derived from a zero array initializer (e.g. (bool)(int[])(int[3]){}) would incorrectly return false.
  • Casting a constant to an alias to an enum would be incorrectly checked.
  • Cast to aliased bitstruct would fail when non-aliased cast would succeed.
  • Casting an optional distinct index would incorrectly fail.
  • Aarch64 MacOS did not zero/sign-extend args (and Win32 Aarch64 incorrectly did). #3593
  • Overaligned structs were not passed correctly on Linux Aarch64.
  • Add alignstack for Linux Aarch64 ABI for correct HFA/HVA handling.
  • string::unescape and string::escape would not work correctly after initial size was reached. #3592
  • $defined on an untyped list that cannot be converted, e.g. $defined(int[*] y = $list) with var $list = { 1, "a" };, would print an error instead of being false.
  • An empty constant string could not be converted to an array, e.g. char[4] x = "";.
  • A constset inlining a constset that in turn inlines a third one, could not be implicitly converted to the one in the middle.
  • $defined on an implicit pointer conversion was true for pointers to distinct types, and printed an error instead of being false when the pointee alignment would increase.
  • Casting a typeid or fault to a wider signed integer, e.g. (int128)t, would sign extend instead of zero extend.
  • An implicit cast between unrelated distinct types would report the error twice.
  • The hints in cast errors (for enums, distinct types, ZString and pointers to slices) were missing when the value was optional.
  • Casting between float16 and bfloat, also in vectors, would abort the compiler. An explicit cast now converts through float.
  • Casting a constant float to an integer it does not fit, e.g. (int)1e30, (char)300.5, infinity or NaN, would abort the compiler or give a wrong value. It is now an error, also for the elements of a constant float vector or array cast to an integer vector.
  • An inline distinct signed integer, or an inline constset of one, would implicitly convert to an unsigned integer of the same size, e.g. uint y = my_inline_int;, which int to uint does not allow.
  • Explicitly casting a pointer wrapped in an inline distinct type to any or an interface was an error, even though the implicit cast worked.
  • A struct implementing an interface through an alias, e.g. struct Foo (IAlias), would not convert to the interface, and an interface inheriting through an alias would not convert to its parent.
  • Casting an enum to a distinct type over that enum, e.g. (MyFoo)foo with typedef MyFoo = Foo;, was an error. It is now allowed with an explicit cast, like for a distinct type over an integer.
  • Errors in multithreaded compilation tasks, such as a failing c-sources file, could crash the compiler. A failing task now aborts the remaining tasks and the compiler exits with an error. #3584
  • Fix error message rethrowing in a non-function context.
  • Fix name mangling of untyped list.
  • "max-vector-size" was not whitelisted as a project property.
  • In some cases, optional ref was extracted before it was created during LLVM IR codegen #3598.
  • Bitstruct method did not check if field with the same name existed #3602.
  • Casting a constant string (or constant bytes) to a vector at compile time would cause a crash. #3601
  • Support unicode paths on Windows when running processes from the compiler. #3605
  • Fixes to i128 compiler_rt.
  • Using a const from another module that refers to a private symbol would fail to link in some cases e.g. static List{int} g = list::ONHEAP{int};.
  • Reading an enum associated value at runtime from another module would fail to link if it referred to a private symbol.
  • A function only used in an @ensure could be stripped when it wasn't the first statement to run on return, failing to link.
  • The sign of constant NaN expressions was ignored during codegen.
  • Generic type inference fails with optional expected types #3612.
  • Signed shift amounts narrower than the shifted value were zero extended affecting the panic message of an invalid shift amount.
  • A function with @test @benchmark would effectively never be included.
  • Incorrect compiler error using from_ordinal when exceeding max.
  • Concatenated base64 literals lost decoded bytes after the first chunk.
  • Improve accuracy of log and logf without a C library (wasm32, freestanding, or -D C3_MATH).
  • 16-byte align JmpBuf on Win32.
  • Win64 returned @simd vectors through a hidden pointer instead of XMM0. #3628
  • Fix regression when slices or pointers exceed 2 GB. #3595
  • XmlSerializer.start_doc was missing the closing quote after the encoding.
  • XmlSerializer escaped comments and processing instructions when serializing a document.
  • gfx::get_vulkan_physical_device_presentation_support was broken.
  • argon2::tencode took key_id as a char instead of a char[].
  • The libc allocator was incorrectly declared in std::core::mem::allocator.
  • Fixes to the benchmarks.c3, map.c3 and xml/3_xml_write_stream.c3 examples.
  • A global slice of vectors, e.g. int[<2>][] g = { { 1, 2 } };, would be lowered incorrectly.
  • Instantiating a generic with the type of an anonymous inner struct/union would crash the compiler. #3631
  • Anonymous inner structs/unions of the same parent shared a name. They are now named by member index, e.g. Foo.0.
  • Inner structs/unions of a generic struct lacked the generic parameters in their name.
  • Fix Win32 compiler support for paths containing non-ASCII characters. #3606
  • A constant struct containing a union initialized by a smaller member was emitted too small.
  • Codegen for % on float vectors was incorrect.
  • /, % and >> on unsigned integer vectors used signed operations.
  • Assigning to v[^i] on a vector with a non-constant i wrote the wrong element.
  • Assigning to a vector element or swizzle discarded would not evaluate right to left.
  • ++ and -- on a multi-component swizzle, e.g. v.xy++, did not change the vector.
  • Comparing constant bitstructs could crash the compiler.
  • Constant != on faults and on addresses of globals returned the result of ==.
  • a += e on an optional a did not assign the fault when e was a fault. It now works like a = a + e.
  • Constant folding of int::min / -1 was incorrect.
  • return, break, continue, nextcase and rethrow in a macro's trailing body were allowed when the macro invoked the body from a defer.
  • ! on a non-bool vector gave the wrong lanes when stored into a bool vector.
  • The $$overflow_add, $$overflow_sub and $$overflow_mul were lowered incorrectly for vectors.
  • ! on an optional vector lost the optional and could cause the compiler to crash.
  • math::overflow_add and math::overflow_sub failed on vectors.

Stdlib changes

  • Added termios support for OpenBSD.
  • Add file::open_fd to open files directly from file descriptors.
  • Changed regex fault and flag names, removed NOSUB. #3585
  • Added missing __muloti4 to compiler_rt.
  • List.insert_at and List.push_front now use a single memmove instead of an element-by-element shift. List.push has an inline fast path when there is spare capacity.
  • List.remove_item, List.compact (and the same functions on FixedList) are now single pass instead of quadratic.
  • Cryptos now try to use funnel shift and rotation.
  • Add unordered parameter to List.remove_first and .remove_last
  • Added matrix::perspective_infinite, matrix::perspective_reverse_z and matrix::perspective_infinite_reverse_z.
  • Added column, row, set_column and set_row to Matrix2x2, Matrix3x3 and Matrix4x4, and scale_factors to Matrix3x3 and Matrix4x4.
  • math::sign now supports floats.
  • Add chmod support #3536.
  • std::hash::crc32 now uses slicing-by-16, about 5x faster. This also speeds up gzip and zip.
  • std::hash::adler32 update is about 3.5x faster (blocked modulo and portable vectors), results unchanged.
  • Rewrite std::compression::deflate inflate: table driven decoder, 2-3x faster decompression; truncated input is now rejected and Inflater.init works with the default bit_reader_buf.
  • Add an optional level (1-9) to std::compression::deflate compress, compress_stream and Deflater.init; the default level produces the same output as before. Faster deflate encoding, and incompressible data is stored instead of expanded.
  • Add an optional level to std::compression::gzip compress, compress_stream and GzipWriter.init (sets XFL per RFC 1952); the default is unchanged.
  • Add an optional level to std::compression::zip ZipArchive.open_writer and ZipArchive.write_file (DEFLATE only); the default is unchanged.
  • Fix several std::compression::zip problems: open and recover closed the file twice on errors, open failed on ZIP64 archives with a comment or exactly 65535 entries, rejected data after the end record, and silently dropped entries after a broken central directory header. recover now reads ZIP64 local headers, extract verifies the CRC32 and no longer overwrites the modification time, ZipEntryReader.seek rejects negative positions, and the central directory flags match the local header.
  • Added XmlSerializer.add_raw to write unescaped text.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Discuss this post on Reddit.

Have questions? Come and chat with us on Discord.

C3 0.8.5 - A windowing library and a C backend??

C3 0.8.5 is now available, and we really have some big news this time, despite the small version bump. We've added both a standard cross-platform windowing module and an experimental C backend(!).

But let's look at them one at a time. You might ask "Why is C3 adding a windowing library?", so let's try to answer that first:

What should a standard library contain?

There is no real consensus on what a standard library should contain. When Java added both concurrency and cross-platform graphics to its standard library, that shook up the industry.

Before that, selling graphics libraries was good business. Some compilers bundled their own graphics libraries, which also helped with customer retention.

That said, just as Java's concurrency story had its problems, so did its cross-platform graphics library. Providing an experience that can compete with native UI is hard. Java's attempt ended up being something of a cautionary tale. However, Qt, wxWidgets and other libraries still managed to eke out a niche for cross-platform UIβ€”at least until the browserification of apps changed the landscape. Today, native UI is competing with things like Electron and Flutter, and it is far from clear that native is winning.

Java's problems seem to have made language designers reluctant to add anything related to graphics. To some extent, this is reasonable: a UI abstraction necessarily makes a lot of assumptions about operating systems and user needs. C++ had a proposal for 2D graphics in its standard library, but it was not adopted for similar reasons.

So should there be nothing? If we look at the C++ discussions, one idea floated was something much narrower: functionality for creating windows, drawable surfaces, and handling user input. And if we look at almost any cross-platform graphics library, this is really the common substrate needed.

Enter std::gfx

So in C3, this is exactly what we're building. std::gfx handles windows, input, monitors, the clipboard and drag-and-drop, but leaves the drawing to you. It gets you to the point where you have a window and everything needed to render into itβ€”and then it gets out of the way.

module hello;
import std;

fn void main()
{
    gfx::init("Hello")!!;
    defer gfx::finalize();
    GfxWindow* win = gfx::create_window("Hello", 200, 200, 900, 600)!!;
    defer win.destroy();

    while (!win.should_close())
    {
        if (try e = gfx::wait(gfx::WAIT_FOREVER))
        {
            switch (e.type)
            {
                case WINDOW_CLOSE: 
                    return;
                case KEY_PRESSED:
                    if (e.key.value == ESCAPE) return;
                    if (e.key.value == F) win.set_fullscreen(!win.is_fullscreen());
                default:
                    break;
            }
        }
    }
}

It runs on Windows, macOS, Linux and BSDs (using Wayland or X11) and in the browser through Emscripten. Wayland support includes the wlr-layer-shell protocol.

For rendering, you can create OpenGL or EGL contexts, Vulkan or WebGPU surfaces, a DirectX swap chain on Windows, or a Metal layer on macOS. Alternatively, you can display a plain pixel buffer using GfxWindow::blit_surface.

That might sound like fairly limited functionality, but implementing it is a substantial undertaking. std::gfx already contains more than 15,000 lines of code, with the X11 and Wayland backends alone accounting for nearly 6,000.

For anyone writing a cross-platform library or application, that is a lot of platform-specific work before the actual rendering can even begin. It is also notoriously difficult to test every backend thoroughly and ensure that it behaves well across all supported platforms.

By making this a part of the standard library, the library will see much broader use, and will consequently be able to shake out many more bugs than an independent C3 library could. It also establishes a set of common types, making interoperability between libraries easier.

And so a word of caution: std::gfx is brand new, and while it lives in std::gfx rather than std::experimental, it should be treated as experimental until 0.9.0. The module is still alpha, and we will want to tweak the API to be butter-smooth.

The other big thing: an experimental C backend

Having a C backend has long been a goal for C3, but little progress had been made until Manu Linares decided to give it a go.

Now, after a lot of hard work, the compiler can emit C instead of going through LLVM, using --backend=c.

The generated C is then built with the C compiler that c3c already knows about (set it with --cc), and extra flags for that compiler can be passed using --cflags:

c3c compile --backend=c --cflags "-std=c23" hello.c3

To be clear, the C backend is still pre-alpha, and nowhere near parity with the LLVM one. The source has a list of known problems, and the list is still long.

A few of the more important ones:

  • Safe mode checks are mostly missing: slice and array bounds, null dereferences, division by zero and enum ranges are not checked, and panic messages are dropped.
  • @section and @noinline are ignored.
  • Some builtins, like $$sysclock and $$breakpoint, don't work.

The test suite runner takes --backend as well, so the existing tests can be run against both backends.

So why do we need a C backend? It allows C3 to target platforms not supported by LLVM, as well as environments where you are contractually required to use a particular vendor-provided C or C++ compiler.

Treat this backend as something to experiment with, and please report what you run into.

Language changes & improvements

$feat takes the same expressions as @feat

@feat has been able to combine features with !, &, | and parentheses, or to list alternatives separated by commas. $feat now accepts exactly the same syntax, so a feature condition can be written the same way no matter where it appears:

const String PLATFORM = $feat(MACOS & AARCH64, LINUX | WIN32) ? "listed" : "not listed";

As with @feat, a comma-separated list means "any of these".

@section on local statics

Static variables declared inside a function can now be placed in a named section, just like globals:

fn int counter()
{
    static int calls @section(".counters");
    return ++calls;
}

Keyword clean-up

In 0.8.3, a set of alternative keywords was added as an experiment. Three of them have now been removed: cenum (for constset), faultset (for excuse) and distinct (for typedef).

constdef, attrdef and faultdef still work, and constset, attrmacro and excuse are what the documentation and the grammar use from now on.

Cross-compiling for Windows: fetching MSVC headers

c3c fetch-sdk windows has until now downloaded the libraries needed to link Windows binaries. With --fetch-headers it also downloads the C and C++ headers, so that they are available for cross-compiling with a C compiler:

c3c fetch-sdk windows --arch x64 --fetch-headers

Smaller binaries: --function-sections and --data-sections

--function-sections places every function in its own section, and --data-sections does the same for global variables. This allows the linker to discard whatever turned out to be unused, in the same way as -ffunction-sections and -fdata-sections do in C compilers.

c3c compile --function-sections=yes --data-sections=yes -o app main.c3

Build fixes

  • C3_LLD_DIR was only taken into account for static builds of the compiler. It's now also used when linking LLD dynamically.
  • compile-run when cross compiling to Windows from a POSIX host looked for the executable using backslashes, and failed to find it.
  • The compiler's virtual memory handling was reworked, which also resolved a NetBSD issue.

Standard Library Updates

Reference types: NonNull and Nullable

std::collections::ref adds two pointer wrappers that move nullability into the type:

  • NonNull{Type} is a pointer asserted never to be null. It converts implicitly to Type*, and the invariant is checked in safe builds.
  • Nullable{Type} is a pointer that may be null. It can't be converted to a Type* or dereferenced. Instead, every accessor returns an optional, so the type checker makes you handle the null case in every build.
int x = 42;
NonNull{int} nn = ref::nonnull(&x);
Nullable{int} n = nn.to_nullable();

if (try p = n.get()) *p = 7;             // checked access
int v = n.value() ?? -1;                 // or fall back to a default
NonNull{int} back = n.to_nonnull()!!;    // narrow again, as an optional

Operating system bindings

  • The macOS bindings were reorganized by framework and split into std::os::darwin and std::os::macos. The bindings have been expanded and Objective-C message dispatch has been made easier to work with.
  • The Win32 bindings were reorganized by header and gained many functions.
  • Dynamic libraries can now be loaded the same way on every platform, using DylibHandle, os::dlopen, os::dlsym and the related functions in std::os.
  • The POSIX libc bindings add shm_open, shm_unlink, truncate and ftruncate, and the Linux socket bindings gained the missing MSG_* flags.

Time and numbers

Duration arithmetic and the Time operations now use saturating maths: a duration or time that would overflow is capped at long::max or long::min instead of wrapping around. To go with it, there's time::FOREVER_PAST next to time::FOREVER, plus an expanded set of "diff" methods.

There's also Duration.to_sec, double.is_inf and float.is_inf, and the new IntRect, IntVec2, IntVec3 and IntVec4 types.

Fixes

Most fixes this time are in compile-time evaluation, operator overloading and vectors:

  • != overloaded on structs behaved as ==, and arrays or slices of structs with an overloaded == compared as equal regardless of content. #3547
  • The wrong subscript operator overload could be selected when calling a method on the result. #3499
  • Vectors created from a cast in a global initializer, e.g. Foo global = { .v = (int[<3>])8 }, would miscompile.
  • Vectors created from designated initializers with trailing zeroes were lowered incorrectly. #3557
  • Macros with const declarations were folded at compile time even when the declaration was needed.
  • @nostrip was dropped on optimized code if no use of the declaration was found, and wasn't accepted on static variables. #3500
  • A segfault when unifying ternary types through lazily resolved function pointer types. #3552
  • @swap now works with arguments that aren't "pure".
  • A regression for vec.pow(x).
  • $$bswap now supports all power-of-two integer sizes, including single bytes, where it's a no-op.
  • Source locations for byte literals continued on a new line were wrong.

Thank yous

I want to especially thank Riley Mabb, the author of RGFW. The std::gfx implementation used RGFW 2.0 as a starting point, and it helped immensely.

Manu Linares has contributed an exceptional amount, not just the C backend, but a lot of testing and fixes on std::gfx, not to mention creating the new C3 playground.

Again, this release wouldn't have been possible without the C3 community. I'd like to offer my deep thanks to everyone who contributed through code, issue reports, testing, and discussions.

PR contributors for this release

Stdlib: hchac, Manu Linares, Mathis Laroche.

Compiler & toolchain: Fernando LΓ³pez Guevara, LowByteFox, Manu Linares, Michael Pollind, SMFloris.

CI/Infrastructure: Darvisim, Manu Linares.

Change Log

Click for full change log

Changes / improvements

  • Fetching MSVC also optionally fetches C/C++ headers for cross-compilation
  • Experimental C backend.
  • Allow the @section attribute on local static variables.
  • Make $feat accept the same syntax as @feat.
  • Add --function-sections and --data-sections (function-sections / data-sections in project.json) to place each function or global variable in its own section, so the linker can discard unused ones #3051.
  • Removed experimental support for cenum, faultset and distinct alternative names.
  • Docs now use the constset, attrmacro and excuse.
  • The static Linux binary now uses mimalloc for faster codegen and is aliased to c3-linux.tar.gz.

Fixes

  • Regression for vec.pow(x).
  • Macros with "const" declarations were compile time folded despite needing the declaration.
  • Vectors created through casts in globals would miscompile, e.g. Foo global = { .v = (int[<3>])8 }.
  • Support $$bswap for all power of two integers, including single bytes (in which case it is a no-op).
  • Fix source locations for byte literal values continued on a new line.
  • Faulty selection of subscript operator overload when calling method #3499
  • Retain @nostrip on optimized code even if no use is found, static variables may now also have nostrip. #3500
  • != overloads on structs behaved as == #3547.
  • Multi-dimensional array comparisons on types with == overloaded would not compare.
  • Lazy resolution of function pointer types could cause a segfault when unifying ternary types. #3552
  • @swap now works with non-pure arguments.
  • Incorrect lowering of vectors when created from designated initializers with zeroes at the end.

Stdlib changes

  • Added std::gfx module.
  • Added IntRect, IntVec2, IntVec3 and IntVec4 types.
  • Updated organization of the MacOS / Darwin bindings, reorganized by framework, added many functions and split into std::os::darwin and std::os::macos, with among other things easy ObjC dispatch.
  • Updated win32 bindings with many functions, reorganized by header.
  • Cross-platform dylib handling with DylibHandle, os::dlopen, os::dlsym and related functions in std::os.
  • Added Duration.to_sec
  • Updated POSIX libc with shm_open, shm_unlink, truncate and ftruncate.
  • Add NonNull and Nullable types for members with mandatory null checks.
  • Cap durations and time to long::max/long::min with saturating maths.
  • Added time::FOREVER_PAST, Time::diff_years, Time::diff_months, time::day, time::week, time::month, time::year.
  • Added double.is_inf, float.is_inf.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Discuss this post on Hacker News or Reddit.

Have questions? Come and chat with us on Discord.

C3 0.8.4 The last renaming

C3 0.8.4 is now available. This release expands parameter reflection and contracts, adds configurable stack probing and stack protection, introduces iOS support, and brings a new collection type and stream utilities to the standard library.

This release is titled β€œThe Last Renaming” because it very likely contains the final adjustments to C3’s keyword set – and I think we’ve finally got them right.

Changing keywords is hardly the best way to convey stability, but sometimes it is necessary. In C3's case 0.9.0 means syntactic freeze of the language, so getting the choice right in the 0.8.x cycle is vital.

Of course, as usual the old keywords will still work as part of the backwards compatibility with 0.8.3 and earlier, but moving to the new ones are recommended.

Language changes & improvements

Renaming preview for 0.9 – changing the -def family

faultdef and attrdef were introduced in 0.7.0 and constdef in 0.7.10. The idea was to unify them under an easy-to-remember "def" suffix. Unfortunately a downside is that this pushes them to retain a similar syntactic shape. After much discussion, we're experimentally introducing new names:

  • faultdef -> excuse
  • attrdef -> attrmacro
  • constdef -> constset

excuse comes from the general terminology, where the fault returned for an empty optional is called the "excuse" in the documentation. That this becomes a keyword further cements that term. In addition, excuse gains a shape with {}, not supported by faultdef:

excuse
{
    MY_FAULT,
    SOME_ONE_ELSES_FAULT,
}

attrmacro was chosen because attrdef takes parameters and in general acts closer to a macro than an alias.

Both cenum and constset were considered. Eventually the latter won out because it establishes a stronger independent feature, whereas the cenum hints at it just being an "enum that works like in C", which isn't correct.

Note that attrdef, constdef and faultdef will work throughout the entire 0.8.x cycle. The new keywords are considered an experimental change, and will be evaluated until 0.9.0. It will become the standard in 0.9.0 only if it's decided that the names are an improvement.

Pinpointing @require failures

It is now possible to associate a requirement with a particular parameter by placing it in brackets after @require:

<*
 @require [a] a > 0 : "a must be greater than zero"
*>     
fn void example(int a)
{
    // ...
}

This lets diagnostics point directly to the argument responsible for a failed precondition, making contract failures easier to understand at the call site.

Parameter reflection

Parameter reflection is more capable in 0.8.4. $reflect(foo).param_struct and Foo::param_struct provide access to a function's parameters as a struct. This allows macros to build up calls by populating each field, and then use struct splat to make the call:

fn void foo(int a, double b) { }

fn void test()
{
    // The parameter struct has fields `a` and `b`
    var $ParamStruct = $reflect(foo).param_struct;
    // Create a struct 
    $ParamStruct s;
    // Populate them.
    s.a = 10;
    s.b = 2.3;
    // Use struct splatting to make the call
    foo(...s);
}

Parameters can now also carry @tag attributes, and reflected parameters support get_tag, tags, and has_tag.

It is also possible to inspect a parameter's default value through .default_value.

As part of this work, FooFn::params now returns reflected references rather than ReflectedParam values. Code using this reflection API may need to be updated.

Conditional aliases

Alias definitions now accept ternary expressions. This makes it possible to select an implementation using a feature flag without wrapping the alias in a separate conditional declaration:

alias foo = $feat(ABC) ? foo_1 : foo_2;

Compiler and toolchain

Stack hardening and the x86 red zone

C3 0.8.4 adds several controls for stack hardening. The new --stack-probe, --stack-probe-size, and --stack-protector options configure stack probing and stack-canary generation from the command line.

Individual functions and lambdas can override these settings with @stackprobe(level), @nostackprobe, @stackprotector(level), and @nostackprotector. Naked functions no longer receive stack protectors by default.

The new @noredzone attribute disables use of the x86-64 red zone for a function. As a temporary safety measure, functions on ELF x86-64 are emitted without red-zone use. ELF and COFF output also gained support for the dso_local attribute.

The --implicit-float setting provides control over the emission of floating-point operations. This is useful for low-level targets where implicitly generated floating-point instructions are undesirable.

Platform and build improvements

This release adds support for targeting iOS. LLVM compilation scheduling has also been adjusted so the largest modules are built first, helping make better use of parallel compilation.

Project target types benchmark and test now work correctly with c3c benchmark and c3c test, including targeted benchmark functions. Project names passed to c3c init may now contain -, and the new --keep-obj option preserves object files after building and linking.

The compiler can now warn about unused locals and parameters with --warn-unusedlocal and --warn-unusedparam. Diagnostics have been improved for incorrect panic-function names, excessively long source lines or underlines, and build options incorrectly written with =.

Faster slice equality

Equality comparisons between slices of flat types are now lowered to memcmp instead of scalar loops, reducing the cost of comparing suitable slices.

Standard Library Updates

Streams and collections

The standard library now includes CachedInStream and CachedOutStream, as well as the new std::collections::Tree collection.

InStream.read has been made consistent: it returns 0 at end of file instead of throwing io::EOF, and it now requires a non-empty destination buffer. Code which catches io::EOF from InStream.read should be updated to check the returned byte count instead.

Ranges gained a range::slice macro, and Path.is_link can be used to determine whether a path is a symbolic link.

Networking, logging and memory

Sockets can now report both ends of a connection through Socket.peer_address, Socket.peer_port, Socket.local_address, and Socket.local_port. Logging gained log::get_logger.

RefCounted has been reworked slightly to differentiate between finalization and freeing.

Thread pool changes

The old deprecated pool::ThreadPool is now disabled by default, and the new thread::ThreadPool is enabled by default. The old implementation is exposed as ThreadPoolOld, with -D OLD_THREADPOOL available for restoring the old name, for projects that do not want to change the name to ThreadPoolOld.

Fixes

This release fixes a broad set of compiler crashes and miscompilations, particularly around generics, vectors, bitstruct operations, default arguments, and deferred control flow.

Notable compiler fixes include:

  • Generic declarations disabled with @feat are no longer instantiated.
  • Generic aliases inside functions no longer cause the compiler to hang.
  • Generic function pointers are resolved before use regardless of generic resolution order.
  • Dead-code detection is preserved correctly when leaving a scope.
  • A switch inside defer is no longer copied incorrectly during compilation.
  • Debug information for default-initialized arguments and code generation for decrementing bitstruct fields no longer crash with LLVM 23.
  • Struct initializers containing arrays of vectors are aligned correctly.
  • Reversing vectors larger than 128 elements no longer corrupts the heap.
  • $$mod on unsigned integers now emits an unsigned remainder operation.
  • Optional-vector-to-array casts now generate correct code.
  • Overloading from-end subscripts such as foo[^1] = 2 now works correctly.
  • The configured C compiler is now used consistently.

The standard library also received several correctness fixes. Large left shifts now work for BigInt, copying a DString no longer overwrites its allocator, and FixedBlockPool handles changes to grow_capacity correctly. Fixes also landed for mem::equals, UTF-16 surrogate pairs in String.escape, virtual-memory reserve page sizes, and stale thread IDs returned by thread::current on POSIX.

String case-conversion attributes such as @str_pascalcase and @str_camelcase now retain digits instead of treating them as discarded word separators.

Thank yous

Again, this release wouldn't have been possible without the C3 community. I'd like to extend a deep thank you to everyone who contributed through code, issue reports, testing, and discussions.

PR contributors for this release

Stdlib: Archishman Nag, m0tholith, Manu Linares, Ricardo Tomasi, Senthilnathan, Velikiy Kirill.

Compiler & toolchain: Alexandru PaniΘ™, Darvisim, Manu Linares, neerajnangireddy, rickyadastra, ssimb, Vyacheslav Denisov.

CI/Infrastructure: Darvisim, Fernando LΓ³pez Guevara, Manu Linares.

Change Log

Click for full change log

Changes / improvements

  • Improved error message when providing an incorrect name for the panic function.
  • Add the ability to pinpoint a @require using the @require [a] a > 0 : "a must be greater than zero" syntax. #1804
  • benchmark and test project targets now work properly with c3c benchmark and c3c test.
  • Add $reflect(foo).param_struct and Foo::param_struct properties. #3099
  • Allow the parse faultset { ... }. For faultset, faultconst and excuse.
  • Improved error messages when underlined error is too long, or lines are too long. #3383
  • Add --implicit-float setting to control emission of floating point operations. #3449
  • Allow parameters to have @tag #3084
  • FooFn::params no longer returns ReflectedParam but a reflected reference. This allows get_tag/tags/has_tag.
  • Allow parameters to be queried for the default value using .default_value.
  • Allow ternary in alias definitions, e.g. alias foo = $feat(ABC) ? foo_1 : foo_2. #3464
  • On elf-x64, add noredzone to functions as a stopgap solution.
  • Add --stack-probe, --stack-probe-size and --stack-protector to configure stack probing and stack canary generation. #3437
  • Add stack-probe and stack-protector project options. #3437
  • Add @stackprobe(level), @nostackprobe, @stackprotector(level) and @nostackprotector attributes on functions and lambdas. #3437
  • Naked functions no longer have stack protectors by default.
  • Add @noredzone attribute.
  • Add dso_local attribute on ELF/COFF.
  • Let LLVM build the biggest modules first.
  • Support for iOS.
  • Allow - in c3c init some-project.
  • --keep-obj added, to prevent object files from being deleted after building/linking.
  • Slice equality for flat types is now lowered to memcmp, avoiding scalar loops. #3491
  • Add --warn-unusedlocal and --warn-unusedparam to detect unused parameters and locals. #3485
  • Improve the error message for build options which use =.
  • Add control registers to x86 inline assembly

Stdlib changes

  • CachedInStream and CachedOutStream added.
  • InStream.read now consistently returns 0 on EOF, and never throws io::EOF, and requires a non-zero buffer target.
  • pool::ThreadPool now only available using ThreadPoolOld unless -D OLD_THREADPOOL is used.
  • Add std::collections::Tree.
  • std::net added Socket.peer_address, peer_port, local_address, and local_port for retrieving remote and local socket address information. #3460
  • Add a range::slice macro.
  • Add log::get_logger.
  • RefCounted now correctly makes a difference between dealloc and free.
  • Path.is_link added.

Fixes

  • Vmem incorrectly handled reserve page sizes.
  • @return was accepted in doc comments for non-function/macro declarations.
  • Can not run targeted benchmark function in project #1651.
  • Detection of dead code would get reset after visiting a scope, causing a crash in codegen. #3453.
  • Switch was incorrectly copied inside of defer, causing crash in codegen. #3454
  • Codegen for debug info was incorrect for default init on arguments, causing crash with LLVM23.
  • Codegen for bitstruct b.foo-- was incorrect, causing crash with LLVM23.
  • @str_pascalcase/@str_camelcase treated digits as word separators and dropped them from the output. #3287
  • Typedef access resolution preferred inner type field over method. #3457
  • Generic gets instantiated despite being disabled with @feat #3459
  • Compiler hangs with generic alias in function. #3470
  • Resolution order for generics may cause generic function pointers to not get resolved before use. #3471
  • Stale tid in thread::current on POSIX. #3472
  • Compiler crashes on implicitly casting a &a - &b to another type. #3477
  • BigInt shift left would not work correctly with shifts over 32 bits.
  • DString copy would accidentally overwrite the allocator.
  • FixedBlockPool would not correctly handle changes to grow_capacity.
  • Fix bug in mem::equals calculating the last part to compare.
  • Miscompilation of struct initializers when a struct contained an array of vectors, causing incorrect alignment. #3483
  • String.escape would not correctly handle UTF16 pairs.
  • Heap buffer corruption when reversing vectors larger than 128 elements due to an undersized allocation.
  • $$mod on unsigned integers emitted signed division instead of unsigned remainder.
  • Improved codegen for casting an optional vector to an array.
  • Overload for ^1, e.g. foo[^1] = 2 did not work correctly. #3496
  • Correctly use defined C compiler in all cases. #3495

Want To Dive Into C3?

Check out the documentation or download it and try it out.

https://news.ycombinator.com/item?id=49798843 Have questions? Come and chat with us on Discord.

Discuss this article on Hacker News or Reddit.

If You Have Users, You Have to Market Your Programming Language

I don't like to think about marketing. I think few programmers do. We tend to dream of a β€œbuild it and they will come” situation, where if we just make something great then it will be picked up and get the recognition it deserves. But real life doesn't work like that.

We know of what's popular and that occupies most of our attention. It is a useful evolutionary trait: often what's popular is popular because it managed to out-compete the alternatives, so it saves us from diving into details for everything.

Also, by virtue of being popular it falls under greater scrutiny, so where something less used might have problems few know about, the popular thing's downsides will usually be well explored. It might not be the best but it communicates a lot, just being popular.

The fact that something or someone has the money/influence to do marketing is in itself a signal of quality: you don't get money/influence without having demonstrated some kind of success. And you get successful by doing something right. Marketing something you don't believe would sell would mean you're making the wrong decision, which you then – by virtue of being successful – have fewer reasons to do. This boils down to the message that someone spending money on marketing, on average, means that the product is good!

This doesn't mean that only good products are marketed of course! But it is what it strongly signals to us. That's why we respond to it even if we don't resonate with the marketing message itself. Without digging into the subject too deeply: just keep in mind that popularity and marketing shape people's opinions on many levels. It creates trust.

Now let's get to the main point: programming languages and marketing. I'm not going to talk about how to market programming languages or what's difficult about it. No, I want to talk about the effects of marketing, or not marketing your language.

People have different goals: it might be a hobby project for their own consumption or ...ahem... a way to world domination in the domain of programming πŸ˜„. It might be reasonable to think that marketing isn't needed for the former, but what I'd like to demonstrate is that the case is not clear-cut.

For the sake of argument, let's say I neither go to the extreme of β€œthe goal is to make this language the most popular in the world” nor β€œonly I will ever lay my eyes on this language”. If so, you're building a language for yourself and others to use. – You might tell yourself: β€œit doesn't matter if this becomes popular, it's enough that I can use it”. This seems to be a pretty common attitude: trying to be realistic about success without closing the door to success. I'm going to tell you this is the wrong thing to do. You should try to make it popular no matter what!

We need to approach this at an uncommon angle: Let's say you have a few people also using your language, what value are you providing them? The language certainly, and usually the toolchain and standard library.

Now, what does the quality of those tools depend on? Roughly: (1) the amount of time people dedicate to them (2) the quality of the effort (3) how much testing has gone into them.

No matter how good your language is, no matter how well you've selected features for the standard libraries for the toolchain: the more people you have using your language, the more people you will have testing it (but the more people, the fewer bugs a single user will encounter!), finding missing functionality in your standard library, or just finding ways the toolchain fails on their particular setup.

What editor are you using? It might be reasonable that you'd make sure that this one has syntax highlighting, but what about all the many different editors people use? Editors you might not even have heard of?

Tutorials? You might do great tutorials, but they will be from your point of view, for tasks you think are relevant.

Even if your users produce these for themselves, the extent of how those tools are tested also depends on the number of users. We can look at anything: platform support, library bindings, sample projects. Your users will benefit the more users there are. It will just be a better experience and raise the value of everything your users do: if they make a library, they will know others will use it, same for editor syntax highlighting, tutorials and bindings. This is a virtuous cycle where everyone can benefit.

This also explains why people get invested in their language of choice and might become zealous in their promotion of it. We can't just dismiss it with people being β€œcultish”: they're actually sometimes simply doing what is in their power to improve the value in their favorite investment: it is their way to try bringing better tooling, libraries and other things to the language they enjoy.

Tsoding once remarked:

β€œMajority of people treat Programming Languages as some sort of Cryptocurrencies.”

For new languages this is a fairly accurate description of what's happening. Users are aware on some level that more focus on their language of choice will mean a better experience using that language.

We might not like this, but it is what it is.

While your users can contribute, they can't decide on the central marketing message. This is intrinsically tied to the author of the language. If the author says β€œI don't care if it just remains a hobby project” that might seem self-deprecating and humble. But what it means in practice is: β€œI don't care about providing value to my users”.

It's the β€œbuild it and they will come”. Well your users arrived and you thought that was the end of your responsibility. All you need to do is just make the language really good and your job is done. Right? Except your users put effort into learning and contributing, and you're wasting this potential. β€œI'm fine with it being a hobby project” is the excuse. Your language is abandonware. It's not humility, it's failing to understand your obligations.

Marketing is not about making your language β€œwin”. It's not about aggrandisement. It's about fulfilling the obligation to your users, the ones who liked what you did, and had the courtesy of investing time in using it. Your language might never catch on, that is fine, but there is a difference between trying and failing, and not trying at all. If you don't do it, you can hardly expect them to fork the compiler and continue the work. You've shown the world you don't think the language is good enough to market. That's a signal too, a very strong one.

So language designers: we should either make it clear that something is just a toy language which isn't going anywhere – or we should take responsibility and do the marketing as much as we can. Do or do not, there is no try.


Discuss this article on Reddit and Lobste.rs.

I thought I was building a C replacement. I was wrong

I made a rather fundamental mistake when I started marketing C3.

I called it a C alternative.

That seemed completely obvious to me. I had been looking for a better C for years, I found C2 to contribute to, and from that C3 was eventually born. Of course C3 was a C alternative.

But I've gradually realized that the phrase "C alternative" means something very different today from what it meant to me.

And the reason is embarrassingly simple: I'm old enough to remember when C was an application language. So to understand what I got wrong, we have to go back to the 80s and 90s.

I started in BASIC, and for the longest time, assembly and BASIC were the main options. This was during the home computer "revolution" of the 80s. With the advent of 16-bit home computers, more languages suddenly became viable β€” mainly Pascal and C.

At that time, the most difficult thing was actually finding a compiler if you were young and had hardly any budget. I even ended up doing serious programming in QBasic of all things, simply because it was bundled with my install of DOS! And later I ... ehem ... "got hold" of a copy of Turbo Pascal...

My Pascal days were ultra productive. If I wanted to do something, it was just a matter of sitting down and writing it. Compared to writing things with labyrinthine BASIC goto/gosub, Pascal was super nice and easy to organize. This was easy without imposing structure or architecture up front – after all, it was procedural.

I did learn a smattering of C by getting hold of GCC, and later did some introductory C++ at university. And at this point, everything I wrote on my own was procedural in style. Then I had a summer course in Java. I think this might have been 1996 and the internet was all new.

And Java introduced OO as the core approach.

It was novel, it was interesting, and despite having written C++, I felt I hadn't really understood what OO was about until then. So that started my love affair with OO. Not that it was needed that much initially. Even getting a job in C++ later on gave me little room to do OO. But let's fast forward.

I ended up being exposed to Objective-C, and a job on Java game servers gave me a massive amount of practice writing Java quickly and well, but I still felt a nagging dissatisfaction. Because I never really got the raw development speed I had in my early days with Turbo Pascal, I was much better at programming, I was somehow slower – thinking much more about design than before for equivalent code. And this was something I didn't really reflect on that until I had a long-term contracting gig doing PHP.

The codebase was somewhat OO, but not overly so. And the interesting thing was that, at its core, it was basically:

"The user made a call, route it to running this function and present this result."

Even if bits and pieces inside were wrapped in OO classes, it was mostly procedural. And by god, development was ruthlessly efficient.

Step by step it dawned on me that the OO parts were superfluous: – the whole thing could have been C with an arena allocator + good dynamic arrays and strings. No OO needed.

And that got me thinking. Here it was: the development speed of Turbo Pascal, so why didn't OO give me that? I discovered an answer in OOPs requirement of up-front architecture.

In OOP, you need to think about architecture from the first β€” what objects own what objects, what objects know of what objects. And a big part of the "best practices" in OO, such as programming to interfaces, are really ways of trying to mitigate the problem of this deep coupling.

The methods are fundamentally linked to this problem. As soon as we write:

foo.do_something(bar)

we have created a hierarchy where the class of foo is more fundamental than bar.

In programming, we talk about exploring the problem space as we develop a program. As we gain a deeper understanding, we will usually restructure the program so that it more easily solves the problem.

The problem with OOP β€” or, if I may spread the net wider, "methods first" β€” is that the up-front architecture and the use of methods inhibit these changes. When we have placed B in A, reversing that relationship doesn't just mean moving the field: it also means rewriting all the methods that rely on it. This makes us reluctant to do such refactorings, which in turn means that bad decisions made up front tend to get locked in.

This is not just a problem for OO, but for anything that is "methods first" β€” that is, when you think:

game.run()

instead of:

run(&game)

The latter is procedural thinking. The former is "methods first".

As an aside, C3 has methods because they are unreasonably effective for avoiding the need for function overloading in things like foo.to_string() But this also makes it harder to drag people out of the "method first" mindset.

(Here Odin has an advantage: it gets people into a good mindset from the start.)

For me, things were starting to come together.

My PHP experience showed me that the OO parts were never actually needed to create nice abstractions. And the realization that OO β€” or more broadly, "methods first" β€” was making it harder to write good programs with good architecture gave me a better understanding of what I had been looking for all along.

C wasn't quite there, because libc was extremely bare-bones and overall didn't have the necessary ergonomics anymore. And yet a subset of C++ wasn't the solution either. It was marinated in "methods first", but above all, it was so slow to compile.

So yes, I was looking for a C alternative.

But what I was actually looking for was a C alternative with the ergonomics that made it suitable for current-day application development. And that distinction turned out to matter a lot.

When I would say "C3 is a C-like for people who like C", I was thinking about people who would write everything in C. People like Sean Barrett.

But something I've gradually come to terms with is that "C-like" or "C alternative" today means something rather different. It means "a programming language for what C is predominantly used for today". That means OS development, embedded, and high-performance niche libraries and backends.

People write databases in C, not their next video editor or game. For that they use C++ or something like that. Because with C++, you get the added ergonomics C lacks: dynamic strings and arrays, maps, a sprinkle of overloading. And that makes perfect sense.

But that isn't what I was thinking about. I was thinking of C as a language for writing programs. General-purpose applications. The kinds of things people now reach for C++, Objective-C, Swift, Java or Kotlin to build.

I always thought of C3 as being just as nice β€” or better β€” to write those applications in. This is not because I wanted to make C3 into another C++, or another Java, or another Swift. Quite the opposite. I wanted the raw simplicity and performance of C, but with the ergonomics needed to make it pleasant to build modern software. Just a language you pick to get things done, with enough ergonomics to make that as nice and convenient as possible.

In the eyes of most programmers today, this is not what they associate with C. But it was what I associated with C, because I'm old enough to remember when that was indeed the role of C.

(And this is also why Zig's decisions are so different from C3's: Zig's primary goal seems to be to replace C where it's used today.)

When people would say "why do people want to replace C?", I mistakenly always took it as:

"C is good enough, we don't need a replacement."

But what they meant was really:

"C isn't used for anything interesting, so why bother with a replacement?"

And that's where I had misunderstood the discussion. I was using an old definition of "C alternative" in a world where C means something else. So when I marketed C3 as a C alternative, I made a pretty fundamental mistake.

C3 was always an alternative to anything from C to C++ to Swift. – I just never thought enough about how much the world had changed. Which changes how I need to talk about C3.

So moving ahead, C3 will talk less about being a C-like, and more about being a programming language for building general-purpose applications. Because the point was always:

refreshing C to make it a pleasant general-purpose application language again.

In other words, I spent years marketing C3 as a C replacement, but what I was really trying to build was something much broader.

I just marketed that wrong.


Discuss this blog post on Reddit, Lobste.rs or HackerNews.

C3 0.8.3 Feature flags

C3 0.8.3 is now available. The most notable change is the addition of the @feat attribute, which will be the new way to do top-level conditional compilation.

Language changes & improvements

@feat

This release adds the @feat attribute, which is replacing the older @if usage on non-generic top-level declarations. This attribute is essentially the same as @if($feature(...)) before 0.8.3, but top-level @if is now getting phased out except for use in generics.

The problem with @if

Before @if we had $if on the top level, mirroring C's #if and #ifdef. However, this gave very little information for the compiler when someone tried to use a declaration guarded by $if. So @if was introduced to give more granular support and to make it easier for IDEs to figure out "go-to definition" style functionality.

Unfortunately, @if still had the problem of $if, which is that its argument could be any arbitrary compile time constant expression. But what happens if we use something like @if(Foo::methods.len == 3)? Well, if the compiler hasn't yet reached a stage where Foo is analysed, its analysis is now forced. Furthermore, even if Foo is analysed, methods might not be registered yet, giving subtly wrong answers. Because C3 runs in multiple passes, there is no canonical ordering of which thing to analyse first, so it was quite unpredictable. Even though refinements were constantly made to the ordering, the underlying flaw was unsolvable.

The insight

Surveying the use of @if they roughly fell into one of three categories:

  1. Conditionally include depending on compilation target, e.g. @if(env::WIN32)
  2. Include "if no other implementation is found", e.g. @if(!$defined(String)).
  3. Conditionally add functionality depending on generic type, e.g. @if($defined((Type){} + (Type){}))

Of these, the (3) didn't pose a problem in most cases, because in general it happened fairly late. In the case of (2), this was now mostly covered by the new behaviour of @weak (introduced in 0.7.11).

So what about @if(env::WIN32)? This was almost completely confined to the std::core::env module, which would define constants and derived constants based on compiler builtins, and all of those cases could actually be directly passed in by the compiler.

Introducing @feat

As previously mentioned, @feat is essentially $if($feature(...)) but to make this work, the compiler now provides many feature flags built-in, such as WIN32, LINUX, BIG_ENDIAN etc. In general any use of env::CONSTANT has a @feat(CONSTANT) counterpart.

This solves the ordering problem because all these constants are known before declarations are registered, so any symbols with @feat can be filtered early. This is not only beneficial for the compiler – LSPs and editors can take advantage of this and accurately determine what's available at compile time.

In more detail, @feat may take multiple constants, in which case it is satisfied on any match:

fn void foo() @feat(WIN32, MACOS) // available on WIN32 and MACOS
{}

It's also possible to use | instead:

fn void foo() @feat(WIN32 | MACOS) // alternative syntax
{}

! may be used to negate a feature:

fn void foo() @feat(!WIN32) // available on anything other than WIN32
{}

Multiple @feat means all must be satisfied:

fn void foo() @feat(MACOS) @feat(AARCH64) // only on Aarch64 MacOS
{}

It's possible to use & as an alternative:

fn void foo() @feat(MACOS & AARCH64) // only on Aarch64 MacOS
{}

More complex expressions using | & and ! are allowed:

fn void foo() @feat((MACOS & AARCH64) | !LIBC) // On Aarch64 MacOS or not with libc
{}

Together with this the $feature function has been renamed $feat ($feature is now deprecated and will be removed in 0.9.0).

Possible keyword changes

0.7.0 introduced the family of "-def" keywords: constdef, typedef, faultdef and attrdef. After evaluating this for a little over a year, there has been some doubt whether these are really good enough. For this reason 0.8.3 introduces experimental aliases for each:

  • constdef -> constset, cenum
  • typedef -> distinct
  • faultdef -> faultconst, faultset, excuse
  • attrdef -> attrgroup, attrmacro

These are considered experimental, meaning that there is no stability guarantee. Versions 0.8.4 and onwards are free to remove these aliases.

Please take time to try them out and offer feedback on them.

Fetching MacOS

Previously, automated fetching of the Windows SDK was added. This version adds SDK fetching for MacOS as well, allowing completely effortless cross-compilation to MacOS from any other platform.

Windows Aarch64 support

With this version the C3 compiler finally supports Aarch64 as a Windows target.

Standard Library Updates

Regex

With this release, C3 finally has regex support. Please note that the API is considered experimental and may change in a later 0.8.x version.

Other changes

JSON/object unmarshaling is now available through json::unmarshal, object::unmarshal, and related functions. json::temp_load is deprecated in favor of json::tload.

Geometry code should note one behavior change: Rect.contains_point is now exclusive on the maximum edge. A new Bounds type has also been added for rectangular regions represented by min and max, with operations inclusive along the boundary edge.

LinkedList and Deque now have a prepend method, FixedList has is_full(), and ranges gained range::upto, range::inclusive, range::exclusive, Range.to_array, and ExclusiveRange.to_array.

Other useful additions include io::read_buffer and comparison operators for DateTime.

Finally, the tracking allocator is now able to track cross-thread allocation.

Fixes

This release fixes a wide range of correctness issues, including generics, compile-time execution, JSON parsing, IO streams, compression, networking, AES CTR, RISC-V ABI handling, doc generation, and several compiler crashes.

Notable fixes include:

  • Generic functions and values now correctly require a prefix.
  • The arena allocator no longer errors in safe mode when freeing the last memory while the arena is full.
  • LinkedList.push_front_all now preserves the expected order.
  • BitSet.len now reports the bit set length instead of the underlying type size.
  • RFC 3339 formatting now emits the correct value for microseconds.
  • Timed tcp::connect now returns the real result instead of always failing with io::GENERAL_ERROR.
  • Generic methods are no longer checked before their generic type is fully registered.
  • _erff now invokes C erff instead of erf.
  • AES CTR no longer loses sync for data whose size is not a multiple of 16.
  • JSON no longer accepts \v as whitespace.
  • JSONC parsing no longer loops indefinitely on unterminated comments.
  • io::read_all now handles split data.
  • Scanner now handles chunked data correctly.
  • Gzip readers can now handle streams without seek support.
  • io::printf("%3d", 1) now returns the correct printed length.
  • RISC-V structs with mixed floating-point and integer fields are now passed and returned by value correctly.

Doc generation also received several fixes: attrdef declarations and docs are emitted, alias doc comments are included, @return contracts are rendered, compiler_rt is excluded when --emit-stdlib=no, and empty JSON fields are omitted for slimmer output.

Thank yous

Again, this release wouldn't have been possible without the C3 community. I'd like to extend a deep thank you to all who have contributed, both through filed issues, PRs and just plain discussions.

PR contributors for this release

Stdlib: cmann1, Darvisim, hchac, Lado, Manu Linares, Omar Alani

Compiler & toolchain: Darvisim, Fernando LΓ³pez Guevara, Lado, LowByteFox, Manu Linares,

CI/Infrastructure: LowByteFox, Manu Linares, ttambow

Change Log

Click for full change log

Changes / improvements

  • Windows aarch64 is now supported.
  • Tracking allocator can now accept cross-thread allocations.
  • Filter test backtraces #3368
  • Improved GDB compatibility for macros.
  • Fail when "emcc" is unavailable instead of falling back to the built-in wasm linker.
  • Support fetching MacSDK for easy cross compilation.
  • Add @feat attribute, deprecate @if on non-generic top level declarations.
  • Add $feat compile time function. $feature is deprecated and replaced by $feat.
  • Experimental support for constset, cenum, faultconst, faultset, excuse, attrgroup, attrmacro, distinct.
  • Defer resolution of typedef alignment and generics, allowing more recursive definitions.
  • Improve error message on multiple <* *> in a row. #2971
  • Add the lgdt and lidt instructions to x86 inline assembly.

Stdlib changes

  • LinkedList and Deque added a prepend method.
  • Added FixedList.is_full() method
  • Rect.contains_point is now exclusive on the maximum edge.
  • Add Bounds - a rectangular region stored as a min and max value, with all operations being inclusive along the boundary edge.
  • Experimental regex support.
  • Improved RFC 3986 compatibility.
  • JSON unmarshaling support, json::unmarshal and family.
  • Object unmarshaling support, object::unmarshal and family.
  • json::temp_load deprecated in favour of json::tload.
  • Object::is_map now returns true for empty objects.
  • Add range::upto, range::inclusive and range::exclusive macros.
  • Add io::read_buffer.
  • DateTime enable comparison operators.
  • Add Range.to_array and ExclusiveRange.to_array methods.

Fixes

  • Generic functions and values incorrectly would not require a prefix. #3374
  • Arena allocator would error in safe mode when freeing the last memory and the arena was full. #3378
  • LinkedList push_front_all was appending in the wrong order.
  • BitSet.len would yield the size of the underlying type, not the length.
  • RFC3339 formatting would yield incorrect value for microseconds.
  • --obj will always retain the object files. #3380
  • ThreadGroup with function returning void was broken.
  • Timed tcp::connect always failed with io::GENERAL_ERROR instead of the real result.
  • Compile time struct with zeroed union member access causes compiler error #3382.
  • Generic methods checked before the generic type is fully registered.
  • Math function _erff invoked C erf function instead of erff function #3391
  • Defining local constants inside a macro causes it to fail to @const fold. #3397
  • AES CTR would lose sync on data not multiples of 16.
  • In some cases, on macros rethrowing optional values codegen could fail.
  • Json accepted incorrectly accepted \v as whitespace.
  • JSONC parsing on unterminated comments would loop indefinitely.
  • ZII array constdef would cause an assert. #3411
  • Calling a constant void macro inside a macro stops it from being constant. #3410
  • Using io::struct_to_format with $force_dump = true failed to compile.
  • $foo += 1 would not do a copy, leading to incorrect update of $foo. #3400
  • foreach (foo::Type t : x) would not parse properly. #3423
  • Docgen improvements and fixes: emit attrdef declarations/docs, include alias doc comments, render @return contracts, exclude compiler_rt when --emit-stdlib=no, and omit empty JSON fields for slimmer output #3422.
  • Several uses of InStream didn't properly handle io::EOF.
  • Fixes to memory handling during zip loading.
  • Multireader reading after a final empty read would crash.
  • RISC-V structs with mixed FP and integer fields were corrupted when passed or returned by value. #3428
  • io::read_all didn't handle split data.
  • Scanner would not correctly handle chunked data.
  • Gzip reader couldn't handle a stream without seek.
  • io::printf("%3d", 1) would return the wrong printed length. #3432
  • Crash when using ternary operator with vector type and inline constdef #3433
  • Crash on assign-op to compile-time subscript with non-const result #3419

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

C3 0.8.2 A modest improvement

C3 0.8.2 is out. This release brings a batch of language ergonomics improvements β€” most notably reusable target templates for libraries and generic constdef β€” along with new reflection capabilities and a round of bug fixes.

Compared to 0.8.1, this is a rather modest release, but anyway, here's what's new:

Language changes & improvements

Reusable target configurations for libraries

Libraries can now expose a templates map in their manifest, and projects can pull one into a target with template: "library/template". Template properties load first and target-local settings override them, so libraries can ship sensible, shareable build configurations instead of asking every user to copy-paste setup.

Reflection for generics

Foo::is_generic(...), Foo::generic_qname, and Foo::generic_args finally lets you inspect generic instantiations.

alias Foo = List{int};

fn void main()
{
    typeid t = List{String}::generic_args[0]; //String::typeid
    String name = Foo::generic_qname; // "std::collections::list::List"
    bool x = Foo::is_generic(List); // true
    bool y = int::is_generic(List); // false
}

Bitstruct properties

Bitstruct members gain bitoffset and bitsize properties:

bitstruct Foo : uint
{
   uint a : 0..1;
   bool b : 6;
   bool c : 10;
   int d : 14..20;
}

fn void main()
{
    int a = $reflect(Foo.a).bitsize; // 2
    int b = $reflect(Foo.d).bitoffset; // 14
    int c = $reflect(Foo.c).bitoffset; // 10
}

New @param options: own, init and drop

It's now possible to annotate more than in/inout/out on parameters:

  1. own signifies that the function will in some way retain the variable beyond the scope of the function.
  2. drop means that the pointer and the data pointed to will be invalid when the function returns.
  3. init means that the data pointed to will be initialized by the function.

This can be used by the compiler for static analysis and also documents the function or macro.

<* 
 @param [own] f
*> 
fn void foo(Foo* f)
{
    some_global.foo = f;
}

Asm stack alignment

Before 0.8.2, all asm blocks would be stack aligned, which didn't match C behaviour. This is now fixed. Stack alignment is now opt-in by using @align:

asm // Unaligned
{
  syscall;
}

asm @align // Stack aligned
{
  syscall;
}

Windows improvements

The Windows subsystem can now be set directly. Windows can still use @winmain to implicitly set the Window subsystem, but all subsystems are now available, allowing for things like EFI applications.

Standard library

  • Atomic.compare_exchange added.
  • array::contains_slice and array::index_of_slice for substring-style searches over arrays.
  • log::register_dynamic_category, for libraries to define their own logging categories dynamically.
  • String.index_of and rindex_of now accept finding empty strings.

Fixes

Stdlib fixes

  • Denormal (subnormal) results are now handled correctly by String.to_double() β€” very small magnitudes near the floating-point floor previously came out wrong.
  • A float literal with an uppercase F suffix was incorrectly typed as a double instead of a float.
  • JSON serialization now correctly handles Unicode and \v (#3353).
  • IPv6 parsing: "a::b:c:d:e:f:0" is now accepted as valid.
  • A regression in BackedArenaAllocator when destroying it could cause a crash.
  • Fixed macOS regression which was breaking stack traces.

Compiler fixes

  • $stringify would sometimes wrap output in extra parentheses.
  • constdef vector with an alias was incorrectly lowered
  • The compiler asserted when concatenating a struct to an untypedlist
  • untypedlist was not detected as invalid as an enum associated-value type or as a pointer

Thank yous

Again, this release wouldn't have been possible without the C3 community. I'd like to extend a deep thank you to all who have contributed, both through filed issues, PRs and just plain discussions.

PR contributors for this release

Stdlib: AtomicSynth, Book-reader, GuineaPigUuhh and Manu Linares

Compiler & toolchain: Book-reader, Johannes MΓΌller, Manu Linares, Osama Badeeb, rickyadastra and Rodrigo Camacho

CI/Infrastructure: Hade and Manu Linares

Change Log

Click for full change log

Changes / improvements

  • @weak now works with all declarations.
  • Add @align for asm blocks to stack align them. Stack alignment is no longer default.
  • Allow setting Windows subsystem directly.
  • Add bitoffset and bitsize reflection properties to bitstruct members. #3219
  • Improve error message on trying to cast char array to String. #3343
  • Add Foo::is_generic(...), Foo::generic_qname and Foo::generic_args. #2909 #3329
  • Add own, init and drop parameter annotations.
  • constdef can now be generic.
  • Libraries can now expose reusable target configurations via a templates map in their manifest, which projects reference from a target using template: "library/template". Properties from the template are loaded first and can be overridden by target-local settings.

Stdlib changes

  • Atomic.compare_exchange added.
  • Added array::contains_slice and array::index_of_slice.
  • String.index_of and rindex_of will now accept finding empty strings.
  • Add log::register_dynamic_category, for libraries that wish to define their own categories dynamically.

Fixes

  • $stringify would sometimes include parens.
  • Regression when destroying a BackedArenaAllocator in some cases #3332.
  • "a::b:c:d:e:f:0" was not parsed as a valid ipv6 string.
  • constdef vector with alias incorrectly lowered #3335.
  • Compiler asserts on concatenating a struct to an untypedlist #3326.
  • untypedlist was not detected as invalid in enum associated value type or as a pointer #3342.
  • Regression using non-posix libc.
  • Crash with an optional struct recursively defined with a function type. #3358
  • Denormal results were not handled correctly by String.to_double().
  • A float literal with an uppercase 'F' suffix would be a double instead of a float.
  • Json serialization would not correctly handle unicode and \v. #3353
  • Semantic checking was incorrect in the case of &a - &b where one or both are optional and the result isn't assigned.
  • Regression on MacOS, breaking stack trace.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

C3 0.8.1 Raiding the stdlib for bugs

"Bug fixes" is probably the least exciting thing you can lead with, but after 0.8.0 settled things in the language, it was natural to spend 0.8.1 hunting bugs.

Quite a few were found: over 100 fixes landed β€” most being corner cases, but a few are real, including a textbook Zip-Slip vulnerability in the zip extractor(!).

Outside of compiler development, there's also been significant progress on the specification here, which is necessary for C3 to be able to sign off on 1.0 in two years.

Language changes

Access reflected fields using a.$field and a.$field = b.

Previously, you could mutate and access fields on a struct by using $field.get(struct_value) and $field.set(struct_value, new_field_value). This is replaced by a much more streamlined struct_value.$field and struct_value.$field = new_field_value with the old variants deprecated:

struct Foo
{
    int i;
    bool b;
}

fn void test()
{
    var $field = $reflect(Foo.i);
    Foo f = { 45, true };
    // 0.8.0 prints 45
    io::printn($field.get(f)); 
    // 0.8.1 prints 45
    io::printn(f.$field);
    // 0.8.1 alternative syntax, also prints 45
    io::printn(f.$eval($reflect(Foo.i)));

    // 0.8.0 sets f.i to 44
    $field.set(f, 44); 
    // 0.8.1 sets f.i to 44
    f.$field = 44;
    // 0.8.1 alternative syntax
    f.$eval($reflect(Foo.i)) = 44;
}

Access to project path at compile time

env::PROJECT_PATH now returns the path to the project at compile time. This is mainly useful with $embed and similar compile time commands.

$vaarg[^1] supported

macro foo(...)
{
    // 0.8.0
    var x = $vaarg[$vaarg.len - 1];
    // 0.8.1 – with ^ support
    var x = $vaarg[^1];
}

Stdlib changes

Changes to API:

  • LinkedHashMap renamed OrderedMap, LinkedHashSet renamed OrderedSet. Old names are deprecated.
  • Enhanced path::ls functionality: it now supports wildcard search.
  • ini::parse and related take an error_line argument to identify the line with error.
  • Stricter JSON marshaling: it will return INVALID_NUMBER when encountering an inf or NaN for a float, and reject 1. literals.
  • @loop_over_ai would leak fds, deprecated and replaced by @loop_over_addresses.
  • spawn now allows binding I/O and using different settings per pipe.
  • io::write_all now retries on incomplete writes.
  • FixedThreadPool and ThreadPool are deprecated; they will be replaced by a new thread pool in a later 0.8.x version.

Additions to the stdlib

Concurrency & Threads

The BufferedChannel and UnbufferedChannel both gained non-blocking push/pop, using try_push and try_pop. In addition, UnboundedChannel was added. Such a channel will grow on demand, unlike BufferedChannel which has a fixed size.

This release also offers a preview of ThreadGroup for running tasks in parallel and collecting the results.

Platform support

libc::errno is now available on FreeBSD.

Compile time

The new values::expand macro turns strings containing expressions into values. This can be useful in some cases, as $reflect only creates statements.

Fixes

The 0.8.1 release contains over 100 fixes to stdlib, almost exclusively on the stdlib. The vast majority are stdlib corner cases.

The largest category is encoding and parsing: UTF-8/16/32 conversion edge cases (BOM handling, surrogate pairs, length detection), JSON's float precision and surrogate pair handling, varint signed-integer round-trips, base32/base64/codepage encoders that leaked memory on error, and PEM parsing on malformed input.

Memory management got a sweep. BackingArenaAllocator, DynamicArenaAllocator, OnStackAllocator, and the Vmem temp allocator each had at least one bug in a realloc, calloc, or destroy path. SortedMap had a possible array overflow. Deque.free didn't reset capacity, leaving the struct in a state that crashed on reuse. Wasm allocation could over-reserve unnecessarily.

Threading and synchronization got a check too: __atomic_compare_exchange had an incorrect implementation, lock_timeout on POSIX would sleep the full requested duration on every retry, the stack_size setting for thread creation was ignored on POSIX, and thread priority on Win32 was off by one. UnbufferedChannel could yield unpredictable values because its memory wasn't zeroed at creation.

Security: the Zip extraction code had a textbook Zip-Slip vulnerability β€” a malicious archive could write outside the target directory via .. components, absolute paths, or sibling-prefix paths in entry names. Fixed with up-front rejection of obviously-bad names plus a post-normalization containment check.

A handful of small correctness fixes touched types people use every day: DateTime.diff_years now handles leap years correctly, DString.replace handles empty needles and uninitialized strings without crashing, Formatter saturates instead of overflowing on absurd widths like %2147483648d, the URL parser stopped failing on user@host form and stopped silently dropping ports from bracketed IPv6 hosts, and process::run_capture_stdout no longer strips a character from output that doesn't end in \n.

Most of these are invisible if you've been using C3 successfully as they tighten the code paths nobody normally hits. But the stdlib now does the obviously right thing in considerably more corners than it did in 0.8.0.

Summarizing

To summarize: 0.8.1 is basically a stdlib release. Over 100 fixes β€” most are corner cases, a few are real (the Zip-Slip vulnerability in the zip extractor being the most notable). The user-visible additions worth a look: UnboundedChannel, try_push/try_pop on the existing channels, and a preview of ThreadGroup. The old FixedThreadPool and ThreadPool are deprecated; replacements are planned for later in 0.8.x.

So, same language as 0.8.0, just a bit sturdier library under it.

Thank yous

Again, this release wouldn't have been possible without the C3 community. I'd like to extend a deep thank you to all who have contributed, both through filed issues, PRs and just plain discussions.

PR contributors for this release

Stdlib: Alexandru PaniΘ™, as2te, cmann1, Elusive239, Eugene Blikh, Fernando LΓ³pez Guevara, Johannes MΓΌller, Kevin HovsΓ€ter, Manu Linares, Maxine Bonnette and Zathyy

Compiler & toolchain: Akshat, Book-reader, Fernando LΓ³pez Guevara, Johannes MΓΌller, Kevin HovsΓ€ter, Manu Linares and Matthew Nagy

CI/Infrastructure: Fernando LΓ³pez Guevara, Manu Linares and plapinski

Change Log

Click for full change log

Changes / improvements

  • Add $$PROJECT_PATH, accessible through env::PROJECT_PATH.
  • Deprecate $field.get(a) and $field.set(a, b). Replaced by a.$field and a.$field = b.
  • Add a.$eval($field) as a variant of a.$field.
  • Add JSON pretty print.
  • $$atomic_store and $$atomic_load take an alignment parameter.
  • $vaarg[^1] is supported. #3276
  • Improve error message when a keyword is used as a block parameter. #3275
  • Correct tag method error messages from tagof/has_tagof to get_tag and has_tag
  • Don't resume parsing when implicit module names yield invalid names.

Stdlib changes

  • Add math::TAU / math::TWO_PI
  • Add values::expand to turn strings containing expressions into values.
  • Enhanced path::ls functionality, like searching for wildcard.
  • LinkedHashMap renamed OrderedMap, LinkedHashSet renamed OrderedSet. Old names are deprecated.
  • Added initial cpudetect on Linux / MacOS Aarch64.
  • Enable libc::errno for FreeBSD.
  • Checking filesize on Win32 now correctly reports errors. Getting the filesize now rejects directories.
  • ini::parse and related takes an error_line argument to identify the line with error.
  • JSON marshaling will return INVALID_NUMBER when encountering an inf or NaN for a float.
  • JSON decoding will reject 1. literals.
  • spawn now allows binding I/O and using different settings per pipe.
  • @loop_over_ai would leak fds, deprecated and replaced by @loop_over_addresses.
  • Correctly return error on native_fwrite and native_fread.
  • Prevent infinite spin on io::read_fully, File.load_buffer, File.load and File.save.
  • io::write_all now retries on incomplete writes.
  • GrowableBitSet.max_bit_set added.
  • Added UnboundedChannel.
  • BufferedChannel and UnbufferedChannel get non-blocking push/pop.
  • FixedThreadPool and ThreadPool deprecated.

Fixes

  • @volatile_store on arrays were sometimes incorrectly lowered.
  • NPOT vectors as associated variables were incorrectly lowered on load. #3228
  • .get_tag and .has_tag did not work properly for globals and locals.
  • Vectors stored in unions lowered incorrectly causing an assert #3234
  • Segmentation fault during library fetch when the "dependencies" key is missing in project.json. #3233
  • .tags would crash if no attribute with arguments were present.
  • Rect.merge_point would sometimes result in a point outside of the rect.
  • Possible array overflow in SortedMap.
  • Possible memory overwrite in BackingArenaAllocator on realloc.
  • Realloc could cause data corruption in DynamicArenaAllocator.
  • OnStackAllocator would not correctly clear memory on calloc.
  • Vmem temp allocator would not correctly free all vmem on destroy.
  • Wasm memory allocation could overallocate unnecessarily.
  • VirtualMemory contract off by one error.
  • CPU detect of leaf7 on x86 incorrect.
  • Fixed project benchmark target parsing. #3237
  • Incorrect type on UIntLE and UIntBE.
  • CVaList would behave different incorrectly for types larger than 8 bytes on some platforms.
  • UTF32 BOM detection was broken.
  • Sort from DString.less was inconsistent.
  • Fix io::skip using 'read' vs 'read_byte', causing an error.
  • Slice2d.slice incorrectly handled slices with x/y offset and 0/negative length together.
  • String.to_integer incorrectly accepted some invalid characters for hex.
  • Removed broken StringIterator.get.
  • Fix to refcount behaviour, preventing issue on release.
  • File.close should always invalidate the pointer on close, even on failures.
  • Overlong conversions to unicode for %c at boundaries.
  • Do not rely on implicit allocation for getcwd.
  • Skipping symlinks wasn't properly implemented for Win32.
  • Reverse indexing a value that overloads indexing would index an anonymous copy of the value.
  • Fix case where member.set would hit an assert.
  • Same type casts would not become rvalues.
  • Hex decoding would leak memory on failure.
  • Codepage.by_name would not use normalized name.
  • @return? bar! didn't work if the identifier matched a macro.
  • Copying compile time strings during compile time folding with strings containing 0 would sometimes get truncated. #3267
  • Pem parsing did not correctly handle an empty body, nor when the first line was too short.
  • Additional pem parsing bugs on malformed data handled.
  • Compiler would crash when getting the kind, qname, or alignment of an untypedlist.
  • untypedlist incorrectly had size property.
  • JSON handling of UTF16 surrogate pairs fixed.
  • base32, base64 and codepage would leak memory on encode/decode errors.
  • Indexing into a type with a $reflect value would sometimes cause a crash.
  • Using a faultdef hidden behind @if would cause a crash.
  • Taking the type of a macro method would cause a crash.
  • Cap array size to avoid overflow when making multidimensional arrays that are too large.
  • DynamicArenaAllocator would incorrectly handle some reuse cases.
  • __atomic_compare_exchange had an incorrect implementation.
  • channel::create_unbuffered would not correctly zero out memory, potentially yielding unpredictable results.
  • lock_timeout on Posix would sleep the entire sleep before retrying, and it would fail if it managed to sleep.
  • stack_size setting for threads was ignored on Posix.
  • Setting thread priority on Win32 was off by one.
  • Non-power-of-two-sized member of @bigendian bitstruct backed by char array wasn't working #3283.
  • Binary bitwise operations were not considered simple.
  • $expand was incorrectly made generic in generic modules. #3274
  • Mangle lambdas in macros without @ to ensure they work correctly on elf #3217.
  • DString.replace("", "X"); would crash.
  • DString.read_from_stream would not return the correct length when available was not supported by the stream.
  • @str_camelcase would yield same result as @str_pascalcase. #3287
  • conv::utf8to32 would not zero terminate when the zero would be at the end of the buffer.
  • char16_to_utf8_unsafe would not load low byte unaligned when required.
  • Not all invalid UTF8 was detected.
  • UTF16 length detection was incorrect for utf16 with surrogate pairs.
  • Initializing a variable which has the type of an optional struct using a const value would fail codegen. #3288
  • Parsing a malformed hex float would not correctly get reported.
  • Parsing an integer with trailing space would incorrectly be reported as an error.
  • String.escape used the incorrect default for stripping quotes.
  • mem::equals would not correctly compare slices with element size > 1.
  • AsciiCharset.contains incorrectly handled char > 127.
  • Reuse of recently freed DynamicArenaAllocator allocations failed.
  • Crash in codegen in some cases when RHS of a && or || was unreachable at lowering.
  • Visibility modifiers were incorrectly allowed on enum/constdef members.
  • Datetime format could not handle negative offsets with non-zero minutes.
  • NormalDist.random could occasionally return inf.
  • Url parser would fail on [email protected].
  • Url parser would drop the port on http://[::1]:8080.
  • Ipv6 classification - is_link_local etc, was incorrect
  • env::get/set_var for Win32 would appear to fail when succeeding.
  • env::get_var had a race condition on Win32.
  • process::run_capture_stdout would remove the last character, even when it wasn't \n.
  • Add missing __powisf2 to compiler_rt.
  • // would count newlines twice when parsing JSONC.
  • Path::for_posix(".a/..") was not parsed correctly.
  • SortedMap.clear and SortedMap.free would work incorrectly on map initialized with ONHEAP.
  • GrowableBitSet would yield the wrong length.
  • GrowableBitSet would not work correctly on backing types bigger than uint.
  • DString.replace would not work correctly in some cases.
  • ByteWriter.ensure_capacity did realloc unnecessarily when the data exactly matched capacity.
  • DString.equals used int rather than sz for len comparison.
  • DString.replace_char would crash on empty DString.
  • io::read_varint and io::write_varint: handling for signed integers was broken.
  • io::write_tiny_bytearray and io::write_short_bytearray could have incomplete writes.
  • Splatting a partially raw array into a macro would miscompile. #3302
  • Getting the tag for an enum parameter caused a crash. #3307
  • Json marshalling of floats would lose precision.
  • Crash when initializing a bitstruct from an untyped list.
  • Shifting a vector by a non-numeric type would cause a crash rather than a compiler error.
  • Recursive macros were not detected when going by way of a lambda.
  • Compile time concatenation with an empty slice was lacking checks, causing a compiler crash.
  • Fix zip slip vulnerability.
  • Fixed issues with Object.to_value.
  • DString.len was incorrectly marked @dynamic.
  • Qoi decoder wasn't correctly signaling all invalid data.
  • Casting a constant string to a float vector was buggy, causing a compiler crash.
  • Codepage detection could fail values after the last element.
  • Xml parsing could leak memory if root was preceeded by Pi nodes.
  • DateTime.diff_years would not handle leap years properly.
  • Deque.free would not reset the capacity, making it break if later reused.
  • Formatter would overflow in cases like %2147483648d.
  • Distributions would drop convergence control setting on recursion.
  • In some rare cases available() could leave the stream in an unexpected state.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

Discuss this article on Reddit.

C3 0.8.0 The Core Language Is Settling

C3 is a programming language that evolves C – the same low-level model but with modern ergonomics. With 0.8.0, we're getting our first real glimpse of what C3 1.0 will look like: the core language design is now locking into its final shape.

Two changes are big enough to deserve their own posts. The compile time reflection system has been reworked into its final form β€” and in the process we got to kill off half the builtins: link. And we're making the move to signed by default, correcting what turned out to be a five-year mistake: link.

As with all 0.x.0 releases, we've tried to collect all the breaking changes into this single release, so the rest of the 0.8.x line can stay backwards compatible.

Aside from the reflection changes, these are the major changes:

Distinct types

Distinct types now defaults to be "structlike"

0.7.4 introduced the concept of @structlike which meant a distinct type could not implicitly convert from a literal. With 0.8.0 we flip the defaults: by default distinct types do not convert from literals, but with @constinit they do:

// 0.7.4
distinct Foo7 = int;
distinct Bar7 @structlike = int;

Foo7 f = 1;      // Ok
// Bar7 b = 1; ERROR!
Bar7 b = (Bar7)1; // Ok 

// 0.8.0
distinct Foo8 @constinit = int;
distinct Bar8 = int;

Foo8 f = 1;      // Ok
// Bar8 b = 1; ERROR!
Bar8 b = (Bar8)1; // Ok 

Often people were assuming "structlike" behaviour, cause accidental bugs. For this reason we flipped the defaults.

Enums and constdef

No more +/-

Doing addition on enums is no longer possible:

// 0.7.4
MyEnum foo = BAR;
MyEnum baz = foo + 1; // OK!

// 0.8.0
MyEnum foo = BAR;
// MyEnum baz = foo + 1; ERROR!
MyEnum baz = (MyEnum)(foo.ordinal + 1); // OK

Enhanced ++/--

Enums still support ++/-- and get implicit wrapping when overshooting the ends:

enum MyEnum
{
    ABC,
    DEF,
    GHI
}
fn void test()
{
    MyEnum e = ABC;
    e++; // e is now DEF
    e++; // e is now GHI
    e++; // e is now ABC through overflow wrapping
    e--; // e is now GHI through underflow wrapping
}

Removed inline enums

To simplify the language, inline enums were dropped.

// Ok in 0.7.x, error in 0.8.0
enum YourEnum : inline int
{
    TEST
}

int x = YourEnum.TEST; // Valid due to inline

Implicit conversion to ordinal when used as index

Enums will now implicitly convert to ordinals when used as index:

enum YourEnum8
{
    HELLO,
    WORLD
}

fn void test()
{
    int[2] x;
    x[YourEnum8.WORLD] = 123; // Same as x[1] = 123
}

.nameof has become .description

enum Foo
{
    TESTING,
    THE,
    NEW,
    DESCRIPTION
}

// 0.7.11
String s = TESTING.nameof;      // "TESTING"

// 0.8.0
String s = TESTING.description; // "TESTING"

Constdef now infers through unary negations

Unary negation would previously prevent constdef inference

constdef Abc
{
    ABC = 4,
    DEF = -4
}

Abc a = ABC; // Ok
Abc b = -ABC; // Ok in 0.8.0
Abc c = ~ABC; // Ok in 0.8.0

Operator overloading

Overloads for comparisons

@operator(<) is now added, so that a type which overloads both < and == can participate in all comparisons.

@operator(!=) removed

@operator(!=) was removed as it had limited usefulness.

Compile time enhancements

Shadow an a = ... parameter if it's not defined

Shadowing is now allowed:

macro @foo(a = ...)
{
    $if !$defined(a)
        var a = 123; // Previously a shadowing error
    $endif
    return a * 2;    
}

Use $eval as the name of a named parameter

foo(arg: 2);

// Possible since 0.8.0:
foo($eval("arg"): 2);

untypedlist as a new builtin type

During compile time, some compile time arrays get the type "untypedlist", containing a (possibly heterogenous) list of values. Previously it was possible to create it, but it wasn't possible to directly reference the type. This changes with 0.8.0:

// 0.7.11 - 'var' is the only possibility
var $foo = { 1, "hello", 3.14 };

// 0.8.0
untypedlist $foo2 = { 1, "hello", 3.14 };

The main use is to be able to test whether an expression is an untyped list or not. Regular variables cannot have this type, it's a compile time type only.

Added a tags property

An often requested functionality is getting all the tags on a type or member. This is now finally possible in 0.8.0:

struct Foo @tag("a", "hello") @tag("b", "test")
{
    int x @tag("c", 5);
}

int a_global @tag("d", 3.14);

String[] $tags_of_foo = Foo::tags; // { "a", "b" }
String[] $tags_of_x = $reflect(Foo.x).tags; // { "c" }
String[] $tags_of_a_global = $reflect(a_global).tags; // { "d" }

Also note that name for retrieving and testing for tags have changed to get_tag and has_tag.

Allow taking the type of an interface method

It's now possible to take the type of an interface method:

interface TestInterface
{
    fn void hello_world(String name);
}

$Typeof(TestInterface.hello_world) x;

$expand compile time function

The new $expand compile time function allows turning any string to code. For this reason it's inherently LSP/IDE hostile, but simplifies cases where otherwise $exec was the only option:

import std::io;

// This code prints "Hello"
$expand(`fn void hello() { io::printn("Hello"); }`);

fn void main()
{
    $expand(@sprintf("%s();", "hello"));
}

Use with care.

Removal of $xxxxof style builtins, updated type access

This has its own article: link

Contracts

@mustinit

This attribute enforces initialization of a type:

struct Foo @mustinit
{
    int a;
}

fn void test()
{
    // Foo f @noinit; ERROR, must be initialized
    Foo f2; // Ok, zero initialization is fine
}

Generics

Generic inference now looks through pointers

This did not work in 0.7.11, but will infer {int} in 0.8.0:

// "create_buffered" is a generic function
BufferedChannel{int}* c = channel::create_buffered(mem, 1);

Nested generics in generic functions/methods

An omission prevented this from working:

fn List{List{Type}} return_nested_list() <Type>
{
    List{List{Type}} l;
    l.init();
    return l;
}

Syntax changes

Removal of deprecated syntax:

  • Removed iXX and uXX suffixes.
  • Removed Enum.lookup.
  • Removed ? as suffix operator in the expression io::EOF?.

?? and ?: has new precedence and binds tighter than + and -

This change is tied to the removal of ? as a suffix operator. Previously there were essentially parser hacks to get the right precedence in various situations, such as foo() ?? io::EOF?!. The new precedence places ?? and ?: tighter than + -, but looser than | & ^.

With this tighter precedence, some things change meaning:

// Parses as (foo() ?? io::EOF~) + 3 in 0.8.0
int a = foo() ?? io::EOF~ + 3;

// Parses as (b ?: 4) + foo() in 0.8.0
int a = b ?: 4 + foo();

// Still parses as as foo() ?? (b | 3)
int a = foo() ?? b | 3;

$Typeof and $Typefrom instead of $typeof, $typefrom

This is a simple name change, to make it clearer that they can be in a type slot, as opposed to being a value.

alias Foo = int::typeid now works

macro typeid @get_type()
{
    return SOME_CONST ? int : double;
}

// 0.7.11
alias Foo = $Typefrom(@get_type());

// 0.8.0
alias Foo = @get_type();

Toolchain changes

docgen command for html documentation generation

This major addition to the C3 compiler now allows you to produce high quality documentation out of the box. This is also (finally!) ensuring that this site has up to date docs: link.

Support for Emscripten

The C3 compiler now has experimental support for Emscripten out of the box.

Reduced library dependency scanning

Only used libraries are now scanned for dependencies. Let's say you have two libraries in your /lib folder "a.c3l" and "b.c3l", "b.c3l" depends on "c.c3l" which isn't in the folder. However, your code only uses "a.c3l". Previously this would have been an error, signalling it could not find "c.c3l" despite it not being used which complicated cross platform compilation setups.

Windows installer for the C3 compiler

To simplify getting the C3 compiler up on Windows for beginners, there's now a Windows installer.

Stdlib changes

Changes to API:

  • BufferedChannel and UnbufferedChannel are now pointers
  • JSON api changes: parse -> load, parse_string -> parse. It now supports two flavors of JSON: JSON and JSONC
  • std::math name changes: PI_2 => HALF_PI, PI_4 => QUARTER_PI, DIV_PI => INV_PI etc, cosec => csc, cotan => cot, muladd => mad
  • std::time name changes: diff_hour => diff_hours. DateTime.set_date => DateTime.set, datetime::from_date_* => datetime::at_*
  • std::hash method name convention changes: updatec / update_char => update_byte.
  • std::string name changes: strip => strip_prefix, strip_end => strip_suffix.
  • Ordering of object::new_* arguments are now "allocator first".
  • InStream.set_cursor is renamed seek, and the old seek is removed.
  • The Path API now is split into PathPosix and PathWin, Path is implicitly castable to String and loses the str_view() method. Use path::tnew instead of path::temp for a temporary path.
  • PanicFn now takes an int for row.
  • ElasticArray renamed FixedList.
  • Updated ref::new argument order.

Additions to the stdlib

  • Add SortedMap based on skip lists.
  • Add OneShotChannel to std::thread::channel for single-send/single-receive thread synchronization.
  • Add std::collections::Deque.
  • Ini parser and encoder.
  • Mergesort added.
  • std::encoding::xml for XML parsing and serialization.

  • Make DString.append_repeat polymorphic adding append_string_repeat and append_char_repeat. DString.append_inline for optimized uses.

  • List and FixedList adds a remove_unordered_at.
  • channel::create_unbuffered and channel::create_buffered to create channels
  • String.compare_to and String.compare_to_ignore_case added.
  • RingBuffer now conforms to foreach and adds additional functions.
  • conv::detect_bom, converts utf16/utf32 from bytes with byteswaped / unaligned data.
  • Object.to_value was added to convert an object to a value.
  • Add multi part and extension support to Path.append.

Fixes

The 0.8.0 release contains around 50 fixes to stdlib but also addressing issues around vector lowering.

Summarizing

This is just the diff between 0.7.11 and 0.8.0. Compared against 0.7.0 the list would be huge β€” which might raise a worry: is the language not stabilizing?

But the opposite is true. The difference between 0.7.11 and 0.8.0 is actually fairly small. Most of the work from 0.7.0 to 0.7.11 went into refinements β€” closing gaps in semantics and functionality, and responding to concerns from real-world use. If you're looking at C3 from the outside, a small breaking-change diff this late in the 0.7 series is a good sign: the design is settling, and 1.0 is coming into view. The docgen work is a key piece of that puzzle, and it's now in place.

For 0.8.x we're looking forward to continuing to flesh out the stdlib, improving generics inference, and tackling the remaining tasks needed for 1.0.

Thank yous

Again, this release wouldn't have been possible without the C3 community. I'd like to extend a deep thank you to all who have contributed, both through filed issues, PRs and just plain discussions.

I'd like to especially thank Manu Linares who this month single-handedly implemented the Docgen in the compiler, and refreshed the website as well.

PR contributors for this release

Stdlib: Christian Reifberger, cmann1, corleypc, Darvisim, Fernando LΓ³pez Guevara, Manu Linares, Mathis Laroche, Nyr24, Peng He, Sander van den Bosch, surrealism21.

Compiler & toolchain: Dmitry Atamanov, Fernando LΓ³pez Guevara, Johannes MΓΌller, Manu Linares, TomΓ‘s Lopes, Zack Puhl,

CI/Infrastructure: Fernando LΓ³pez Guevara, Manu Linares, LowByteFox

Change Log

Click for full change log

Changes / improvements

  • Removed "old-enums, old-slice-copy and old-compact-eq" feature flags.
  • Removed deprecated $evaltype.
  • Removed all deprecated (as of 0.7.11) types and functions from the stdlib.
  • Removed deprecated iXX and uXX suffixes.
  • Removed deprecated Enum.lookup.
  • Removed deprecated ? as suffix operator in the expression io::EOF?.
  • Removed deprecated module foo {Type} generic syntax.
  • Distinct types now defaults to be "structlike"
  • Removed @structlike attribute.
  • Removed deprecated @extern attribute.
  • : in contracts before description is now mandatory.
  • Removed deprecated Enum.associated (use Enum::members).
  • Removed deprecated Enum.elements (use Enum::len).
  • Removed deprecated foo_function.params (use $reflect(foo_function).params).
  • Removed deprecated $is_const.
  • Removed deprecated $assignable.
  • Enums now no longer directly support + and - – use ordinals instead.
  • For enums, using ++ and -- will step through enums with implicit wrap-around.
  • Rename isz -> sz.
  • Make $sizeof, $alignof and all similar functions return sz instead of usz.
  • Align literal types with C semantics.
  • Use value promotion instead of signedness promotion to int. So that small unsigned types promote to int, not uint.
  • Add a @mustinit attribute to enforce zero-initialization of a type. #3094
  • Improve error message when keyword is used instead of an expression. #3088
  • Add --warn-recursivecontracts.
  • Mutex.destroy and friends no longer return optionals.
  • Remove @operator(!=) overload.
  • Add @operator(<) overload, enabling type comparison overloads.
  • Generic inference can now look through pointer.
  • Enums now implicitly convert to their ordinal when used as indices.
  • Enums can no longer declare themselves inline.
  • Nested generics allowed inside generic functions/methods.
  • a = ... parameters may be shadowed if not defined.
  • $eval can now be used with named parameters, e.g. foo($eval("arg"): 2) #3090
  • Type properties are now accessed using :: and the "of" suffix, removed: int.sizeof -> int::size
  • Added $reflect with properties name, cname, qname, offset, alignment, size.
  • Added @kindof, @alignof and @sizeof macros.
  • Removed $nameof, $extnameof, $qnameof, $offsetof, $alignof, $kindof, $sizeof.
  • .nameof is changed to .description on fault and enum types.
  • Type property is_eq is renamed has_equals.
  • Type function tagof is renamed get_tag.
  • Add untypedlist as a usable type #2647.
  • ?? and ?: has new precedence and binds tighter than + and -
  • Added the tags property for types and $reflect.
  • Allow taking the type of an interface method.
  • Add $expand compile time function to convert strings to code.
  • Constdef now infers through unary negations.
  • Only used libraries are scanned for dependencies. #3144
  • $vaconst, $vaexpr and $vatype removed.
  • Improve error message on unsupported typeid runtime access at runtime. #3170
  • Added support for Emscripten.
  • Replace $vacount by $vaarg.len, replace $vasplat by ...$vaarg.
  • $vaarg behaves as $vaexpr.
  • Added docgen command to generate documentation.
  • Added jmpabs x86 CPU feature.
  • Implicit unsigned <-> signed integer conversions removed.
  • Added C3 Compiler setup installer for Windows
  • alias Foo = int::typeid now works.
  • $typeof => $Typeof, $typefrom => $Typefrom.

Stdlib changes

  • Add List.remove_unordered_at.
  • PanicFn now takes an int for row.
  • Add std::collections::Deque.
  • Add compare_to and compare_to_ignore_case to String. #3096
  • Add SortedMap based on skip lists.
  • Add OneShotChannel to std::thread::channel for single-send/single-receive thread synchronization.
  • BufferedChannel and UnbufferedChannel are now pointers, create using create_unbuffered and create_buffered
  • RingBuffer now conforms to foreach and adds additional functions.
  • Ini parser and encoder.
  • Updated ref::new argument order.
  • Support setting thread stack size.
  • Support setting thread priority.
  • Support syscall on RISCV.
  • Make DString.append_repeat polymorphic adding append_string_repeat and append_char_repeat.
  • Add DString.append_inline for optimized uses.
  • Ordering of object::new_* arguments are now "allocator first".
  • Add remove_unordered_at to FixedList.
  • Changed json to support two flavors of JSON: JSON and JSONC.
  • Changed json API: parse -> load, parse_string -> parse.
  • conv::detect_bom, convert utf16/utf32 from bytes with byteswap / unaligned data.
  • Mergesort added.
  • set_cursor is renamed seek, and the old seek is removed.
  • std::math name changes: PI_2 => HALF_PI, QUARTER_PI => PI_4, DIV_PI => INV_PI etc, cosec => csc, cotan => cot, muladd => mad
  • std::time name changes: diff_hour => diff_hours. DateTime.set_date => DateTime.set, datetime::from_date_* => datetime::at_*
  • std::hash method name convention changes: updatec / update_char => update_byte.
  • std::string name changes: strip => strip_prefix, strip_end => strip_suffix.
  • std::collections::object added Object.to_value to convert from an object to a value.
  • std::encoding::xml added for XML parsing and serialization.
  • Fix Path.append separator not honoring the specified environment.
  • Add multi part and extension support to Path.append.
  • The Path API now is split into PathPosix and PathWin, Path is implicitly castable to String and loses the str_view() method. Use path::tnew instead of path::temp for a temporary path.

Fixes

  • Slice comparison lowering would not work correctly in macros in some cases. #3095
  • Attributes @allow_deprecated, @constinit, @noalias, @nostrip, and @optional would erroneously accept parameters. #3098
  • Fix pipe handle leaks across concurrent process spawns #10067.
  • $$trap was incorrectly marked noreturn.
  • Recursive inclusion of contracts was not detected.
  • \r was not filtered when piping a source file from stdin.
  • SHA-3 and Keccak contexts are now explicitly @mustinit structures. #3110
  • UnbufferedChannel would deadlock on multiple producers.
  • Don't override sigaltstack when running with --sanitize=address. #3115
  • Binary search broken for some supported functions.
  • Fix bug casting (void*[<3>])x.
  • Compiler crash compiling a switch with a constant case range overlapping a constant case value. #3127
  • Incorrect handling of overaligned struct fields #3136
  • EnumSet with more than 128 entries was broken.
  • Handle underflow in zip.
  • Bugs in check for name suggestions on name mismatch.
  • Fix bug where only one ensure would not be inlined correctly. #3162
  • Incorrect error message when casting to non-existent enum.
  • Macro $Type = ... would not work correctly with $defined
  • Fix enum value handling in Object (std::collections::object) to conform with changes in enums.
  • Compiler assert in certain cases with ?? and void returns. #3168
  • Bug in compiler-rt for i128 shift.
  • LinkedBlockingQueue.push_timeout did not work correctly.
  • Splat into vaarg macro, where vaarg is not used #2782.
  • Comparison with floats had incorrect codegen, leading to incorrect results for NaN #3175.
  • Zeroing out simd vectors in a struct could in some cases lead to incorrect lowering #3179.
  • Incorrect lowering when returning a struct to an optional value on Win64 in some cases #3180.
  • Fix bug where a method is considered doubly generic if declared in a generic module for a generic type. #3176
  • Fix exp10 on platforms without exp10 as an LLVM builtin.
  • LLVM 23 compatibility: map Os/Oz to O2 pass pipeline, fix returnaddress intrinsic signature, add optsize/minsize function attributes.
  • Warning for ignored visibility modifiers was not emitted for macro methods #3071
  • while (String? x = foo()!) was accidentally allowed causing a lowering error.
  • Crash casting uint to bitstruct inside struct field assignment #3187
  • Vec2/Vec3 transform missed matrix translation.
  • Matrix rotation ignored matrix itself.
  • Fix BigInt shr, to_format, and others.
  • Fix ends in TDist.quantile, FDist.pdf, ChiSquaredDist.pdf
  • Fix to easing expo_in and bounce_inout.
  • deque with shrinking a zero sized list caused infinite loop.
  • Printing an enummap yielded the wrong character count.
  • Incorrect contract in FixedList allowed insert out of range.
  • Fix double-free in InterfaceList.
  • Object.set_at was incorrect.
  • Bitstruct with backing char[n] would occasionally be incorrectly stored.
  • fmuladd lowering crashes on a + -(b * c) with fastmath.
  • Constant folding -30 % -7 would incorrectly yield "2".
  • Parsing << in asm would not be correctly handled.
  • Incorrect lowering for float[<3>] when placed aligned in a struct.

Want To Dive Into C3?

Check out the documentation or download it and try it out.

Have questions? Come and chat with us on Discord.

Let's kill off half the builtins in 0.8.0

Goodbye $sizeof, $alignof and all the rest

The problem

From the beginning, C3 mirrored C in having a sizeof builtin, which would take an expression and return the size. This was extended with additional similar builtins that would take an expression and return some reflection information about it.

To avoid colliding with anything else, it would get the -of suffix, and in line with C3's "$ means compile-time", they were prefixed with $.

At some point this became an actual challenge for naming things. $paramstructof doesn't quite roll off the tongue.

The solution: $reflect

So starting from 0.8.0, all of those are removed in favour of using the new $reflect builtin containing all attributes. For example, our $sizeof(a) becomes $reflect(a).size. In general, *of(a) becomes $reflect(a).*.

This might seem onerous, but first of all this allows us to store the reflection data and pass it to macros: some_macro($reflect(a)). But it's also trivial to construct macros that do the same thing. For example, @sizeof and @alignof are new macros that act as drop-in replacements for the previous builtins.

In 0.7.x you would use things like $defined($sizeof(x)) to test if something had a size defined. After the changes in 0.8.0, this becomes $defined($reflect(x).size) instead.

This change allows us to reduce the number of keywords and overall "language surface". Without explicit keywords, compile-time reflection actually becomes more flexible as well – it's easier to introduce additional introspection at some later time in the 0.8.x cycle if needed.

Stripping types of their properties

The problem

Up to 0.7.x, the corresponding type properties (related to the expression reflection previously discussed) were accessed using dot notation, e.g. int.sizeof. This reflected the corresponding builtin: int.sizeof <=> $sizeof(x), int.alignof <=> $alignof(x) and so on.

It would certainly have been nicer to use int.size, but properties would shadow method names:

struct Foo
{
    int len;
}

fn int Foo.size(self)
{
    return self.len;
}

fn void test()
{
    Foo f;
    Foo.size(f); // Valid C3    
}
So the .*of suffix was used exactly to avoid colliding with methods and field names.

However, as previously mentioned, naming with "-of" was increasingly a blocker. Could something be done?

The solution: :: to the rescue

The :: scope operator unequivocally separates module names from an identifier, and module names are always lower case. mymodule::submodule::Bar always means that everything left of the last :: is a module.

But what if we allowed Bar::baz?

This had no meaning in C3, because "Bar" could never be a module name.

In fact, int::size could not match anything. So what if we made :: the way to get type properties?

int::size now works, because the only thing that can appear to the right of a type name after :: is a type property.

// 0.7.11
const INT_SIZE = int.sizeof;
const INT_ALIGNMENT = int.alignof;
const INT_MAX = int.max;
// 0.8.0
const INT_SIZE = int::size;
const INT_ALIGNMENT = int::alignment;
const INT_MAX = int::max;

So the change both allows dropping "-of", and make it more consistent – like in the int.max case.

The big $vaarg reduction

The problem

The final big removal in C3 is the elimination of $vaexpr, $vacount, $vaconst, $vatype and $vasplat.

These builtins helped support macro "raw" vaargs. $vaexpr would paste in a vaarg as if it was declared like #foo, $vaconst like $foo and $vatype like $Foo.

Unfortunately, most of these were sparsely used.

$vasplat did splat the arguments, perfectly forwarding them into an initializer or another call.

However, $vasplat was conceived well before most of the ... splat functionality was in the compiler. In fact, it could be considered the precursor of most of the splat code.

The fact that it looked so very different from regular splats seemed unnecessary.

And $vacount? A keyword just to get the number of vaargs available?

The solution: one $vaarg to rule them all

While $vaarg did an adequate job to try to ensure that the expression was only evaluated once, the raw vaargs for macros largely behaved like lazy expression parameters ("#foo"). It was also always possible to recreate types and constants from $vaexpr as needed.

So if we just had $vaexpr, almost all functionality would still be available!

Since we already have ... for splat and .len to get length, we could derive everything from a single value. We renamed $vaexpr and called it $vaarg:

// 0.7.11
$for var $i = 0; $i < $vacount; $i += 2:
    self.set($vaarg[$i], $vaarg[$i + 1]);
$endfor

call_log(CRITICAL, category, fmt, $vasplat);

// 0.8.0
$for var $i = 0; $i < $vaarg.len; $i += 2:
    self.set($vaarg[$i], $vaarg[$i + 1]);
$endfor

call_log(CRITICAL, category, fmt, ...$vaarg);

Final thoughts

There is a natural contract-expand cycle to language design: first add features, then learn how they're used and how they can be simplified for the actual real use cases.

The builtins we're removing solved real problems, but they also added surface area: extra keywords, naming conventions, subtle interactions.

These simplifications were only possible now, after things like maturing the splat functionality, observing $vaarg usage-patterns and reaching the point where the old type property syntax prevented growth.

We hope you'll like them!