MALDA™ Reference Manual

The AI-First Programming Language - Version 1.0.11

26. Browser JavaScript UI Backend

MALDA also supports a browser-hosted UI model where MALDA source is transpiled to JavaScript and runs with the MALDA JS runtime. This is separate from server-side @PAGE, @AIPAGE, and server-component flows.

Start here: Read 23. Web UI Overview to compare UI models. This chapter covers browser-side MALDA execution, DOM helpers, template mode, and the canvas/game API. For server-rendered UI, see Web UI Server Components or HttpServer & HTML UI Generation.

26.1 Chapter Map

26.2 What This Mode Provides

26.2.1 Target decorators in JS output

When compiling to JavaScript, target decorators can be used to keep browser and server concerns in one source file:

Route decorators are still server-oriented. A declaration such as @GET(...) is treated as server code and is not emitted in JavaScript output.

26.3 Compile Commands

# API mode (.malda source)
malda compile Examples/Web/js/hello_dom.malda --mode js -o Examples/Web/js/hello_dom.js

# Alias form (equivalent)
malda compile Examples/Web/js/counter.malda --target js -o Examples/Web/js/counter.js

# PWA output directory
malda compile Examples/Web/js/hello_dom.malda --target pwa -o dist/hello-dom

# Template mode (.malda.html source)
malda compile Examples/Web/js/hello_template.malda.html --mode js -o Examples/Web/js/hello_template.generated.js

--target js is an alias for --mode js. --target pwa is an alias for --mode pwa and produces a directory containing the app shell, runtime, and transpiled JavaScript.

The inner loop for canvas games is malda play. It compiles --mode js, writes a host page, and serves a local URL so you do not have to author HTML. Packaging for itch.io stays --mode pwa.

malda new game my-game
cd my-game
malda play app.malda

Canvas plus a score API is malda new game --fullstack. That emits @client() / @GET / @POST and schema Score in one file. Compile with --mode fullstack; malda play refuses those sources.

malda new game my-scores --fullstack
cd my-scores
malda compile app.malda --mode fullstack -o dist

26.4 Required Script Loading Order

The host page must include a container such as #app, then load scripts in this order. Same file: Examples/Web/js/host.html.

  1. Any required browser dependency for the chosen namespace, for example three.js for three.*
  2. malda-js-runtime.js
  3. The compiled MALDA script
  4. The bootstrap call, such as MaldaApp.main()
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>MALDA JS host</title>
</head>
<body>
  <div id="app"></div>
  <script src="../wwwroot/malda-js-runtime.js"></script>
  <script src="./counter.js"></script>
  <script>
    if (window.MaldaApp && typeof window.MaldaApp.main === "function") {
      window.MaldaApp.main();
    }
  </script>
</body>
</html>

println is a JavaScript-backend console helper. The interpreter has no println (use io.print / print there — see 12. Input/Output).

26.5 API Mode DOM Helpers

The JavaScript runtime exposes browser helpers under dom for direct DOM manipulation. Same program: Examples/Web/js/counter.malda (compile with the command in 26.3, then open the host page from 26.4).

var root = dom.query("#app");

if (root == null) {
    println("No #app container found.");
} else {
    dom.clear(root);

    var count = 0;

    var title = dom.create("h1");
    dom.setText(title, "Counter example");
    dom.append(root, title);

    var valueLabel = dom.create("p");
    dom.append(root, valueLabel);

    function renderCount() {
        dom.setText(valueLabel, "Count: " + string(count));
    }

    var button = dom.create("button");
    dom.setText(button, "Increment");
    dom.append(root, button);

    function onIncrementClick() {
        count = count + 1;
        renderCount();
    }

    dom.on(button, "click", onIncrementClick);
    renderCount();
}

26.6 Sum Types and Pattern Matching in JS Mode

You can use the same sum-type and pattern-matching model in JavaScript backend mode as in interpreted mode.

type Result = Ok(value) | Err(message);

function divide(a, b) {
    if (b == 0) return Err("divide by zero");
    return Ok(a / b);
}

var outcome = divide(10, 2);
var text = match outcome {
    case Ok(v): "ok: " + v;
    case Err(msg): "error: " + msg;
};
println(text);

