GizStack
View .mdOpen in ChatGPTOpen in Claude

Gizlang for TypeScript programmers

Gizlang uses the spelling of TypeScript. This page shows only the differences. TypeScript features that are not on this page are compile errors. They never get a different meaning. giz check FILE gives the line, the rule and usually the fix.

This page is about .giz files: every form compiles to static native code. A .ts, .mts or .cts file keeps its TypeScript meaning on the JavaScript compatibility engine, with the types erased (COMPILER: JavaScript entry).

Types. i64 and f64 (number is f64), u64, bool, string, bytes, Vec<T> (T[]). A string is UTF-8. Its .length and its indexes count bytes. There are no implicit conversions. Use i64(x), f64(x) and string(x) (a number or bool as text). Integer / truncates. An integer literal is an i64: a let sum = 0 that later adds a number is let sum = 0.0 or let sum: number = 0. a ** b (and **=) is a.pow(b): libm pow on f64, exact on i64 (b >= 0, overflow is a fault). Overflow and bad indexes stop the program: they are faults, not values. === compares content: scalars, strings, and the fields of records, unions and Vecs. === on class instances (Ref values) compares identity. Records with hidden fields, views and functions have no ===.

Records and unions are values. Assignment copies them. They are never shared: where TypeScript would show a change of a copy through another name (const b = a; b.x = 1;, or m = a; m.x = 1; with a read after), the compiler refuses it and names a class, which is shared. An operator on a record calls its method of that name: a + b is a.add(b) (- * / ** < <= > >= -x: sub mul div pow lt le gt ge neg).

gizlang
type Line = { sku: string; qty: i64; };
type Status = | { kind: "open"; } | { kind: "paid"; amount: i64; };

function main(): void {
    const s = Status.paid(5);                  // or an object literal with an expected type
    const line: Line = { sku: "A1", qty: 1 };
    const l2 = { ...line, qty: 2 };            // spread a value
    const xs = [1, 2];
    const ys = [3];
    const all = [0, ...xs, 9, ...ys];          // join vectors in order: [0].concat(xs).concat([9]).concat(ys)
    print(`${s.kind === "paid"} ${l2.qty} ${all.length}`);
}

A let local changes in place at any path: o.lines[i].qty = 3; o.lines.push(l);. A function changes the value of its caller only through a mut parameter: function add(o: mut Order, l: Line). An inner scope cannot declare a name again (E_SHADOW). You cannot assign to a const. let x: T; declares a local with no value yet: an optional type starts undefined, any other type its zero value (0, "", false, [], an empty Map, a record of zero fields), where TypeScript refuses a read before the first assignment. A class instance has no zero value, so let c: C; is an error.

A const made from an object or array literal can change one step in: o.f = x, v[i] = x, v.push(x). This is allowed only when nothing else holds the value: no other name, argument or lambda. A return or a spread may hold it. Otherwise the const stays constant, because a copy would not see the change that TypeScript shows.

An object spread can call a function. The call runs once. It must be the first expression that runs in the statement and must run unconditionally. The compiler rejects a call spread when holding it before the statement would change this order.

Classes are objects, as in TypeScript: every name of an instance shares it, and === compares identity.

  • The fields, and the public x: T parameters of a constructor, make the record. new Point(1, 2) runs the constructor.
  • A method gets this, the instance, and changes it in place.
  • A getter get area() reads as p.area, and a setter set area(v) runs for p.area = v. Static members are reached as Point.origin().
  • In the constructor, this exists from the statement where the last field with no initializer gets its value (this.x = x;).
  • A generic class class Box<T> is generic in its record, constructor and methods: new Box(3) is a Box<i64>, and a method can have type parameters of its own.
  • There is no extends (no inheritance). implements names interfaces that the methods implement.
gizlang
class Counter {
    constructor(private count: i64) {}
    add(n: i64): void { this.count += n; }
    get value(): i64 { return this.count; }
}

function main(): void {
    const c = new Counter(1);
    const d = c;
    d.add(2);
    print(`${c.value}`);   // 3
}

Enums are numbers or strings. enum Color { Red, Green = 5, Blue } makes Color.Blue 6, and the type Color is an i64. enum Dir { Up = "UP" } makes a string. A literal union ("a" | "b", 1 | 2) is the type of its literals.

An untagged union A | B (string | number, string[] | boolean) holds a value of one of its members. A value of a member's type is one where the union is expected. typeof x === "string" (or !==) narrows x to that member, as kind narrows a union: inside the if, and after an if that returns, once one member is left. typeof names "number", "boolean", "string" or "object" (every other type), so two members of one name (number | i64, two records) are an error: use a union with kind. typeof x is only compared with a name.

