PRELUDE + STANDARD LIBRARY · GENERATED SOURCE OF TRUTH

Functor API reference

Every type and function a .fun game can call: the engine modules the runner provides, and the standard library that ships with Functor Lang itself. This page is generated from the exact sources embedded in native, wasm, and editor tooling.

38 modules 419 declarations

Engine

Provided by the game runner — available to a hosted .fun game.

Scene & rendering

Scene27 entries

Declarative 3D scene nodes, materials, models, animation, and transforms.

Constructors produce immutable scene values. Most modifiers take the scene last so a node can be built as a readable pipeline.

Coordinates are Y-up and right-handed: +Y is up, +X is right, and the ground is the XZ plane.

Transforms wrap outward, so the OUTER call applies last in world space — s |> Scene.rotateY(r) |> Scene.translate(v) rotates in place and then moves, which is the order the source reads. The primitives take no size arguments, so a box is Scene.cube() |> Scene.scaleXYZ(w, h, d).

Scene.ttype

type t = host

An opaque scene node.

Scene.cubevalue

let cube : () => t

Create a unit cube centered at the origin.

Scene.spherevalue

let sphere : () => t

Create a unit sphere centered at the origin.

Scene.cylindervalue

let cylinder : () => t

Create a unit cylinder aligned to the Y axis.

Scene.quadvalue

let quad : () => t

Create a unit quad in the XY plane, facing +Z.

Scene.planevalue

let plane : () => t

Create a unit plane in the XZ ground plane.

Scene.modelvalue

let model : (Asset.Model) => t

Create a scene node from a model asset.

A locator whose file is missing logs an error and renders the empty fallback asset, so a mistyped or unfetched model shows as nothing rather than failing the frame.

Scene.heightmapvalue

let heightmap : (List<List<float>>) => t

Create terrain from a rectangular grid of height values.

Scene.terrainvalue

let terrain : (Terrain.t) => t

Create a scene node from an asset-backed terrain descriptor.

Scene.groupvalue

let group : (List<t>) => t

Group scene nodes under one transform.

Scene.colorvalue

let color : (Color.t, t) => t

Apply an unlit solid color; the scene is last for piping.

Scene.litvalue

let lit : (Color.t, t) => t

Apply a lit solid-color material; the scene is last for piping. It needs lights to be visible — under a plain Frame.create it renders black.

Scene.emissivevalue

let emissive : (Color.t, t) => t

Apply an emissive solid-color material; the scene is last for piping.

Scene.litTexturevalue