All pattern families are supported in JS mode: literals, identifiers, wildcard, variant payloads, array patterns with ...rest, and nested object patterns.

26.7 Actors in JS Mode

The JavaScript backend includes local actor runtime support through mlRuntime.actors. This provides browser-side or Node-local actor concurrency with mailbox semantics aligned to MALDA actor behavior.

Boundary: this is local actor runtime support only. Direct transparent communication between browser JS actors and server-side actors is not enabled by default.

26.8 Template Mode Syntax

Template mode uses .malda.html files. Use {{ ... }} for interpolation and {% ... %} for statements.

<div class="hello-template">
  <h1>Hello from template mode</h1>
  <p>User: {{ "Andrea" }}</p>
  {% var count = 2; %}
  <p>Count value: {{ count }}</p>
</div>

See Examples/Web/js/host.html for a minimal static page that demonstrates runtime loading and compiled-script bootstrap.

26.9 Game Canvas API

The JavaScript backend includes a browser canvas/game API under game.* for simple 2D interactive loops.

Image and camera guidance: game.loadImage returns a handle immediately; decode is async. Draws no-op until game.imageIsReady is true (missing files stay unready, no throw). game.setCamera subtracts from subsequent world draws (fillRect, drawImage, lines, text). It does not move setPixel / blitPixels. Mouse coordinates stay in canvas pixels. See Examples/Games/game_sprite_smoke.malda.

Input guidance: Key and gamepad edges are snapshotted at the start of update and are false in render. Read wasKeyPressed / wasKeyReleased / wasGamepadButtonPressed in update only (same key names as isKeyDown). getTouches() returns { id, x, y } in canvas pixels (empty when none). The first active touch still aliases mouse button 0. Missing Gamepad API or disconnected pads return disconnected / axis 0. game.stop() clears keys and button edges. See Examples/Games/game_input_smoke.malda.

Collision guidance: Overlap helpers are inclusive (touching edges count) and return false when any width, height, or radius is ≤ 0. They are pure functions: no canvas, and they do not subtract setCamera. Not swept AABB or physics. See Examples/Games/game_collision_smoke.malda.

Loop guidance: updateFn receives dtMs. Scale movement by dtMs so animation stays frame-rate independent. game.startFixed(updateFn, renderFn?, tickMs?) defaults tickMs to 1000 / 60, accrues wall time, and calls update(tickMs) zero or more times per frame (max 5), then render once. It cannot run at the same time as game.start. See Examples/Games/game_fixed_save_smoke.malda.

Save guidance: game.save / game.load / game.removeSave store JSON in origin-scoped localStorage under a malda.game. prefix (not files). Missing storage, quota errors, or corrupt JSON: load returns null and save is a no-op (no throw).

Audio guidance: call game.audioInit() after a user gesture to satisfy browser autoplay policies. game.audioPlaySample(url, volume?, options?) decodes a WAV/OGG once per URL and caches it (volume default 1, clamp [0, 1]; { loop: true } repeats). Overlapping one-shots are allowed. game.audioStopSample(url?) stops samples only — it does not stop the v1 track, pattern, or tones. game.stop() still does not implicit-stop audio. Samples share the 32-node cap with tones. See Examples/Games/game_audio_sample_smoke.malda.

For a full quick start and best practices, see docs/javascript-backend.md. For a complete sample, see Examples/Games/game_bounce.malda. For PNG atlas blit plus a scrolling camera, see Examples/Games/game_sprite_smoke.malda. For key edges, touches, and gamepad, see Examples/Games/game_input_smoke.malda. For AABB and circle overlap, see Examples/Games/game_collision_smoke.malda. For overlapping WAV one-shots next to a looping pattern, see Examples/Games/game_audio_sample_smoke.malda. For a fixed timestep plus a high score that survives reload, see Examples/Games/game_fixed_save_smoke.malda. For a short side-scroller that uses the G1–G5 kit together (atlas tiles, camera, AABB, key edges, sample SFX, startFixed), see Examples/Games/malda_platform.malda. For a canvas client plus schema / validate scores, use malda new game --fullstack. For a Boulder Dash-style tile cave (dirt, gravity rocks, diamonds, fireflies), see Examples/Games/maldadash.malda. For a CPU ray tracer that writes pixels into an ImageData buffer and blits once per frame, see Examples/Games/ray_tracer.malda.

