Skip to content

Commit 4c4d8d1

Browse files
hyperpolymathclaude
andcommitted
feat(stage3): TEA stdlib — counter + titlescreen running in interpreter
Stage 3 of the AffineScript TEA dogfood plan: The Elm Architecture running in the AffineScript interpreter. Closes 3a–3d. **Interpreter fixes (3b foundation):** - `interp.ml` — `TopType TyEnum`: register enum constructors into the runtime env. Nullary → VVariant value; N-ary → VBuiltin constructor function. Previously all enum constructors were "Unbound variable" at runtime. - `interp.ml` — `tea_run` builtin: reads stdin line-by-line, dispatches each line as a VVariant Msg to the user's `update` function, prints the result of `view`. Clean EOF exit. Full REPL-style TEA loop. - `value.ml` — `binop_string`: add `OpConcat` (the `++` operator) so string concatenation works at runtime. Was falling through to "not supported on strings". - `resolve.ml` — `create_context`: register ALL interpreter builtins (int_to_string, tea_run, float_to_string, read_line, etc.) so the resolver doesn't reject them as UndefinedVariable. - `typecheck.ml` — `register_builtins`: add `tea_run` with flexible type `∀a. a → unit ! IO`. - `bin/main.ml` — `eval_file` (both JSON and non-JSON paths): call `register_builtins` before type-checking declarations (the eval path was skipping this); auto-call `main()` after `eval_program` if main is defined in the env. **Examples (3c + 3d):** - `examples/counter.afs` — canonical TEA counter: Increment/Decrement/ Reset messages, affine model, view renders "Count: N" - `examples/titlescreen.afs` — IDApTIK title screen: TitleModel struct, TitleMsg enum (NewGame/LoadGame/Settings/Credits), view renders screen dimensions and selected option. Bridge target for Stage 4. **Tests (5 new, 85 total):** - counter compiles: frontend + eval_program succeeds - counter init=0: apply_function counter_init [] returns VInt 0 - counter update transitions: Increment 0→1, Decrement 1→0 - titlescreen compiles: frontend + eval_program succeeds - titlescreen NewGame→new_game: update sets selected field correctly Stage 3 done when: ✓ counter.afs runs correctly (Increment/Decrement/Reset via stdin) ✓ ownership violations caught (Stage 1 QTT enforcement still live) ✓ titlescreen.afs compiles and interprets without errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fcf1c36 commit 4c4d8d1

10 files changed

Lines changed: 659 additions & 14 deletions

File tree

