Flux is a compiled, stack-first, general-purpose language with a refreshingly direct philosophy: you own your memory, you write your intent, and the compiler takes you seriously. If you haven't looked at it in a while - or at all - now is a great time to pay attention.
Over the past development cycle, Flux has gained several major features that collectively shift it from a capable low-level language into something with serious expressive power. Let's walk through what's new.
- Compile-Time Execution with comptime This is the headline feature. Flux can now execute Flux at compile time, powered by a dedicated VM built specifically for this purpose. The same VM will also power the upcoming REPL. The model is simple: wrap any code in a comptime block and it runs during the compilation pass, before any runtime code is generated.
#import <standard.fx>;
comptime
{
def foo() -> void
{
compiler.io.console.print("Hello from compile time!\n");
};
foo();
};
def main() -> int
{
return 0;
};
When you compile this, Hello from compile time! prints in the middle of the compiler's own output - during the AST codegen pass. It's not a preprocessor macro, it's not a constexpr evaluation hack. It's real Flux code running in a VM while your program is being compiled.
You can split comptime work across multiple blocks, and they share scope:
comptime
{
def greet(int n) -> void
{
compiler.io.console.print(f"Step {n} complete\n");
};
};
comptime
{
greet(1);
greet(2);
};
What's Already Supported
A large portion of the Flux keyword set is available at comptime right now. The full list of what's live:
and, as, bool, byte, case, char, constraint, data, def, default, do, double, elif, else, emitflux, enum, false, float, for, goto, if, int, is, label, long, not, or, return, signed, sizeof, struct, switch, true, uint, ulong, union, unsigned, while
Control flow, arithmetic, structs, enums, unions, loops, conditionals - the core of the language is already there. Standard I/O and file I/O work. Networking is deferred to a future update.
A few keywords like lext are explicitly deferred for the bootstrap phase. The rest are being filled in progressively.
What This Unlocks
Compile-time execution is the foundation for build-time code generation, compile-time validation, configuration baking, and eventually a full macro system that operates on real Flux ASTs rather than text substitution. The comptime VM running the same semantics as the runtime means there's no separate "template language" to learn - it's just Flux.
- Algebraic Types - Type Algebra with constraint
Flux has always had templates. Now it has relational constraints on templates, which is a meaningful step up.
The
constraintkeyword lets you define named constraint sets that express algebraic relationships between type parameters. These constraints are then attached to template functions to restrict what combinations of types are valid at instantiation.
constraint SafeArith(A, B)
{
A ~= B,
A !`< A
};
def add<T: int, U: int, :{SafeArith}>(T x, U y) -> T
{
return x + y;
};
The Operators
Operator Meaning ~= A and B must be compatible (same pointer depth, matching bit width) !~= A and B must be incompatible - enforces they remain distinct !@ Address-of (@) is forbidden on values of this type within the template body !< No truncation - type cannot appear in a narrowing context !<= No truncation between these two specific types !> No widening !>= No widening between these two specific types !-= A and B cannot participate in unsigned arithmetic together
Constraints can be written inline directly on the template parameter:
def foo<T: int, :{T !`< T}>(T x) -> void
{
// T can never be truncated anywhere in this function
};
Or chained - the right-hand side of one relation becomes the left-hand side of the next:
constraint Chain(A, B, C)
{
A ~= B !~= C !`< C
};
That reads as three independent relations:
A ~= B, B !~= C, C !< C`
You can also merge constraint sets:
constraint Combined = MyCS1 + MyCS2;
The merge checks for contradictions at definition time - you can't combine ~= and !~= on the same operand pair.
Why This Matters
Most languages give you bounded generics at best. Flux's type algebra lets you express things like "these two type parameters must not be able to truncate into each other," or "this type can never have its address taken inside this template." These constraints fire at instantiation time with precise error messages, not vague template errors deep in a call stack.
- Interfaces - Typed, Named, and Enforced Flux already had traits (behavioral contracts on objects). Interfaces build on top of them to define how two objects communicate with each other.
trait Readable
{
def read(byte*,int)->int,
write(byte*,int)->int,
flush()->int;
};
trait Writable
{
def ack()->int;
};
interface Stream(A: Readable, B: Writable)
{
A : B
{
read(byte*,int)->int,
write(byte*,int)->int,
flush()->int
};
B : A
{
ack()->int
};
};
The interface is generic - A and B are type parameters constrained by traits. The body defines which methods each side is permitted to call on the other.
You attach it to an object at the definition site:
object Pipe
{
///...///
} : Stream(this, Socket);
Once attached, any method call between Pipe and Socket that isn't explicitly listed in the interface is a compile-time error - even if the method is public. The interface is the contract and it is enforced statically.
If Pipe tries to use a Socket that doesn't satisfy Writable, the compiler catches it. The Socket side doesn't need to declare the interface at all - only Pipe owns it.
This is a principled answer to the "I need to formalize how these two things talk to each other" problem without runtime overhead and without ceremony on the other side of the connection.
- Object Inheritance Flux now supports object inheritance, including multiple inheritance, with rules designed to eliminate the diamond problem entirely - the compiler simply won't let it exist.
object A
{
int a1, a2, a3;
def __init() -> this { return this; };
def __exit() -> void { (void)this; };
};
object B
{
int b1, b2, b3;
def __init() -> this { return this; };
def __exit() -> void { (void)this; };
};
object C : A, B
{
int c1, c2, c3;
def __init() -> this { return this; };
def __exit() -> void { (void)this; };
};
The rules are clear and predictable:
Mandatory methods (__init, __expr, __exit) are not inherited - the child always defines its own lifecycle.
If two parents define a function with matching signatures but different implementations, it's a compile error. No silent resolution.
Private members are not inherited unless the parent explicitly grants access via private : ChildName.
A child member that already exists in the child takes precedence - the parent's version is not inherited.
You can use !+ on a parent method to mark it non-overridable:
object B
{
!+ def foo() -> void {};
};
object C : B
{
+ def foo() -> void {}; // Compiler error - B said no override
};
The diamond problem is eliminated structurally. The rules ensure that any ambiguity in the inheritance tree is always an error, never a silent wrong answer.
- Type Functions - Methods on Any Type Type functions let you define methods directly on any type - including built-in primitives - that are called using dot notation.
byte.clamp(byte lo, byte hi) -> byte
{
if (_ < lo) { return lo; };
if (_ > hi) { return hi; };
return _;
};
byte value = 200;
byte result = value.clamp(0, 127);
The _ identifier is the implicit receiver inside the type function body. It cannot be redeclared. The receiver can be written as the type name itself (byte, int, float) or as the null literal of that type (false for bool, 0l for long, 0d for double).
For named struct types, use the struct name directly:
struct Vec2
{
float x, y;
};
Vec2.length() -> float
{
return sqrt(_.x * _.x + _.y * _.y);
};
Vec2 v {x = 3.0, y = 4.0};
float len = v.length(); // 5.0
This gives you the ergonomics of method syntax on data you define - including types you don't own. It's not monkey-patching; it compiles down to a normal function call. The dot notation is purely syntactic.
- Try It Online Flux now has an online IDE at https://fluxspl.org/ide. No toolchain setup. No build configuration. Write Flux, run Flux, see the output. It's the fastest way to get your hands on the language and test out the features above.
The Bigger Picture
What's interesting about this release cluster is that the features reinforce each other. Comptime execution gives you a way to validate and generate things before the binary exists. Algebraic types let you put mathematical guarantees on your generic code. Interfaces formalize object collaboration without runtime overhead. Inheritance gives you composable object hierarchies with clear, non-surprising rules. Type functions make the results of all of that genuinely pleasant to work with.
Flux has always been a language that takes a position - stack first, explicit semantics, no hidden costs. These additions keep that philosophy intact while expanding what you can express. Worth watching.
Flux source, documentation, and the compiler are available on GitHub. Join the Flux Discord if you want to follow development or ask questions directly.
Top comments (0)