Skip to content

Commit 9b654c8

Browse files
hyperpolymathclaude
andcommitted
feat: TEA CS bridge + opsm.toml manifest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent af96323 commit 9b654c8

4 files changed

Lines changed: 373 additions & 1 deletion

File tree

bin/main.ml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,44 @@ let router_bridge_cmd_fn output =
392392
Format.printf "Custom sections: affinescript.ownership, affinescript.tea_layout@.";
393393
`Ok ()
394394

395+
(** Generate the CharacterSelect TEA Bridge Wasm module.
396+
397+
Produces a WebAssembly 1.0 module that implements the AffineScript TEA
398+
ABI for CharacterSelectScreen. The model holds the player's background
399+
selection as a single i32 selected_tag (0=none, 1-6=background, 7=confirmed).
400+
401+
The exported API surface is identical to the TitleScreen bridge, so the
402+
same AffineTEA.js / AffineTEA.res bindings work without modification.
403+
404+
Msg tags (input to affinescript_update):
405+
0=SelectAssault 1=SelectRecon 2=SelectEngineer
406+
3=SelectSignals 4=SelectMedic 5=SelectLogistics 6=Confirm
407+
408+
selected_tag (output from affinescript_get_selected):
409+
0=none 1=Assault 2=Recon 3=Engineer 4=Signals 5=Medic 6=Logistics
410+
7=confirmed (navigate to JessicaCustomise)
411+
412+
No source file needed — generated from the CharacterSelect TEA ABI.
413+
Write to a .wasm file, copy to IDApTIK's public/assets/wasm/ directory. *)
414+
let cs_bridge_cmd_fn output =
415+
let m = Affinescript.Tea_cs_bridge.generate () in
416+
(* Auto-verify ownership constraints before writing. *)
417+
(match Affinescript.Tw_verify.verify_from_module m with
418+
| Ok () ->
419+
Format.printf "typed-wasm ownership verification: OK@."
420+
| Error errs ->
421+
Affinescript.Tw_verify.pp_report Format.std_formatter errs);
422+
Affinescript.Wasm_encode.write_module_to_file output m;
423+
Format.printf "CharacterSelect TEA bridge written to %s@." output;
424+
Format.printf " affinescript_init() — initialise CharacterSelectModel@.";
425+
Format.printf " affinescript_update(msg: i32) — 0-5=SelectClass 6=Confirm@.";
426+
Format.printf " affinescript_get_selected() -> i32 — 0=none 1-6=class 7=confirmed@.";
427+
Format.printf " affinescript_get_screen_w/h() -> i32 — current screen dimensions@.";
428+
Format.printf " affinescript_set_screen(w: i32, h: i32) — handle resize events@.";
429+
Format.printf " memory — exported linear memory (model at offset 64)@.";
430+
Format.printf "Custom sections: affinescript.ownership, affinescript.tea_layout@.";
431+
`Ok ()
432+
395433
(** Start the REPL *)
396434
let repl_cmd_fn () =
397435
(* TODO: Re-enable when REPL module is restored *)

lib/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
(name affinescript)
33
(public_name affinescript)
44
(modes byte native)
5-
(modules ast borrow codegen codegen_gc desugar_traits effect error error_collector error_formatter face formatter interp js_face julia_codegen json_output lexer linter lsp_server module_loader opt parse_driver parse parser parser_errors pseudocode_face python_face quantity resolve span symbol tea_bridge tea_router token trait tw_interface tw_verify typecheck types unify value wasm wasm_encode wasm_gc wasm_gc_encode wasi_runtime)
5+
(modules ast borrow codegen codegen_gc desugar_traits effect error error_collector error_formatter face formatter interp js_face julia_codegen json_output lexer linter lsp_server module_loader opt parse_driver parse parser parser_errors pseudocode_face python_face quantity resolve span symbol tea_bridge tea_cs_bridge tea_router token trait tw_interface tw_verify typecheck types unify value wasm wasm_encode wasm_gc wasm_gc_encode wasi_runtime)
66
(libraries str unix sedlex fmt menhirLib yojson)
77
(preprocess
88
(pps ppx_deriving.show ppx_deriving.eq ppx_deriving.ord sedlex.ppx)))

