Language

TinyFX language

TinyFX is a statically type-checked language for CPU orchestration and GPU rendering or compute. A source program is checked once, then can run through the native VM/runtime, browser WASM/runtime, or generated TypeScript/WebGPU target. The target changes execution and host integration, not the language.

This page covers source structure and control flow. Read types and functions for the type and call rules, and GPU programming for shader, resource, draw, and compute rules. The generated operation reference is authoritative for individual operation signatures, stages, and target availability.

Source form

TinyFX source files use the .tfx extension. Statements may omit ; before a line break, }, or the end of the file. Semicolons remain required between statements on the same line and between the initializer, condition, and step of a C-style for. A continued expression can cross a line break, so use an explicit semicolon when a following (, [, ., or operator would otherwise continue the previous expression. Blocks use braces; both // line comments and /* ... */ block comments work. The formatter writes canonical explicit semicolons.

// Mutable binding with an inferred type.
let count = 0;
 
// Immutable binding with an explicit type.
const scale: float = 0.5;
 
/* A block comment. */
count += 1;

let bindings are mutable and const bindings cannot be rebound. A const binding to a reference value does not make that value deeply immutable: a permitted mutable field can still be changed through it. See structs and aggregates.

Integer literals may be decimal, hexadecimal (0xFF), or binary (0b1010). Floating literals support decimal and exponent notation. Strings, booleans, array literals, struct literals, tuples used as vector shorthand, and ordinary operators are expressions. Numeric tuple syntax such as (1.0, 2.0, 3.0) constructs a vector; explicit constructors such as float3(1, 2, 3) are often clearer at API boundaries.

TinyFX has familiar arithmetic, comparison, logical, bitwise, assignment, and prefix/postfix increment/decrement operators. Binary precedence follows the JavaScript ordering. Matrix multiplication is directional: matrix * vector is a column-vector product, while vector * matrix is the corresponding row-vector product.

Statements and control flow

TinyFX has if/else, while, C-style for, array for ... of, break, continue, return, and the conditional ?: expression.

fn classify(value: float) -> int {
  if (value > 0.0) {
    return 1;
  } else if (value < 0.0) {
    return -1;
  }
  return 0;
}
 
fn sum(values: int[]) -> int {
  let total = 0;
  for (let value of values) {
    total += value;
  }
  return total;
}

parallel for is intentionally not an ordinary CPU loop. It describes a restricted, race-safe compute operation and has its own resource and feedback rules; see data-parallel work. Likewise, profile "label" { ... } is a CPU-only profiling scope with a literal label; see profiling.

Program lifecycle

Top-level declarations establish program state. The runtime recognizes two optional entry functions:

  • setup() runs once when the program starts.
  • loop() runs once per frame when present.

Both must be unannotated, zero-parameter functions because the host invokes them directly. A program need not have either: static work performed during initialization or setup() can leave an image in the runtime's persistent target. Runtime lifecycle explains frame ordering, input, camera state, and host ownership.

metadata {
  title: "Warm gradient",
  version: "1.0.0",
  description: "A minimal TinyFX program.",
  category: "Examples",
  tags: ["color", "intro"],
  credits: "TinyFX contributors",
}
 
fn loop() {
  draw(shader {
    frag {
      out.color = (in.uv.x, in.uv.y * 0.5, 0.7, 1.0);
    }
  });
}

metadata { ... } is optional file-level display metadata. The supported fields are title, version, description, category, tags, and credits. All except tags take string literals; tags takes a list of unique string literals. The compiled program exposes the entry file's metadata and preserves every source file's metadata for tools. Metadata is display information, not a compatibility or artifact-version declaration.

Files, imports, and names

An import asks the host resolver to add another source file to the program:

import shade, Palette as Colors from "./palette";
 
fn loop() {
  draw(shader {
    frag { out.color = shade(in.uv); }
  });
}

Imports are currently organizational rather than module scopes. Every compiled user file and the embedded standard library share one program namespace; imports are deduplicated across transitive and cyclic graphs. The imported entries, aliases, and export { ... }; declarations are accepted syntax but do not create a JavaScript-style namespace or private module boundary today. Avoid duplicate top-level names across program files.

The embedded standard library is always available. An import from /_std/... is accepted for source organization but does not invoke the application's resolver. See the standard-library index.

:: addresses static methods and namespace-style APIs, such as noise::fbm2(...). . addresses fields, instance methods, vector swizzles, and enum members, such as camera.position, mesh.clone(), color.rgb, or Blend.alpha. Struct fields and methods are private unless declared pub; that member visibility is checked even though files share a namespace.

Staging at a glance

The source language is staged: code that reaches a shader or compute kernel must be GPU-compatible, while CPU-only code remains available for orchestration, heap data, assets, and host services. This is checked through the complete call chain, not inferred from a function's spelling alone.

Use gpu and cpu annotations to make intent explicit, or an ordinary fn when GPU compatibility should be inferred from uses. Functions and staging has the rules; GPU programming has shader and kernel restrictions. The generated TypeScript/WebGPU target supports the current general language/runtime surface rather than only early fullscreen examples; use the operation reference for precise per-operation evidence and target-specific limitations.

Reading the rest of the manual

  • Types covers values, collections, structs, enums, traits, and GPU-safe data.
  • Functions covers calls, defaults, overloads, methods, closures, and stage annotations.
  • GPU programming covers shaders, resources, draw passes, feedback, depth, and compute.
  • Assets and media covers CPU-side opaque asset and media handles.
  • Tooling covers checking, formatting, editor support, and emitted targets.