Skip to content

Compile your first program

In this first tutorial we will install nothing new, write a tiny Bynk program, compile it to TypeScript, and read what the compiler produced. By the end you will have run bynkc end to end and seen the shape of its output.

We assume you have already installed bynkc. Check that it is on your path:

Terminal window
bynkc --help

Starting a real project? The fastest way from nothing to a running service is bynk new: bynk new hello && cd hello && bynk dev scaffolds and serves a complete project. This tutorial deliberately takes the other path — a single bare file, compiled by hand — to show you what bynkc produces and how a Bynk source maps to TypeScript.

Create a file called demo.bynk with this content:

commons demo {
type Id = Int
}

That is a complete, valid Bynk program. commons demo { … } declares a module named demo; inside it we declare one type, Id, an alias for the built-in Int.

Ask bynkc to compile the file to TypeScript:

Terminal window
bynkc compile demo.bynk --output demo.ts

If all is well the command prints nothing and exits successfully. Open demo.ts and read it:

// Generated by bynkc — do not edit by hand.
// commons demo
import { Ok, Err, Some, None, type Result, type Option, type ValidationError } from "./runtime.js";
export type Id = number & { readonly __brand: "Id" };
export const Id = {
of(value: number): Result<Id, ValidationError> {
if (!Number.isInteger(value)) {
return Err({ field: "Id", message: "must be an integer", value });
}
return Ok(value as Id);
},
unsafe(value: number): Id {
return value as Id;
},
};

Notice that one line of Bynk produced more than a bare type alias. Id is a branded type — number & { readonly __brand: "Id" } — so an Id is not interchangeable with any other number. The compiler also generated two constructors, Id.of and Id.unsafe. You will meet both properly in Tutorial 4; for now, just note that Bynk types carry an identity, even when they look like plain aliases.

Let us make the program do something. Replace the contents of demo.bynk with:

commons demo {
type Id = Int
fn classify(n: Int) -> String {
if n < 10 {
"small"
} else if n < 100 {
"medium"
} else {
"large"
}
}
}

fn classify(n: Int) -> String { … } declares a function taking an Int and returning a String. Bynk is expression-oriented: the if/else if/else is itself an expression, and its value is the function’s result — there is no return keyword.

Compile again:

Terminal window
bynkc compile demo.bynk --output demo.ts

The new function appears at the bottom of demo.ts:

export function classify(n: number): string {
return (n < 10 ? "small" : (n < 100 ? "medium" : "large"));
}

Int became number, String became string, and the if-expression became a conditional expression with an explicit return. The TypeScript is meant to be read: there is no hidden runtime magic in commons.

While editing, you often just want to know whether the program is valid without writing any output. That is what bynkc check is for:

Terminal window
bynkc check demo.bynk

It runs the same analysis as compile but stops before code generation. It exits successfully on a valid program and reports diagnostics otherwise. Try introducing a mistake — say, changing the function body to n + "oops" — and run check again to see a Bynk diagnostic (+ requires Int operands, so mixing in a String is rejected).

You wrote a small commons module with a type and a function, compiled it to readable TypeScript, and learned the difference between compile and check.

Next, we leave single files behind and build something that runs: a small HTTP service.

➡️ Tutorial 2: Build a small HTTP service


Want the bigger picture behind commons, branding, and compiling to TypeScript? See How a Bynk program is shaped and Why compile to TypeScript.