bin/main.ml

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ let eval_file face json path =
240240
add (Affinescript.Json_output.of_resolve_error e span)
241241
| Ok (resolve_ctx, type_ctx) ->
242242
let type_ctx = { type_ctx with symbols = resolve_ctx.symbols } in
243+
(* Register builtins (print, int_to_string, tea_run, etc.) before type-checking *)
244+
Affinescript.Typecheck.register_builtins type_ctx;
243245
(match List.fold_left (fun acc decl ->
244246
match acc with
245247
| Error _ as e -> e
@@ -254,7 +256,14 @@ let eval_file face json path =
254256
add (Affinescript.Json_output.of_quantity_error (err, span))
255257
| Ok () ->
256258
(match Affinescript.Interp.eval_program prog with
257-
| Ok _env -> ()
259+
| Ok env ->
260+
(* Auto-call main() if defined — entry point for TEA apps and scripts *)
261+
(match Affinescript.Value.lookup_env "main" env with
262+
| Ok main_fn ->
263+
(match Affinescript.Interp.apply_function main_fn [] with
264+
| Ok _ -> ()
265+
| Error e -> add (Affinescript.Json_output.of_eval_error e))
266+
| Error _ -> ()) (* no main — that's fine, just eval declarations *)
258267
| Error e ->
259268
add (Affinescript.Json_output.of_eval_error e)))))
260269
with
@@ -276,6 +285,8 @@ let eval_file face json path =
276285
`Error (false, "Resolution error")
277286
| Ok (resolve_ctx, type_ctx) ->
278287
let type_ctx = { type_ctx with symbols = resolve_ctx.symbols } in
288+
(* Register builtins (print, int_to_string, tea_run, etc.) before type-checking *)
289+
Affinescript.Typecheck.register_builtins type_ctx;
279290
(match List.fold_left (fun acc decl ->
280291
match acc with
281292
| Error _ as e -> e
@@ -294,9 +305,20 @@ let eval_file face json path =
294305
`Error (false, "Quantity error")
295306
| Ok () ->
296307
(match Affinescript.Interp.eval_program prog with
297-
| Ok _env ->
298-
Format.printf "Program executed successfully@.";
299-
`Ok ()
308+
| Ok env ->
309+
(* Auto-call main() if defined — entry point for TEA apps and scripts *)
310+
(match Affinescript.Value.lookup_env "main" env with
311+
| Ok main_fn ->
312+
(match Affinescript.Interp.apply_function main_fn [] with
313+
| Ok _ -> `Ok ()
314+
| Error e ->
315+
Format.eprintf "@[<v>Runtime error: %s@]@."
316+
(Affinescript.Value.show_eval_error e);
317+
`Error (false, "Runtime error"))
318+
| Error _ ->
319+
(* No main function — declarations evaluated, nothing to run *)
320+
Format.printf "Program evaluated successfully@.";
321+
`Ok ())
300322
| Error e ->
301323
Format.eprintf "@[<v>Runtime error: %s@]@."
302324
(Affinescript.Value.show_eval_error e);

examples/counter.afs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// SPDX-FileCopyrightText: 2025-2026 hyperpolymath
3+
//
4+
// AffineScript TEA counter — Stage 3c dogfood
5+
//
6+
// Demonstrates The Elm Architecture in AffineScript:
7+
// - Model is an owned value: each update consumes the old model, produces a new one
8+
// - Msg is linear: consumed by update, not reusable
9+
// - Cmd carries obligations: None means no side-effects, Log carries a message to handle
10+
//
11+
// Run: affinescript eval examples/counter.afs
12+
// At the stdin prompt, type one of: Increment Decrement Reset
13+
// Ctrl-D (EOF) exits cleanly.
14+
15+
// ── Msg ─────────────────────────────────────────────────────────────────────
16+
// Linear: each message is consumed exactly once by update.
17+
18+
enum CounterMsg {
19+
Increment,
20+
Decrement,
21+
Reset
22+
}
23+
24+
// ── Cmd ─────────────────────────────────────────────────────────────────────
25+
// Linear obligation. None → no effect. Log(String) → something to handle.
26+
// Dropping a non-None Cmd at compile time is a quantity violation (Stage 1).
27+
28+
enum CounterCmd {
29+
None,
30+
Log(String)
31+
}
32+
33+
// ── init ─────────────────────────────────────────────────────────────────────
34+
// Produces the initial model. No commands needed at start-up.
35+
36+
fn counter_init() -> Int {
37+
0
38+
}
39+
40+
// ── update ───────────────────────────────────────────────────────────────────
41+
// Consumes Msg and old Model; produces new Model plus any commands.
42+
// The old model is gone after this call — AffineScript enforces this via QTT.
43+
44+
fn counter_update(msg: CounterMsg, model: Int) -> Int {
45+
match msg {
46+
Increment => model + 1,
47+
Decrement => model - 1,
48+
Reset => 0
49+
}
50+
}
51+
52+
// ── view ─────────────────────────────────────────────────────────────────────
53+
// Borrows the model (read-only), renders a string.
54+
// Rebuilding the view does not consume the model.
55+
56+
fn counter_view(model: Int) -> String {
57+
"Count: " ++ int_to_string(model)
58+
}
59+
60+
// ── subscriptions ────────────────────────────────────────────────────────────
61+
// No real-time subscriptions at the interpreter level.
62+
63+
fn counter_subs(model: Int) -> String {
64+
"None"
65+
}
66+
67+
// ── main ─────────────────────────────────────────────────────────────────────
68+
// Wire up the TEA runtime and hand off to the interpreter loop.
69+
70+
fn main() -> () {
71+
tea_run({
72+
init: counter_init,
73+
update: counter_update,
74+
view: counter_view,
75+
subscriptions: counter_subs
76+
})
77+
}

