Spec M12 — Tier 3: syntax taught through code (syntax hooks), type aliases, #dylib,

and the examples/api example #

.mc only. Stage0 stays the seed: it knows nothing of #dylib, hooks, or aliases, and never compiles the example (examples/ is outside the check-* targets that run mc0). Motivation: #rule stmt: cannot introduce class/interface (top-level position, variable-length lists, generating compound names). The way out is the same principle as M10: the user writes a .mc module that runs inside the compiler and uses the parser's public API.

A. .mc core #

  1. src/core.mc: the compiler's #include list without user.mc; src/mc.mc becomes #include "core.mc" + #include "user.mc". A taught compiler is its own file: #include "../../src/core.mc" + modules + void user_init().
  2. Syntax hooks (src/hooks.mc + src/parse.mc):

    • void syntax(uptr word, uptr fn): registers word as a word (tok_add) and fn in a linear table (MAXSYNTAX 32, registration order). void syntax_stmt(uptr word, uptr fn): the same, for statement position.
    • parse_top: before requiring a type, if the current token is a syntax word, it calls callp(fn); the handler consumes tokens starting from the word (inclusive) and produces declarations via the public API. parse_stmt: before dispatching #rule, likewise for syntax_stmt; the handler returns the statement node's index (0 = nothing).
    • Public parser API (fixed names, in parse.mc): p_id(), p_val(), p_name() (copy of the current lexeme), p_line(), p_file(), p_next(), p_accept(id) (consumes if it matches; 1/0), p_expect(id, msg), p_ident() (requires an identifier; returns the name and advances), p_type() (requires a type, including aliases; returns TY_*), parse_expr(0), parse_stmt(), parse_block(), parse_params() (reads (...), returns a list of N_PARAM), parse_function(ty, name, params) (receives the already-built params list — the handler may have prepended self —, reads the block, registers the signature, and returns the N_FUNC), top_add(n) (appends N_FUNC/N_GLOBAL/N_EXTERN to the unit, in order), def_add(name, val, line, file), param_new(ty, name), list_append(head, n), plus node_new/nd_*/set_nd_* from ast.mc.
    • Handlers run synchronously during parsing; linear tables; no hashing anywhere.
  3. type_alias(uptr name, i64 base): registers name as a word; type_of_token returns base for it (MAXALIAS 64). Declarations, params, casts, and p_type accept aliases with no further changes (they all go through type_of_token).
  4. #dylib "path": a new directive (D_DYLIB, at the end of the list in lex.mc, so as not to renumber anything). parse.mc stores the path in a table (MAXDYLIBS 8; ordinal = index + 2, libSystem is 1) and a cur_dylib; every extern declared afterward gets cur_dylib recorded in a per-name table (extern_lib_find(name), default 1). backend_exe.mc: one extra LC_LOAD_DYLIB per dylib, in order; n_desc and BIND_SET_DYLIB_ORD_IMM per symbol according to the table. The path need not exist on disk (shared cache) — not validated. .o+ld ignores #dylib.
  5. Core-level proofs: lib/user_syntax_demo.mc teaches unless (c) block (via syntax_stmt), enum Name { A, B, C } (via syntax, generating #defines), and type_alias("bool", TY_U8); lib/syntax_demo_test.mc uses all three and exits 42. A new case in scripts/check-surface.sh (wires up the demo with its own entry point that includes src/core.mc, compiles it, and runs the test). make check green; golden re-recorded once, with the --dump-asm diff between mc1 and mc2 empty.

B. examples/api — compiled only by the self-hosted mc #

examples/api/
  Makefile        make -C examples/api {mc-api,api,test,clean}; uses ../../build/mc1 (builds it if missing)
  mc-api.mc       #include "../../src/core.mc" + oop.mc + user_init (syntax/type_alias)
  oop.mc          teaches class/interface — runs inside the compiler
  lib/rt.mc       program runtime: its own arena (u8 heap[4 MiB]), str utils, strbuf, itoa/atoi
  lib/http.mc     socket/setsockopt/bind/listen/accept (sockaddr_in in bytes), HTTP request, response
  lib/sqlite.mc   #dylib "/usr/lib/libsqlite3.dylib" + externs + wrappers (open/exec/prepare/step/...)
  main.mc         the full API: GET /health, GET /todos, POST /todos (body = title), DELETE /todos/N; JSON
  test.sh         starts the server on a free port, curl, compares, kills it; exit 0/1
  README.md

Syntax taught by oop.mc (all through the public API, without touching the core):

interface Handler { i64 handle(self, Request req, Response res); }
class Todo { i64 id; str title; bool done;  str json(self) { ... } }
class TodoHandler : Handler { Db db;  i64 handle(self, Request req, Response res) { ... } }

Acceptance: make -C examples/api test green (starts the server, POSTs 2 todos, GETs the expected JSON list, DELETE, GET again, /health); the --exe binary passes codesign --verify; otool -L shows libSystem and libsqlite3; make check at the root stays green and gains check-examples. README with the step-by-step walkthrough and a note that lib/rt.mc has a fixed-size arena (see M13).

Edit this page