i started pouf with a simple idea: write a program in one language, then compile it to either python or c. python makes it easy to try things out; c lets the same project reach into freestanding and low-level work

the fun part isn’t just having two code generators. i want both targets to feel like different ways to run the same language. pouf reads the source, builds an ast, checks bindings, control flow and types, then generates python or c. the syntax stays familiar across both, while the compiler still tells you when a feature doesn’t fit the target you picked

the language bits

the basics are familiar: functions, typed parameters and return values, local bindings, loops, enums, arrays and expressions. bindings don’t change unless you mark them mut. enums work with match, including a wildcard arm, so you can see the cases right where you use them. for c, there are also types like Ptr<T>, const<T>, Volatile<T>, Array<T> and Vec<T> to make pointer and data intent visible

here’s an enum and a match in c mode:

mode c

enum Direction {
    North
    South
    East
    West
}

def describe(direction: Direction) -> Ptr<const<char>> {
    return match direction {
        Direction::North => "north",
        Direction::South => "south",
        Direction::East => "east",
        _ => "west",
    }
}

it’s a small example, but it gives you the feel of the syntax. the compiler turns these constructs into the right code for the target, and checks for problems along the way. the shared subset is still growing, so portability means sticking to features both sides support

what the runtime does

the runtime isn’t a big standard library of its own. it’s a little bit of support for language features that need it after compilation. in python mode, generated programs use pouf_runtime.py for things like mutable references, option/result values and collection helpers. pouf run sets up the runtime, compiles a python-mode file and runs it; pouf compile writes out the generated python if you want to look at it

in c mode, pouf emits c you can feed into a regular c toolchain. most language features turn into ordinary c declarations, expressions and control flow. the support header, pouf_runtime.h, provides a little machinery for things like Vec<T>: growing its storage, pushing and popping values, clearing it and freeing it. from there, your build can use a host compiler or a freestanding toolchain. pouf doesn’t bundle libc or take care of platform setup for you

some c types and operations don’t have a python equivalent, and python has features that don’t fit the c subset. the compiler checks against the target you chose, and the overlap is smaller than either language on its own. that’s the tradeoff: one language for quick experiments and systems code, without pretending every program will work unchanged on both

one more example

this one adds up the squares of even numbers, and can be compiled to c by just changing line 1 from mode python to mode c

mode python

import stdio

def sum_even_squares(numbers: Array<int>, count: int) -> int {
    mut total: int = 0
    for i in range(0, count) {
        let value: int = numbers[i]
        if value % 2 == 0 {
            total += value * value
        }
    }
    return total
}

def main() {
    let numbers: Array<int> = [1, 2, 3, 4, 5, 6]
    let result: int = sum_even_squares(numbers, 6)
    printf("sum of squares of evens: %d\n", result)
}

demo projects

i've come up with a few demo projects to demo pouf's usability:

  • backend for a hardware renting service
  • rewriting parts of mboot userspace
  • quick and dirty unix-like os
  • cfd data analysis (demo pouf's functional features)

still figuring it out

there’s still plenty to build. the two targets don’t share every type or feature yet, and real projects are how i find the gaps. python makes it quick to try an idea; c lets me take pouf into constrained environments and systems projects. the goal is simple: keep one language that’s useful in both places, and be clear about where the edges are