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 #
src/core.mc: the compiler's#includelist withoutuser.mc;src/mc.mcbecomes#include "core.mc"+#include "user.mc". A taught compiler is its own file:#include "../../src/core.mc"+ modules +void user_init().-
Syntax hooks (
src/hooks.mc+src/parse.mc):void syntax(uptr word, uptr fn): registerswordas a word (tok_add) andfnin 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 asyntaxword, it callscallp(fn); the handler consumes tokens starting from the word (inclusive) and produces declarations via the public API.parse_stmt: before dispatching#rule, likewise forsyntax_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; returnsTY_*),parse_expr(0),parse_stmt(),parse_block(),parse_params()(reads(...), returns a list ofN_PARAM),parse_function(ty, name, params)(receives the already-built params list — the handler may have prependedself—, reads the block, registers the signature, and returns theN_FUNC),top_add(n)(appendsN_FUNC/N_GLOBAL/N_EXTERNto the unit, in order),def_add(name, val, line, file),param_new(ty, name),list_append(head, n), plusnode_new/nd_*/set_nd_*fromast.mc. - Handlers run synchronously during parsing; linear tables; no hashing anywhere.
type_alias(uptr name, i64 base): registersnameas a word;type_of_tokenreturnsbasefor it (MAXALIAS 64). Declarations, params, casts, andp_typeaccept aliases with no further changes (they all go throughtype_of_token).#dylib "path": a new directive (D_DYLIB, at the end of the list inlex.mc, so as not to renumber anything).parse.mcstores the path in a table (MAXDYLIBS 8; ordinal = index + 2, libSystem is 1) and acur_dylib; everyexterndeclared afterward getscur_dylibrecorded in a per-name table (extern_lib_find(name), default 1).backend_exe.mc: one extraLC_LOAD_DYLIBper dylib, in order;n_descandBIND_SET_DYLIB_ORD_IMMper symbol according to the table. The path need not exist on disk (shared cache) — not validated..o+ldignores#dylib.- Core-level proofs:
lib/user_syntax_demo.mcteachesunless (c) block(viasyntax_stmt),enum Name { A, B, C }(viasyntax, generating#defines), andtype_alias("bool", TY_U8);lib/syntax_demo_test.mcuses all three and exits 42. A new case inscripts/check-surface.sh(wires up the demo with its own entry point that includessrc/core.mc, compiles it, and runs the test).make checkgreen; golden re-recorded once, with the--dump-asmdiff 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) { ... } }
class:#define NAME_FIELD off,NAME_SIZE, accessorsname_field(self)/set_name_field(self, v),Name name_new()(rt_alloc(NAME_SIZE)zeroed; if it implements an interface, word 0 = vtable), methods becomename_method(uptr self, ...)with an implicitself;type_alias("Name", TY_UPTR).interface:#define IFACE_METHOD idx*8, dispatchersiface_method(self, ...)=callp(ld64(ld64(self) + IFACE_METHOD), self, ...); name → methods registry inoop.mcglobals. A class implementing it: a global vtableu8 name_vt[8*N]filled in byname_vt_init()(called byname_new()); a missing method is a clear compile-time error.- New types:
bool(u8),str(uptr), and each class/interface.
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).