examples/titlescreen.afs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// SPDX-FileCopyrightText: 2025-2026 hyperpolymath
3+
//
4+
// AffineScript TEA TitleScreen — Stage 3d dogfood
5+
//
6+
// IDApTIK title screen in AffineScript TEA.
7+
// This is the bridge target for Stage 4: when the Wasm/JS bridge is done,
8+
// this exact file compiles to a .wasm that AffineTEA.js drives via PixiJS.
9+
//
10+
// Model: screen state (dimensions, music playing, selected option)
11+
// Msg: navigation events from the title menu
12+
// Cmd: side-effects (BGM, SFX, screen navigation)
13+
//
14+
// Run: affinescript eval examples/titlescreen.afs
15+
// Type: NewGame LoadGame Settings Credits Ctrl-D to exit
16+
17+
// ── Msg ──────────────────────────────────────────────────────────────────────
18+
19+
enum TitleMsg {
20+
NewGame,
21+
LoadGame,
22+
Settings,
23+
Credits
24+
}
25+
26+
// ── Cmd ──────────────────────────────────────────────────────────────────────
27+
28+
enum TitleCmd {
29+
None,
30+
PlayBGM(String),
31+
PlaySFX(String),
32+
Navigate(String)
33+
}
34+
35+
// ── Model ────────────────────────────────────────────────────────────────────
36+
37+
struct TitleModel {
38+
screen_w: Int,
39+
screen_h: Int,
40+
bgm_playing: Int,
41+
selected: String
42+
}
43+
44+
// ── init ─────────────────────────────────────────────────────────────────────
45+
46+
fn title_init() -> TitleModel {
47+
{
48+
screen_w: 1280,
49+
screen_h: 720,
50+
bgm_playing: 0,
51+
selected: "none"
52+
}
53+
}
54+
55+
// ── update ────────────────────────────────────────────────────────────────────
56+
// Consumes msg and model; returns updated model.
57+
// In Stage 4, this also emits Cmd(Navigate(...)) which the JS bridge handles.
58+
59+
fn title_update(msg: TitleMsg, model: TitleModel) -> TitleModel {
60+
match msg {
61+
NewGame => { screen_w: model.screen_w, screen_h: model.screen_h, bgm_playing: model.bgm_playing, selected: "new_game" },
62+
LoadGame => { screen_w: model.screen_w, screen_h: model.screen_h, bgm_playing: model.bgm_playing, selected: "load_game" },
63+
Settings => { screen_w: model.screen_w, screen_h: model.screen_h, bgm_playing: model.bgm_playing, selected: "settings" },
64+
Credits => { screen_w: model.screen_w, screen_h: model.screen_h, bgm_playing: model.bgm_playing, selected: "credits" }
65+
}
66+
}
67+
68+
// ── view ─────────────────────────────────────────────────────────────────────
69+
// Borrows model, renders the title screen state as a string.
70+
// In Stage 4, this returns Html that the JS bridge applies to the Pixi scene.
71+
72+
fn title_view(model: TitleModel) -> String {
73+
"=== IDApTIK ===" ++ "\n" ++
74+
"Screen: " ++ int_to_string(model.screen_w) ++ "x" ++ int_to_string(model.screen_h) ++ "\n" ++
75+
"Selected: " ++ model.selected
76+
}
77+
78+
// ── subscriptions ─────────────────────────────────────────────────────────────
79+
80+
fn title_subs(model: TitleModel) -> String {
81+
"None"
82+
}
83+
84+
// ── main ──────────────────────────────────────────────────────────────────────
85+
86+
fn main() -> () {
87+
tea_run({
88+
init: title_init,
89+
update: title_update,
90+
view: title_view,
91+
subscriptions: title_subs
92+
})
93+
}

