Skip to content

Quickstart ​

From a fresh clone to a running native binary. With bun and LLVM already installed, every command on this page (clone, setup, first build including the runtime, a rejection) finishes in seconds. Nothing is released yet (2.0.0-alpha.1 is in preparation, see the changelog). ChadScript builds from a source checkout; there is no prebuilt binary yet.

1. Install the prerequisites ​

You need bun 1.3 (it runs the compiler), LLVM's clang and opt, and Node.js 20.6 or newer (for the test suite). ChadScript is tested on macOS arm64 and Linux x86-64.

sh
curl -fsSL https://bun.sh/install | bash
brew install llvm          # then put "$(brew --prefix llvm)/bin" on your PATH
sh
curl -fsSL https://bun.sh/install | bash
sudo apt-get install -y clang-18 llvm-18
export CHAD_CLANG=clang-18 CHAD_OPT=opt-18

2. Clone and set up ​

sh
git clone https://github.com/cs01/ChadScript.git
cd ChadScript
bun install
sh scripts/setup-milo.sh      # fetches the pinned Milo compiler that builds the runtime
bin/chad doctor

The last command checks everything in one go, then compiles and runs a hello world to prove the whole path works. Expect this (versions will differ):

text
  ok    bun          1.3.10
  ok    node         v25.3.0
  ok    clang        22.1.8 (clang)
  ok    opt          22.1.8 (opt)
  ok    milo         pinned ba9484985954
  ok    hello world  compiled and ran

all checks passed

A failed check prints the fix on the next line. Node is only needed for the test suite; building programs does not use it.

3. Compile a program ​

Save this as hello.ts in the repository root. It is ordinary TypeScript: an interface, an array of objects, a Map, a template string.

ts
interface Order {
  customer: string;
  total: number;
}

const orders: Order[] = [
  { customer: "ada", total: 120 },
  { customer: "lin", total: 80 },
  { customer: "ada", total: 45.5 },
];

const byCustomer = new Map<string, number>();
for (const o of orders) {
  byCustomer.set(o.customer, (byCustomer.get(o.customer) ?? 0) + o.total);
}
for (const name of byCustomer.keys()) {
  console.log(`${name.padEnd(5)}${byCustomer.get(name)}`);
}
sh
bin/chad run hello.ts
text
ada  165.5
lin  80

The first build also compiles the runtime and caches it; later builds reuse it. run builds into a temp directory and runs the result. build writes the binary where you say:

sh
bin/chad build hello.ts -o hello
./hello
ls -lh hello

The binary is self-contained: on macOS, otool -L hello lists only the system C library. The same file runs under Node unchanged (node hello.ts on Node 23.6 or later) and prints the same bytes.

4. See a rejection ​

Now a program outside the subset: it iterates a Map directly and uses loose equality. Save it as rejected.ts:

ts
// @expect-reject: CS1222
const stock = new Map<string, number>();
stock.set("apples", 3);
stock.set("pears", 0);

for (const [fruit, count] of stock) {
  if (count == 0) console.log(`${fruit}: sold out`);
}
sh
bin/chad check rejected.ts
text
$ bin/chad check rejected.ts
error[CS1222]: iterating a Map directly with `for...of` is not supported yet
  --> rejected.ts:6:1
  help: iterate its keys and read each value: `for (const k of m.keys())`
error[CS1203]: `==` is not supported
  --> rejected.ts:7:7
  help: use `===`

2 error(s)

check runs only the type checker and the subset checks, so it is the fast way to ask "is this in the subset?". Each diagnostic has a code (explained in the error reference), a location, and a rewrite that stays inside the subset. Applying both hints:

ts
// The rewrite of rejected.ts that the diagnostics suggest.
const stock = new Map<string, number>();
stock.set("apples", 3);
stock.set("pears", 0);

for (const fruit of stock.keys()) {
  const count = stock.get(fruit) ?? 0;
  if (count === 0) console.log(`${fruit}: sold out`);
}
text
pears: sold out

Next ​