A tuple [A, B] (up to six items, labels allowed) is Tuple2<A, B> of gizlang:core. [a, b] makes one where it is expected, and where nothing is expected its items have different types (new Map([["a", 1]])). t[0] reads an item. const [a, , c] = t; binds items of a tuple or a Vec.

async and await: an async function runs to its end when it is called. So Promise<T> is T, and await e is e: Promise.all(xs) and Promise.resolve(x) are their argument. Concurrent work uses Task (Task.zip, Task.thread).

Module-level let is state of the module, such as let count = 0; or let names: string[] = [];.

  • Its functions read and assign it. Its initializer runs at its first use.
  • It changes only by assignment (names = [...names, n]). Only the functions of its own module reach it.
  • Under giz live it keeps its value across code changes.

A module const of one literal is that literal at each use. Any other value (const NAMES = ["a", "b"];, an object, a call) is computed once, at its first use, and no assignment changes it; other modules read it too.

as, readonly: e as T is checked, as e satisfies T is (there is no unchecked cast). as const changes nothing, and readonly on a field or a type changes nothing: a value is never changed through another name anyway.

match reads a union. Write one arm for each case and {} for no action. There is no default arm. status.kind === "paid" (or !==) tests one case, and the compiler checks the name.

gizlang
type Status = | { kind: "open"; } | { kind: "paid"; amount: i64; };

export function paid(status: Status): i64 {
    match (status) { Status.open() => { return 0; } Status.paid(amount) => { return amount; } }
}

status.kind === "paid" also narrows a const or parameter, so r.value reads the case:

  • in if (r.kind === "ok") { ... };
  • after if (r.kind === "err") { return ...; }, once one case is left (each such if rules out its case).

Tests joined by &&, || and ! narrow too. In a.kind === "ok" && a.value > 0, the right side sees a ok. After if (a.kind === "err" || a.value < 0) { return ...; }, a is ok. Inside one expression (?:, &&, ||, ?.) a let narrows too, because no assignment can run there. An if block narrows only a const or parameter.

A while (true) (or for (;;)) with no break does not end, so a function needs no return after it.

