Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ jobs:
id: wasm-cache
with:
path: |
clayterm.wasm
wasm.ts
layout.wasm
input.wasm
layout.wasm.ts
input.wasm.ts
key: wasm-${{ hashFiles('Makefile', 'src/**', 'tasks/bundle-wasm.ts') }}

- name: Build WASM
Expand All @@ -62,7 +64,9 @@ jobs:
with:
name: bench-build
retention-days: 1
path: wasm.ts
path: |
layout.wasm.ts
input.wasm.ts

simulation:
name: Run benchmarks (simulation)
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/verify.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ jobs:
with:
name: clayterm-wasm
path: |
clayterm.wasm
wasm.ts
layout.wasm
input.wasm
layout.wasm.ts
input.wasm.ts

test-alt-os:
needs: test
Expand Down
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/.agent-shell/
/clayterm.wasm
/wasm.ts
/layout.wasm
/input.wasm
/layout.wasm.ts
/input.wasm.ts
/build/
/node_modules/
22 changes: 13 additions & 9 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ The local source build is driven by `make`.

It generates:

- `clayterm.wasm` — the compiled WebAssembly module built from the C sources
- `wasm.ts` — a generated TypeScript file derived from `clayterm.wasm`
- `layout.wasm` — the layout WebAssembly module built from the C sources
- `input.wasm` — the input WebAssembly module built from the C sources
- `layout.wasm.ts` — generated TypeScript derived from `layout.wasm`
- `input.wasm.ts` — generated TypeScript derived from `input.wasm`

`wasm.ts` is generated output, not hand-maintained source.
These `.ts` files are generated output, not hand-maintained source.

## Clone the repo with submodules

Expand Down Expand Up @@ -182,8 +184,10 @@ make

This should produce:

- `clayterm.wasm`
- `wasm.ts`
- `layout.wasm`
- `input.wasm`
- `layout.wasm.ts`
- `input.wasm.ts`

For a clean rebuild:

Expand All @@ -197,7 +201,7 @@ Re-run `make` when:

- you change files under `src/`
- you update the `clay` submodule
- `clayterm.wasm` or `wasm.ts` is missing
- `layout.wasm`, `input.wasm`, `layout.wasm.ts`, or `input.wasm.ts` is missing
- generated outputs look stale after switching branches or pulling changes

When in doubt, use a clean rebuild:
Expand Down Expand Up @@ -247,7 +251,7 @@ make clean && make
Symptoms may include:

- target-related `clang` errors mentioning `wasm32`
- linker failures while producing `clayterm.wasm`
- linker failures while producing the `.wasm` outputs

Recovery:

Expand All @@ -267,8 +271,8 @@ If the smoke test fails, fix the toolchain first and only then rerun `make`.

Symptoms may include:

- `clayterm.wasm` is missing
- `wasm.ts` is missing
- `layout.wasm` or `input.wasm` is missing
- `layout.wasm.ts` or `input.wasm.ts` is missing
- you changed `src/` or updated `clay/`, but the generated outputs do not match

Recovery:
Expand Down
66 changes: 42 additions & 24 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
CC = clang
TARGET = clayterm.wasm
SRC = src/module.c

CFLAGS = --target=wasm32 -nostdlib -O2 \
-ffunction-sections -fdata-sections \
-mbulk-memory \
-DCLAY_IMPLEMENTATION -DCLAY_WASM \
-Isrc -I.
CFLAGS_BASE = --target=wasm32 -nostdlib \
-ffunction-sections -fdata-sections \
-mbulk-memory \
-Isrc -I.

EXPORTS = \
INPUT_OPT ?= -Oz
LAYOUT_OPT ?= -O2

LAYOUT_CFLAGS = $(CFLAGS_BASE) $(LAYOUT_OPT) -DCLAY_IMPLEMENTATION -DCLAY_WASM
INPUT_CFLAGS = $(CFLAGS_BASE) $(INPUT_OPT)

LDFLAGS_COMMON = -Wl,--no-entry \
-Wl,--import-memory \
-Wl,--stack-first \
-Wl,--strip-all \
-Wl,--gc-sections

LAYOUT_EXPORTS = \
-Wl,--export=__heap_base \
-Wl,--export=clayterm_size \
-Wl,--export=init \
Expand All @@ -24,35 +33,44 @@ EXPORTS = \
-Wl,--export=error_count \
-Wl,--export=error_type \
-Wl,--export=error_message_length \
-Wl,--export=error_message_ptr \
-Wl,--export=error_message_ptr

