Instant iteration
Development builds skip the slow parts. Your code runs through an interpreter and a fast compiler, while dependencies are built once and cached for every project.
Early: the interpreter runs most of the tourFast to write. Fast to build. Fast to run.
A safe, compiled language for games and tools. Designed so that the easy way to write something is also the readable way and the fast way, for people and for the AI they work with.
from std.math import Vec3
from std.game import Id, IdMap
type EnemyId = Id[Enemy] # a safe handle to an enemy stored in an IdMap
struct Enemy:
position: Vec3
velocity = Vec3.zero
health = 100
def take_damage(mut self, amount: int):
self.health -= amount
struct World:
enemies = IdMap[Enemy]()
player = Vec3.zero
error TargetGone:
target: EnemyId
error OutOfRange:
distance: f32
noalloc def step(mut world: World, dt: f32):
for mut enemy in world.enemies:
enemy.position += enemy.velocity * dt
export def attack(mut world: World, target: EnemyId):
mut enemy = world.enemies.get(target)
if enemy is None:
fail TargetGone(target)
distance = (enemy.position - world.player).length()
if distance > 5.0:
fail OutOfRange(distance)
enemy.take_damage(10)
world.enemies[target] = enemy
test "attacking a distant enemy fails":
mut world = World()
far = world.enemies.insert(Enemy(position=Vec3(x=10.0, y=0.0, z=0.0)))
assert attack(mut world, far) == Err(OutOfRange(distance=10.0))Development builds skip the slow parts. Your code runs through an interpreter and a fast compiler, while dependencies are built once and cached for every project.
Release builds go through a heavy optimizing compiler. The goal is to land within a few percent of hand-tuned native code, and a benchmark suite holds us to it.
No unchecked None, no data races, no unchecked memory access. Unsafe code exists only at the boundary with C, in marked files, with a written reason.
Everything is a value, so there are no references to manage. Mutation is marked where it happens, and the obvious way to write something is also the fast way.
It reads like typed Python, the language models know best, with one way to do each thing, guardrails for the mistakes models make, and errors that say how to fix them.
No garbage collector and no hidden allocation. Mark a function noalloc and the compiler proves it never allocates, so your frame loop stays smooth.
Most bugs and most slowdowns hide in code that does more than it looks like it does. Fennec puts it on the page.
Names are immutable unless declared mut. A function that changes its argument says mut in its signature, and so does every call to it.
apply_damage(mut player, 10) # player changes
describe(player) # guaranteed not toGenerator expressions compile down to plain loops. Brackets build a new collection, so allocation is visible from the syntax itself.
total = sum(hit.damage for hit in hits) # no allocation
names = [player.name for player in players] # a new list: allocatesnoalloc is checked through every call, so a stray allocation in the frame loop is a compile error, not a stutter you find in profiling.
noalloc def update(mut world: World, dt: f32):
world.particles.remove_where(p => p.life <= 0.0)Developing and shipping are different jobs. Fennec doesn't make one build do both.
Changed code runs in an interpreter right away, hot functions get compiled in the background, and dependencies come prebuilt from a shared cache with a size cap.
Every generic specialized, everything inlined across modules, and aliasing guarantees from the language passed to the optimizer. Slower to build, as fast as it gets to run.
Fennec is in early design. This site describes where the language is going, and the syntax will change as real programs push on it. The interpreter already runs most of it: structs, enums, errors, match, comprehensions, generators, tests and a REPL. The next milestone is the type checker, which moves Fennec's checks from runtime to compile time.