feat(lang): L11 views and templates - the UI is markup, not code
A `view Name { field = x; function q(..); on e(..) }` declaration is the one bridge between a
program and its UI: it writes view_<name>() -> UiView, whose model is a Value of every field and
whose call runs a query or an event by name.
ludic.ui is a template runtime:
- HTML-shaped XML screens and components, loaded at run time;
- {expression} bindings, if/else/each, props, slots, per-instance state;
- on-press / onclick actions (event, set, emit);
- component libraries (export="true", <import src as>).
Styling:
- stylesheets in <style> or importable .lss files (@import);
- CSS selectors (#id, .class, [attr=v], descendant and > combinators, :hover, :disabled,
:first-child, :last-child, :nth-child, :not) weighed by specificity;
- the box model and flex under CSS's property names.
Also:
- default parameters, and positional-then-named calls;
- Value gains a float kind;
- a function shadowing a runtime one is refused;
- an index is evaluated before the slice is read;
- runtime errors name the right file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
334469ef61
commit
66a2bc2214
64 changed files with 74806 additions and 63975 deletions
|
|
@ -64,6 +64,7 @@ function ns_alias_labels(i: int) -> []pointer {
|
|||
return out
|
||||
}
|
||||
function emit_alias_call(i: int, e: Node) -> Val {
|
||||
call_mixed_to_named(e, ns_alias_labels(i))
|
||||
reorder_named(e, ns_alias_labels(i))
|
||||
let id = node(E_ID)
|
||||
id.s = g_al_target[i]
|
||||
|
|
|
|||
96
selfhost/frontend/defaults.ludic
Normal file
96
selfhost/frontend/defaults.ludic
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# defaults.ludic — L11: a parameter may have a default, `pad: float = 8.0`, and a call may leave it
|
||||
# out - positionally from the end, or by not naming it. That is what makes a component's props
|
||||
# usable: `button(label: "Buy", on_press: fn buy)` rather than every prop of a button spelt out.
|
||||
# The default is an expression written where the function is and evaluated for each call.
|
||||
# `f(a, b, size: 2)`: the leading arguments are the first parameters, by position, and the rest
|
||||
# are named - the natural call of a component, whose first prop is the one it is about
|
||||
function call_mixed_to_named(e: Node, labels: []pointer) -> void {
|
||||
var first_named = -1
|
||||
var i = 0
|
||||
while i < len(e.kids) {
|
||||
if e.kids[i].kind == E_FINIT and first_named < 0 { first_named = i }
|
||||
if e.kids[i].kind != E_FINIT and first_named >= 0 {
|
||||
g_err_file = e.file
|
||||
g_err_line = e.line
|
||||
perr("a positional argument after a named one")
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
if first_named <= 0 { return }
|
||||
let out = new []Node
|
||||
var k = 0
|
||||
while k < len(e.kids) {
|
||||
var a = e.kids[k]
|
||||
if k < first_named and k < len(labels) {
|
||||
let fi = node(E_FINIT)
|
||||
fi.s = labels[k]
|
||||
fi.a = a
|
||||
fi.line = a.line
|
||||
fi.file = a.file
|
||||
a = fi
|
||||
}
|
||||
push(out, a)
|
||||
k += 1
|
||||
}
|
||||
e.kids = out
|
||||
}
|
||||
function call_fill_defaults(e: Node, f: Node) -> void {
|
||||
if f == null { return }
|
||||
let labels = new []pointer
|
||||
let defs = new []Node
|
||||
var any = false
|
||||
var i = 0
|
||||
while i < len(f.kids) {
|
||||
let p = f.kids[i]
|
||||
if p.kind == N_PARAM {
|
||||
push(labels, p.s)
|
||||
push(defs, p.a)
|
||||
if p.a != null { any = true }
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
call_mixed_to_named(e, labels)
|
||||
if not any { return }
|
||||
if len(e.kids) > 0 and e.kids[0].kind == E_FINIT {
|
||||
let out = new []Node
|
||||
var li = 0
|
||||
while li < len(labels) {
|
||||
var found: Node = null
|
||||
var k = 0
|
||||
while k < len(e.kids) {
|
||||
if (e.kids[k].s == labels[li]) { found = e.kids[k] }
|
||||
k += 1
|
||||
}
|
||||
if found == null and defs[li] != null {
|
||||
found = node(E_FINIT)
|
||||
found.s = labels[li]
|
||||
found.a = default_copy(defs[li], e)
|
||||
}
|
||||
if found != null { push(out, found) }
|
||||
li += 1
|
||||
}
|
||||
# a label the function does not have stays, so the call is refused for it
|
||||
var k2 = 0
|
||||
while k2 < len(e.kids) {
|
||||
if not default_label_in(labels, e.kids[k2].s) { push(out, e.kids[k2]) }
|
||||
k2 += 1
|
||||
}
|
||||
e.kids = out
|
||||
return
|
||||
}
|
||||
while len(e.kids) < len(labels) and defs[len(e.kids)] != null { push(e.kids, default_copy(defs[len(e.kids)], e)) }
|
||||
}
|
||||
function default_label_in(labels: []pointer, s: pointer) -> bool {
|
||||
var i = 0
|
||||
while i < len(labels) {
|
||||
if (labels[i] == s) { return true }
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
# each call gets its own copy of the default, at the call's line for its errors
|
||||
function default_copy(d: Node, at: Node) -> Node {
|
||||
let c = gen_clone(d, new []pointer, new []pointer)
|
||||
c.line = at.line
|
||||
return c
|
||||
}
|
||||
|
|
@ -577,7 +577,9 @@ function parse_fn() -> Node {
|
|||
eat_op("(")
|
||||
if is_reserved_word(n.s) { perr(`'{n.s}' is a reserved word and cannot name a function`) }
|
||||
while not is_op(")") {
|
||||
let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); p.ty = ptype(); push(n.kids, p)
|
||||
let p = node(N_PARAM); p.s = eat_id(); eat_op(":"); p.ty = ptype()
|
||||
if is_op("=") { pi += 1; p.a = expr() } # L11: a default, for a call that leaves it out
|
||||
push(n.kids, p)
|
||||
if is_op(",") { pi += 1 }
|
||||
}
|
||||
eat_op(")")
|
||||
|
|
@ -1014,6 +1016,7 @@ function parse_one_decl() -> void {
|
|||
if is_id("namespace") { parse_namespace(); return } # #76 namespace block
|
||||
if is_id("registry") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "of") { parse_registry(); return } # L8
|
||||
if is_id("def") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].kind == TK_ID) { parse_def(); return } # L8
|
||||
if is_id("view") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_view(); return } # L11
|
||||
if is_id("var") { push(prog, parse_var()); return }
|
||||
if is_id("const") { push(prog, parse_const()); return }
|
||||
var is_unsafe = false
|
||||
|
|
@ -1510,4 +1513,5 @@ function parse_program() -> void {
|
|||
parse_one_decl()
|
||||
}
|
||||
registries_finish() # L8: each registry gets its defs
|
||||
views_finish() # L11: each view gets its model and call
|
||||
}
|
||||
|
|
|
|||
113
selfhost/frontend/view.ludic
Normal file
113
selfhost/frontend/view.ludic
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# view.ludic — L11: a view is the one bridge between a program and its UI templates. The
|
||||
# program says what a template may read and what it may do, and nothing else crosses:
|
||||
#
|
||||
# view Yard {
|
||||
# purse = hm_purse # a field: the global's own type
|
||||
# spots: int = yard_spots() # any expression, with its type said
|
||||
# function placed(hf: int) -> int { return yard_placed(hf) } # a query a template may ask
|
||||
# on buy(hf: int) { yard_buy(hf) } # an event a template may send
|
||||
# }
|
||||
#
|
||||
# gives view_yard() -> UiView, whose model() is a Value object of every field (a record is an
|
||||
# object of its fields, a list and a registry a list) and whose call(name, args) runs a query or
|
||||
# an event by name with its arguments out of a Value list. Everything generated belongs to the
|
||||
# view's own file, so it is in the view's module and exported only when the view is.
|
||||
var g_vw_name: []pointer = new []pointer
|
||||
var g_vw_node: []Node = new []Node # view_<name>(), which carries the file, line and export
|
||||
var g_vw_fields: [][]Node = new [][]Node # the getters, one per field
|
||||
var g_vw_fnames: [][]pointer = new [][]pointer
|
||||
var g_vw_calls: [][]Node = new [][]Node # the queries and the events
|
||||
var g_vw_cnames: [][]pointer = new [][]pointer
|
||||
var g_vw_helpers: []pointer = new []pointer # conversion helpers already written, program-wide
|
||||
|
||||
function vw_prefix(name: pointer) -> pointer { return `view_{reg_lower(name)}_` }
|
||||
|
||||
# view NAME { members }
|
||||
function parse_view() -> void {
|
||||
pi += 1
|
||||
let name = eat_id()
|
||||
let px = vw_prefix(name)
|
||||
g_uses_value = true
|
||||
let fields = new []Node
|
||||
let fnames = new []pointer
|
||||
let calls = new []Node
|
||||
let cnames = new []pointer
|
||||
skipnl()
|
||||
eat_op("{")
|
||||
while true {
|
||||
skipnl()
|
||||
if is_op("}") { break }
|
||||
if is_id("function") or is_id("on") {
|
||||
let ev = is_id("on")
|
||||
if ev { toks[pi].text = "function" }
|
||||
let f = parse_fn()
|
||||
push(cnames, f.s)
|
||||
if ev { f.s = px + "on_" + f.s } else { f.s = px + f.s }
|
||||
push(calls, f)
|
||||
push(prog, f)
|
||||
continue
|
||||
}
|
||||
push(fnames, eat_id())
|
||||
push(fields, vw_field(px, fnames[len(fnames) - 1]))
|
||||
}
|
||||
eat_op("}")
|
||||
let head = vw_head(name, px)
|
||||
push(g_vw_name, name)
|
||||
push(g_vw_node, head)
|
||||
push(g_vw_fields, fields)
|
||||
push(g_vw_fnames, fnames)
|
||||
push(g_vw_calls, calls)
|
||||
push(g_vw_cnames, cnames)
|
||||
}
|
||||
# one field: NAME [: TYPE] = EXPR, as a getter function whose type the finish fills in if unsaid
|
||||
function vw_field(px: pointer, fname: pointer) -> Node {
|
||||
let g = node(N_FN)
|
||||
g.s = px + "get_" + fname
|
||||
g.ty = null
|
||||
if is_op(":") {
|
||||
pi += 1
|
||||
g.ty = ptype()
|
||||
}
|
||||
if not is_op("=") { perr(`view field {fname}: say what it is - {fname} = some_global`) }
|
||||
pi += 1
|
||||
let r = node(S_RETURN)
|
||||
r.a = expr()
|
||||
let body = node(N_BLOCK)
|
||||
push(body.kids, r)
|
||||
g.a = body
|
||||
push(prog, g)
|
||||
return g
|
||||
}
|
||||
# view_<name>() -> UiView, written now so it takes the export the declaration is under
|
||||
function vw_head(name: pointer, px: pointer) -> Node {
|
||||
let src = `function view_{reg_lower(name)}() -> UiView {{\n let v = new UiView\n v.name = "{name}"\n v.model = fn {px}model\n v.call = fn {px}call\n return v\n}}\n`
|
||||
let at = len(prog)
|
||||
vw_parse(src, g_parse_file, toks[pi].line, 0)
|
||||
return prog[at]
|
||||
}
|
||||
# parse generated source into the program, as if it were written in `file` at `line`
|
||||
function vw_parse(src: pointer, file: pointer, line: int, vis: int) -> void {
|
||||
let saved_toks = toks
|
||||
let saved_pi = pi
|
||||
let saved_file = g_parse_file
|
||||
let saved_parsing = g_parsing
|
||||
g_parse_file = file
|
||||
g_parsing = true
|
||||
lex_at(src, line)
|
||||
pi = 0
|
||||
let at = len(prog)
|
||||
while true {
|
||||
skipnl()
|
||||
if toks[pi].kind == TK_EOF { break }
|
||||
parse_one_decl()
|
||||
}
|
||||
var k = at
|
||||
while k < len(prog) {
|
||||
prog[k].vis = vis
|
||||
k += 1
|
||||
}
|
||||
toks = saved_toks
|
||||
pi = saved_pi
|
||||
g_parse_file = saved_file
|
||||
g_parsing = saved_parsing
|
||||
}
|
||||
68
selfhost/frontend/view_conv.ludic
Normal file
68
selfhost/frontend/view_conv.ludic
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# view_conv.ludic — L11: a value of the program as a Value a template reads. A number, a bool
|
||||
# and a string are themselves; a list is a list of its items; a record is an object of every
|
||||
# field a template could read (a field it could not - a function, a buffer - is left out).
|
||||
# Anything else answers null, and the caller says so.
|
||||
function vw_conv(v: int, ty: pointer, x: pointer, head: Node) -> pointer {
|
||||
if (ty == "int") { return `value_int({x})` }
|
||||
if (ty == "float") { return `value_float({x})` }
|
||||
if (ty == "string") { return `value_str({x})` }
|
||||
if (ty == "bool") {
|
||||
vw_helper(`function view_val_bool(b: bool) -> Val {{\n if b {{ return value_bool(1) }}\n return value_bool(0)\n}}\n`, "view_val_bool", head)
|
||||
return `view_val_bool({x})`
|
||||
}
|
||||
if len(ty) > 2 and ty[0] == '[' and ty[1] == ']' {
|
||||
let et: pointer = ty[2..len(ty)]
|
||||
let name = `view_val_list_{vw_mangle(et)}`
|
||||
if vw_have(name) { return `{name}({x})` }
|
||||
let item = vw_conv(v, et, "xs[i]", head)
|
||||
if item == null { return null }
|
||||
push(g_vw_helpers, name)
|
||||
vw_parse(`function {name}(xs: {ty}) -> Val {{\n let out = value_list()\n var i = 0\n while i < len(xs) {{\n value_add(out, {item})\n i += 1\n }}\n return out\n}}\n`, head.file, head.line, 1)
|
||||
return `{name}({x})`
|
||||
}
|
||||
let comp = find_comp(ty)
|
||||
if comp == null { return null }
|
||||
let name = `view_val_{vw_mangle(ty)}`
|
||||
if vw_have(name) { return `{name}({x})` }
|
||||
push(g_vw_helpers, name)
|
||||
var src = `function {name}(r: {ty}) -> Val {{\n if r == null {{ return value_null() }}\n let o = value_object()\n`
|
||||
var i = 0
|
||||
while i < len(comp.kids) {
|
||||
let f = comp.kids[i]
|
||||
if f.ty != null {
|
||||
let c = vw_conv(v, f.ty, `r.{f.s}`, head)
|
||||
if c != null { src = src + ` value_put(o, "{f.s}", {c})\n` }
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
vw_parse(src + " return o\n}\n", head.file, head.line, 1)
|
||||
return `{name}({x})`
|
||||
}
|
||||
function vw_have(name: pointer) -> bool {
|
||||
var i = 0
|
||||
while i < len(g_vw_helpers) {
|
||||
if (g_vw_helpers[i] == name) { return true }
|
||||
i += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
function vw_helper(src: pointer, name: pointer, head: Node) -> void {
|
||||
if vw_have(name) { return }
|
||||
push(g_vw_helpers, name)
|
||||
vw_parse(src, head.file, head.line, 1)
|
||||
}
|
||||
# a type as part of a name: []int -> list_int, a.B -> a_B
|
||||
function vw_mangle(ty: pointer) -> pointer {
|
||||
let n = len(ty)
|
||||
let out = bytes(n + 1)
|
||||
var i = 0
|
||||
while i < n {
|
||||
var c = ty[i]
|
||||
let ok = (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9')
|
||||
if not ok { c = '_' }
|
||||
out[i] = c
|
||||
i += 1
|
||||
}
|
||||
out[n] = 0
|
||||
return out
|
||||
}
|
||||
84
selfhost/frontend/view_finish.ludic
Normal file
84
selfhost/frontend/view_finish.ludic
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# view_finish.ludic — L11: when the program is read, each view gets its model and its call.
|
||||
# A field's type is the one it was given or its global's; converting it to a Value is a helper
|
||||
# written once for the whole program per type (view_val_<type>), so two views of one record
|
||||
# share it.
|
||||
function views_finish() -> void {
|
||||
var v = 0
|
||||
while v < len(g_vw_name) {
|
||||
vw_finish(v)
|
||||
v += 1
|
||||
}
|
||||
}
|
||||
function vw_err(v: int, msg: pointer) -> void {
|
||||
g_err_file = g_vw_node[v].file
|
||||
g_err_line = g_vw_node[v].line
|
||||
g_parsing = false
|
||||
perr(`view {g_vw_name[v]}: {msg}`)
|
||||
}
|
||||
function vw_finish(v: int) -> void {
|
||||
let head = g_vw_node[v]
|
||||
if find_comp("UiView") == null { vw_err(v, `a view is shown by ludic.ui - import "ludic.ui"`) }
|
||||
let px = vw_prefix(g_vw_name[v])
|
||||
var model = `function {px}model() -> Val {{\n let o = value_object()\n`
|
||||
let fields = g_vw_fields[v]
|
||||
var i = 0
|
||||
while i < len(fields) {
|
||||
let g = fields[i]
|
||||
let fname = g_vw_fnames[v][i]
|
||||
if g.ty == null { g.ty = vw_infer(v, g, fname) }
|
||||
let conv = vw_conv(v, g.ty, `{g.s}()`, head)
|
||||
if conv == null { vw_err(v, `{fname} is a {g.ty}, which a template cannot read`) }
|
||||
model = model + ` value_put(o, "{fname}", {conv})\n`
|
||||
i += 1
|
||||
}
|
||||
model = model + " return o\n}\n"
|
||||
vw_parse(model, head.file, head.line, head.vis)
|
||||
vw_parse(vw_call_src(v, px, head), head.file, head.line, head.vis)
|
||||
}
|
||||
# a field written `name = some_global` has that global's declared type
|
||||
function vw_infer(v: int, g: Node, fname: pointer) -> pointer {
|
||||
let e = g.a.kids[0].a
|
||||
if e.kind == E_ID {
|
||||
let d = find_global(e.s)
|
||||
if d != null and d.ty != null { return d.ty }
|
||||
}
|
||||
g_vw_node[v].line = g.line
|
||||
vw_err(v, `say what type {fname} is - {fname}: int = ...`)
|
||||
return "int"
|
||||
}
|
||||
# call(name, args): each query and event by its name, its arguments out of the list
|
||||
function vw_call_src(v: int, px: pointer, head: Node) -> pointer {
|
||||
var src = `function {px}call(name: string, args: Val) -> Val {{\n`
|
||||
let calls = g_vw_calls[v]
|
||||
var i = 0
|
||||
while i < len(calls) {
|
||||
let f = calls[i]
|
||||
var args = ""
|
||||
var p = 0
|
||||
while p < len(f.kids) {
|
||||
if p > 0 { args = args + ", " }
|
||||
args = args + vw_arg(v, f.kids[p], p)
|
||||
p += 1
|
||||
}
|
||||
let call = `{f.s}({args})`
|
||||
src = src + ` if name == "{g_vw_cnames[v][i]}" {{\n`
|
||||
if f.ty == "void" { src = src + ` {call}\n return value_null()\n` } else {
|
||||
let conv = vw_conv(v, f.ty, call, head)
|
||||
if conv == null { vw_err(v, `{g_vw_cnames[v][i]} answers a {f.ty}, which a template cannot read`) }
|
||||
src = src + ` return {conv}\n`
|
||||
}
|
||||
src = src + " }\n"
|
||||
i += 1
|
||||
}
|
||||
return src + " return value_null()\n}\n"
|
||||
}
|
||||
# a parameter out of the template's argument list
|
||||
function vw_arg(v: int, p: Node, i: int) -> pointer {
|
||||
let at = `value_at(args, {i})`
|
||||
if (p.ty == "int") { return `value_as_int({at})` }
|
||||
if (p.ty == "float") { return `value_as_float({at})` }
|
||||
if (p.ty == "bool") { return `value_as_int({at}) != 0` }
|
||||
if (p.ty == "string") { return `value_as_str({at})` }
|
||||
vw_err(v, `{p.s} is a {p.ty}; a template can pass an int, a float, a bool or a string`)
|
||||
return ""
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue