ludic/selfhost/frontend/ports.ludic

194 lines
6.3 KiB
Text

# ports.ludic — L3 ports: what a module needs from outside itself, filled in by whoever puts the
# program together, so the module never names (or `uses`) the code that answers it.
#
# port Clock { now: fn() -> float, day: fn() -> int = fn default_day } # in module clock
# bind Clock { now: fn game_now, day: g_day } # in the app; a variable for a member taking nothing
# Clock.now() # anywhere that sees it
#
# A port is a record of function values (the record `Clock_port`, members without a default are
# required) and a global `Clock` of that record, in the port's module and exported with it. The
# one `bind` is that global's initializer. When the program is read (ports_finish) every bind
# must name a port, give each required member, give no member twice-over, and see the port;
# a port used anywhere must be bound (port_check_bound, at the use) - unless every member has a
# default, when an unbound port answers with its defaults. The binding's values are
# checked for visibility from the bind's file, here, so the port's module needs no `uses` of it.
var g_pt_name: []pointer = new []pointer
var g_pt_comp: []Node = new []Node
var g_pt_var: []Node = new []Node
var g_pt_bind: []Node = new []Node # the bind that filled it; null until one does
var g_bd: []Node = new []Node # every bind, in source order: s=port a=record
# port NAME { member: fn(...) -> T [= fn default], ... }
function parse_port() -> void {
let c = parse_component()
let name = c.s
c.s = `{name}_port`
push(prog, c)
let v = node(N_VAR)
v.s = name
v.ty = c.s
v.line = c.line
push(prog, v)
push(g_pt_name, name)
push(g_pt_comp, c)
push(g_pt_var, v)
push(g_pt_bind, null)
}
# bind NAME { member: fn f, ... }
function parse_bind() -> void {
pi += 1
let b = node(N_VAR)
b.s = eat_id()
skipnl()
b.a = record()
push(g_bd, b)
}
function port_find(name: pointer) -> int {
var i = 0
while i < len(g_pt_name) {
if (g_pt_name[i] == name) { return i }
i += 1
}
return -1
}
function is_port_var(d: Node) -> bool {
if d.kind != N_VAR { return false }
let k = port_find(d.s)
if k < 0 { return false }
return (d.ty == g_pt_comp[k].s)
}
# a port used where none is bound: the program is not put together yet
function port_check_bound(d: Node) -> void {
if not is_port_var(d) { return }
if g_pt_bind[port_find(d.s)] != null { return }
perr(`port {d.s} is used here but never bound: the program has to say 'bind {d.s} {{ ... }}' once, where it is put together`)
}
function port_err(b: Node, msg: pointer) -> void {
g_err_file = b.file
g_err_line = b.line
perr(msg)
}
function port_bind_one(b: Node) -> void {
let k = port_find(b.s)
if k < 0 { port_err(b, `bind {b.s}: there is no port {b.s}`) }
let first = g_pt_bind[k]
if first != null { port_err(b, `port {b.s} is bound twice (first at {first.file}:{itoa(first.line)})`) }
g_pt_bind[k] = b
let c = g_pt_comp[k]
let rec = b.a
var i = 0
while i < len(rec.kids) {
if field_index(c, rec.kids[i].s) < 0 { port_err(b, `port {b.s} has no member {rec.kids[i].s}`) }
i += 1
}
i = 0
while i < len(c.kids) {
if c.kids[i].a == null and not reg_rec_has(rec, c.kids[i].s) {
port_err(b, `bind {b.s} leaves out {c.kids[i].s}, which has no default`)
}
i += 1
}
# `member: some_global` binds a member that takes nothing to a variable: a getter is written
i = 0
while i < len(rec.kids) {
let e = rec.kids[i].a
if e != null and e.kind == E_ID { port_bind_var(b, c, rec.kids[i]) }
if e != null and e.kind == E_MEMBER and e.a != null and e.a.kind == E_ID and is_state_ty(e.a.s) { port_bind_var(b, c, rec.kids[i]) }
i += 1
}
# what the bind names is seen from the bind's file: the port itself, and every `fn name`
g_err_file = b.file
g_err_line = b.line
let v = g_pt_var[k]
vis_check(v, b.s)
i = 0
while i < len(rec.kids) {
let e = rec.kids[i].a
if e != null and e.kind == E_FNREF {
let f = find_fn(e.s)
if f != null { vis_check(f, e.s) }
}
i += 1
}
let nw = node(E_NEW)
nw.s = c.s
nw.a = rec
nw.file = b.file
nw.line = b.line
v.a = nw
}
# a port whose every member has a default needs no bind: left alone it answers with its defaults
function port_defaults_only(k: int) -> void {
let c = g_pt_comp[k]
var i = 0
while i < len(c.kids) {
if c.kids[i].a == null { return }
i += 1
}
let v = g_pt_var[k]
let nw = node(E_NEW)
nw.s = c.s
nw.file = v.file
nw.line = v.line
v.a = nw
g_pt_bind[k] = nw
}
# `money: gm_money` for a `money: fn() -> int` member: the function `bind_<Port>_<member>` that
# returns the variable, written in the bind's own file (so it sees what the bind sees)
function port_bind_var(b: Node, c: Node, fi: Node) -> void {
mg_bind_var(fi) # 0.S2: ludic migrate state moves it to the state's field
# 0.S: `money: Wallet.cash` - a field of a state, read through the state the runtime supplies
var sty: pointer = null
var name: pointer = null
if fi.a.kind == E_MEMBER {
sty = fi.a.a.s
name = fi.a.s
} else {
name = fi.a.s
let g = find_global(name)
if g == null { port_err(b, `bind {b.s}: {fi.s}: {name} is not a variable (a function is bound as fn {name})`) }
}
let fty = field_type(c, fi.s)
let n = len(fty)
if n < 7 or not (fty[0 .. 6] == "fn()->") {
port_err(b, `bind {b.s}: {fi.s} is a {fty}, and only a member that takes nothing can be bound to a variable`)
}
let ret = fty[6 .. n]
let gen = `bind_{b.s}_{fi.s}`
var src = `function {gen}() -> {ret} {{\n return {name}\n}}\n`
if sty != null { src = `function {gen}(st: {sty}) -> {ret} {{\n return st.{name}\n}}\n` }
let saved_toks = toks
let saved_pi = pi
let saved_file = g_parse_file
let saved_parsing = g_parsing
g_parse_file = b.file
g_parsing = true
lex_at(src, b.line)
pi = 0
skipnl()
parse_one_decl()
toks = saved_toks
pi = saved_pi
g_parse_file = saved_file
g_parsing = saved_parsing
let r = node(E_FNREF)
r.s = gen
r.file = b.file
r.line = b.line
fi.a = r
}
function ports_finish() -> void {
let saved = g_parsing
g_parsing = false
var i = 0
while i < len(g_bd) {
port_bind_one(g_bd[i])
i += 1
}
i = 0
while i < len(g_pt_name) {
if g_pt_bind[i] == null { port_defaults_only(i) }
i += 1
}
g_parsing = saved
}