lib/interp.ml

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,70 @@ let create_initial_env () : env =
872872
("time_now", VBuiltin ("time_now", fun _args ->
873873
Ok (VFloat (Sys.time ()))
874874
));
875+
876+
(* -- TEA (The Elm Architecture) interpreter runtime -------------------- *)
877+
("tea_run", VBuiltin ("tea_run", fun args ->
878+
(* tea_run expects a record with fields: init, update, view, subscriptions.
879+
- init : () -> (Model, [Cmd])
880+
- update : Msg -> Model -> (Model, [Cmd])
881+
- view : Model -> Html (Html is rendered as a string or shown)
882+
- subscriptions: Model -> [Sub] (unused at interpreter level)
883+
884+
The interpreter loop:
885+
1. Call init() to get the initial model.
886+
2. Render and print the initial view.
887+
3. Read lines from stdin; each line becomes a variant Msg.
888+
4. Call update(msg, model) -> (new_model, cmds).
889+
5. Print new view. Repeat until EOF. *)
890+
match args with
891+
| [VRecord fields] ->
892+
let get f = match List.assoc_opt f fields with
893+
| Some v -> Ok v
894+
| None -> Error (RuntimeError (Printf.sprintf "tea_run: missing field '%s'" f))
895+
in
896+
let render_html v =
897+
let s = match v with
898+
| VString s -> s
899+
| VVariant ("Text", Some (VString s)) -> s
900+
| VVariant ("Node", _) -> "[Html node]"
901+
| other -> show_value other
902+
in
903+
print_endline s
904+
in
905+
let* init_fn = get "init" in
906+
let* update_fn = get "update" in
907+
let* view_fn = get "view" in
908+
(* Call init() -> (model, cmds) — 0-param function, no args *)
909+
let* init_result = apply_function init_fn [] in
910+
let model0 = match init_result with
911+
| VTuple (m :: _) -> m
912+
| m -> m
913+
in
914+
(* Render initial view *)
915+
let* view0 = apply_function view_fn [model0] in
916+
render_html view0;
917+
(* Read-eval-print loop: stdin lines become variant Msgs until EOF *)
918+
let rec loop model =
919+
let input = try Some (read_line ()) with End_of_file -> None in
920+
match input with
921+
| None -> Ok VUnit (* EOF — done, clean exit *)
922+
| Some "" -> loop model (* blank line — skip *)
923+
| Some line ->
924+
let msg = VVariant (String.trim line, None) in
925+
let* upd_result = apply_function update_fn [msg; model] in
926+
let new_model = match upd_result with
927+
| VTuple (m :: _) -> m
928+
| m -> m
929+
in
930+
let* new_view = apply_function view_fn [new_model] in
931+
render_html new_view;
932+
loop new_model
933+
in
934+
loop model0
935+
| _ ->
936+
Error (RuntimeError
937+
"tea_run expects a record: {init, update, view, subscriptions}")
938+
));
875939
] in
876940
builtins
877941

@@ -892,8 +956,46 @@ let eval_decl (env : env) (decl : top_level) : env result =
892956
let* v = eval env tc.tc_value in
893957
Ok (extend_env tc.tc_name.name v env)
894958