TypeScript forms that lower onto the forms above:

  • `n=${n}` is "n=" + string(n). A hole takes what string(x) takes. The text may hold newlines; \` and \$ are the characters.
  • c ? a : b runs only the branch it picks.
  • x ?? y takes an Option<T> x (TypeScript's T | undefined). It gives the value of x when there is one, else y. y runs only then.
  • switch (x) { case a: case b: ...; break; default: ... } is an if (x === a || x === b) chain. A case ends with break, return or continue, or is the last. There is no fall-through into a case with statements, and break leaves the switch only at the end of a case.
  • A switch (u.kind) whose cases name every case of the union, each ending in return, needs no default. It also needs no return after it, as in TypeScript. A missed case is E_RETURN.
  • x => e and (a, b) => e take their parameter types from the expected function type.
  • if (c) f();, else g(); and loops take one statement without braces. a ||= b, a &&= b.
  • An integer literal beside an f64 is an f64: x / 2, n === 0.

Errors work as in TypeScript, with the type in the signature.

  • function f(): T throws E returns a Result<T, E>. throw e; fails it.
  • Without throws, a function whose own throw no try block in it catches throws what it throws: X for throw new X(...), string for a string, else Error. A call of it propagates the same way only where the caller throws too.
  • A call gives the T. A failure returns from the caller.
  • try { ... } catch (e) { ... } handles a failure. finally runs when the statement ends.
  • In a function that does not throw, a call that fails outside a try block faults (R004).
  • The standard Error (name, message, cause) needs no import: throw new Error("bad input") in a throws Error function, and e.message in its catch.
  • A thrown Error or string that nothing catches is printed after the fault trace.

See LANGUAGE: Errors.

gizlang
function half(n: i64): i64 throws Error { if (n % 2 !== 0) { throw new Error(`odd: ${n}`); } return n / 2; }
function quarter(n: i64): i64 throws Error { return half(half(n)); }

Errors that are values are Result<T, E> and Option<T> from gizlang:core (Result.ok(v), Result.err(e), Option.some(v), Option.none()). try e unwraps ok or some. It returns any other case from the function. The result type of the function must have that case.

gizlang
import {Result} from "gizlang:core";

type Line = { sku: string; qty: i64; };

function find(id: string): Result<Line, string> {
    if (id === "") { return Result.err("no id"); }
    return Result.ok({ sku: id, qty: 1 });
}
export function total(id: string): Result<i64, string> { const o = try find(id); return Result.ok(o.qty); }

Use opt ?? fallback, res.valueOr(fallback) and x!. x! faults when the value is absent.

Loops: for (const x of v), for (const i of range(0, n)), for (let i = 0; i < n; i++) (a continue runs the update first), while, break, continue. i++, i--, ++i and --i are statements, not values. for (const [k, v] of m) and for (const { a } of xs) destructure each element. A Map gives its [key, value] entries and a Set its values, in insertion order. A label (outer: for (...)) names the loop that break outer; and continue outer; leave. a ??= b, a ||= b and a &&= b assign as in TypeScript. A parameter with a literal default and no type (greeting = "hi") has the literal's type. ${x} of an optional prints its value or undefined.

Optional values are Option<T> of gizlang:core, with no import. x?: T (a field or parameter), T | undefined and T | null are Option<T>.

  • Where an Option is expected, undefined (or null) is none, and a T is some of it. So these work: return undefined;, { name: "a" } without an optional field, Options("h", 5) without the trailing optional fields, and a call without an optional parameter.
  • a?.b, a?.[i] and a?.m() give the Option of the rest of the chain. a runs once, in its order in the expression.
  • x! is x.unwrap(): a fault when there is none.
  • x === undefined and x !== undefined test the case and narrow x, as x.kind tests do. Where x is some, x reads its value (x.value). x.kind, x.value and the Option methods still read the Option.
  • A parameter default is a literal, [] or undefined: function f(n: i64 = 1, tag?: string).

Destructuring: const { a, b: c } = e; and const [a, , c] = e; (also with let) and a parameter { x, y }: Point bind fields or items; e runs once. A last parameter ...xs: T[] takes the rest of the arguments as one array, and f(...v) passes an array whole. let a = 0, b = 1; declares both. Numbers take _ between digits: 1_000_000.

Interfaces of fields (interface User { id: i64; email?: string; }) are record types, as type User = { ... }. Interfaces of methods are the interfaces of TypeScript, with self: Self written first. As a type, an interface holds any value that implements it. Arrow functions (x: T): R => { ... } capture const values by value. A let that an arrow function names is shared, as in TypeScript: a change on either side is seen by both, and each pass of a loop has its own. A block body with no return of a value is void. Function types (x: T) => R and => void are value types: fields, Vec elements, parameters and results.

A module constant that is an arrow function (const inc = (x: i64) => x + 1;) is a function declaration.

Console: console.log(a, b) (also info, debug, trace) prints its arguments joined by spaces, and console.error and console.warn print them on standard error. An async function may return Promise<void>.

Built-in methods need no import, as in TypeScript. A lambda takes its parameter types from the method, with no annotation, and compiles to a direct call.

  • Arrays: v.filter(f), v.map(f), v.find(f) (an Option), v.findIndex(f) (-1 when none), v.some(f), v.every(f), v.count(f), v.reduce((acc, x) => ..., init), v.includes(x), v.indexOf(x), v.at(i) (an Option; -1 is the last), v.concat(w), v.slice(a, b), v.push(x), v.pop() and v.shift() (an Option, in place), v.join(sep) (numbers and bools too), v.sumBy(f), v.findLast(f), v.findLastIndex(f), v.lastIndexOf(x), v.flat(), v.flatMap(f), v.toReversed(), v.with(i, x), v.toSpliced(i, n, ...items), v.keys(), v.entries(); in place on a let: v.reverse(), v.splice(i, n, ...items) (gives the removed items), v.unshift(...items), v.fill(x).
  • v.toSorted(compare) and v.sort(compare) (in place, on a let) take a TypeScript comparator, negative when a goes first ((a, b) => a - b, a.name.localeCompare(b.name)), or a bool before(a, b), true when a goes first. giz fmt writes the former name v.sorted(f) as v.toSorted(f).
  • new Map<K, V>() with set, get (an Option), has, delete, size, keys, values and merge; new Set<K>() with add, has, delete, size and values. new Map([[k, v], ...]) and new Set([a, b]) start with those items, and [...s] spreads a Set (or a Map's values) as it spreads a Vec. new Map() takes its type arguments from the type expected. m[k] is m.get(k) and m[k] = v is m.set(k, v).
  • Array.from(x) makes a Vec of a Vec, a Set or a Map's values, and Array.from({ length: n }, (_, i) => f(i)) makes n items.
  • A regex literal /a+b/u is a gizlang:regex Regex (test, exec, matchAll, replaceAll, split), with no import. A pattern that does not compile faults when it runs.
  • JSON.stringify(x) writes any value whose type is known: a record as an object of its fields (an undefined field is left out), a union's case with its kind, an untagged union's case as its value, a Vec as an array, a Map<string, V> as an object and undefined as null. JSON.stringify(x, null, n) indents. JSON.parse(s) reads the type expected (const u: User = JSON.parse(s);): a missing field is an error unless its type takes undefined, an unknown field is skipped, and a wrong value throws a gizlang:json JsonError. Where no type is expected it reads a Json value. A class instance is not written or read yet.
  • An object type { a: T; b?: U } is a record of those fields; two of the same fields are one type.
  • Record<K, V> is Map<K, V>, and {} where one is expected is an empty one. Over a record type, Partial<T> makes each field an Option, Readonly<T> is T, and Pick<T, "a" | "b"> and Omit<T, "a"> keep those fields or the others.
  • new X(...) calls the function newX(...). new ns.X(...) calls the exported newX of the module that ns names. A const made by new changes in place as one made by a literal does.
  • A method with no other parameter also reads as a property: m.size.
  • Strings: indexOf, includes, startsWith, endsWith, split, replaceAll, trim, padStart, toUpperCase, charAt, substring, localeCompare, ... Numbers: x.toFixed(digits).
  • TypeScript's ambient functions: Math.max(a, b, ...), Math.min, Math.PI, Math.E and the other Math functions (an integer argument is an f64; Math.max and Math.min of integers are integers), Number.isInteger, Number.isNaN, Number.isFinite, String(x), Object.keys(record) (its field names), and parseInt(s, radix) and parseFloat(s), which give an Option (parseInt(s) ?? 0) where TypeScript gives NaN. console.log and ${x} print an Option as its value or undefined.
  • Import the rest by name: gizlang:text (parseI64, a union IntegerParse.ok(n)/invalid/overflow, parseF64, an Option, and fixed(x, digits)) and gizlang:math (floor, round, min, max, pow, ...).

Markup is TSX that compiles to Html (gizlang:html). It is escaped by default.

gizlang
import {Html, each, when} from "gizlang:html";

type Todo = { id: string; title: string; done: bool; };
function Row(t: Todo): Html { return <li class="todo" data-k={t.id}>{t.title}{when(t.done, () => <b>done</b>)}</li>; }
type ListProps = { title: string; };
function List(p: ListProps, children: Html): Html { return <section><h2>{p.title}</h2>{children}</section>; }

export function page(todos: Vec<Todo>): Html {
    return <List title="Todos"><ul>{each(todos, (t: Todo) => Row(t))}</ul></List>;
}

Holes take text, numbers and Html. A bool attribute is present or absent. Markup has no spread, &&, ? : or .map. Use when, each and raw(trusted). h.text() is the response body.

Functions: function f(x: T): R { ... }, and : void for no result. Declare a method as function Order.total(self: Order): i64 and call it as o.total(). The compiler infers effects.

Tests are export function testName(): void in a *.test.giz file. They assert with import * as Test from "gizlang:test" (Test.equal(a, b), Test.check(c)). giz test FILE runs them.

Optional results. find gives an Option rather than a -1 index, and ?? takes a missing value's fallback lazily. Task.zip(first, second) starts two independent tasks and joins both, returning a typed pair or Fault.

Map optional and successful results. option.map((value: T) => f(value)) returns Option<U> without unwrapping. option.andThen(f) chains a function that returns Option<U>; result.map(f) changes only the ok value and preserves err. The compiler dispatches Vec.map to its vector builtin and other map calls to their typed receiver methods.

Inferred callback results. For Option<T> or Result<T, E>, .map(x => f(x)) infers both the x: T input and the result type from f(x). Generic callback parameters can use the same syntax after earlier call arguments establish their types. The compiler keeps effect checking and preserves absent/error branches.

Imported functions are receiver-callable. An imported free function whose first argument accepts the receiver is also a method: x.f(a) calls f(x, a), with its signature, effects and mut rules. A generic callback parameter bounded by a function type (F extends (value: T) => bool) gives a lambda its parameter types, and F stays the lambda's own closure type.

Composing fallible results. result.mapErr(e => message(e)) converts only an error. result.andThen(x => next(x)) chains another Result with the same error type. try parser(input).mapErr(e => explanation(e)) propagates a mapped error without a temporary default and a two-arm match.