INPUT_EXPORTS = \
-Wl,--export=__heap_base \
-Wl,--export=input_size \
-Wl,--export=input_init \
-Wl,--export=input_scan \
-Wl,--export=input_count \
-Wl,--export=input_event \
-Wl,--export=input_delay

LDFLAGS = -Wl,--no-entry \
-Wl,--import-memory \
-Wl,--stack-first \
-Wl,--strip-all \
-Wl,--gc-sections \
-Wl,--undefined=Clay__MeasureText \
-Wl,--undefined=Clay__QueryScrollOffset \
$(EXPORTS)
LAYOUT_LDFLAGS = $(LDFLAGS_COMMON) \
-Wl,--undefined=Clay__MeasureText \
-Wl,--undefined=Clay__QueryScrollOffset \
$(LAYOUT_EXPORTS)

all: $(TARGET) wasm.ts
@echo "Built $(TARGET) ($$(wc -c < $(TARGET)) bytes raw, $$(gzip -c $(TARGET) | wc -c) bytes gzip)"
INPUT_LDFLAGS = $(LDFLAGS_COMMON) \
$(INPUT_EXPORTS)

DEPS = $(wildcard src/*.c src/*.h)

$(TARGET): $(DEPS)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(SRC)
all: layout.wasm input.wasm layout.wasm.ts input.wasm.ts
@echo "Built layout.wasm ($$(wc -c < layout.wasm) bytes raw, $$(gzip -c layout.wasm | wc -c) bytes gzip)"
@echo "Built input.wasm ($$(wc -c < input.wasm) bytes raw, $$(gzip -c input.wasm | wc -c) bytes gzip)"

layout.wasm: $(DEPS)
$(CC) $(LAYOUT_CFLAGS) $(LAYOUT_LDFLAGS) -o $@ src/module-layout.c

input.wasm: $(DEPS)
$(CC) $(INPUT_CFLAGS) $(INPUT_LDFLAGS) -o $@ src/module-input.c

layout.wasm.ts: layout.wasm
deno run --allow-read --allow-write tasks/bundle-wasm.ts layout.wasm layout.wasm.ts

wasm.ts: $(TARGET)
deno run --allow-read --allow-write tasks/bundle-wasm.ts
input.wasm.ts: input.wasm
deno run --allow-read --allow-write tasks/bundle-wasm.ts input.wasm input.wasm.ts

clean:
rm -f $(TARGET) wasm.ts
rm -f layout.wasm input.wasm layout.wasm.ts input.wasm.ts

.PHONY: all clean
4 changes: 3 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
},
"exports": {
".": "./mod.ts",
"./layout": "./layout.ts",
"./input": "./input.ts",
"./validate": "./validate.ts"
},
"publish": {
"include": ["*.ts"],
"exclude": ["!wasm.ts"]
"exclude": ["!layout.wasm.ts", "!input.wasm.ts"]
},
"nodeModulesDir": "auto",
"fmt": {
Expand Down
10 changes: 1 addition & 9 deletions input-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export interface InputNative {
delay(st: number): number;
}

import { compiled } from "./wasm.ts";
import { compiled } from "./input.wasm.ts";

export async function createInputNative(
escLatency: number,
Expand All @@ -178,14 +178,6 @@ export async function createInputNative(

let instance = await WebAssembly.instantiate(compiled, {
env: { memory },
clay: {
measureTextFunction() {},
queryScrollOffsetFunction(ret: number) {
let v = new DataView(memory.buffer);
v.setFloat32(ret, 0, true);
v.setFloat32(ret + 4, 0, true);
},
},
});

let exports = instance.exports as unknown as {
Expand Down
4 changes: 4 additions & 0 deletions layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./ops.ts";
export * from "./term.ts";
export * from "./settings.ts";
export * from "./termcodes.ts";
6 changes: 6 additions & 0 deletions src/module-input.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* module-input.c — input-only compilation unit, no Clay layout engine */

#include "mem.c"
#include "utf8.c"
#include "trie.c"
#include "input.c"
10 changes: 10 additions & 0 deletions src/module-layout.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* module-layout.c — layout-only compilation unit, no input parser */

#include "../clay/clay.h"

#include "mem.c"
#include "buffer.c"
#include "cell.c"
#include "utf8.c"
#include "wcwidth.c"
#include "clayterm.c"
7 changes: 6 additions & 1 deletion tasks/build-npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ if (!version) {
}

await build({
entryPoints: ["./mod.ts"],
entryPoints: [
"./mod.ts",
{ name: "./layout", path: "./layout.ts" },
{ name: "./input", path: "./input.ts" },
{ name: "./validate", path: "./validate.ts" },
],
outDir,
shims: {
deno: false,
Expand Down
23 changes: 13 additions & 10 deletions tasks/bundle-wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ function encodeZ85(data: Uint8Array): string {
return out.join("");
}

const wasm = await Deno.readFile("clayterm.wasm");
const [input, output] = Deno.args;
if (!input || !output) {
console.error("Usage: bundle-wasm.ts <input.wasm> <output.ts>");
Deno.exit(1);
}

const wasm = await Deno.readFile(input);

const compressed = new Uint8Array(
brotliCompressSync(wasm, {
Expand All @@ -41,18 +47,15 @@ const compressed = new Uint8Array(

const z85 = encodeZ85(compressed);

// Decoder uses division instead of >>> to avoid 32-bit truncation on values near 0xFFFFFFFF.
const source = `import{brotliDecompressSync}from"node:zlib";
const Z="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#";
const T=new Uint8Array(128);for(let i=0;i<85;i++)T[Z.charCodeAt(i)]=i;
function d(s:string,n:number){const b=new Uint8Array(n);let o=0;for(let i=0;i<s.length&&o<n;i+=5){const v=T[s.charCodeAt(i)]*52200625+T[s.charCodeAt(i+1)]*614125+T[s.charCodeAt(i+2)]*7225+T[s.charCodeAt(i+3)]*85+T[s.charCodeAt(i+4)];if(o<n)b[o++]=Math.floor(v/16777216);if(o<n)b[o++]=Math.floor(v/65536)%256;if(o<n)b[o++]=Math.floor(v/256)%256;if(o<n)b[o++]=v%256;}return b;}
const compressed=d(${JSON.stringify(z85)},${compressed.byteLength});
export const compiled=await WebAssembly.compile(new Uint8Array(brotliDecompressSync(compressed)));
// args separated to keep deno fmt happy
const args = `${JSON.stringify(z85)}, ${compressed.byteLength}`;
const source = `import { decode } from "./wasm-decode.ts";
export const compiled = await decode(${args});
`;

await Deno.writeTextFile("wasm.ts", source);
await Deno.writeTextFile(output, source);
console.log(
`wrote wasm.ts (${wasm.length} → ${compressed.byteLength} bytes compressed, ${z85.length} bytes z85, ${
`wrote ${output} (${wasm.length} → ${compressed.byteLength} bytes compressed, ${z85.length} bytes z85, ${
Math.round(z85.length / wasm.length * 100)
}%)`,
);
2 changes: 1 addition & 1 deletion term-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface Native {
errorMessage(ct: number, index: number): string;
}

import { compiled } from "./wasm.ts";
import { compiled } from "./layout.wasm.ts";

export async function createTermNative(
w: number,
Expand Down
39 changes: 39 additions & 0 deletions wasm-decode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { brotliDecompressSync } from "node:zlib";

const Z85 =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#";

const DECODE = new Uint8Array(128);
for (let i = 0; i < 85; i++) DECODE[Z85.charCodeAt(i)] = i;

// Division instead of >>> avoids 32-bit truncation on values near 0xFFFFFFFF.
function decodeZ85(s: string, n: number): Uint8Array {
let bytes = new Uint8Array(n);
let o = 0;
for (let i = 0; i < s.length && o < n; i += 5) {
let v = DECODE[s.charCodeAt(i)] * 52200625 +
DECODE[s.charCodeAt(i + 1)] * 614125 +
DECODE[s.charCodeAt(i + 2)] * 7225 +
DECODE[s.charCodeAt(i + 3)] * 85 +
DECODE[s.charCodeAt(i + 4)];
if (o < n) bytes[o++] = Math.floor(v / 16777216);
if (o < n) bytes[o++] = Math.floor(v / 65536) % 256;
if (o < n) bytes[o++] = Math.floor(v / 256) % 256;
if (o < n) bytes[o++] = v % 256;
}
return bytes;
}

/**
* Decode an inlined WASM blob: z85 → brotli-decompress → compile.
*
* `byteLength` is the brotli-compressed length, used to size the z85
* decode buffer before decompression restores the original module.
*/
export function decode(
z85: string,
byteLength: number,
): Promise<WebAssembly.Module> {
let compressed = decodeZ85(z85, byteLength);
return WebAssembly.compile(new Uint8Array(brotliDecompressSync(compressed)));
}
Loading