lib/tea_cs_bridge.ml

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
(* SPDX-License-Identifier: PMPL-1.0-or-later *)
2+
(* SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell (hyperpolymath) *)
3+
4+
(** CharacterSelect TEA Bridge Wasm Generator.
5+
6+
Generates a valid WebAssembly 1.0 module that implements the
7+
AffineScript TEA runtime ABI for the CharacterSelectScreen.
8+
9+
The bridge module stores CharacterSelectModel in linear memory at a fixed
10+
layout, and exports clean i32 functions that JS can call to drive
11+
a PixiJS scene without needing a full AffineScript → Wasm compiler.
12+
13+
{2 Memory layout}
14+
15+
Model state is stored starting at byte offset 64 (matching the TitleScreen
16+
layout convention) with the following field layout:
17+
18+
{v
19+
Offset Type Field Default
20+
+0 i32 screen_w 1280
21+
+4 i32 screen_h 720
22+
+8 i32 bgm_playing 0 (unused; kept for API compatibility)
23+
+12 i32 selected_tag 0
24+
v}
25+
26+
{2 selected_tag encoding}
27+
28+
{v
29+
0 = none (no class chosen yet)
30+
1 = Assault
31+
2 = Recon
32+
3 = Engineer (Combat Engineer)
33+
4 = Signals
34+
5 = Medic
35+
6 = Logistics
36+
7 = confirmed (navigate to JessicaCustomise)
37+
v}
38+
39+
{2 Msg tag encoding (input to affinescript_update)}
40+
41+
{v
42+
0 = SelectAssault
43+
1 = SelectRecon
44+
2 = SelectEngineer
45+
3 = SelectSignals
46+
4 = SelectMedic
47+
5 = SelectLogistics
48+
6 = Confirm
49+
v}
50+
51+
The update function is branchless: [selected_tag := msg + 1].
52+
This maps SelectAssault=0 → 1, …, Confirm=6 → 7 (navigate).
53+
54+
{2 Exported functions}
55+
56+
Identical API surface to the TitleScreen bridge so the same
57+
[AffineTEA.js] / [AffineTEA.res] bindings work without modification:
58+
59+
{ul
60+
{li [affinescript_init()]}
61+
{li [affinescript_update(msg: i32)]}
62+
{li [affinescript_get_screen_w() -> i32]}
63+
{li [affinescript_get_screen_h() -> i32]}
64+
{li [affinescript_get_bgm_playing() -> i32]}
65+
{li [affinescript_get_selected() -> i32]}
66+
{li [affinescript_set_screen(w: i32, h: i32)]}
67+
{li [memory]}
68+
}
69+
70+
{2 Ownership annotations}
71+
72+
The [affinescript.ownership] custom section marks [update]'s [msg]
73+
parameter as Linear (kind byte 1) — consumed exactly once per TEA
74+
update cycle — encoding the AffineScript linearity invariant for
75+
typed-wasm Level 10 verification.
76+
77+
A companion [affinescript.tea_layout] custom section encodes the
78+
model field layout for tooling.
79+
*)
80+
81+
open Wasm
82+
83+
(** Base address of the CharacterSelectModel in linear memory. *)
84+
let model_base = 64
85+
86+
(** Field offsets relative to [model_base]. *)
87+
let off_screen_w = 0
88+
let off_screen_h = 4
89+
let off_bgm_playing = 8
90+
let off_selected = 12
91+
92+
(** [load_field off] — Wasm instructions that load an i32 from
93+
[(model_base + off)], leaving the value on the stack. *)
94+
let load_field off : instr list = [
95+
I32Const (Int32.of_int (model_base + off));
96+
I32Load (2, 0);
97+
]
98+
99+
(** [store_const off v] — Wasm instructions that store constant [v]
100+
to [(model_base + off)]. *)
101+
let store_const off v : instr list = [
102+
I32Const (Int32.of_int (model_base + off));
103+
I32Const (Int32.of_int v);
104+
I32Store (2, 0);
105+
]
106+
107+
(* -------------------------------------------------------------------------
108+
Type section
109+
-------------------------------------------------------------------------
110+
Index Signature Used by
111+
0 () -> () fn_init
112+
1 (i32) -> () fn_update
113+
2 () -> i32 fn_get_screen_w/_h/_bgm/_selected
114+
3 (i32, i32) -> () fn_set_screen
115+
------------------------------------------------------------------------- *)
116+
117+
let types : func_type list = [
118+
{ ft_params = []; ft_results = [] };
119+
{ ft_params = [I32]; ft_results = [] };
120+
{ ft_params = []; ft_results = [I32] };
121+
{ ft_params = [I32; I32]; ft_results = [] };
122+
]
123+
124+
(* -------------------------------------------------------------------------
125+
Function bodies
126+
------------------------------------------------------------------------- *)
127+
128+
(** fn 0: affinescript_init() — write default CharacterSelectModel to memory. *)
129+
let fn_init : func = {
130+
f_type = 0;
131+
f_locals = [];
132+
f_body =
133+
store_const off_screen_w 1280 @
134+
store_const off_screen_h 720 @
135+
store_const off_bgm_playing 0 @
136+
store_const off_selected 0;
137+
}
138+
139+
(** fn 1: affinescript_update(msg: i32) — branchless update:
140+
[selected_tag := msg + 1]. msg is Linear (consumed exactly once).
141+
142+
msg 0 → 1 (Assault selected)
143+
msg 1 → 2 (Recon selected)
144+
msg 2 → 3 (Engineer selected)
145+
msg 3 → 4 (Signals selected)
146+
msg 4 → 5 (Medic selected)
147+
msg 5 → 6 (Logistics selected)
148+
msg 6 → 7 (confirmed — navigate to JessicaCustomise) *)
149+
let fn_update : func = {
150+
f_type = 1;
151+
f_locals = [];
152+
f_body = [
153+
(* address of selected_tag *)
154+
I32Const (Int32.of_int (model_base + off_selected));
155+
(* compute msg + 1 *)
156+
LocalGet 0;
157+
I32Const 1l;
158+
I32Add;
159+
I32Store (2, 0);
160+
];
161+
}
162+
163+
(** fn 2: affinescript_get_screen_w() -> i32 *)
164+
let fn_get_screen_w : func = {
165+
f_type = 2;
166+
f_locals = [];
167+
f_body = load_field off_screen_w;
168+
}
169+
170+
(** fn 3: affinescript_get_screen_h() -> i32 *)
171+
let fn_get_screen_h : func = {
172+
f_type = 2;
173+
f_locals = [];
174+
f_body = load_field off_screen_h;
175+
}
176+
177+
(** fn 4: affinescript_get_bgm_playing() -> i32 *)
178+
let fn_get_bgm_playing : func = {
179+
f_type = 2;
180+
f_locals = [];
181+
f_body = load_field off_bgm_playing;
182+
}
183+
184+
(** fn 5: affinescript_get_selected() -> i32 *)
185+
let fn_get_selected : func = {
186+
f_type = 2;
187+
f_locals = [];
188+
f_body = load_field off_selected;
189+
}
190+
191+
(** fn 6: affinescript_set_screen(w: i32, h: i32) — store new dimensions.
192+
Handles PixiJS resize events by updating the model. *)
193+
let fn_set_screen : func = {
194+
f_type = 3;
195+
f_locals = [];
196+
f_body = [
197+
I32Const (Int32.of_int (model_base + off_screen_w));
198+
LocalGet 0;
199+
I32Store (2, 0);
200+
I32Const (Int32.of_int (model_base + off_screen_h));
201+
LocalGet 1;
202+
I32Store (2, 0);
203+
];
204+
}
205+
206+
(* -------------------------------------------------------------------------
207+
Custom sections
208+
------------------------------------------------------------------------- *)
209+
210+
(** Build the [affinescript.ownership] custom section payload.
211+
212+
Identical encoding to the TitleScreen bridge — only [fn_update]'s msg
213+
param is Linear; all other params and returns are Unrestricted. *)
214+
let build_ownership_section () : bytes =
215+
let buf = Buffer.create 64 in
216+
let u32 n =
217+
Buffer.add_char buf (Char.chr (n land 0xff));
218+
Buffer.add_char buf (Char.chr ((n lsr 8) land 0xff));
219+
Buffer.add_char buf (Char.chr ((n lsr 16) land 0xff));
220+
Buffer.add_char buf (Char.chr ((n lsr 24) land 0xff))
221+
in
222+
let u8 n = Buffer.add_char buf (Char.chr (n land 0xff)) in
223+
u32 7; (* 7 annotated functions *)
224+
(* fn 0 init: () → (), no params, Unrestricted return *)
225+
u32 0; u8 0; u8 0;
226+
(* fn 1 update: (msg: Linear) → (), return Unrestricted *)
227+
u32 1; u8 1; u8 1 (* Linear=1 *); u8 0;
228+
(* fn 2-5 getters: () → i32, Unrestricted *)
229+
u32 2; u8 0; u8 0;
230+
u32 3; u8 0; u8 0;
231+
u32 4; u8 0; u8 0;
232+
u32 5; u8 0; u8 0;
233+
(* fn 6 set_screen: (i32, i32) → (), both Unrestricted *)
234+
u32 6; u8 2; u8 0; u8 0; u8 0;
235+
Buffer.to_bytes buf
236+
237+
(** Build the [affinescript.tea_layout] custom section.
238+
239+
Compact binary descriptor for the CharacterSelectModel memory layout:
240+
{v
241+
u8 version = 1
242+
u8 base_addr = 64
243+
u8 field_count = 4
244+
per field: u8 name_len, name_bytes, u8 offset, u8 type_tag (0x49=i32)
245+
v} *)
246+
let build_tea_layout_section () : bytes =
247+
let buf = Buffer.create 64 in
248+
let u8 n = Buffer.add_char buf (Char.chr (n land 0xff)) in
249+
let field name off =
250+
u8 (String.length name);
251+
Buffer.add_string buf name;
252+
u8 off;
253+
u8 0x49 (* i32 type tag *)
254+
in
255+
u8 1; (* version 1 *)
256+
u8 model_base; (* base = 64 *)
257+
u8 4; (* 4 fields *)
258+
field "screen_w" off_screen_w;
259+
field "screen_h" off_screen_h;
260+
field "bgm_playing" off_bgm_playing;
261+
field "selected" off_selected;
262+
Buffer.to_bytes buf
263+
264+
(* -------------------------------------------------------------------------
265+
Module assembly
266+
------------------------------------------------------------------------- *)
267+
268+
(** Generate the complete TEA bridge Wasm module for CharacterSelectScreen.
269+
270+
The resulting module is suitable for use with AffineTEA.js in IDApTIK.
271+
It shares the same exported function names and memory layout as the
272+
TitleScreen bridge, so the same ReScript/JS bindings work unchanged.
273+
274+
selected_tag semantics after update(msg):
275+
- 1-6: one of the six operative backgrounds is highlighted
276+
- 7: player confirmed — navigate to JessicaCustomiseScreen
277+
278+
Write with [Wasm_encode.write_module_to_file]. *)
279+
let generate () : wasm_module = {
280+
types;
281+
funcs = [
282+
fn_init;
283+
fn_update;
284+
fn_get_screen_w;
285+
fn_get_screen_h;
286+
fn_get_bgm_playing;
287+
fn_get_selected;
288+
fn_set_screen;
289+
];
290+
tables = [];
291+
mems = [{ mem_type = { lim_min = 1; lim_max = None } }];
292+
globals = [];
293+
imports = [];
294+
elems = [];
295+
datas = [];
296+
start = None;
297+
exports = [
298+
{ e_name = "affinescript_init"; e_desc = ExportFunc 0 };
299+
{ e_name = "affinescript_update"; e_desc = ExportFunc 1 };
300+
{ e_name = "affinescript_get_screen_w"; e_desc = ExportFunc 2 };
301+
{ e_name = "affinescript_get_screen_h"; e_desc = ExportFunc 3 };
302+
{ e_name = "affinescript_get_bgm_playing"; e_desc = ExportFunc 4 };
303+
{ e_name = "affinescript_get_selected"; e_desc = ExportFunc 5 };
304+
{ e_name = "affinescript_set_screen"; e_desc = ExportFunc 6 };
305+
{ e_name = "memory"; e_desc = ExportMemory 0 };
306+
];
307+
custom_sections = [
308+
("affinescript.ownership", build_ownership_section ());
309+
("affinescript.tea_layout", build_tea_layout_section ());
310+
];
311+
}

opsm.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
#
4+
# affinescript package manifest — OPSM native.
5+
# This file makes affinescript a first-class OPSM/HFR package.
6+
7+
[package]
8+
name = "affinescript"
9+
description = "AffineScript compiler and standard library — affine-typed, WASM-first functional language"
10+
license = "PMPL-1.0-or-later"
11+
authors = ["Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>"]
12+
keywords = ["affinescript", "hyperpolymath", "nextgen-languages", "compiler"]
13+
homepage = "https://github.com/hyperpolymath/nextgen-languages"
14+
repository = "https://github.com/hyperpolymath/nextgen-languages"
15+
forth = "affinescript"
16+
17+
[opsm]
18+
trust_level = "hyperpolymath"
19+
registry = "hf"
20+
21+
[dependencies]
22+
proven = { git = "https://github.com/hyperpolymath/proven", registry = "hf" }
23+
groove = { git = "https://github.com/hyperpolymath/groove", registry = "hf" }

0 commit comments

Comments
 (0)