Early: the interpreter runs most of the tour

Fennec

Fast 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.

combat.fen · proposed syntax
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))

What makes it Fennec

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.

Full speed when it ships

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.

Safe by default

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.

Readable and fast by default

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.

Written well by AI

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.

Made for games

No garbage collector and no hidden allocation. Mark a function noalloc and the compiler proves it never allocates, so your frame loop stays smooth.

Read it, and you know what it does

Most bugs and most slowdowns hide in code that does more than it looks like it does. Fennec puts it on the page.

You can see what changes

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 to

You can see what costs

Generator 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: allocates

You can prove what matters

noalloc 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)

Two speeds, on purpose

Developing and shipping are different jobs. Fennec doesn't make one build do both.

While you work

Optimized for your time

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.

When you ship

Optimized for runtime

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.

How the two speeds work →

Where things stand

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.