DEV Community

COMMENTERTHE9
COMMENTERTHE9

Posted on • Originally published at cx-lang.com

Cx Dev Log — 2026-05-23

Two real features landed today in the Cx language project, changing both the type system and the runtime layer itself: void now has its own AST type, and exit() made its debut as a comprehensive control-flow builtin. Let’s break down why these changes matter and what they bring to the table.

Type::Void — No Longer Just an Identifier

Previously, void in Cx function signatures was parsed simply as an identifier, resolved through the same path as user-defined struct types. This was functional but suboptimal, sometimes leading to edge-case bugs where void might accidentally shadow or collide with user-defined types. PR #278 addressed this by making void a first-class citizen in our type system. Now, every layer gives void its distinct representation: Token::TypeVoid in the lexer, Type::Void in the AST enum, direct parser mapping, and SemanticType::Void in the semantic phase.

Two unit tests ensure nothing broke in the process: one verifies that a function signature like fnc: void main() {} correctly produces Type::Void, and another confirms that a function ending with a void call, like print(), clears semantic analysis without hiccups. With 74 lines added over four files, we integrated this cleanly into main with commit 2a2e1b2.

This change is more than an aesthetic cleanup. It clarifies how the parser resolves types — void is now specifically and explicitly defined as a keyword with its own token.

exit() as a Control-Flow Signal

The bigger update here is the introduction of exit(), which recently landed in submain via commit 4d612df. This update spanned 25 files across four crucial layers: types, semantic analysis, runtime, and the main event loop.

The core decision was making exit() propagate an RuntimeError::Exit(i32) signal instead of calling process::exit directly. This means exit() integrates seamlessly with our existing runtime’s control-flow model, sitting alongside constructs like EarlyReturn, BreakSignal, and ContinueSignal. This unified signal propagation offers consistent control flow handling in Cx.

In normal interpreter mode, the Exit signal is caught and handled in main.rs before rendering any error messages. Standard output is flushed before process::exit(code), ensuring all output is seen (a crucial safeguard since process::exit skips Drop). In --test mode, things get interesting: exit(0) results in a PASS while exit(n != 0) leads to a FAIL, but crucially, the test runner continues instead of crashing.

From a semantic perspective, exit() has restrictions: it only takes 0 or 1 arguments, checked during analysis. The runtime evaluates these, ensuring they fit within i32, flagging out-of-bounds as TypeMismatch. A new safety-net arm in diagnostics.rs was also added for unrendered RuntimeError::Exit.

Seven New Matrix Fixtures

Our testing matrix expanded to include .expected_exit files, allowing fixtures to assert specific exit codes and verify stdout. Seven new tests cover various scenarios:

  • t_exit_default — no-arg exits with code 0.
  • t_exit_zero_explicit — explicit exit(0).
  • t_exit_nonzeroexit(3).
  • t_exit_before_output — exit before prints.
  • t_exit_after_partial — exit after partial output, ensuring stdout flush.
  • t_exit_in_branch — exit from an if-branch.
  • t_exit_in_function — exit crosses function-call boundaries.

Additional integration tests in diff_harness.rs handle --test mode actions: exit(1) marks a FAIL, runner continues; exit(0) records as PASS, runner continues, supporting robust testing of exit conditions.

Matrix Status and a Line Ending Note

Right now on main, we have 172 passing tests and 10 failing out of 182. The failures are due to output-verification tests — not code regressions. The discrepancy stems from .expected_output files using CRLF line endings versus the interpreter's LF-only output on Windows. This is purely a platform-specific mismatch, hinting at a need for CRLF-tolerant comparison in the harness for Windows.

What's Next?

The immediate goal is merging the exit() builtin from submain to main — it’s the sole commit waiting to make the leap. Beyond that, tackling DotAccess in compound forms remains the last hurdle for phase 11 completion. Our work on the exit() feature set a clear pattern: semantic analysis, runtime integration, and robust testing — all this will guide us as we extend other functions like print and println to support additional formatting options.


Follow the Cx language project:

Originally published at https://cx-lang.com/blog/2026-05-23

Top comments (0)