895-
| TopType _ | TopTrait _ | TopImpl _ ->
896-
(* Type/trait/impl declarations don't affect the runtime value environment *)
959+
| TopType td ->
960+
(* Register enum constructors so they can be used as values / constructor functions.
961+
Nullary constructors become VVariant values.
962+
N-ary constructors become VBuiltin constructor functions. *)
963+
begin match td.td_body with
964+
| TyEnum variants ->
965+
let env' = List.fold_left (fun env (vd : variant_decl) ->
966+
let name = vd.vd_name.name in
967+
match vd.vd_fields with
968+
| [] ->
969+
(* Nullary constructor: bind directly as a VVariant value *)
970+
extend_env name (VVariant (name, None)) env
971+
| [_] ->
972+
(* Single-payload constructor: wrap arg in VVariant *)
973+
extend_env name
974+
(VBuiltin (name, fun args ->
975+
match args with
976+
| [v] -> Ok (VVariant (name, Some v))
977+
| _ -> Error (TypeMismatch (Printf.sprintf "%s expects 1 argument" name))))
978+
env
979+
| fields ->
980+
(* Multi-payload constructor: pack args into a VTuple *)
981+
let n = List.length fields in
982+
extend_env name
983+
(VBuiltin (name, fun args ->
984+
if List.length args = n then
985+
Ok (VVariant (name, Some (VTuple args)))
986+
else
987+
Error (TypeMismatch
988+
(Printf.sprintf "%s expects %d arguments" name n))))
989+
env
990+
) env variants in
991+
Ok env'
992+
| _ ->
993+
(* Struct, alias, and other type declarations don't create runtime bindings *)
994+
Ok env
995+
end
996+
997+
| TopTrait _ | TopImpl _ ->
998+
(* Trait and impl declarations don't affect the runtime value environment *)
897999
Ok env
8981000

8991001
| TopEffect ed ->

lib/resolve.ml

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,32 @@ let create_context () : context =
5151
imports = [];
5252
references = [];
5353
} in
54-
(* Register built-in functions *)
55-
let _ = Symbol.define ctx.symbols "print" SKFunction Span.dummy Public in
56-
let _ = Symbol.define ctx.symbols "println" SKFunction Span.dummy Public in
57-
let _ = Symbol.define ctx.symbols "int" SKFunction Span.dummy Public in
58-
let _ = Symbol.define ctx.symbols "float" SKFunction Span.dummy Public in
59-
let _ = Symbol.define ctx.symbols "sqrt" SKFunction Span.dummy Public in
54+
(* Register built-in functions — must match the interpreter's create_initial_env *)
55+
let def name = let _ = Symbol.define ctx.symbols name SKFunction Span.dummy Public in () in
56+
(* Console I/O *)
57+
def "print"; def "println"; def "eprint"; def "eprintln";
58+
(* String / char builtins *)
59+
def "len"; def "string_get"; def "string_sub"; def "string_find";
60+
def "char_to_int"; def "int_to_char"; def "show";
61+
def "to_lowercase"; def "to_uppercase"; def "trim";
62+
def "int_to_string"; def "float_to_string"; def "string_length";
63+
def "parse_int"; def "parse_float";
64+
(* Numeric coercions and math *)
65+
def "int"; def "float";
66+
def "sqrt"; def "cbrt"; def "pow_float"; def "floor"; def "ceil"; def "round";
67+
def "abs"; def "max"; def "min";
68+
def "sin"; def "cos"; def "tan"; def "atan"; def "atan2";
69+
def "exp"; def "log"; def "log10"; def "log2";
70+
(* I/O — files, process, environment *)
71+
def "read_line"; def "read_file"; def "write_file"; def "append_file";
72+
def "file_exists"; def "is_directory";
73+
def "getenv"; def "setenv"; def "getcwd"; def "chdir";
74+
def "list_dir"; def "create_dir"; def "remove_file"; def "remove_dir";
75+
def "panic"; def "exit";
76+
(* Time *)
77+
def "time_now";
78+
(* TEA runtime — The Elm Architecture interpreter loop *)
79+
def "tea_run";
6080
ctx
6181

6282
(** Record a use-site reference for a symbol (Phase C: find-references). *)

0 commit comments

Comments
 (0)