26.10 three.js Scene API

The JavaScript backend includes a curated 3D scene API under three.*. Use it when you want a browser-hosted scene in MALDA without writing raw THREE.* calls. The namespace is JavaScript mode only; interpreter and C# transpile do not run these helpers. Calls map to mlRuntime.three.* in the generated script.

Build a scene in this order: renderer, scene, camera, then meshes and lights, then three.start. three.createRenderer(width, height, mountSelector?) creates the WebGL canvas and appends it to the host element (default "#app"). Call it once, before three.render or three.start. If you need a new size or a second canvas, call three.stop() first — do not create another renderer while the loop is running.

Positions, scales, and camera near/far are world units. Rotations are radians (a floor lying in XZ is about -1.5708 on X). Standard-material options that the wrapper understands include "color" (#rrggbb), "roughness", "metalness", and "map" (a three.createTexture handle). createTexture returns immediately; decode is async and missing files stay unready (no throw). The material leaves map unset until the handle is ready. three.loadGLTF(url) returns a group you can add immediately; children appear when three.modelIsReady(handle) is true (JSON .gltf or .glb; failures stay unready). The runtime owns the loader — host pages do not add a fourth <script>. three.lookAt(object, x, y, z) requires lookAt on the three.js object. Groups parent meshes so you can move a cluster with one setPosition / setRotation. Input keys use the same names as the canvas API ("arrowleft", "arrowright", …).

A first scene with a spinning cube, one key light, and a dtMs loop:

var width = 800;
var height = 500;
var aspect = width / height;

var renderer = three.createRenderer(width, height, "#app");
three.setClearColor(renderer, "#101722");
three.setRendererSize(renderer, width, height);

var scene = three.createScene();
var camera = three.createPerspectiveCamera(70, aspect, 0.1, 100.0);
three.setCameraAspect(camera, aspect);
three.setPosition(camera, 0, 0, 5);

var geometry = three.createBoxGeometry(1, 1, 1);
var material = three.createStandardMaterial({ "color": "#44aaff" });
var cube = three.createMesh(geometry, material);
three.add(scene, cube);

var light = three.createDirectionalLight("#ffffff", 1.1);
three.setPosition(light, 2, 3, 4);
three.add(scene, light);

var angle = 0.0;

function update(dtMs) {
    angle = angle + ((dtMs * 1.2) / 1000);
    three.setRotation(cube, angle, angle * 0.8, 0.0);
}

function render() {
    three.render(renderer, scene, camera);
}

three.start(update, render);

Loop guidance: updateFn receives dtMs, same as game.start. Scale spin and motion by dtMs / 1000 so animation stays frame-rate independent. Cap huge dtMs values after a tab was backgrounded if a single step would jump too far. Put drawing in renderFn (or omit it and render at the end of update).

Loading order: the host page must load a compatible browser bundle that defines globalThis.THREE before malda-js-runtime.js and before the compiled MALDA script, then call MaldaApp.main(). The repository includes Examples/Web/wwwroot/vendor/three.min.js for this purpose.

  1. Examples/Web/wwwroot/vendor/three.min.js
  2. malda-js-runtime.js
  3. compiled MALDA script
  4. MaldaApp.main()

Resize guidance: if the host page changes the render viewport after setup, update both the renderer and the camera: three.setRendererSize(renderer, width, height) and three.setCameraAspect(camera, width / height).

MVP scope: orbit controls and generic raw JS interop stay outside this API. Textures and glTF loading are in: see Examples/Games/three_textured.malda. Lit meshes use three.createStandardMaterial. Custom GLSL uses three.createShaderMaterial and three.setUniform — see 26.10.1.

Compile the cube demo with:

malda compile Examples/Games/three_cube.malda --mode js -o Examples/Games/three_cube.js

See Examples/Games/three_cube.malda and Examples/Games/three_runtime_smoke_test.html for the end-to-end host page. For a PNG material map, a glTF cube, and lookAt, see Examples/Games/three_textured.malda. A shorter listing is in 33.15 JavaScript Backend (three.js scene API). Working notes: docs/javascript-backend.md.

26.10.1 Shader kernels (MALDA to GLSL)

When CPU game.setPixel is too slow, put per-pixel work on the GPU. This is a JavaScript compile-time subset, not a fourth execution backend. Host MALDA still owns the three.* loop and uniform updates. The kernel owns the rays.

Mark GPU functions with @shader() and gather them into a GLSL string with glsl.compile({ ... }). Those functions are not emitted as JavaScript or C#. Do not call them from host MALDA. Interpreter and C# transpile skip them.

@shader()
function vertexMain() {
    vUv = uv;
    gl_Position = vec4(position.xy, 0.0, 1.0);
}

var vert = glsl.compile({
    varyings: ["vec2 vUv"],
    functions: ["vertexMain"],
    main: "vertexMain"
});

var material = three.createShaderMaterial({
    "vertexShader": vert,
    "fragmentShader": frag,
    "uniforms": { "uTime": 0 }
});
three.setUniform(material, "uTime", t);

glsl.compile takes a single object literal. Keys:

Pass the resulting strings to three.createShaderMaterial as "vertexShader" / "fragmentShader". Uniform values from host MALDA are wrapped as { value }. Arrays of length 2/3/4 become Vector2 / Vector3 / Vector4. #rrggbb strings become Color. Optional flags: "depthWrite": false, "depthTest": false, "transparent": true.

For a fullscreen pass, use three.createOrthographicCamera(-1, 1, 1, -1, 0, 1) and three.createPlaneGeometry(2, 2).

Types in kernels

vec2, vec3, vec4, and other GLSL vector/matrix names are not MALDA types. They are GLSL names stored as type hints and copied into the shader. Outside @shader(), var p: vec3 = ... is an unknown-hint Error. Names that already exist in both languages — float, int, bool, void — remain ordinary MALDA hints.

Shader parameters and locals need a GLSL type hint. Parameters that GLSL would write with out use a two-word hint: tHit: out float. Host uniforms stay MALDA arrays such as [camX, camY, camZ], not vec3 values.

Language subset

Allowed: if / else, while, for, return, arithmetic and comparisons, and / or / not (emitted as && / || / !), member swizzles such as position.xy, GLSL constructors and builtins (vec3, dot, normalize, mix, clamp, …), and math.sqrt → GLSL sqrt (same for the other mapped math.* names).

Not allowed in kernels: actors, prompts, match, strings, foreach, await, classes, and MALDA I/O. A construct the GLSL emitter does not know fails JS compile with a GLSL transpile error at the MALDA line.

IDE

Because kernels are ordinary MALDA function declarations, Desktop IDE and LSP rename, find references, highlights, and outline apply to identifiers in the kernel — unlike raw GLSL inside a """ string, which is one token. Rename still matches the lexeme across the file (same as the rest of MALDA). It does not rewrite string entries in glsl.compile (functions: ["hitSphere"], uniforms: ["vec3 uCamPos"], main: "fragmentMain"). After renaming a kernel or uniform identifier, update those strings. GLSL builtins (dot, gl_FragColor, type name vec3) have no MALDA declaration, so go to definition has nowhere to go.

Raw GLSL

Triple-quoted GLSL strings still work when you need syntax the subset does not emit. Use """, not $""", so braces stay literal. Prefer @shader() when the kernel fits the subset so rename and outline can see it.

Compile the realtime GPU sphere tracer with:

malda compile Examples/Games/three_shader_raytracer.malda --mode js -o Examples/Games/three_shader_raytracer.js
malda compile Examples/Games/three_shader_path_tunnel.malda --mode js -o Examples/Games/three_shader_path_tunnel.js

Load three.min.js first, same as other three.* demos. A short listing is in 33.15.1 Shader materials. Working notes: docs/javascript-backend.md. The path-tunnel sample is CC-BY-NC-SA-4.0 (Frostbyte).

26.11 Common Pitfalls and Guidance

See Also