let litTexture : ('texture, t) => t

Apply a lit texture from Texture.t or Asset.Texture.

Scene.emissiveTexturevalue

let emissiveTexture : ('texture, t) => t

Apply an emissive texture from Texture.t or Asset.Texture.

Scene.litNormalMappedvalue

let litNormalMapped : (Color.t, 'texture, t) => t

Apply a lit color and a TANGENT-SPACE normal-map texture.

The map perturbs the surface normal used for lighting, so its bumps catch the scene's diffuse and specular response without changing the geometry.

Scene.screenvalue

let screen : (RenderTarget.t, t) => t

Display a render target on this surface.

The surface is emissive, so the feed is shown unlit. A target no frame writes renders magenta with one warning. A quad's front face is +Z, so a monitor built from Scene.quad has to be rotated to face the viewer or the feed reads mirrored.

Scene.opacityvalue

let opacity : (float, t) => t

Make a subtree TRANSLUCENT — alpha from 0 (invisible) to 1 (unchanged); the scene is last for piping.

The alpha applies uniformly to everything below it, whatever material each node uses — solid colors, textures, lit models, terrain — so a ghost copy of a craft is craftScene(...) |> Scene.opacity(0.35). Nested opacities multiply. An alpha outside 0..1 is an error, not a clamp.

HOW IT RENDERS, so the caveats are predictable:

- Translucent subtrees draw in a pass AFTER all opaque geometry, with depth testing on and depth WRITING off — opaque things in front hide them; they never hide each other. - They are sorted back-to-front by the VIEW-SPACE depth of the average world position of the leaves under each Scene.opacity node. That is the whole sorting granularity: there is no per-triangle sort, so within ONE translucent subtree overlapping surfaces (including the back faces of a closed mesh) read denser where they overlap, and two INTERPENETRATING translucent objects sort by their averages, which is wrong for the overlapping sliver. - A translucent subtree casts no shadow. - Scene.opacity(1.0, scene) is exactly scene — the identity. Nothing that never calls this changes in cost or appearance. The flip side is a STEP at that boundary: the instant alpha drops below 1 the subtree leaves the opaque pass, stops writing depth AND stops casting a shadow, so a fade beginning at exactly 1.0 pops on its first frame rather than easing. - An alpha of 0 draws nothing at all — the subtree is skipped rather than rasterized — so fading fully out costs nothing.

Scene.instancedvalue

let instanced : (List<Instance.t>, t) => t

Stamp a scene once per instance — one node for thousands of copies.

scene |> Scene.instanced(instances) is semantically a group holding one transformed copy of the template per Instance.t, with each copy's Instance.tint multiplied into the template's material colors. Materials come FROM the template: Scene.cube() |> Scene.lit(color) instanced is lit and shadowed exactly like its copies would be.

The renderer draws recognized templates with hardware instancing — a single cube/sphere/cylinder/quad/plane leaf under any transforms and at most one solid Scene.color / Scene.lit / Scene.emissive material (one draw call), or a Scene.model leaf under transforms (one draw call per mesh primitive, textured exactly like the ordinary model draw). A SKINNED model template — with an attached Scene.animate pose or the zero-config first-clip autoplay — instances at the SHARED pose: the pose is sampled and uploaded once, and every copy skins from it (a crowd in step; per-instance playheads are planned, not yet available). Any other template still renders correctly, expanded copy-by-copy on the CPU with a once-per-topology [functor] perf note — comparable to writing the group by hand, but not faster.

An empty list draws nothing. Scene.opacity inside the template is a teaching error — wrap the whole instanced node instead (… |> Scene.instanced(xs) |> Scene.opacity(a)).

Scene.animatevalue

let animate : (Anim.t, t) => t

Attach an animation pose to model nodes; the scene is last for piping.

Without an attached pose, a skinned model plays its FIRST clip on the game clock — attaching one is what puts the playhead under the game's control.

Scene.translatevalue

let translate : (Vec3.t, t) => t

Translate a scene node; the scene is last for piping.

Scene.scalevalue

let scale : (float, t) => t

Scale a scene node uniformly; the scene is last for piping.

Scene.scaleXYZvalue

let scaleXYZ : (float, float, float, t) => t

Scale a scene node independently on each axis.

Scene.rotateXvalue

let rotateX : (Angle.t, t) => t

Rotate a scene node around X.

Scene.rotateYvalue

let rotateY : (Angle.t, t) => t

Rotate a scene node around Y.

Scene.rotateZvalue

let rotateZ : (Angle.t, t) => t

Rotate a scene node around Z.

Scene.equalsvalue

let equals : (t, t) => bool

Compare two scene nodes structurally — the escape hatch for Scene.t, which is opaque and therefore supports no ==.

Intended for inline expect tests over draw output, NOT for per-frame logic: the walk is O(scene size), which is why it is an explicit call rather than an operator.

Three things it is literal about:

- Floats compare EXACTLY — transforms, colors, and playheads. There is no epsilon, so build both sides of a test from the same arithmetic. (The comparison happens after the engine boundary's narrowing to 32-bit, so two numbers that narrow to the same float are equal.) - Assets compare by LOCATOR, not by loaded content: the same path or URL (and the same Asset.whilePending chain) is equal, whether or not either has finished loading. - Animation compares as DECLARED — the clip name and playhead seconds in the attached pose, never a sampled skeleton.

Children are ordered, so two groups holding the same nodes in a different order are not equal.

The answer is a bare bool, so a failing expect reports only that the two scenes differ — scene values are opaque and cannot be printed. Write the assertion over the smallest node that makes the point, or bisect by comparing sub-scenes, rather than one expect over a whole frame.

Instance9 entries

Per-copy placement for Scene.instanced.

Build one Instance.t per copy — where it sits, how it is scaled and rotated, and an optional color tint — then hand the list to Scene.instanced, which stamps its template scene once per instance.

Instances are CHANNELS, not free-form transform chains: the copy's transform always applies scale, then rotation, then translation, whatever order the combinators were piped in. Combinators compose within their own channel — rotations multiply (the outer pipe applies last, like Scene.rotate*), scales multiply componentwise, and tints multiply componentwise.

Instance.ttype

type t = host

One copy's placement: position, rotation, per-axis scale, and tint.

Instance.atvalue

let at : (Vec3.t) => t

Start an instance at position, with no rotation, unit scale, and no tint.

Instance.scalevalue

let scale : (float, t) => t

Multiply the instance's scale uniformly.

Instance.scaleXYZvalue

let scaleXYZ : (float, float, float, t) => t

Multiply the instance's per-axis scale componentwise.

Instance.rotateXvalue

let rotateX : (Angle.t, t) => t

Rotate the instance about the X axis; piped later means applied later.

Instance.rotateYvalue

let rotateY : (Angle.t, t) => t

Rotate the instance about the Y axis; piped later means applied later.

Instance.rotateZvalue

let rotateZ : (Angle.t, t) => t

Rotate the instance about the Z axis; piped later means applied later.

Instance.trsvalue

let trs : (Vec3.t, Angle.t, float, float, float) => t

The flat fast path: position, yaw, and per-axis scale in ONE call.

Exactly Instance.at(position) |> Instance.scaleXYZ(sx, sy, sz) |> Instance.rotateY(rotationY), minus the per-copy builder-call overhead — reach for it when a large per-frame field makes construction cost visible. Compose further channels (another rotation axis, Instance.tint) on top.

Instance.tintvalue

let tint : (Color.t, t) => t

Multiply the copy's material colors by color — a per-instance tint over the template's material, not a second material system. Tints compose by multiplying; white is the identity. The tint is RGB only — a copy's alpha always comes from the template's material. A bare Scene.model template has no material colors to multiply, so tint has no effect there (exactly as in the stamped group).

Frame11 entries

Complete frame descriptions returned by a game's draw function.

Frame.ttype

type t = host

An opaque frame description.

Frame.createvalue

let create : (Camera3D.t, Scene.t) => t

Create an unlit frame from a camera and scene.

Frame.createLitvalue

let createLit : (Camera3D.t, Scene.t, List<Light.t>) => t

Create a lit frame from a camera, scene, and lights.

Frame.create2Dvalue

let create2D : (Camera2D.t, Sprite.t) => t

Create a standalone 2D frame from a camera and sprite tree.

Frame.withRenderTargetvalue

let withRenderTarget : (RenderTarget.t, t, t) => t

Render another frame into a target before rendering the main frame.

The target frame is a complete Frame.create / Frame.createLit with its OWN lights, so a lit or shadowed feed needs createLit plus Light.castShadows there. A scene that samples the very target it is drawn into sees the previous frame's image. The main frame is last for piping.

Frame.withUiTargetvalue

let withUiTarget : (RenderTarget.t, Ui.view, t) => t

Paint a UI view into a target before rendering the main frame.

The view is a Ui.* tree painted at the target's declared size and read back like any render target (Scene.quad() |> Scene.screen(target)) — a monitor mesh, a cockpit panel. Views on targets are display-only for now: interactive widgets (buttons, sliders, text inputs) render, but their handlers are ignored. The screen clears to the engine's default background. Like withRenderTarget, the FIRST declaration of a target id wins; one id must not be declared by both writers. Runs on every shell — native, web, and VR. The main frame is last for piping.

Frame.withFogvalue

let withFog : (Fog.t, t) => t

Attach fog to a frame; the frame is last for piping.

Frame.withSkyboxvalue

let withSkybox : (Skybox.t, t) => t

Attach a cubemap skybox to a frame; the frame is last for piping.

Frame.withClearColorvalue

let withClearColor : (Color.t, t) => t

Override a frame's background clear color.

This overrides the default of clearing to the fog color, and paints the background only — it does not change how fog blends over geometry.

Frame.with2Dvalue

let with2D : (Camera2D.t, Sprite.t, t) => t

Add an ordered sprite pass above the frame's 3D scene.

Layers render in call order, so a second with2D draws above the first. The main frame is last for piping.

Frame.equalsvalue

let equals : (t, t) => bool

Compare two frames structurally — the escape hatch for Frame.t, which is opaque and therefore supports no ==.

Compares every part of the frame: camera, scene, lights, render-target passes, fog, skybox, clear color, and 2D layers (all ordered). It also distinguishes HOW the frame was built — a Frame.create2D frame is never equal to a 3D frame carrying the same layer through with2D. Intended for inline expect tests over draw output, NOT for per-frame logic — the walk is O(frame size).

It inherits Scene.equals's rules: floats compare exactly, assets compare by locator rather than by loaded content, animation compares as declared, and a failing expect can only report THAT the two frames differ — assert over the smallest piece that makes the point.

Camera3D8 entries

Cameras for viewing a Y-up, right-handed 3D scene.

Camera3D.ttype

type t = host

An opaque camera description.

Camera3D.lookAtvalue

let lookAt : (Vec3.t, Vec3.t) => t

Create a camera from an eye position and target with a fixed 45° field of view.

Camera3D.firstPersonvalue

let firstPerson : (Vec3.t, Angle.t, Angle.t, Angle.t) => t

Create a first-person camera from position, yaw, pitch, and field of view.

Zero yaw and pitch look down +Z.

On XR this camera is the authored reference center-eye rig: live head and eye deltas compose in its local basis. Position, orientation, and the near/ far planes stay game-owned, while OpenXR owns IPD and per-eye optical FOV.

Camera3D.raytype

type ray = { origin: Vec3.t, direction: Vec3.t }

A world-space ray from the camera eye through a logical surface point.

Camera3D.toWorldRayvalue

let toWorldRay : (Input.mouse, t) => Option.t<ray>

Map a sampled mouse position through the authored perspective camera.

Mouse position and extent share one top-left-origin logical coordinate space, so the result stays stable across resize and Retina/device-pixel ratio changes. The direction is normalized and both fields feed directly into Physics.cast / Physics.raycast. Returns Option.None while the pointer is outside the surface or the camera is degenerate.

Camera3D.mappedPosetype

type mappedPose = { position: Input.point3, forward: Input.point3, up: Input.point3 }

A tracked pose mapped into world space through an authored camera.

Camera3D.mapTrackedPosevalue

let mapTrackedPose : (t, Input.pose) => mappedPose

Map a rig-local tracked pose through the authored camera.

The returned position, forward, and up vectors are suitable for placing a controller representation or aiming a ray in the authored world.

Camera3D.clipvalue

let clip : (float, float, t) => t

Set near and far clipping distances; the camera is last for piping.

Both distances must be finite with 0 < near < far; anything else is a teaching error rather than a degenerate projection. Large outdoor worlds should set the far plane explicitly. Keep the near plane as large as gameplay permits to preserve depth-buffer precision.

Camera2D5 entries

Center-origin, Y-up cameras for Sprite scenes.

Camera2D.ttype

type t = host

An opaque 2D camera description.

Camera2D.createvalue

let create : (float, float) => t

Create a camera with the visible world width and height at zoom 1.

The renderer preserves this aspect ratio and letterboxes rather than stretching. Width and height must be positive.

Camera2D.atvalue

let at : (float, float, t) => t

Center a camera at the given world position; the camera is last for piping.

Camera2D.zoomvalue

let zoom : (float, t) => t

Set a positive camera zoom; the camera is last for piping.

LARGER is closer: the zoom divides the visible world extent, so 2.0 shows half as much world at twice the size.

Camera2D.toWorldvalue

let toWorld : (Input.mouse, t) => Option.t<Input.point2>

Map a sampled mouse position through the camera's fitted viewport.

Returns Option.None while the pointer is in a letterbox/pillarbox bar. The mouse carries its logical surface extent, so this remains correct across window resizes and Retina/device-pixel-ratio changes.

Sprite25 entries

Pure, inspectable 2D picture values.

Unlike Scene.t and Frame.t, Sprite.t is represented at runtime as ordinary Functor Lang data. It can be compared, inspected, serialized, stored in a model, and carried through time travel while its internal rendering schema stays private.

Sprite.ttype

type t

An abstract plain-data 2D picture.

Sprite.regiontype

type region

A rectangular section of an image in whole source pixels. Coordinates use the conventional image origin: x grows right and y grows down from the top-left corner.

Sprite.metricstype

type metrics = {
  width: float,
  height: float
}

The size of a laid-out run of text, in the same world units as the sprite itself.

Sprite.blankvalue

let blank : () => t

Create an empty picture.

Sprite.rectanglevalue

let rectangle : (Color.t, float, float) => t

Create a centered rectangle with positive width and height.

Sprite.squarevalue

let square : (Color.t, float) => t

Create a centered square with a positive side length.

Sprite.textvalue

let text : (Color.t, float, string) => t

Draw text in the built-in font, centered on its own box like every other primitive, with size the height of one line in world units. Needs no asset: the font is compiled into the runtime. The text comes last, so a formatted value pipes straight in:

Text.fixed(model.score, 0.0) |> Sprite.text(Color.rgb(0.0, 1.0, 1.0), 1.2)

The font is a monospace 8x8 bitmap, so each character advances by exactly size — ask measure for a run's size rather than assuming it. \n starts a new line, stacked at exactly one size of line height, and each line is centered within the block. That stride is the glyph cell, so lines never overlap but sit tight — a descender nearly meets the next line's capitals, as in a terminal. For airier text, draw the lines yourself and space them by more than size with group and moveY. Characters outside printable ASCII occupy their cell but draw nothing, so unsupported text leaves gaps instead of shifting the rest of the line.

Text is centered, so align it by shifting half its measured width — Sprite.moveX(Sprite.measure(size, s).width * 0.5, …) puts its LEFT edge at the origin, and negating that puts its right edge there. That aligns the BLOCK: with several lines only the widest reaches the edge, since each line stays centered until a left-aligning text block exists.

Glyphs are sampled like any other sprite image, so nearest gives crisp pixel edges and the default linear gives smoother ones at large sizes.

Sprite.measurevalue

let measure : (float, string) => metrics

Measure what text at size would occupy, without rendering it, so labels and columns can be laid out in game logic. The width is the widest line's; the height is size per line, counting a trailing newline as a line, so stacking blocks by their measured height never overlaps them.

Sprite.circlevalue

let circle : (Color.t, float) => t

Create a filled circle of the given radius, centered on the origin like square — so it spans 2 * radius across. Approximated by a 32-sided polygon, which is under a pixel from true at any size that reads as a circle.

Sprite.polygonvalue

let polygon : (Color.t, List<Input.point2>) => t

Fill a CONVEX polygon through the given points, in order.

Unlike every other primitive, a polygon is NOT re-centered: the points are the geometry, in the sprite's own coordinate space, so an outline computed in game logic lands where it was computed. Either winding works (clockwise or counter-clockwise).

The fill is a triangle fan, which is only correct for a convex outline, so anything else is REJECTED with an error rather than filled wrongly — draw it as a group of convex pieces instead. That covers a CONCAVE outline, a self-intersecting STAR (which turns consistently but winds around more than once), fewer than 3 points, and points that are all on one line.

Sprite.linevalue

let line : (Color.t, float, Input.point2, Input.point2) => t

Draw a straight line of the given thickness between two points, in the sprite's own coordinate space (not re-centered, like polygon).

Thickness is measured across the line and is exact at every angle. There are no caps and no joins: the line stops flat at each endpoint, so two lines meeting at an angle leave a notch at the corner — a jointed polyline is not part of this surface yet. A zero-length line draws nothing.

Thickness is geometry, not a screen-space stroke: scale multiplies it along with the length, and scaleXY with unequal factors distorts it for any line that is not axis-aligned.

Sprite.imagevalue

let image : (float, float, Asset.Texture) => t

Create a centered, textured rectangle with positive width and height.

Sprite.imageRegionvalue

let imageRegion : (float, float, region, Asset.Texture) => t

Select a source rectangle without requiring the image's full dimensions.

Sprite.regionvalue

let region : (float, float, float, float) => region

Construct a top-left-origin source rectangle as x, y, width, height.

Sprite.groupvalue

let group : (List<t>) => t

Group pictures in painter's order, with earlier items behind later items.

Sprite.movevalue

let move : (float, float, t) => t

Move a picture by the given X and Y offsets; the picture is last for piping.

Sprite.moveXvalue

let moveX : (float, t) => t

Move a picture along X; the picture is last for piping.

Sprite.moveYvalue

let moveY : (float, t) => t

Move a picture along Y; the picture is last for piping.

Sprite.rotatevalue

let rotate : (Angle.t, t) => t

Rotate a picture around its center; the picture is last for piping.

A positive angle rotates COUNTER-CLOCKWISE, matching the Y-up 2D camera.

Sprite.scalevalue

let scale : (float, t) => t

Scale a picture uniformly; the picture is last for piping.

Sprite.scaleXYvalue

let scaleXY : (float, float, t) => t

Scale a picture independently along X and Y; the picture is last for piping.

Sprite.fadevalue

let fade : (float, t) => t

Multiply a picture's opacity by an alpha from 0 to 1.

Sprite.tintvalue

let tint : (Color.t, t) => t

Multiply a picture's color by a tint; the picture is last for piping.

Sprite.nearestvalue

let nearest : (t) => t

Use crisp nearest-neighbor sampling for every image in the subtree.

Sprite.linearvalue

let linear : (t) => t

Use smooth linear sampling for every image in the subtree (the default).

Light6 entries

Lights used by Frame.createLit.

Light.ttype

type t = host

An opaque light description.

Light.ambientvalue

let ambient : (Color.t) => t

Create uniform ambient light.

Light.directionalvalue

let directional : (Vec3.t, Color.t, float) => t

Create directional light from direction, color, and intensity.

Light.pointvalue

let point : (Vec3.t, Color.t, float, float) => t

Create point light from position, color, intensity, and range.

Light.spotvalue

let spot : (Vec3.t, Vec3.t, Color.t, float, float, Angle.t) => t

Create a spot light from position, direction, color, intensity, range, and cone angle.

Light.castShadowsvalue

let castShadows : (t) => t

Enable shadow casting; the light is first for piping.

Skybox2 entries

Cubemap skyboxes attached to frames with Frame.withSkybox.

Skybox.ttype

type t = host

An opaque cubemap skybox.

Skybox.filesvalue

let files : (string, string, string, string, string, string) => t

Load six cubemap faces in +X, -X, +Y, -Y, +Z, -Z order.

Faces are ordinary fetched image files resolved from the game directory. While they load the frame's clear color shows through, and a face that fails to load warns once and leaves the frame with no sky.

Texture2 entries

Texture values accepted by scene material functions.

Texture.ttype

type t = host

An opaque texture value.

Texture.filevalue

let file : (string) => t

Load an image texture from a path relative to the game directory.

Fog3 entries

Distance fog attached to a frame with Frame.withFog.

Fog applies to every forward material, emissive included, and its color also becomes the frame's clear color unless Frame.withClearColor says otherwise. A skybox is never fogged.

Fog.ttype

type t = host

An opaque fog description.

Fog.linearvalue

let linear : (float, float, Color.t) => t

Create linear fog between near and far distances.

near must be at least 0 and far must exceed near; anything else is a teaching error rather than a silently degenerate ramp.

Fog.expvalue

let exp : (float, Color.t) => t

Create exponential fog with the given density.

The density must be positive.

RenderTarget3 entries

Named off-screen render targets for render-to-texture effects.

Declare a target ONCE and use that value at both sites — the Frame.withRenderTarget writer and the Scene.screen reader — rather than repeating a bare string, exactly as Angle and Physics.tag brand their own identities. A scene that samples the very target it is being rendered into shows the PREVIOUS frame's image.

RenderTarget.ttype

type t = host

An opaque render target identity and size.

RenderTarget.namedvalue

let named : (string) => t

Create a stable, named 512×512 render target.

RenderTarget.sizedvalue

let sized : (float, float, t) => t

Set target width and height; the target is last for piping.

Math & geometry

Vec314 entries

Branded 3D vectors used for positions, directions, velocities, and gravity.

Vectors are **opaque** — like Angle and Color, a Vec3 is built once (Vec3.make) and passed as a value, so three interleaved bare floats can never be mistaken for a position. Read components back with Vec3.x / Vec3.y / Vec3.z, and combine vectors with the arithmetic below rather than unpacking to a record and rebuilding.

**Argument order is thread-last**, matching the rest of the prelude (Scene.translate(v, scene)): the *subject* is the LAST parameter, so a pipeline reads left-to-right as the subject being acted on.

`` // v - origin, scaled by 2, then normalized let dir = v |> Vec3.sub(origin) |> Vec3.scale(2.0) |> Vec3.normalize() ``

For the non-commutative operations this means Vec3.sub(b, a) computes a - b, so v |> Vec3.sub(origin) reads as "v minus origin"; likewise a |> Vec3.cross(b) is a × b, and from |> Vec3.lerp(target, t) moves from toward target.

Components are 32-bit floats and every vector is **finite**: an operation whose result would overflow that range is an error naming the operation, rather than an infinity that becomes a NaN and silently blanks the scene.

Vec3.ttype

type t = host

An opaque 3D vector.

Vec3.makevalue

let make : (float, float, float) => t

Construct a vector from X, Y, and Z components.

Vec3.xvalue

let x : (t) => float

The X component of a vector.

Components are stored as 32-bit floats, so a value that is not exactly representable comes back rounded (Vec3.x(Vec3.make(0.1, 0.0, 0.0)) is 0.10000000149011612). Compare components with a tolerance, not ==.

Vec3.yvalue

let y : (t) => float

The Y component of a vector. Rounded to 32-bit like Vec3.x.

Vec3.zvalue

let z : (t) => float

The Z component of a vector. Rounded to 32-bit like Vec3.x.

Vec3.addvalue

let add : (t, t) => t

Componentwise sum. Vec3.add(b, a) is a + b, so a |> Vec3.add(b) reads as "a plus b" (addition is commutative, so the order is free).

Vec3.subvalue

let sub : (t, t) => t

Componentwise difference. Vec3.sub(b, a) is a - b, so v |> Vec3.sub(origin) reads as "v minus origin".

Vec3.scalevalue

let scale : (float, t) => t

Multiply every component by a scalar: v |> Vec3.scale(2.0) doubles v. Scaling by a negative number negates: v |> Vec3.scale(0.0 - 1.0).

Vec3.dotvalue

let dot : (t, t) => float

The dot product a · b — commutative, so argument order does not matter. Zero when the vectors are perpendicular.

Vec3.crossvalue

let cross : (t, t) => t

The cross product. Vec3.cross(b, a) is a × b, so a |> Vec3.cross(b) reads as "a cross b" — the result is perpendicular to both, right-handed: X × Y = Z.

Mind the order for a strafe axis. In Functor's Y-up right-handed frame the world-space "right" of a gaze is up × forward, which in this argument order is up |> Vec3.cross(forward) — with forward = +Z and up = +Y that is +X. The other order gives -X.

Vec3.lengthvalue

let length : (t) => float

The Euclidean length (magnitude) of a vector.

Vec3.normalizevalue

let normalize : (t) => t

A unit vector pointing the same way.

**A zero-length vector normalizes to zero**, not an error and not NaN — Vec3.normalize(Vec3.make(0.0, 0.0, 0.0)) is the zero vector. Per-frame code normalizes a velocity or an input direction that is legitimately zero all the time, so this must not fault the frame; test the length first when you need a fallback direction. Zero is the ONLY input that yields a non-unit result — the length is computed with enough range that even the largest representable vector normalizes correctly.

Vec3.distancevalue

let distance : (t, t) => float

The distance between two points — symmetric, so argument order is free. a |> Vec3.distance(b) is the length of a - b.

Vec3.lerpvalue

let lerp : (t, float, t) => t

Linear interpolation. Vec3.lerp(target, t, from) moves from toward target by fraction t, so from |> Vec3.lerp(target, 0.5) is the midpoint. t is NOT clamped: 0.0 yields from, 1.0 yields target, and values outside 0..1 extrapolate.

Angle15 entries

Branded angles used by cameras, rotations, and spot lights.

Angle-taking APIs require an Angle.t, preventing radians and degrees from being mixed accidentally. Construct one with Angle.degrees or Angle.radians.

Angle.ttype

type t = host

An opaque angle value.

Angle.degreesvalue

let degrees : (float) => t

Construct an angle from degrees.

Angle.radiansvalue

let radians : (float) => t

Construct an angle from radians.

Angle.addvalue

let add : (t, t) => t

Add two angles. This is what 90deg + 45deg calls.

Angle.subvalue

let sub : (t, t) => t

Subtract one angle from another. This is what 90deg - 45deg calls.

Angle.scalevalue

let scale : (t, float) => t

Scale an angle by a plain number. This is what 45deg * 2.0 calls. It takes the angle FIRST — the shape every declared * has — so unlike most of the prelude it is not written to be piped into.

Angle.equalsvalue

let equals : (t, t) => bool

Are two angles the same? This is what 90deg == 90deg calls.

It is float equality on the underlying radians, with every consequence that implies: 90deg == 90deg is true because both sides build the same number, but 90deg == 1.5708rad is false, and an angle accumulated through arithmetic may miss an exact literal by a rounding step. An angle is one-way (there is no way back to a number), so where a tolerance matters, keep the plain float you built it from and compare THAT.

Angle.lessvalue

let less : (t, t) => bool

Is the first angle smaller than the second? This is what 45deg < 90deg calls, and — swapped or negated — >, <=, and >= too.

Angles are ordered by their raw radians, so this is signed magnitude, NOT a direction on the circle: -270deg < 90deg is true.

Angle.degunit

unit deg = Angle.degrees

Degrees as a literal suffix: 90deg is exactly Angle.degrees(90.0).

Angle.radunit

unit rad = Angle.radians

Radians as a literal suffix: 0.5rad is exactly Angle.radians(0.5).

Angle.deg (+)unit-operator

unit deg (+) = Angle.add

+ on angles: 90deg + 45deg is Angle.add(90deg, 45deg). An operator belongs to the BRAND, so it covers every angle suffix — 90deg + 1.5rad adds too.

Angle.deg (-)unit-operator

unit deg (-) = Angle.sub

- on angles: 90deg - 45deg is Angle.sub(90deg, 45deg).

Angle.deg (*)unit-operator

unit deg (*) = Angle.scale

* scales an angle by a number, on either side: 45deg * 2.0 and 2.0 * 45deg are both Angle.scale(45deg, 2.0). (Multiplying two angles would be a different kind of thing — Functor Lang does not model that.)

Angle.deg (==)unit-operator

unit deg (==) = Angle.equals

== on angles: 90deg == 90deg is Angle.equals(90deg, 90deg), and != is its negation. Underneath it is FLOAT equality on radians — see Angle.equals.

Angle.deg (<)unit-operator

unit deg (<) = Angle.less

< on angles: 45deg < 90deg is Angle.less(45deg, 90deg). >, <=, and >= derive from it (swapped and/or negated), so all four orderings come from this one declaration.

Color2 entries

RGB colors shared by rendering, lighting, fog, and UI APIs.

Color.ttype

type t = host

An opaque RGB color.

Channels are normally in 0..1; emissive and HDR uses may exceed 1.

Color.rgbvalue

let rgb : (float, float, float) => t

Construct a color from red, green, and blue channels.

Simulation

Physics42 entries

Declarative rigid-body physics for the optional physics hook.

Shapes and bodies are values, body attributes pipe naturally, and stable branded tags connect declarations, reads, commands, and collision events.

The hook DECLARES the world each frame and the runtime reconciles that declaration against the live one. A tag is cross-frame identity: the same tag is the same body, and a body is dropped by no longer declaring it. Re-declaring an unchanged body leaves the simulation alone, while changing its declared position or rotation drives that field — a dynamic or fixed body teleports immediately, and a kinematic body takes the new pose as its next-step target, so it carries velocity into contacts. Reading a tag that is not in the live world is a runtime error rather than an empty answer, so read only bodies your hook has declared. Like the model, the world survives hot reload for as long as the hook does; deleting the hook drops it. An error raised inside the hook does NOT stop the world: the previous frame's declaration is kept and stepped, and the error is reported once.

Reads are SYNCHRONOUS and writes are QUEUED. Physics.position, Physics.linearVelocity, and Physics.cast answer in place from the LAST STEPPED world — in any entry point, the physics hook included. Everything that runs before the step (tick, input, a pre-step update, the hook) sees the previous step; only draw and the post-step updates see the world this frame just stepped. The world is primed from init before the first frame, so frame 1's reads answer with the initial declared poses. Every mutation instead returns an Effect.t that applies at the next physics step after it queues — after reconcile, on that step's first substep. A command issued from tick therefore normally lands in time for the same frame's draw; on a frame that takes no substep at all (normal above 60fps) it waits for the next simulated frame.

Physics.bodytype

type body = host

An opaque rigid body description.

Physics.worldtype

type world = host

An opaque physics world description.

Physics.tagtype

type tag

A stable, branded body identity used throughout the physics API.

Physics.positiontype

type position = { x: float, y: float, z: float }

The live world-space position of a body.

Physics.velocitytype

type velocity = { x: float, y: float, z: float }

The live linear velocity of a body, in world units per second.

Physics.rayHittype

type rayHit = {
  hit: bool,
  x: float, y: float, z: float,
  nx: float, ny: float, nz: float,
  distance: float,
  tag: tag
}

A raycast result with hit position, normal, distance, and body tag.

For a miss, hit is false and the remaining fields are zeroed.

Physics.collisionEventtype

type collisionEvent = { started: bool, a: tag, b: tag, sensor: bool }

A contact-begin or contact-end event between two bodies.

Physics.tagvalue

let tag : (string) => tag

Construct a stable body tag from a string.

The empty tag is reserved as the no-body sentinel in a raycast miss.

The brand is check-time only: declare a tag once and use that VALUE at every site — a bare string where a tag is expected is a check error. At runtime a tag simply IS its string, so comparing one against a collision event's a/b with == works.

Physics.boxvalue

let box : (float, float, float) => shape

Create a local-axis box shape from its full width, height, and depth.

Physics.spherevalue

let sphere : (float) => shape

Create a sphere shape from its radius.

Physics.capsulevalue

let capsule : (float, float) => shape

Create a capsule shape from half-height and radius.

Physics.heightfieldvalue

let heightfield : (tag, Terrain.t) => body

Create a fixed heightfield body from a shared terrain descriptor.

The renderer and collider share dimensions, elevation range, asset resolution, and pending-asset chain. Collision uses at most 1025 samples per axis to bound frame-thread work; larger render sources are decimated. The body supports translation via Physics.at; pair it only with an unrotated, unscaled Scene.terrain, using the same translation.

Physics.dynamicvalue

let dynamic : (tag, shape) => body

Create a dynamic body affected by forces and collisions.

Physics.kinematicvalue

let kinematic : (tag, shape) => body

Create a kinematic body driven explicitly by the game.

Physics.fixedvalue

let fixed : (tag, shape) => body

Create a fixed body that does not move.

Physics.atvalue

let at : (Vec3.t, body) => body

Set a body's initial world position; the body is last for piping.

Physics.rotateXvalue

let rotateX : (Angle.t, body) => body

Rotate a body about world X around its center; the body is last for piping.

An outer rotation applies last in world space, matching Scene.rotateX. Heightfield bodies reject rotation because terrain rendering is translation-only.

Physics.rotateYvalue

let rotateY : (Angle.t, body) => body

Rotate a body about world Y around its center; the body is last for piping.

An outer rotation applies last in world space, matching Scene.rotateY. Heightfield bodies reject rotation because terrain rendering is translation-only.

Physics.rotateZvalue

let rotateZ : (Angle.t, body) => body

Rotate a body about world Z around its center; the body is last for piping.

An outer rotation applies last in world space, matching Scene.rotateZ. Heightfield bodies reject rotation because terrain rendering is translation-only.

Physics.velocityvalue

let velocity : (Vec3.t, body) => body

Set a body's initial linear velocity; the body is last for piping.

Physics.massvalue

let mass : (float, body) => body

Set a body's mass; the body is last for piping.

Physics.frictionvalue

let friction : (float, body) => body

Set a body's friction coefficient; the body is last for piping.

Physics.restitutionvalue

let restitution : (float, body) => body

Set a body's restitution; the body is last for piping.

Physics.linearDampingvalue

let linearDamping : (float, body) => body

Damp a body's LINEAR velocity — drag, per second; the body is last for piping.

The default is 0.0 (no drag), so a rolling sphere on a box coasts almost forever: contact friction resists sliding, not rolling. A small value (0.30.8) is what makes a marble, a puck, or a thrown prop actually settle, and it belongs here rather than in a per-frame velocity command: damping is a property of the body, so declaring it keeps tick pure. Must not be negative. Only dynamic bodies integrate, so damping is inert on a kinematic or fixed one. Changing the value writes it onto the live body (the friction/restitution rule) — the new drag applies from the next step, with the body's current pose and velocity untouched.

Physics.angularDampingvalue

let angularDamping : (float, body) => body

Damp a body's ANGULAR velocity — spin resistance, per second; the body is last for piping.

The default is 0.0. Pair it with linearDamping for a ball that stops rolling instead of creeping, or use it alone to bleed off spin while linear motion is preserved. Must not be negative, dynamic-only, and reconciles onto a live body exactly like linearDamping.

Physics.sensorvalue

let sensor : (body) => body

Make a body a non-solid sensor; the body is last for piping.

Physics.uprightvalue

let upright : (body) => body

Lock a body's rotation so it translates but never tips.

The character-controller attribute: an upright capsule that lands, scuffs a ledge, or leans on a wall would otherwise pick up angular velocity and topple, which also invalidates any fixed standing-height assumption a grounding probe makes. The body is last for piping.

Physics.scenevalue

let scene : (Vec3.t, List<body>) => world

Declare a physics world from gravity and bodies.

Physics.positionvalue

let position : (tag) => position

Read a body's live, stepped world position.

Answers with the LAST stepped world, so a pre-step caller (tick, the physics hook) sees the previous step and draw sees this frame's.

Physics.linearVelocityvalue

let linearVelocity : (tag) => velocity

Read a body's live, stepped linear velocity.

The read counterpart of Physics.setVelocity. (Physics.velocity is the body-builder attribute that sets an *initial* velocity.)

Physics.castvalue

let cast : (Vec3.t, Vec3.t, float) => rayHit

Cast a ray against the world and get the nearest hit immediately.

Unlike Physics.raycast — an effect whose answer arrives through update after the step — this answers in place, so tick can branch on it while deciding. It reads the world as of the last step, like Physics.position: in tick that is the previous step, in draw this frame's. A miss is hit: false with zeroed fields, not an error. dir need not be normalized, so maxDist is in world units.

Physics.castExcludingvalue

let castExcluding : (tag, Vec3.t, Vec3.t, float) => rayHit

Physics.cast, ignoring one body — the grounding probe.

A ray cast from inside a character's own capsule would otherwise hit that capsule at distance 0 and report the character standing on itself. Excluding a tag that isn't in the world excludes nothing.

Physics.transformedvalue

let transformed : (tag, Scene.t) => Scene.t

Apply a body's live transform to a scene node.

The scene is last for piping.

Physics.applyImpulsevalue

let applyImpulse : (tag, Vec3.t) => Effect.t

Apply an instantaneous impulse to a body.

Physics.applyForcevalue

let applyForce : (tag, Vec3.t) => Effect.t

Apply a continuous force to a body for the next step.

Physics.setVelocityvalue

let setVelocity : (tag, Vec3.t) => Effect.t

Replace a body's linear velocity — all three axes.

For a character controller prefer Physics.setVelocityXZ: writing the vertical axis every frame fights the solver's own ground contact.

Physics.setVelocityXZvalue

let setVelocityXZ : (tag, float, float) => Effect.t

Replace a body's HORIZONTAL velocity, leaving the vertical axis alone.

The character-controller command. Steering owns x and z while the solver keeps the y it is using to resolve the ground contact, so the game never has to author a vertical velocity it has no opinion about — with Physics.setVelocity it must write all three every frame, and the only values available to it are a one-step-stale read or a guess.

The preserved axis is read from the live world when the command applies — after reconcile, and after any command queued earlier the same frame. So velocity commands in one frame compose as last-write-wins per axis, and an axis nobody wrote is left exactly as the solver left it.

Physics.setVelocityYvalue

let setVelocityY : (tag, float) => Effect.t

Replace a body's VERTICAL velocity, leaving the horizontal plane alone.

A jump that keeps the run: unlike Physics.applyImpulse the result does not depend on the body's mass, and unlike Physics.setVelocity it does not discard the horizontal momentum the character arrived with.

Physics.teleportvalue

let teleport : (tag, Vec3.t) => Effect.t

Move a body immediately to a world position.

Physics.raycastvalue

let raycast : (Vec3.t, Vec3.t, float, (rayHit) => 'msg) => Effect.t

Cast a ray and tag its Physics.rayHit result as a message.

Physics.eventsvalue

let events : ((collisionEvent) => 'msg) => Sub.t

Subscribe to contact begin/end events and tag them as messages.

Anim9 entries

Declarative animation poses attached to scene models with Scene.animate.

Playheads and blend weights are values derived by the game, so the engine owns no hidden animation clock and poses rewind and replay deterministically.

Anim.ttype

type t = host

An opaque animation pose expression.

Anim.clipvalue

let clip : (string, float) => t

Sample a named glTF clip at a playhead in seconds.

The clip loops by its duration; negative playheads wrap backwards from the end. A name the model does not define warns once and renders the bind pose — functor import's generated clip constants (Assets.xbotClips.walk.name) turn that into a check-time error instead.

Anim.blendvalue

let blend : (List<(t, float)>) => t

Blend a list of (animation, weight) pairs.

Weights are normalized and entries with non-positive weights are ignored. Translation and scale are interpolated linearly and rotation as a normalized quaternion mix. An entry may itself be a blend, so blends nest.

Anim.restvalue

let rest : () => t

Return the bind pose as a base for programmatic posing.

Anim.addvalue

let add : (t, float, t) => t

Apply an additive animation layer to a base pose.

The base is last for piping. Weight is clamped to 0..1, and the delta applies only where the base has influence.

Anim.maskvalue

let mask : (List<string>, t) => t

Restrict a pose to the subtrees rooted at the named joints.

Joints the mask does not cover fall out of this pose entirely — they take the bind pose, or the other inputs of an enclosing blend. A joint name the model does not define warns once.

Anim.rotatevalue

let rotate : (string, Angle.t, Angle.t, Angle.t, t) => t

Add a local XYZ Euler rotation to one joint.

The joint counts as FULLY DRIVEN by this node, so a mask BENEATH it cannot drop the joint; an enclosing mask — one applied to this node's result — still can.

Anim.lookAtvalue

let lookAt : (string, Vec3.t, Angle.t, float, t) => t

Aim one joint's local +Z axis at a model-space target after evaluating the pose below it.

The target is baked into the animation value at draw time, and Scene transforms sit deliberately outside the solver — so a world-space aim point has to be inverted through the node's own transforms before it is passed here. maxDeflection limits the shortest correction from the evaluated pose and must be between 0 and 180 degrees. weight is clamped to 0..1. The joint is fully driven by this node, exactly as with Anim.rotate; an enclosing mask can still exclude it.

Anim.reachvalue

let reach : (string, string, string, Vec3.t, float, t) => t

Reach a model-space target with a direct two-bone joint chain.

root, middle, and end must name direct parent/child joints, such as an upper arm, forearm, and hand. Unreachable targets clamp to the chain's nearest extension. The evaluated pose below supplies the elbow bend side, root must have uniform scale, and weight is clamped to 0..1.

Terrain7 entries

Finite, asset-backed heightfield terrain shared by rendering and physics.

Heightmaps should be 16-bit grayscale PNGs. Black maps to the declared minimum height and white maps to the maximum height.

Terrain.ttype

type t = host

An immutable terrain descriptor.

Terrain.heightmapvalue

let heightmap : (Asset.Texture, float, float, float, float) => t

Create a terrain centered on the origin and spanning width by depth in XZ.

Terrain.maxPixelErrorvalue

let maxPixelError : (float, t) => t

Set the maximum projected vertex spacing in pixels; lower is more detailed.

The default is 2 pixels. The terrain is last for piping.

Terrain.colorvalue

let color : (Color.t, t) => t

Set the basic lit terrain color; the terrain is last for piping.

This is the ALTERNATIVE to Terrain.layered, not a stage before it: it clears any layers, so whichever of the two comes last in a pipeline wins and the earlier one is simply dead. Pick one.

Terrain.layeredvalue

let layered : (Color.t, Color.t, Color.t, Color.t, float, t) => t

Blend lowland, highland, rock, and snow colors by height and slope.

snowHeight is in terrain-local world units. The terrain is last for piping.

Terrain.texturedvalue

let textured : (Asset.Texture, Asset.Texture, Asset.Texture, Asset.Texture, float, t) => t

Dress the layered bands with detail maps.

Each map supplies STRUCTURE, not color: it is divided by its own average, so it adds surface detail to the band's layered color without repainting it. (A photographic ground albedo averages brown; used directly it would turn a green hillside to dirt.) The maps blend by the same height and slope weights as the colors, so texturing changes what a band looks like, not where it falls. tileSize is the world-unit span of one tile; each map is sampled at two scales to hide the repeat, and detail fades out with distance. Requires layered. The terrain is last for piping.

Terrain.grassvalue

let grass : (float, float, float, Color.t, t) => t

Add camera-local GPU-instanced grass clusters.

spacing, distance, and bladeHeight are terrain-local world units. Grass is suppressed on steep, low-lying, and snowy samples. The terrain is last for piping.

Time21 entries

Branded durations used by timing APIs such as Sub.every.

Constructing a Time.t explicitly prevents milliseconds and seconds from being mixed accidentally.

Time.ttype

type t = host

An opaque duration.

Time.secondsvalue

let seconds : (float) => t

Construct a duration from seconds.

Time.millisvalue

let millis : (float) => t

Construct a duration from milliseconds.

Time.microsvalue

let micros : (float) => t

Construct a duration from microseconds.

Time.minutesvalue

let minutes : (float) => t

Construct a duration from minutes.

Time.hoursvalue

let hours : (float) => t

Construct a duration from hours.

Time.addvalue

let add : (t, t) => t

Add two durations. This is what 1.5s + 200ms calls.

Time.subvalue

let sub : (t, t) => t

Subtract one duration from another. This is what 1.5s - 200ms calls.

Time.scalevalue

let scale : (t, float) => t

Scale a duration by a plain number. This is what 0.5s * 2.0 calls. It takes the duration FIRST — the shape every declared * has — so unlike most of the prelude it is not written to be piped into.

Time.equalsvalue

let equals : (t, t) => bool

Are two durations the same? This is what 1s == 1000ms calls.

Durations are stored in seconds, so this is FLOAT equality on that number: 1s == 1000ms is true, but a duration accumulated through arithmetic may miss an exact literal by a rounding step. A duration is one-way (there is no way back to a number), so where a tolerance matters, keep the plain float you built it from and compare THAT.

Time.lessvalue

let less : (t, t) => bool

Is the first duration shorter than the second? This is what 200ms < 1.5s calls, and — swapped or negated — >, <=, and >= too.

Time.sunit

unit s = Time.seconds

Seconds as a literal suffix: 0.5s is exactly Time.seconds(0.5).

Time.msunit

unit ms = Time.millis

Milliseconds as a literal suffix: 500ms is exactly Time.millis(500.0).

Time.usunit

unit us = Time.micros

Microseconds as a literal suffix: 250us is exactly Time.micros(250.0).

Time.minunit

unit min = Time.minutes

Minutes as a literal suffix: 2min is exactly Time.minutes(2.0).

Time.hrunit

unit hr = Time.hours

Hours as a literal suffix: 1hr is exactly Time.hours(1.0).

Time.s (+)unit-operator

unit s (+) = Time.add

+ on durations: 1.5s + 200ms is Time.add(1.5s, 200ms). An operator belongs to the BRAND, so every duration suffix shares it — seconds and milliseconds add directly.

Time.s (-)unit-operator

unit s (-) = Time.sub

- on durations: 1.5s - 200ms is Time.sub(1.5s, 200ms).

Time.s (*)unit-operator

unit s (*) = Time.scale

* scales a duration by a number, on either side: 0.5s * 2.0 and 2.0 * 0.5s are both Time.scale(0.5s, 2.0).

Time.s (==)unit-operator

unit s (==) = Time.equals

== on durations: 1s == 1000ms is Time.equals(1s, 1000ms), and != is its negation. Underneath it is FLOAT equality on seconds — see Time.equals.

Time.s (<)unit-operator

unit s (<) = Time.less

< on durations: 200ms < 1.5s is Time.less(200ms, 1.5s). >, <=, and >= derive from it (swapped and/or negated), so all four orderings come from this one declaration.

Input

Input12 entries

Target-neutral continuously sampled input.

The optional sampledInput(model, snapshot) game hook receives one Input.snapshot immediately before every fixed simulation step. XR, gamepad, and touch are typed device domains; further devices belong as siblings beside them on the snapshot.

Input.point2type

type point2 = { x: float, y: float }

A plain two-dimensional point or axis pair.

Input.point3type

type point3 = { x: float, y: float, z: float }

A plain three-dimensional point or direction.

Input.quaterniontype

type quaternion = { x: float, y: float, z: float, w: float }

A quaternion in [x, y, z, w] component order, matching OpenXR and glTF.

Input.posetype

type pose = { position: point3, orientation: quaternion }

A rig-local pose where +X is right, +Y is up, and -Z is forward.

Input.controllertype

type controller = {
  active: bool,
  grip: Option.t<pose>,
  aim: Option.t<pose>,
  trigger: float,
  squeeze: float,
  thumbstick: point2,
  primaryPressed: bool,
  secondaryPressed: bool,
  thumbstickPressed: bool,
  menuPressed: bool
}

One XR controller's availability, poses, analog controls, and buttons.

Input.xrtype

type xr = {
  head: Option.t<pose>,
  left: controller,
  right: controller
}

Head and left/right controller state sampled from an XR runtime.

Input.gamepadtype

type gamepad = {
  leftStick: point2,
  rightStick: point2,
  leftTrigger: float,
  rightTrigger: float,
  south: bool,
  east: bool,
  west: bool,
  north: bool,
  leftBumper: bool,
  rightBumper: bool,
  leftStickPressed: bool,
  rightStickPressed: bool,
  dpadUp: bool,
  dpadDown: bool,
  dpadLeft: bool,
  dpadRight: bool,
  start: bool,
  select: bool
}

The primary connected gamepad's held state, aligned to the standard mapping desktop and web pads share. Face buttons are POSITIONAL — south is the bottom face button (A on Xbox, Cross on PlayStation, B on Nintendo) — because letter names swap between vendors. Sticks are -1..1 with up-positive y (the XR thumbstick convention); triggers are 0..1. Values are raw — apply your own deadzone. Levels only: detect edges against your model, as XR games do. Native's windowed runtime and the web runtime both poll the first connected standard-mapping pad each frame (on native, debug injection wins over the poll); while the window/document is unfocused or the clock is pinned a connected pad reads rest-level controls rather than Option.None. Browsers hide pads until a button is first pressed, and headless polls nothing — there this is Option.None unless injected.

Input.touchPointtype

type touchPoint = { id: float, x: float, y: float }

One touch contact in the same top-left-origin logical coordinate space as mouse (window points natively, CSS pixels on web). id is a small ordinal stable for the contact's lifetime.

Input.touchtype

type touch = {
  touches: List<touchPoint>,
  pressed: List<touchPoint>,
  released: List<touchPoint>
}

Active touch contacts plus this step's transitions — the keyboard contract for fingers: touches are held levels at current positions, pressed/released are de-duplicated one-step edges (a quick tap can appear in both while touches no longer carries it). A contact the platform steals (gesture navigation) reports through released, never a silently vanished touch. On the snapshot, Option.Some signals a touch surface EXISTS (empty lists while idle — the cue to show touch UI); Option.None means no touch input at all.

Input.mouseButtonstype

type mouseButtons = { left: bool, right: bool, middle: bool }

A fixed set of mouse buttons. mouse.buttons uses it for held levels; mouse.pressed / mouse.released use it for one-step transitions.

Input.mousetype

type mouse = {
  x: float,
  y: float,
  surfaceWidth: float,
  surfaceHeight: float,
  buttons: mouseButtons,
  pressed: mouseButtons,
  released: mouseButtons
}

Mouse position in top-left-origin logical surface coordinates, the matching logical extent, plus held levels and transitions since the previous fixed simulation step. Desktop uses window points; web uses CSS pixels, so this stays stable across Retina/device-pixel-ratio changes. A quick click may be both pressed.left and released.left in one sample.

Input.snapshottype

type snapshot = {
  heldKeys: List<Key.t>,
  pressedKeys: List<Key.t>,
  releasedKeys: List<Key.t>,
  mouse: mouse,
  xr: Option.t<xr>,
  gamepad: Option.t<gamepad>,
  touch: Option.t<touch>
}

Keyboard/mouse levels and transitions for one fixed simulation step.

pressedKeys and releasedKeys are de-duplicated transition sets. They survive render frames with no simulation step, are consumed by the first catch-up step, and are empty on later steps. Native OS-repeat events still reach the legacy input hook, but do not repeat pressedKeys.

Effects & messaging

Effect14 entries

Commands returned beside a model and performed by the runtime.

Results are converted into game messages and folded back through update. Effects remain outside the game's pure functional core.

Effect.ttype

type t = host

An opaque effect command.

Effect.nonevalue

let none : () => t

Produce no command.

Effect.nowvalue

let now : ((float) => 'msg) => t

Read the current Unix time in seconds and tag the result as a message.

Effect.randomvalue

let random : ((float) => 'msg) => t

Generate a random float in 0..1 and tag it as a message.

Effect.batchvalue

let batch : (List<t>) => t

Combine effects to be performed together.

Effect.sendvalue

let send : (float, string) => t

Send text over a live connection by connection ID.

Effect.sendMsgvalue

let sendMsg : (float, 'a) => t

Send a plain-data value over a live connection.

The peer receives Net.Data(id, value). Functions and opaque host values cannot be sent.

Effect.httpGetvalue

let httpGet : (string, (Net.HttpResponse) => 'msg) => t

Perform an HTTP GET and tag its Net.HttpResponse as a message.

Effect.httpPostvalue

let httpPost : (string, string, (Net.HttpResponse) => 'msg) => t

Perform an HTTP POST with a text body and tag its response as a message.

Effect.playvalue

let play : (Asset.Sound) => t

Play a non-spatial sound once.

Effect.playAtvalue

let playAt : (Asset.Sound, Vec3.t) => t

Play a spatial sound once at a world position.

Effect.playThenvalue

let playThen : (Asset.Sound, 'msg) => t

Play a sound once and deliver a message when playback finishes.

The completion message is native-only; on wasm the sound plays but the message is not delivered, so do not gate game progress on it.

Effect.preloadvalue

let preload : ('asset) => t

Begin loading a model or texture before it is referenced by draw.

The imperative prefetch; the declarative default is simply that draw references the asset. The parameter is generic, but only an Asset.Model or Asset.Texture is accepted: an Asset.Sound is a teaching error (a sound decodes at play time and has no pending state), and a bare path string is rejected toward Asset.model / Asset.texture like any other asset consumer. preloadThen takes the same values.

Effect.preloadThenvalue

let preloadThen : ('asset, 'msg) => t

Preload an asset and deliver a message when the load settles.

Settlement includes success and failure; Sub.assets reports which assets failed. Preloads count toward Sub.assets totals, and unlike playThen's completion message this one is delivered on wasm too.

Sub9 entries

Declarative event sources returned by the optional subscriptions hook.

Events become game messages and are folded through update before tick.

Sub.ttype

type t = host

An opaque subscription description.

Sub.nonevalue

let none : () => t

Subscribe to no events.

Sub.everyvalue

let every : (Time.t, 'msg) => t

Deliver a message at the requested interval.

The timer is STATELESS: it fires when an integer multiple of its period lies in the interval this frame covers, measured on the global time grid. So a long frame that spans several boundaries fires ONCE (missed boundaries collapse rather than queueing up), and timers keep their phase across a hot reload.

Sub.batchvalue

let batch : (List<t>) => t

Combine subscriptions.

Sub.connectvalue

let connect : (string, (Net.NetEvent) => 'msg) => t

Maintain a client connection and tag its ordered Net.NetEvent values as messages.

A failed attempt delivers Net.Error, then retries forever on the game-time clock after 250ms, doubling through 500ms, 1s, 2s, 4s, and at most 8s. Every failed attempt remains observable as an Error; Connected resets the next retry to 250ms. Dropping the subscription cancels the connection and its pending retry.

Sub.listenvalue

let listen : (string, (Net.NetEvent) => 'msg) => t

Maintain a server listener and tag its Net.NetEvent values as messages.

Sub.AssetFailuretype

type AssetFailure = { path: string, error: string }

One asset byte-load failure.

Only a failed BYTE load lands here — an asset whose bytes arrived but did not decode counts as loaded (it renders its fallback).

Sub.AssetProgresstype

type AssetProgress = { loaded: float, total: float, failed: List<AssetFailure> }

A snapshot of loaded, total, and failed assets.

All assets are settled when total > 0 and loaded + List.length(failed) == total. Failures never join loaded, and frame one can legitimately deliver 0 / 0, so a loading screen must gate on the whole expression rather than on loaded == total.

Sub.assetsvalue

let assets : ((AssetProgress) => 'msg) => t

Subscribe to asset-loading snapshots, including the initial state.

Delivery is driven by CHANGE, not by the time grid: the tagger fires whenever the shell's snapshot differs from the last one it delivered (including the first one on frame one), so this is not a per-frame poll. Like every subscription it requires an update hook.

Audio

AudioScene3 entries

Declarative audio scenes returned by the optional soundScape hook.

AudioScene.createvalue

let create : (List<AudioSource.t>) => t

Create an audio scene from continuous sources.

AudioScene.emptyvalue

let empty : () => t

Create an audio scene with no sources.

AudioSource4 entries

Continuous, declarative voices used in an AudioScene.

Sources are keyed for cross-frame identity so the runtime can reconcile a live voice instead of restarting it every frame.

AudioSource.ttype

type t = host

An opaque continuous audio source.

AudioSource.ambientvalue

let ambient : (string, Asset.Sound) => t

Create a non-spatial source identified by a stable key.

AudioSource.atvalue

let at : (string, Asset.Sound, Vec3.t) => t

Create a spatial source at a world position, identified by a stable key.

AudioSource.gainvalue

let gain : (float, t) => t

Set LINEAR source gain, where 1.0 is full volume; the source is last for piping.

UI

Ui15 entries

Lightweight game UI views returned by the optional ui hook.

Compose text and widgets into rows or columns, then pin them to a screen anchor with Ui.panel. Text is 14pt monospace.

Like draw, the hook is a pure function of the model, and the widgets are CONTROLLED: a view shows what the model says, and interacting with it sends a message — the model stays the only state. Those messages (a button's, and a slider's or text input's tagger result) are folded through update, so a program with interactive UI must define that hook.

Ui.viewtype

type view = host

An opaque UI view.

Ui.anchortype

type anchor = host

An opaque screen anchor.

Ui.textvalue

let text : (string) => view

Create a text view.

Ui.textColorvalue

let textColor : (Color.t, string) => view

Create a colored text view.

Ui.columnvalue

let column : (List<view>) => view

Stack views vertically.

Ui.rowvalue

let row : (List<view>) => view

Arrange views horizontally.

Ui.panelvalue

let panel : (anchor, view) => view

Pin a view to an anchor; the view is last for piping.

Ui.topLeftvalue

let topLeft : () => anchor

Anchor a panel to the top-left corner.

Ui.topRightvalue

let topRight : () => anchor

Anchor a panel to the top-right corner.

Ui.bottomLeftvalue

let bottomLeft : () => anchor

Anchor a panel to the bottom-left corner.

Ui.bottomRightvalue

let bottomRight : () => anchor

Anchor a panel to the bottom-right corner.

Ui.centervalue

let center : () => anchor

Anchor a panel to the center of the screen.

Ui.buttonvalue

let button : (string, 'msg) => view

Create a button that delivers a message through update when clicked.

The message is delivered VERBATIM, like Sub.every's — so the link between its type and update's is a runtime check, not a static one.

Ui.slidervalue

let slider : (float, float, float, 'tagger) => view

Create a controlled slider from minimum, maximum, value, and tagger.

A drag applies the tagger to the new value and folds the resulting message through update. The maximum must exceed the minimum, and the tagger must be a function or constructor.

Ui.textInputvalue

let textInput : (string, 'tagger) => view

Create a controlled text input whose tagger receives edited text.

While a field is FOCUSED the game's input hook is suppressed, so keys type into the field instead of driving the game; Escape defocuses the field first and releases the cursor second. An update that transforms the text resets the caret to the end.

Html11 entries

Elm-style HTML trees returned by the optional webview hook.

Native renders the tree over the 3D frame; wasm renders it as a DOM overlay above the canvas.

Html.nodetype

type node = host

An opaque HTML node.

Html.textvalue

let text : (string) => node

Create an escaped text node.

Html.elementvalue

let element : (string, List<Attr.t>, List<node>) => node

Create an element from a safe tag name, attributes, and children.

Names must start with a letter and then contain only letters, digits, or dashes. Script-capable tags such as script and iframe are refused.

Html.divvalue

let div : (List<Attr.t>, List<node>) => node

Create a div element.

Html.spanvalue

let span : (List<Attr.t>, List<node>) => node

Create a span element.

Html.buttonvalue

let button : (List<Attr.t>, List<node>) => node

Create a button element.

Html.h1value

let h1 : (List<Attr.t>, List<node>) => node

Create an h1 heading element.

Html.h2value

let h2 : (List<Attr.t>, List<node>) => node

Create an h2 heading element.

Html.pvalue

let p : (List<Attr.t>, List<node>) => node

Create a paragraph element.

Html.inputvalue

let input : (List<Attr.t>) => node

Create a controlled, single-line text input.

Pair it with Attr.value and Attr.onInput.

Html.stylevalue

let style : (string) => node

Create a style element containing a raw CSS stylesheet.

Attr10 entries

Attributes and event handlers for Html elements.

Event attributes deliver messages through the game's update function.

Attr.ttype

type t = host

An opaque HTML attribute.

Attr.classvalue

let class : (string) => t

Set the element's CSS class string.

Attr.stylevalue

let style : (string) => t

Set the element's raw inline style string.

Attr.stylesvalue

let styles : (List<Style.t>) => t

Combine typed Style values into one inline style attribute.

Attr.idvalue

let id : (string) => t

Set the element's id.

Attr.attrvalue

let attr : (string, string) => t

Set a safe attribute by name and value.

Names must start with a letter and then contain only letters, digits, or dashes. Executable, navigating, document-embedding, and runtime-reserved names (on*, href, action, formaction, srcdoc, and data-fn-*) are refused.

Attr.valuevalue

let value : (string) => t

Set the controlled value of an Html.input.

Attr.placeholdervalue

let placeholder : (string) => t

Set an input's placeholder text.

Attr.onClickvalue

let onClick : ('msg) => t

Deliver a message through update when the element is clicked.

A click on a descendant counts too, through ordinary DOM bubbling. The message is delivered verbatim, like Ui.button's.

Attr.onInputvalue

let onInput : ('tagger) => t

Apply a tagger to edited text and deliver its message through update.

Works on both shells. Clicking the input focuses it, after which keys type into the field rather than reaching the game's input hook (Escape defocuses first, releases the cursor second), and focus survives the per-edit re-render. As with Ui.textInput, an update that transforms the text resets the caret to the end. IME composition (CJK, dead keys) is deferred on native.

Style26 entries

Typed inline CSS declarations combined with Attr.styles.

Constructors format their values immediately and share Color.t with the 3D APIs. Use Html.style for selectors, pseudo-classes, and keyframes.

Style.ttype

type t = host

An opaque inline CSS declaration.

Style.flexRowvalue

let flexRow : () => t

Use horizontal flexbox layout.

Style.gapPxvalue

let gapPx : (float) => t

Set the flex/grid gap in pixels.

Style.justifyStartvalue

let justifyStart : () => t

Align content to the start of the main axis.

Style.justifyEndvalue

let justifyEnd : () => t

Align content to the end of the main axis.

Style.justifyBetweenvalue

let justifyBetween : () => t

Distribute content with space between items.

Style.alignStartvalue

let alignStart : () => t

Align items to the start of the cross axis.

Style.alignEndvalue

let alignEnd : () => t

Align items to the end of the cross axis.

Style.widthPxvalue

let widthPx : (float) => t

Set width in pixels.

Style.widthPctvalue

let widthPct : (float) => t

Set width as a percentage of the parent.

Style.heightPxvalue

let heightPx : (float) => t

Set height in pixels.

Style.heightPctvalue

let heightPct : (float) => t

Set height as a percentage of the parent.

Style.paddingPxvalue

let paddingPx : (float) => t

Set padding on all sides in pixels.

Style.marginPxvalue

let marginPx : (float) => t

Set margin on all sides in pixels.

Style.colorvalue

let color : (Color.t) => t

Set the text color.

Style.backgroundvalue

let background : (Color.t) => t

Set the background color.

Style.boldvalue

let bold : () => t

Use a bold font weight.

Style.borderPxvalue

let borderPx : (float, Color.t) => t

Add a solid border with pixel width and color.

Style.roundedPxvalue

let roundedPx : (float) => t

Set border radius in pixels.

Style.opacityvalue

let opacity : (float) => t

Set opacity in 0..1. A value outside that range is an error, not a clamp.

Style.rawvalue

let raw : (string, string) => t

Create an inline declaration for an arbitrary CSS property and value.

Property names must start with a letter and then contain only letters, digits, or dashes.

Assets

Asset7 entries

Typed locators for models, textures, and sounds.

Each asset kind has its own branded value, making wrong-kind uses a type error. Prefer constants generated by functor import; constructors remain available where a locator enters the program dynamically.

Asset consumers — Scene.model, Sprite.image/imageRegion, Terrain.heightmap/textured, Effect.play/playAt/playThen/ preload/preloadThen, AudioSource.ambient/at — take these branded values ONLY: a bare path string there is a check error and, at runtime, a teaching error pointing at the generated manifest, and an asset of the wrong kind names the constructor that was wanted. (Texture.file paths, Skybox.files faces, Anim.clip names, and AudioSource keys are not asset locators and stay plain strings.)

Asset.Modeltype

type Model = host

A model asset locator.

Asset.Soundtype

type Sound = host

A sound asset locator.

Asset.modelvalue

let model : (string) => Model

Construct a model locator from a relative path or URL.

Asset.texturevalue

let texture : (string) => Texture

Construct a texture locator from a relative path or URL.

Asset.soundvalue

let sound : (string) => Sound

Construct a sound locator from a relative path or URL.

Asset.whilePendingvalue

let whilePending : ('placeholder, 'asset) => 'asset

Use another asset of the same kind while a model or texture is loading.

Models and textures only — a sound decodes at play time and has no pending state, so asking for one is a teaching error. The placeholder is just another asset of the same kind, so placeholders chain. The requested asset is last for piping. Failed loads use the normal fallback because failure is no longer pending, and Sub.assets still reports the failure.

Standard library

Ships with the language — available in every Functor Lang program, runner or not.

Collections

List25 entries

Immutable list operations.

Functor Lang has no loops: iteration is List.map, List.filter, and List.fold, which run iteratively in the interpreter and so consume no evaluation depth (unlike a hand-rolled recursive walk, which trips the recursion cap).

Every function takes its list LAST, so it threads through the thread-last pipeline operator: xs |> List.map(f) is exactly List.map(f, xs).

The partial accessors — nth, head, last, find — answer with Option.t, never a sentinel, so the absent case has to be handled.

List.mapvalue

let map : (('a) => 'b, List<'a>) => List<'b>

Apply fn to every element, preserving order and length.

List.indexedMapvalue

let indexedMap : ((float, 'a) => 'b, List<'a>) => List<'b>

Apply fn to every element with its 0-based index, as fn(index, element) — index FIRST, unlike the subject-last list argument.

List.filtervalue

let filter : (('a) => bool, List<'a>) => List<'a>

Keep the elements the predicate accepts, in order.

List.foldvalue

let fold : (('b, 'a) => 'b, 'b, List<'a>) => 'b

Reduce left-to-right from an initial accumulator, calling fn(acc, element). This is the iteration primitive to reach for when map and filter do not fit — a recursive walk hits the interpreter's recursion cap around 40–60 elements, while fold has no such limit.

List.concatMapvalue

let concatMap : (('a) => List<'b>, List<'a>) => List<'b>

Map each element to a list and concatenate the results (one level).

List.rangevalue

let range : (float) => List<float>

[0, 1, … n - 1]. A non-positive n gives the empty list; a count that is not finite, or above one million, is an error. A fractional count truncates toward zero, so List.range(3.7) is [0, 1, 2].

List.gridvalue

let grid : ((float, float) => 'a, float, float) => List<List<'a>>

Build a rows x cols grid by calling fn(row, col) for every cell, both 0-based — the procedural-heightmap shape, e.g. Scene.heightmap(List.grid(height, rows, cols)). Both counts must be whole and non-negative, and the cells must total at most one million.

List.maximumvalue

let maximum : (List<float>) => Option.t<float>

The largest number in the list as Option.Some, or Option.None for an empty list — partial like nth/head/last/find. NaN elements are ignored unless every element is NaN.

List.minimumvalue

let minimum : (List<float>) => Option.t<float>

The smallest number in the list as Option.Some, or Option.None for an empty list — maximum's mirror, with the same NaN rule.

List.sumvalue

let sum : (List<float>) => float

The sum of the numbers. The empty list sums to 0.0.

List.lengthvalue

let length : (List<'a>) => float

How many elements the list has, as a number.

List.isEmptyvalue

let isEmpty : (List<'a>) => bool

Whether the list has no elements.

List.reversevalue

let reverse : (List<'a>) => List<'a>

The list in reverse order.

List.appendvalue

let append : (List<'a>, List<'a>) => List<'a>

The piped list followed by other: xs |> List.append(ys) is xs then ys.

List.flattenvalue

let flatten : (List<List<'a>>) => List<'a>

Concatenate a list of lists, one level deep.

List.zipvalue

let zip : (List<'b>, List<'a>) => List<('a, 'b)>

Pair each element of the piped list with the element of other at the same index — the PIPED list fills the first slot of every tuple. The result truncates to the shorter of the two.

List.sortByvalue

let sortBy : (('a) => float, List<'a>) => List<'a>

Sort ascending by the number fn returns. The sort is STABLE and calls fn exactly once per element. NaN keys sort last, tied with each other regardless of sign, so the order is identical on every platform; -0.0 and 0.0 tie.

List.takevalue

let take : (float, List<'a>) => List<'a>

The first count elements, saturating: past the end gives the whole list, and a negative count behaves as 0. A fractional count truncates toward zero rather than raising — unlike nth, where a fractional index is a caller bug.

List.dropvalue

let drop : (float, List<'a>) => List<'a>

Everything after the first count elements, saturating: past the end gives the empty list, and a negative count behaves as 0. A fractional count truncates, like take.

List.anyvalue

let any : (('a) => bool, List<'a>) => bool

Whether ANY element satisfies the predicate, short-circuiting at the first one that does. The empty list is false.

List.allvalue

let all : (('a) => bool, List<'a>) => bool

Whether EVERY element satisfies the predicate, short-circuiting at the first one that does not. The empty list is true.

List.nthvalue

let nth : (float, List<'a>) => Option.t<'a>

The element at a 0-based index, or Option.None when the index is out of range. An index that is not a whole, finite number is an ERROR rather than an absence — that is a caller bug, not a missing element.

List.headvalue

let head : (List<'a>) => Option.t<'a>

The first element, or Option.None for an empty list.

List.lastvalue

let last : (List<'a>) => Option.t<'a>

The last element, or Option.None for an empty list.

List.findvalue

let find : (('a) => bool, List<'a>) => Option.t<'a>

The first element satisfying the predicate, or Option.None when none does. It stops at that first match rather than scanning the whole list.

Map8 entries

Immutable keyed collections.

A Map is plain data: it compares structurally, displays, snapshots, and survives hot reload. Every operation returns a NEW map rather than mutating the old one, and every function takes the map LAST so it threads through a pipeline.

Keys are bounded to bool, FINITE float, and string. Inference keeps a map homogeneous in ordinary code, and a generic or unknown seam is checked again at runtime; NaN and the infinities are refused, while -0.0 and 0.0 are the same key.

Every map is stored in one canonical key order — bool before float before string, then false before true, ascending numerically, and strings by Unicode scalar value (not locale-aware). So values, toList, structural equality, and display all agree byte-for-byte between native and wasm.

get and member are logarithmic; the immutable insert and remove copy the ordered storage and are linear, as are values and toList; fromList is O(n log n).

Map.emptyvalue

let empty : () => Map<'a, 'b>

The empty map.

Map.getvalue

let get : ('a, Map<'a, 'b>) => Option.t<'b>

The value stored under key, or Option.None when the map has no such key.

Map.insertvalue

let insert : ('a, 'b, Map<'a, 'b>) => Map<'a, 'b>

The map with key bound to value, replacing any existing binding.

Map.removevalue

let remove : ('a, Map<'a, 'b>) => Map<'a, 'b>

The map without key. Removing an absent key is not an error.

Map.membervalue

let member : ('a, Map<'a, 'b>) => bool

Whether the map holds a binding for key.

Map.valuesvalue

let values : (Map<'a, 'b>) => List<'b>

Every value, in canonical key order.

Map.toListvalue

let toList : (Map<'a, 'b>) => List<('a, 'b)>

Every (key, value) pair, in canonical key order.

Map.fromListvalue

let fromList : (List<('a, 'b)>) => Map<'a, 'b>

Build a map from (key, value) pairs. For a repeated key the LAST pair wins.

Text

Text14 entries

String building, formatting, and inspection.

There is no string-concatenation operator and no character type: interpolation ($"score: {n}") covers most formatting, Text.concat joins two strings, and single characters are one-character STRINGS.

Functions with a clear subject take the string LAST so they thread through a pipeline: s |> Text.contains("ab") is Text.contains("ab", s).

Lengths and character splits count **Unicode scalar values** — not bytes, and not grapheme clusters. Text.length(s) always equals List.length(Text.chars(s)).

Text.concatvalue

let concat : (string, string) => string

a followed by b.

The subject is LAST here too, which means piping PREPENDS: the piped string lands in b, so "SUF" |> Text.concat("PRE") is "PRESUF".

Text.fromFloatvalue

let fromFloat : (float) => string

A number rendered in Functor Lang's canonical display form — the same text string interpolation produces. That is the shortest round-tripping form, so 1.0 renders as "1"; use Text.fixed when a HUD needs a stable width.

Text.fixedvalue

let fixed : (float, float) => string

A number rendered with exactly decimals digits after the point. Text.fixed(42.0, 0.0) is "42", the integer-formatting shape — the one to reach for in a HUD. The digit count must be a whole number from 0 to 12; anything else is an error. Unlike the string functions, the NUMBER comes first, so this cannot be piped on: write Text.fixed(hp, 0.0), not hp |> Text.fixed(0.0).

Text.toBulletsvalue

let toBullets : (List<string>) => string

The strings as a bulleted block, one item per line.

Text.splitvalue

let split : (string, string) => List<string>

Split the subject on every occurrence of sep. Splitting the empty string gives [""]; an empty separator is an error.

Text.joinvalue

let join : (string, List<string>) => string

Join the strings with sep between them. An empty separator is fine here — unlike Text.split — and simply concatenates.

Text.parseFloatvalue

let parseFloat : (string) => float

Parse a number, ignoring surrounding whitespace. Unparseable text answers 0.0 rather than raising — validate the input yourself when the difference matters. Text that parses to a non-finite number ("inf", "NaN", an overflowing literal) degrades to 0.0 as well, so the result is always finite.

Text.lengthvalue

let length : (string) => float

How many Unicode scalar values the string contains.

Text.charsvalue

let chars : (string) => List<string>

The string's Unicode scalar values, each as a one-character string.

Text.toUppervalue

let toUpper : (string) => string

The string uppercased, Unicode-aware — so the LENGTH may change ("ß" uppercases to "SS").

Text.toLowervalue

let toLower : (string) => string

The string lowercased, Unicode-aware — so the length may change.

Text.trimvalue

let trim : (string) => string

The string without leading or trailing whitespace.

Text.containsvalue

let contains : (string, string) => bool

Whether the subject contains needle. The empty needle is contained in every string.

Text.replacevalue

let replace : (string, string, string) => string

Replace EVERY occurrence of from with to, never re-scanning what was just written. An empty from is an error.

Numbers & randomness

Math24 entries

Numeric functions and constants.

Functor Lang has one number type (float, an f64) and a deliberately small operator set: Math.mod stands in for % and Math.pow for ^. Arithmetic is IEEE throughout, so 1.0 / 0.0 is infinity and NaN compares false against everything, itself included.

Two behaviors differ from the usual defaults and are worth knowing: mod is EUCLIDEAN (its result is never negative) and round goes half AWAY FROM ZERO (not banker's rounding).

Unlike the collections, most of Math reads as ordinary notation rather than as a pipeline: pow, atan2, mod, min, and max take their NUMBER FIRST, so for the order-sensitive ones (pow, atan2, mod) piping feeds the wrong slot. clamp, clamp01, lerp, and smoothstep are the deliberate subject-last exceptions — their bounds and parameters are configuration and their number is the subject, so they pipe: n |> Math.clamp(0.0, 10.0).

Math.pivalue

let pi : float

The ratio of a circle's circumference to its diameter — a constant VALUE, not a function, so it is written Math.pi with no parentheses.

Math.sinvalue

let sin : (float) => float

The sine of an angle in radians.

Math.cosvalue

let cos : (float) => float

The cosine of an angle in radians.

Math.tanvalue

let tan : (float) => float

The tangent of an angle in radians.

Math.asinvalue

let asin : (float) => float

The arcsine, in radians. NaN outside [-1, 1]: it does NOT clamp, so a dot product nudged past 1.0 by float error needs an explicit |> Math.clamp(-1.0, 1.0) first.

Math.acosvalue

let acos : (float) => float

The arccosine, in radians. NaN outside [-1, 1], exactly like asin.

Math.atanvalue

let atan : (float) => float

The arctangent, in radians.

Math.atan2value

let atan2 : (float, float) => float

The angle in radians from the positive x-axis to the point (x, y), using the standard mathematical argument order with y FIRST.

Math.sqrtvalue

let sqrt : (float) => float

The non-negative square root.

Math.powvalue

let pow : (float, float) => float

base raised to exp — the language has no ^ operator.

Math.logvalue

let log : (float) => float

The natural logarithm, base e — the inverse of Math.exp.

Math.expvalue

let exp : (float) => float

e raised to the given power.

Math.absvalue

let abs : (float) => float

The magnitude, without sign.

Math.signvalue

let sign : (float) => float

-1, 0, or 1 — and exactly 0 AT zero, so it is not a two-way branch. NaN answers NaN.

Math.floorvalue

let floor : (float) => float

The largest whole number that is not greater than n.

Math.ceilvalue

let ceil : (float) => float

The smallest whole number that is not less than n.

Math.roundvalue

let round : (float) => float

The nearest whole number, rounding halves AWAY FROM ZERO rather than to even: 0.5 rounds to 1, 2.5 to 3, and -2.5 to -3.

Math.modvalue

let mod : (float, float) => float

The EUCLIDEAN remainder: the result always lands in [0, abs(b)), so negatives wrap positively — Math.mod(-1.0, 8.0) is 7.0, the wraparound games want. A zero divisor answers NaN.

Math.minvalue

let min : (float, float) => float

The smaller of two numbers.

Math.maxvalue

let max : (float, float) => float

The larger of two numbers.

Math.clampvalue

let clamp : (float, float, float) => float

n confined to [low, high], subject-last so it pipes: n |> Math.clamp(0.0, 10.0). A low greater than high is an error, not a silent swap.

Math.clamp01value

let clamp01 : (float) => float

n confined to [0, 1] — the same as Math.clamp(0.0, 1.0, n).

Math.lerpvalue

let lerp : (float, float, float) => float

Linear interpolation from from toward target by t (unclamped): from + (target - from) * t, and t = 1.0 answers target exactly. Mirrors Vec3.lerp(target, t, from) — the pipe subject is the start value, so x |> Math.lerp(target, t) works like its vector sibling. (Deliberately NOT GLSL's mix(a, b, t) order.)

Math.smoothstepvalue

let smoothstep : (float, float, float) => float

Hermite smoothstep of x across [edge0, edge1], clamped to [0, 1]. The edges must be a finite ascending range (edge0 < edge1, both finite and not overflow-wide) — anything else is an error, never a silent NaN.

Random5 entries

Pure, seeded pseudo-random numbers.

There is no hidden global generator: a draw takes a seed and hands back the NEXT seed alongside its value, so randomness is an ordinary part of the model. Thread the next seed through and a run is exactly reproducible — which is what makes rewind, replay, and hot reload work.

Seed a stream once, at init: a fixed Random.seed(42.0) for a reproducible run, or Effect.random / Effect.now for a different stream each session. Distinct seeds produce DECORRELATED streams that share no prefix, so per-entity streams do not visibly rhyme with each other.

Random.Seedtype

type Seed

An opaque PRNG seed.

The brand keeps seeds out of arithmetic: a bare number where a Seed is expected, or seed + 1.0 to derive a sibling stream, is a check-time error — use Random.fork instead. At runtime a seed is still plain data, so it snapshots, hot-reloads, and time-travels like any other model field.

Random.seedvalue

let seed : (float) => Seed

Make a seed from any finite number.

The number's BITS are hashed, so fractional seeds are as usable as whole ones — Random.seed(0.42) from an Effect.random result names a distinct starting point just as Random.seed(42.0) does.

Random.stepvalue

let step : (Seed) => (float, Seed)

Draw the next value, as (value, nextSeed) with value in [0, 1).

The same seed always yields the same pair. Bind both halves and carry the next seed forward — let (v, next) = Random.step(model.seed) in … — otherwise the stream never advances.

Random.rangevalue

let range : (float, float, Seed) => (float, Seed)

Draw one value rescaled into [lo, hi), as (value, nextSeed) — one step draw, interpolated between the bounds.

Only finiteness is checked, so the bounds are yours to get right: reversed ones simply interpolate the other way (landing in (hi, lo]), and equal ones always answer that value.

Random.forkvalue

let fork : (float, Seed) => Seed

The seed of decorrelated child stream i — the typed replacement for deriving sibling streams by arithmetic. Subject-last, so per-entity streams read as model.seed |> Random.fork(i), and any number may name a stream.

Fallibility

Option9 entries

A value that may be absent.

Option is an ordinary generic variant bundled with the language, so it is available in every project and under the plain functor-lang CLI. It is what the partial accessors (List.head, Map.get, …) answer with instead of a sentinel, which is what forces the absent case to be handled.

ALWAYS qualify the constructors: bare Some / None do not resolve — they belong to this module, so write Option.Some(x) and Option.None in both expressions and patterns (or open Option first).

Helpers take the option LAST, so they thread through a pipeline: Option.Some(41.0) |> Option.map((n) => n + 1.0) |> Option.defaultValue(0.0).

Option.ttype

type t<'value> =
  | Some(value: 'value)
  | None

A present value (Option.Some) or its absence (Option.None).

Option.mapvalue

let map : (('value) => 'mapped, t<'value>) => t<'mapped>

Transform a present value; leave None unchanged.

Option.bindvalue

let bind : (('value) => t<'mapped>, t<'value>) => t<'mapped>

Continue with an optional computation when a value is present.

Option.defaultValuevalue

let defaultValue : ('value, t<'value>) => 'value

Return the contained value, or an eager fallback for None.

Option.defaultWithvalue

let defaultWith : (() => 'value, t<'value>) => 'value

Return the contained value, computing the fallback only for None.

Option.isSomevalue

let isSome : (t<'value>) => bool

Whether the option contains a value.

Option.isNonevalue

let isNone : (t<'value>) => bool

Whether the option is None.

Option.filtervalue

let filter : (('value) => bool, t<'value>) => t<'value>

Keep a present value only when it satisfies the predicate.

Option.toListvalue

let toList : (t<'value>) => List<'value>

Convert Some(value) to [value] and None to [].

Result9 entries

A computation that either succeeded or carries an error.

Result is an ordinary generic variant bundled with the language, so it is available in every project and under the plain functor-lang CLI. Use it where a failure needs to explain itself; use Option where absence needs no explanation.

ALWAYS qualify the constructors: bare Ok / Error do not resolve — write Result.Ok(x) and Result.Error(e) in both expressions and patterns (or open Result first).

Helpers take the result LAST, so they thread through a pipeline.

Result.ttype

type t<'value, 'error> =
  | Ok(value: 'value)
  | Error(error: 'error)

A success carrying a value (Result.Ok) or a failure carrying an error (Result.Error).

Result.mapvalue

let map : (('value) => 'mapped, t<'value, 'error>) => t<'mapped, 'error>

Transform a successful value; leave an error unchanged.

Result.mapErrorvalue

let mapError : (('error) => 'mapped, t<'value, 'error>) => t<'value, 'mapped>

Transform an error; leave a successful value unchanged.

Result.bindvalue

let bind : (('value) => t<'mapped, 'error>, t<'value, 'error>) => t<'mapped, 'error>

Continue with a result-producing computation after success.

Result.defaultValuevalue

let defaultValue : ('value, t<'value, 'error>) => 'value

Return the successful value, or an eager fallback for an error.

Result.defaultWithvalue

let defaultWith : (('error) => 'value, t<'value, 'error>) => 'value

Return the successful value, or compute a fallback from the error.

Result.isOkvalue

let isOk : (t<'value, 'error>) => bool

Whether the result is successful.

Result.isErrorvalue

let isError : (t<'value, 'error>) => bool

Whether the result contains an error.

Result.toOptionvalue

let toOption : (t<'value, 'error>) => Option.t<'value>

Convert Ok(value) to Option.Some(value) and an error to Option.None.

Input

Key1 entry

Keyboard keys, as a variant rather than strings.

Key is built in — no declaration and no import. The input hook's key parameter and the Input.snapshot key sets all carry these constructors, so a misspelling (Key.Enterr) is a load-time error instead of an arm that silently never matches. Match them (| Key.W =>) or compare them (key == Key.Enter).

Key.ttype

type t =
  | A | B | C | D | E | F | G | H | I | J | K | L | M
  | N | O | P | Q | R | S | T | U | V | W | X | Y | Z
  | Up | Down | Left | Right
  | Space | Enter | Escape
  | Num0 | Num1 | Num2 | Num3 | Num4 | Num5 | Num6 | Num7 | Num8 | Num9

A keyboard key.

The digit row is Num0Num9 — constructor names have to be identifiers, so a bare digit is not one of them. Keys the platform reports that Functor does not name are never delivered to game logic.

Mouse1 entry

Mouse buttons, as a variant rather than strings.

The mouse twin of Key, and built in the same way: the mouseButton hook's button parameter carries these constructors, so a typo is caught at load time. Match them (| Mouse.Left =>) or compare them (button == Mouse.Right).

Mouse.ttype

type t =
  | Left | Right | Middle

A mouse button. Buttons beyond these three are never delivered to game logic.

Diagnostics

Debug1 entry

The observability escape hatch.

Debug is the one impure corner of the standard library, and it is impure only in the direction of the terminal: it cannot influence the model or the simulation, so a game with and without it evolves identically.

Debug.logvalue

let log : (string, 'a) => 'a

Log label: value and return value UNCHANGED — an Elm-style trace.

The value is rendered exactly as functor-lang run displays it, whatever its type. Because the label comes first and the subject last, it reads standalone (Debug.log("x", model.x)) and threads through a pipeline (model.x |> Debug.log("x") |> Math.clamp01) without changing what flows through.

Under the plain functor-lang CLI the line goes to stdout; under the game runner it goes to the CLI's log stream, or the browser console on wasm. It is NOT rate-limited, so a call in tick or draw fires every frame — prefer an event path such as input or update, or remove the call when you are done with it.