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:
Orkun ÇAKILKAYA 2026-09-24 15:58:47 +03:00
parent 334469ef61
commit 66a2bc2214
64 changed files with 74806 additions and 63975 deletions

View file

@ -275,6 +275,142 @@ The entries are compiled in: nothing is parsed at start-up, and a build that suc
every resource it uses. The path is the project's (where the build runs), else beside the file that every resource it uses. The path is the project's (where the build runs), else beside the file that
declares the registry. declares the registry.
### Default parameters, and calls that name what they change
A parameter can have a default, and a call leaves out what it does not change - the last ones when
it passes arguments by position, or any it does not name. A call can pass its first arguments by
position and the rest by name:
```ludic
program Boxes {
numbers float
function box(label: string, w: float = 300.0, pad: float = 8.0, bold: bool = false) -> string {
return `{label} {w} {pad} {bold}`
}
entry {
print(box("a")) # every default
print(box("b", 120.0)) # the first two by position
print(box("c", bold: true)) # the subject by position, a prop by name
}
}
```
A default is an expression written with the function and evaluated for each call that leaves it
out. A call that leaves out a parameter with no default, names one the function does not have, or
puts a positional argument after a named one is refused with that said.
### Views and templates (`ludic.ui`)
A screen is not code. It is a template - markup in a file the program loads at run time - and it
sees only what the program's `view` lets it: its fields to read, its functions to ask, and its
`on` events to send. Nothing else crosses in either direction.
```ludic
# doc-check: skip — a fragment of a program that imports ludic.ui
view Shop {
purse = purse # a global's own type
stock = stock # a list of records is a list of objects
owned: int = len(bought) # any expression, with its type said
function afford(i: int) -> bool { return purse >= stock[i].cost }
on buy(i: int) { purse -= stock[i].cost }
}
```
`view Shop` writes `view_shop() -> UiView`. Its model is a `Value` object of every field, with a
record as an object of the fields a template can read and a list or registry as a list. Its `call`
runs a function or an event by name with arguments from the template: an int, a float, a bool or a
string.
```xml
<ui>
<import src="kit.xml" as="kit"/>
<screen name="shop" gap="4">
<state pick="{-1}"/>
<text size="24">The purse: ${purse}</text>
<each in="{stock}" as="item" index="i">
<kit:Line item="{item}" picked="{pick == i}" on-chose="set pick = i">
<button enabled="{afford(i)}" on-press="buy(i); emit chose">Buy</button>
</kit:Line>
</each>
<if test="{owned == 0}"><text>Nothing bought yet</text></if>
<else><text>{owned} bought</text></else>
</screen>
</ui>
```
- **Elements.** A template is HTML-shaped:
- `div`, `section`, `header`, `footer`, `nav`, `main`, `article`, `aside`, `ul`, `ol`, `li` and
`form` are boxes laid out in a column; `row` is one laid out in a row.
- `p`, `span`, `label`, `h1`-`h6`, `strong`, `em`, `small`, `b`, `i` and `a` are text; words
inside a box become a text of their own.
- `button`, `img src`, `hr` and `spacer`; `scroll` is a column that scrolls.
A default stylesheet, like a browser's, sizes the headings and pads the buttons.
- **Attributes.** `id`, `class` (which may be bound: `class="{picked ? 'on' : ''} row"`),
`style="padding: 4px; color: gold"`, `hidden`, `disabled`, `onclick` or `on-click` (also
`on-press`), and any attribute a property is named after (`width="300"`). Any other attribute is
kept for `[attr=value]` selectors, as HTML's are.
- **The box model and flex.** Sizes are border boxes.
- `padding` and `margin` take one to four lengths, or one side by name (`padding-left`).
- `border` is `2px solid #ffcc00`, or `border-width` and `border-color`.
- A length is `12`, `12px`, `50%`, `fit`/`auto` or `fill`.
- `flex-grow` (or `flex`) shares out the spare room along `flex-direction`, and `fill` is a share
of 1. `justify-content` takes `flex-start`/`start`, `center`, `end`, `space-between`,
`space-around` or `space-evenly`.
- `align-items` and `align-self` take `start`, `center`, `end` or `stretch`. `flex-wrap: wrap`
breaks a row into lines, and `min-`/`max-width`/`-height` bound it.
- `gap`, `text-align`, `display: none`, `background(-color)`, `color`, `opacity` and `font-size`
are CSS's.
Every property also has a short name (`w`, `h`, `pad`, `bg`, `size`, `grow`, `align`,
`justify`, `self`, `dir`, `wrap`, `alpha`). Rounded corners, font weight and a few more are
things the renderer does not draw, and the runtime says so rather than ignoring them. A colour
is the renderer's name for one, `#rrggbb` or `#rgb`.
- **Stylesheets.** Rules go in a `<style>` or in an `.lss` file (a Ludic StyleSheet: CSS-shaped,
but its own language, so no editor mistakes it for CSS):
```
@import "base.lss";
button { height: 40px }
.card p, #title { color: accent }
ul > li:nth-child(even):not(.keep) { background: bg2 }
[kind=warn] { border: 1px solid bad }
```
- Selectors: a tag, `*`, `#id`, `.class`, `[attr]` or `[attr=value]`, and the pseudo-classes
`:hover`, `:disabled`, `:enabled`, `:first-child`, `:last-child`, `:only-child`,
`:nth-child(odd|even|n)`, `:empty`, `:root` and `:not(...)`. They combine into compounds,
joined by a space (anywhere inside) or `>` (straight inside), and a list is `,`-separated.
- Specificity is CSS's: ids 100, classes, attributes and pseudo-classes 10, tags 1. The default
sheet comes first, then rules from least to most specific (the later rule on a tie), then
`style="..."`, then the element's own attributes.
- A value may hold `{holes}`, read where the element is.
- A file's cascade is its imports' rules, then its own. `<import src="look.lss"/>` brings a
stylesheet in, and one stylesheet can `@import` another, so a look is a file others can use.
A library's components keep the styles of the file they were written in.
- **Bindings.** Any attribute and any text can hold `{expressions}`. An attribute that is one
`{expression}` and nothing else keeps its type. Expressions read loop names, props, state and the
model, and support `.field`, `[index]`, arithmetic, comparisons, `and` / `or` / `not`,
`c ? a : b`, `len()`, `range()`, and the view's functions.
- **Structure.** `<if test>` with an `<else>` after it, and `<each in as index>`.
- **Components.** `<component name="Line">` is used as `<Line ...>`. Its attributes become its
props, and its content goes where it says `<slot/>`. It has its own `<state>`, kept between
frames by where it sits in the tree. It sees its props, its state and the model, and never the
names of whoever used it.
- **Events.** `on-press="buy(i); set pick = i"` sends the view an event and sets a state. `emit
chose` runs what the component's user gave as `on-chose`. The actions run once the frame is drawn,
so no event can change a frame part way through.
- **Libraries.** A component is private to its file unless it says `export="true"`. `<import
src="kit.xml"/>` brings in a file's exported components under their own names; `as="kit"` brings
them in as `<kit:Name>`. So a component library is a file of components, and two libraries never
collide. Screens belong to the program and are found by name.
- **The renderer is registered** (`ui_backend`). It must provide rectangles, text and a text's
width. It can also provide its own button (focus, keys, sound), colour names, a scale, scroll
regions and a file reader. `import "ludic.ui/screen.ludic"` gives the 2D screen's renderer
(`ui_screen_backend()`). `ui_load(path)` reads a file, and `ui_show(screen, view_shop(), x, y, w,
h)` shows a screen, answers the pointer and runs what was pressed. `ui_nodes`, `ui_place`,
`ui_hit`, `ui_press` and `ui_dump` do the same steps one at a time, for a test.
### Memory is safe unless it says `unsafe` ### Memory is safe unless it says `unsafe`
The typed buffers are slices: `words(n)`, `floats(n)`, `fixeds(n)`, `doubles(n)` and The typed buffers are slices: `words(n)`, `floats(n)`, `fixeds(n)`, `doubles(n)` and

View file

@ -0,0 +1,37 @@
bump: minor
type: feature
**Default parameters, views and templates.** A parameter can have a default (`pad: float = 8.0`).
A call leaves out what it does not change, and may pass its first arguments by position and the
rest by name.
A `view` declaration is the one bridge between a program and its UI. It names the fields a
template may read, the functions it may ask and the `on` events it may send, and it writes
`view_<name>() -> UiView`.
`ludic.ui` is now a template runtime. Screens and components are XML files loaded at run time,
with:
- `{expression}` bindings;
- `<if>`, `<else>` and `<each>`;
- props, `<slot/>` and per-instance `<state>`;
- `on-press` actions that send events, `set` state or `emit` to the component's user;
- component libraries (`export="true"`, `<import src as>`);
- HTML's elements (`div`, `p`, `h1`-`h6`, `ul`/`li`, `img`, `hr`, ...), with a default stylesheet;
- HTML's attributes: `id`, `class`, `style`, `hidden`, `disabled` and `onclick`, with any other
attribute kept for selectors;
- the CSS box model (padding and margin in 1-4 values, borders, `px` and `%`) and flex layout
(`flex-grow`, `justify-content`, `align-items`/`align-self`, `flex-wrap`, min and max sizes)
under CSS's property names;
- stylesheets, in a `<style>` or an `.lss` file (a Ludic StyleSheet) that others import and that
can `@import` more;
- CSS's selectors: `#id`, compound classes, `[attr=value]`, descendant and `>` combinators,
`:hover`, `:disabled`, `:first-child`, `:last-child`, `:nth-child`, `:not` and more, weighed by
specificity.
Screens are drawn through a registered renderer. `Value` gains a float kind.
Also:
- A program's function named like one of the runtime's is refused; it had been silently taking the
runtime's own calls.
- An index is evaluated before the slice's elements are read. A `xs[f()]` whose `f` grew `xs`
read stale memory.
- A runtime error names the file its expression is in, not the program's.

View file

@ -0,0 +1,14 @@
---
id: value-as_float
name: Value.as_float
category: value
kind: namespace-method
tokens: Value.as_float
sig: Value.as_float(value) -> float
tip: Read a number node as a float.
order: 37
ns: Value
member: as_float
---
Returns the number any number node carries as a float: a <code>float</code> as it is, an <code>int</code> widened, a <code>fixed</code> converted from Q16.16. Other nodes return <code>0.0</code>.

View file

@ -11,4 +11,4 @@ ns: Value
member: as_int member: as_int
--- ---
Returns the raw scalar an int/fixed/bool node carries (a <code>fixed</code> comes back as its Q16.16 bits). Non-scalar nodes return <code>0</code>. Returns the raw scalar an int/fixed/bool node carries, or a float node's whole part (a <code>fixed</code> comes back as its Q16.16 bits). Non-scalar nodes return <code>0</code>.

View file

@ -0,0 +1,14 @@
---
id: value-float
name: Value.float
category: value
kind: namespace-method
tokens: Value.float
sig: Value.float(x: float) -> value
tip: Wrap a float in a value node.
order: 36
ns: Value
member: float
---
Builds a <code>float</code> node (kind <code>7</code>) carrying <code>x</code>. Read it back with <a href="value-as_float"><code>Value.as_float</code></a>; <a href="value-as_int"><code>Value.as_int</code></a> gives its whole part. <code>Json.encode</code> writes it as a number.

View file

@ -0,0 +1,18 @@
# defaults.ludic — L11: a parameter can have a default, and a call leaves out what it does not
# change - from the end when positional, or by not naming it. The leading arguments can be
# positional and the rest named, which is how a component is called: its subject, then its props.
#
# Running it prints: a 300.0 8.0 / b 120.0 8.0 / c! 300.0 8.0 / d 300.0 2.0 / x / y
program Defaults {
numbers float
const WIDE: float = 300.0
function box(label: string, w: float = WIDE, pad: float = 8.0, bold: bool = false) -> string {
var b = ""
if bold { b = "!" }
return `{label}{b} {w} {pad}`
}
function first<T>(xs: []T, at: int = 0) -> T { return xs[at] }
entry {
print(`{box("a")} / {box("b", 120.0)} / {box("c", bold: true)} / {box(pad: 2.0, label: "d")} / {first(["x", "y"])} / {first(["x", "y"], 1)}`)
}
}

View file

@ -0,0 +1,25 @@
# ui_flex.ludic — ludic.ui's flex layout and stylesheets without a renderer: grow shares the spare
# room, justify spreads it, a row wraps, align stretches; a stylesheet file (which @imports another)
# styles whoever imports it, a more specific rule wins, a bound class switches one on, and an attribute beats all.
import "ludic.ui"
program UiFlex {
numbers float
var picked: int = 1
view Tags {
picked = picked
}
const BASE: string = "/* base.lss - every text small */\ntext { size: 10 }\n"
const THEME: string = "/* theme.lss - the base, and a colour for what is on */\n@import \"base.lss\";\n.on { color: #ff0000 }\n"
const PAGE: string = "<ui>\n<import src='theme.lss'/>\n<style>\nrow.bar { gap: 10; pad: 5 }\ntext.big { size: {picked * 20} }\n</style>\n<screen name='flex' gap='8'>\n <row class='bar' w='fill'>\n <text grow='1'>a</text><text grow='3'>b</text>\n </row>\n <row w='fill' justify='between'><text>l</text><text>m</text><text>r</text></row>\n <row w='200' wrap='true' gap='4'>\n <each in='{range(6)}' as='i'><text w='60' class='{i == picked ? \"on\" : \"\"}'>t{i}</text></each>\n </row>\n <row h='40' align='stretch' w='fill'><col w='30' bg='#00ff00'/><text self='end' class='big' size='12'>x</text></row>\n</screen>\n</ui>\n"
entry {
ui_load_text(BASE, "ui/base.lss")
ui_load_text(THEME, "ui/theme.lss")
ui_load_text(PAGE, "ui/flex.xml")
let root: UiNode = ui_nodes("flex", view_tags())
ui_place(root, 0.0, 0.0, 400.0, 300.0)
let bar = root.children[0]
let wrap = root.children[2]
let tall = root.children[3]
print(`grow {int(bar.children[0].cw)}:{int(bar.children[1].cw)}; between at {int(root.children[1].children[1].x)}; wrap lines at {int(wrap.children[0].y)}, {int(wrap.children[3].y)}; red {wrap.children[1].fg}; stretched {int(tall.children[0].ch)}; own size {int(tall.children[1].rsize)}`)
}
}

View file

@ -0,0 +1,28 @@
# ui_html.ludic — ludic.ui read as HTML and CSS: HTML's elements with a browser's defaults, ids,
# classes, style="...", hidden and disabled, onclick; selectors with ids, descendants, children,
# attributes and pseudo-classes, weighed by specificity; CSS's own property names and the box model.
import "ludic.ui"
program UiHtml {
numbers float
var clicks: int = 0
view Page {
clicks = clicks
on clicked() { clicks += 1 }
}
const LOOK: string = "/* look.lss */\n.card { padding: 4 8 12 16; margin: 3; border: 2px solid #0000ff; width: 50% }\n#title { font-size: 40 }\nh1.big { font-size: 20 }\n.card p { color: #00ff00 }\nul > li { height: 30 }\nli:first-child { color: #ff0000 }\nli:nth-child(even) { color: #0000ff }\nli:last-child:not(.keep) { display: none }\n[kind=warn] { background-color: #ffff00 }\nbutton:disabled { opacity: 0.5 }\n"
const PAGE: string = "<ui>\n<import src='look.lss'/>\n<screen name='page'>\n <h1 id='title' class='big'>Title</h1>\n <div class='card'><p>inside</p><span kind='warn'>careful</span></div>\n <p>outside</p>\n <ul><li>one</li><li>two</li><li>three</li><li>gone</li></ul>\n <hr/>\n <p style='text-align: center; width: 200px'>mid</p>\n <button disabled='{clicks > 0}' onclick='clicked()'>Go</button>\n <p hidden='true'>never</p>\n</screen>\n</ui>\n"
entry {
ui_load_text(LOOK, "ui/look.lss")
ui_load_text(PAGE, "ui/page.ludic.xml")
var root: UiNode = ui_nodes("page", view_page())
ui_place(root, 0.0, 0.0, 400.0, 600.0)
let card = root.children[1]
let items = root.children[3].children
print(`id beats class: {int(root.children[0].rsize)}; card {int(card.cw)} wide at {int(card.x)},{int(card.y)} pad {int(card.pt)} {int(card.pr)} {int(card.pb)} {int(card.pl)} border {int(card.border)}`)
print(`inside {card.children[0].fg}, outside {root.children[2].fg}; warn bg {card.children[1].bg}; items {len(items)} of 30: {int(items[0].ch)}; colours {items[0].fg} {items[1].fg} {items[2].fg}; hr {int(root.children[4].ch)}; children {len(root.children)}`)
ui_press(root.children[6])
root = ui_nodes("page", view_page())
ui_place(root, 0.0, 0.0, 400.0, 600.0)
print(`clicks {clicks}; the button now disabled {not root.children[6].enabled} at opacity {root.children[6].alpha}; text-align {root.children[5].talign}`)
}
}

View file

@ -0,0 +1,48 @@
# ui_templates.ludic — ludic.ui without a renderer: a shop written as a template, reading a view
# of the program's state and answering with its events. A press is made as a pointer would make
# it; it runs the view's event once the frame is built, and a component's own state keeps. The
# row comes from a component library, whose unexported Price no other file can use.
import "ludic.ui"
program UiTemplates {
numbers float
property Item {
name: string = ""
cost: int = 0
}
var stock: []Item = new []Item
var purse: int = 100
var bought: []int = new []int
function add(name: string, cost: int) -> void {
let it = new Item
it.name = name
it.cost = cost
push(stock, it)
}
view Shop {
purse = purse
stock = stock
owned: int = len(bought)
function afford(i: int) -> bool { return purse >= stock[i].cost }
on buy(i: int) {
purse -= stock[i].cost
push(bought, i)
}
}
const KIT: string = "<ui>\n<component name='Price'><text>${cost}</text></component>\n<component name='Line' export='true'>\n <state picks='{0}'/>\n <row gap='8' w='fill' align='center'>\n <text color='{picked ? \"#ffcc00\" : \"#eeeeee\"}'>{item.name} -</text>\n <Price cost='{item.cost}'/>\n <spacer w='fill'/>\n <text>{picks}</text>\n <button enabled='{afford(i)}' on-press='buy(i); set picks = picks + 1; emit chose'>Buy</button>\n </row>\n</component>\n</ui>\n"
const PAGE: string = "<ui>\n<import src='kit.xml' as='kit'/>\n<screen name='shop' gap='4' pad='10'>\n <state pick='{-1}'/>\n <text size='24'>The purse: ${purse}</text>\n <each in='{stock}' as='item' index='i'>\n <kit:Line item='{item}' i='{i}' picked='{pick == i}' on-chose='set pick = i'/>\n </each>\n <if test='{owned == 0}'><text>Nothing bought yet</text></if>\n <else><text>{owned} bought, the last {pick + 1}</text></else>\n</screen>\n</ui>\n"
const PRIVATE: string = "<ui><import src='kit.xml' as='kit'/><screen name='peek'><kit:Price cost='{1}'/></screen></ui>"
entry {
add("Rope", 30)
add("Lantern", 80)
ui_load_text(KIT, "ui/kit.xml")
ui_load_text(PAGE, "ui/shop.xml")
ui_load_text(PRIVATE, "ui/peek.xml")
var root: UiNode = ui_nodes("shop", view_shop())
ui_place(root, 0.0, 0.0, 600.0, 400.0)
let press = root.children[1].children[4]
ui_press(ui_hit(root, press.x + 1.0, press.y + 1.0))
root = ui_nodes("shop", view_shop())
ui_place(root, 0.0, 0.0, 600.0, 400.0)
print(`purse {purse}, bought {len(bought)}; the lantern is enabled: {root.children[2].children[4].enabled}`)
}
}

View file

@ -0,0 +1,5 @@
# L11: a call leaves out only what has a default
program DefaultMissing {
function f(a: int, b: int = 2) -> int { return a + b }
entry { print(f(b: 3)) }
}

View file

@ -0,0 +1,5 @@
# L11: a named argument names a parameter the function has
program DefaultUnknown {
function f(a: int, b: int = 2) -> int { return a + b }
entry { print(f(1, c: 3)) }
}

View file

@ -0,0 +1,5 @@
# L11: positional arguments come first, then named ones
program PositionalAfterNamed {
function f(a: int, b: int = 2) -> int { return a + b }
entry { print(f(a: 1, 2)) }
}

View file

@ -0,0 +1,9 @@
# a view field that is not a bare global has to say its type
import "ludic.ui"
program ViewUntyped {
var a: int = 1
view Sums {
total = a + 2
}
entry { print(view_sums().name) }
}

View file

@ -0,0 +1,8 @@
# a view is for templates, which ludic.ui shows
program ViewWithoutUi {
var a: int = 1
view Sums {
a = a
}
entry { print(a) }
}

View file

@ -0,0 +1,62 @@
# act.ludic - what a press does: `buy(i)` asks the view, `set pick = i` changes a state, and
# `emit picked` runs what the component's user said with on-picked. Several are `;` apart.
const A_CALL: int = 0
const A_SET: int = 1
const A_EMIT: int = 2
export property UiAct {
kinds: []int = null
names: []string = null
exprs: []UiExpr = null
}
function act_parse(src: string, err: []string) -> UiAct {
let a = new UiAct
a.kinds = new []int
a.names = new []string
a.exprs = new []UiExpr
let r = new UiRd
r.s = src
while r.err == "" {
rd_ws(r)
if r.i >= len(r.s) { break }
if rd_word(r, "set") {
push(a.kinds, A_SET)
push(a.names, rd_name(r))
if not rd_op(r, "=") { rd_fail(r, "'='") }
push(a.exprs, px_cond(r))
} else if rd_word(r, "emit") {
push(a.kinds, A_EMIT)
push(a.names, rd_name(r))
push(a.exprs, null)
} else {
push(a.kinds, A_CALL)
push(a.names, "")
push(a.exprs, px_cond(r))
}
if not rd_op(r, ";") { break }
}
rd_ws(r)
if r.err == "" and r.i < len(r.s) { rd_fail(r, "';' or the end") }
if r.err != "" { push(err, r.err) }
return a
}
function act_run(a: UiAct, e: UiEnv) -> void {
for i in 0 .. len(a.kinds) {
if a.kinds[i] == A_SET { env_set(e, a.names[i], ui_eval(a.exprs[i], e)) }
if a.kinds[i] == A_CALL { ui_eval(a.exprs[i], e) }
if a.kinds[i] == A_EMIT { act_emit(a.names[i], e) }
}
}
function act_emit(name: string, e: UiEnv) -> void {
var at = e
while at != null {
if at.on_names != null {
for i in 0 .. len(at.on_names) {
if at.on_names[i] == name {
act_run(at.on_acts[i], at.on_envs[i])
return
}
}
}
at = at.parent
}
}

View file

@ -0,0 +1,58 @@
# attr.ludic - what a property sets, by its CSS name or the short one (width or w, background or
# bg, font-size or size, ...). An attribute on an element is the same property, applied last.
const AT_ALIAS: string = " width=w height=h min-width=min-w max-width=max-w min-height=min-h max-height=max-h padding=pad background=bg background-color=bg font-size=size opacity=alpha flex-grow=grow flex-direction=dir flex-wrap=wrap justify-content=justify align-items=align align-self=self row-gap=gap column-gap=gap text-align=talign "
const AT_UNDRAWN: string = " border-radius font-weight font-family font-style box-shadow text-shadow cursor transition line-height letter-spacing text-decoration overflow z-index position "
# a stylesheet's property: one that means nothing is said
function el_attr(n: UiNode, key: string, v: Val) -> void {
if el_apply(n, key, v) { return }
if lk_in(AT_UNDRAWN, at_alias(key)) { ui_err(`{key}: this renderer does not draw it`) } else { ui_err(`{key}: there is no such property`) }
}
# an element's attribute: a property when it names one, otherwise only there for [selectors]
function el_apply(n: UiNode, key: string, v: Val) -> bool {
let k = at_alias(key)
return el_size(n, k, v) or el_box(n, k, v) or el_flex(n, k, v) or el_look(n, k, v)
}
function at_alias(key: string) -> string {
let pat = ` {key}=`
let n = len(pat)
for i in 0 .. len(AT_ALIAS) - n + 1 {
if AT_ALIAS[i..i + n] == pat {
var j = i + n
while AT_ALIAS[j] != 32 { j += 1 }
return AT_ALIAS[i + n..j]
}
}
return key
}
function el_size(n: UiNode, key: string, v: Val) -> bool {
if key == "w" {
n.w_pct = el_pct(v)
n.w = el_len(v)
} else if key == "h" {
n.h_pct = el_pct(v)
n.h = el_len(v)
} else if key == "min-w" { n.min_w = el_len(v) } else if key == "max-w" { n.max_w = el_len(v) } else if key == "min-h" { n.min_h = el_len(v) } else if key == "max-h" { n.max_h = el_len(v) } else if key == "gap" { n.gap = el_len(v) } else { return false }
return true
}
function el_flex(n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
if key == "dir" {
n.dir = UI_COL
if s == "row" { n.dir = UI_ROW }
} else if key == "grow" or key == "flex" { n.grow = el_num(s) } else if key == "wrap" { n.wrap = s == "wrap" or (vl_truthy(v) and s != "false" and s != "nowrap") } else if key == "align" { n.align = el_place(v) } else if key == "self" { n.self = el_place(v) } else if key == "justify" { n.justify = el_place(v) } else if key == "talign" { n.talign = el_place(v) } else if key == "display" { n.hidden = s == "none" } else { return false }
return true
}
function el_look(n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
if key == "size" { n.size = el_len(v) } else if key == "color" { n.fg = ui_color(s) } else if key == "bg" { n.bg = ui_color(s) } else if key == "alpha" { n.alpha = el_num(s) } else if key == "enabled" { n.enabled = vl_truthy(v) and s != "false" } else if key == "border-color" { n.border_c = ui_color(s) } else if key == "border-width" { n.border = el_len(v) } else if key == "border" { el_border(n, s) } else { return false }
return true
}
# border: 1 #ffcc00, or 1px solid accent
function el_border(n: UiNode, s: string) -> void {
let parts = lss_split(s, 32)
for i in 0 .. len(parts) {
let p = parts[i]
if len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { n.border = ui_px(el_num(p)) } else if p == "none" { n.border = 0.0 } else if len(p) > 0 and p != "solid" { n.border_c = ui_color(p) }
}
if n.border_c == UI_NONE { n.border_c = n.rfg }
}

View file

@ -0,0 +1,81 @@
# attr_box.ludic - padding and margin (one to four lengths, as CSS writes them, or one side by
# name), lengths with px or %, and the words that place things
function el_box(n: UiNode, key: string, v: Val) -> bool {
if key == "pad" {
el_sides(v)
n.pt = el_s0
n.pr = el_s1
n.pb = el_s2
n.pl = el_s3
} else if key == "margin" {
el_sides(v)
n.mt = el_s0
n.mr = el_s1
n.mb = el_s2
n.ml = el_s3
} else if key == "padding-top" { n.pt = el_len(v) } else if key == "padding-right" { n.pr = el_len(v) } else if key == "padding-bottom" { n.pb = el_len(v) } else if key == "padding-left" { n.pl = el_len(v) } else if key == "margin-top" { n.mt = el_len(v) } else if key == "margin-right" { n.mr = el_len(v) } else if key == "margin-bottom" { n.mb = el_len(v) } else if key == "margin-left" { n.ml = el_len(v) } else { return false }
return true
}
var el_s0: float = 0.0
var el_s1: float = 0.0
var el_s2: float = 0.0
var el_s3: float = 0.0
# top right bottom left out of 1 to 4 lengths
function el_sides(v: Val) -> void {
let parts = new []float
if vl_num(v) { push(parts, ui_px(Value.as_float(v))) } else {
let words = lss_split(vl_text(v), 32)
for i in 0 .. len(words) {
if len(words[i]) > 0 { push(parts, ui_px(el_num(words[i]))) }
}
}
while len(parts) < 4 {
if len(parts) == 0 { push(parts, 0.0) } else if len(parts) == 1 { push(parts, parts[0]) } else if len(parts) == 2 { push(parts, parts[0]) } else { push(parts, parts[1]) }
}
el_s0 = parts[0]
el_s1 = parts[1]
el_s2 = parts[2]
el_s3 = parts[3]
}
# a length: a number, 10px, fill, fit or auto (a percentage is el_pct's)
function el_len(v: Val) -> float {
if vl_num(v) { return ui_px(Value.as_float(v)) }
let s = vl_text(v)
if s == "fill" { return UI_FILL }
if s == "fit" or s == "auto" or (len(s) > 0 and s[len(s) - 1] == 37) { return 0.0 }
return ui_px(el_num(s))
}
function el_pct(v: Val) -> float {
let s = vl_text(v)
if len(s) > 1 and s[len(s) - 1] == 37 { return el_num(s) }
return 0.0
}
# the number at the front of `12.5px`
function el_num(s: string) -> float {
var f = 0.0
var unit = 0.0
var neg = false
for i in 0 .. len(s) {
let c = s[i]
if c == 45 and i == 0 { neg = true } else if c == 46 { unit = 0.1 } else if c >= 48 and c <= 57 {
if unit > 0.0 {
f = f + float(c - 48) * unit
unit = unit * 0.1
} else { f = f * 10.0 + float(c - 48) }
} else { break }
}
if neg { return 0.0 - f }
return f
}
# start, center, end, stretch; for justify also between, around, evenly - with CSS's spellings
function el_place(v: Val) -> int {
var s = vl_text(v)
if len(s) > 6 and s[0..6] == "space-" { s = s[6..len(s)] }
if len(s) > 5 and s[0..5] == "flex-" { s = s[5..len(s)] }
if s == "center" { return UI_CENTER }
if s == "end" or s == "right" { return UI_END }
if s == "stretch" or s == "between" { return 3 }
if s == "around" { return UI_AROUND }
if s == "evenly" { return UI_EVENLY }
return UI_START
}

View file

@ -0,0 +1,26 @@
# backend.ludic - the renderer seam
# the renderer. Only rect, text and measure are needed; the rest improve on a default:
# button draws, focuses and answers a press itself; color names a colour (else #rrggbb);
# image draws an <img src>; px scales a template's lengths; scroll_begin answers how far a region is scrolled; read
# reads a template file (else Fs.read_text)
export property UiBackend {
rect: fn(float, float, float, float, int, float) -> void = null
text: fn(float, float, string, float, int) -> void = null
measure: fn(string, float) -> float = null
pointer_x: fn() -> float = null
pointer_y: fn() -> float = null
pointer_down: fn() -> bool = null
button: fn(float, float, float, float, string, bool) -> bool = null
image: fn(string, float, float, float, float, int, float) -> void = null # src x y w h tint alpha
color: fn(string) -> int = null
px: fn(float) -> float = null
scroll_begin: fn(string, float, float, float, float, float) -> float = null
scroll_end: fn() -> void = null
read: fn(string) -> string = null
}
var ui_be: UiBackend = null
export function ui_backend(b: UiBackend) -> void { ui_be = b }
function ui_px(v: float) -> float {
if ui_be != null and ui_be.px != null { return ui_be.px(v) }
return v
}

View file

@ -0,0 +1,68 @@
# build.ludic - a template and the model, made into this frame's nodes. <if>/<else> and
# <each in= as= index=> are the template's own; a tag its file linked to a component is that one,
# given its attributes as props, its on-* as what `emit` runs, and its content as <slot/>.
function bd_kids(kids: []UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
var last_if = true
for i in 0 .. len(kids) {
let k = kids[i]
let ck = `{key}/{i}`
if k.tag == "state" { continue }
if k.tag == "if" {
last_if = bd_test(k, e)
if last_if { bd_kids(k.kids, e, ck, out) }
} else if k.tag == "else" {
if not last_if { bd_kids(k.kids, e, ck, out) }
last_if = true
} else if k.tag == "each" { bd_each(k, e, ck, out) } else if k.tag == "slot" { bd_slot(e, ck, out) } else if not bd_comp(k, e, ck, out) { push(out, bd_elem(k, e, ck)) }
}
}
function bd_test(k: UiTpl, e: UiEnv) -> bool {
let t = tpl_attr(k, "test")
if t == null {
ui_err("<if> needs a test=\"{...}\"")
return false
}
return ui_eval_bool(t, e)
}
function bd_name(k: UiTpl, attr: string, dflt: string) -> string {
let x = tpl_attr(k, attr)
if x == null or x.op != X_LIT { return dflt }
return Value.as_str(x.v)
}
function bd_each(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
let src = tpl_attr(k, "in")
if src == null {
ui_err("<each> needs an in=\"{...}\"")
return
}
let list = ui_eval(src, e)
let as_name = bd_name(k, "as", "item")
let at_name = bd_name(k, "index", "index")
for j in 0 .. Value.count(list) {
let le = env_new(e)
env_bind(le, as_name, Value.at(list, j))
env_bind(le, at_name, Value.int(j))
bd_kids(k.kids, le, `{key}#{j}`, out)
}
}
function bd_slot(e: UiEnv, key: string, out: []UiNode) -> void {
var at = e
while at != null and at.slot == null { at = at.parent }
if at != null { bd_kids(at.slot.kids, at.slot_env, key, out) }
}
function bd_comp(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> bool {
if k.comp == null { return false }
let c = k.comp
let ce = env_new(null)
ce.model = e.model
for i in 0 .. len(k.keys) { env_bind(ce, k.keys[i], ui_eval(k.vals[i], e)) }
ce.on_names = k.on_keys
ce.on_acts = k.on_acts
ce.on_envs = new []UiEnv
for i in 0 .. len(k.on_keys) { push(ce.on_envs, e) }
ce.slot = k
ce.slot_env = e
ce.state = st_for(key, c, ce)
bd_kids(c.kids, ce, key, out)
return true
}

View file

@ -0,0 +1,44 @@
# cascade.ludic - once the frame's tree is built, every element is styled as a browser would:
# the default sheet, then the file's rules from least to most specific, then its style="...", then
# its own attributes. Then what is display: none leaves the tree.
function cs_tree(n: UiNode) -> void {
for i in 0 .. len(n.children) {
let c = n.children[i]
c.parent = n
c.index = i
}
if n.tag != "" { cs_node(n) }
for i in 0 .. len(n.children) { cs_tree(n.children[i]) }
var shown = new []UiNode
for i in 0 .. len(n.children) {
if not n.children[i].hidden { push(shown, n.children[i]) }
}
n.children = shown
}
function cs_node(n: UiNode) -> void {
cs_sheet(n, ua_sheet())
if n.sheet != null { cs_sheet(n, n.sheet) }
if n.style != null { cs_rule(n, n.style) }
for i in 0 .. len(n.akeys) { el_apply(n, n.akeys[i], n.vals[i]) }
}
function cs_sheet(n: UiNode, sh: UiSheet) -> void {
for i in 0 .. len(sh.rules) {
if sel_match(sh.rules[i].sel, n) { cs_rule(n, sh.rules[i]) }
}
}
function cs_rule(n: UiNode, r: UiRule) -> void {
for j in 0 .. len(r.keys) { el_attr(n, r.keys[j], ui_eval(r.vals[j], n.env)) }
}
# what every element looks like before anyone styles it, as a browser's own sheet does
const UA_LSS: string = "h1 { font-size: 32 } h2 { font-size: 26 } h3 { font-size: 22 } h4 { font-size: 19 } h5 { font-size: 16 } h6 { font-size: 14 } small { font-size: 13 } button { padding: 5 10 } hr { height: 1; width: fill; background: #808080; margin: 4 0 } scroll { width: fill; height: fill } img { width: 24; height: 24 }"
var ua_sh: UiSheet = null
function ua_sheet() -> UiSheet {
if ua_sh != null { return ua_sh }
let f = new UiFile
f.path = "the default sheet"
f.imp = new []UiFile
f.own = new []UiRule
lss_text(f, UA_LSS)
ua_sh = sheet_of(f)
return ua_sh
}

View file

@ -0,0 +1,66 @@
# draw.ludic - the laid-out nodes through the renderer: background, border, then content
function fr_draw(n: UiNode) -> void {
if n.kind == UI_BUTTON {
fr_button(n)
return
}
if n.bg >= 0 and ui_be.rect != null { ui_be.rect(n.x, n.y, n.cw, n.ch, n.bg, n.alpha) }
fr_border(n)
if n.kind == UI_TEXT { ui_be.text(fr_text_x(n), n.y + n.pt + n.border, n.text, n.rsize, n.rfg) }
if n.kind == UI_IMAGE and ui_be.image != null { ui_be.image(n.src, n.x + n.pl + n.border, n.y + n.pt + n.border, n.cw - ly_hin(n), n.ch - ly_vin(n), n.rfg, n.alpha) }
var off = 0.0
if n.kind == UI_SCROLL and ui_be.scroll_begin != null { off = ui_be.scroll_begin(n.key, n.x, n.y, n.cw, n.ch, n.mh) }
if off != 0.0 {
for i in 0 .. len(n.children) { fr_shift(n.children[i], off) }
}
if n.children != null {
for i in 0 .. len(n.children) { fr_draw(n.children[i]) }
}
if n.kind == UI_SCROLL and ui_be.scroll_end != null { ui_be.scroll_end() }
}
# where a text starts: text-align within the room the box gives it
function fr_text_x(n: UiNode) -> float {
let x = n.x + n.pl + n.border
if n.talign == UI_START { return x }
let spare = n.cw - ly_hin(n) - ly_text_w(n.text, n.rsize)
if n.talign == UI_CENTER { return x + spare / 2.0 }
return x + spare
}
function fr_border(n: UiNode) -> void {
if n.border <= 0.0 or ui_be.rect == null { return }
var c = n.border_c
if c < 0 { c = n.rfg }
let b = n.border
ui_be.rect(n.x, n.y, n.cw, b, c, n.alpha)
ui_be.rect(n.x, n.y + n.ch - b, n.cw, b, c, n.alpha)
ui_be.rect(n.x, n.y, b, n.ch, c, n.alpha)
ui_be.rect(n.x + n.cw - b, n.y, b, n.ch, c, n.alpha)
}
# the renderer's own button when it has one: it draws, focuses and answers the press
function fr_button(n: UiNode) -> void {
if ui_be.button != null {
if ui_be.button(n.x, n.y, n.cw, n.ch, n.text, n.enabled) and n.enabled { push(fr_pressed, n) }
return
}
var bg = n.bg
if bg < 0 { bg = 4210752 }
if ui_be.rect != null { ui_be.rect(n.x, n.y, n.cw, n.ch, bg, n.alpha) }
fr_border(n)
ui_be.text(fr_text_x(n), n.y + n.pt + n.border, n.text, n.rsize, n.rfg)
}
function fr_shift(n: UiNode, off: float) -> void {
n.y = n.y - off
if n.children != null {
for i in 0 .. len(n.children) { fr_shift(n.children[i], off) }
}
}
# every node and where it went, one line each - for a test, or a look at a layout
export function ui_dump(n: UiNode, depth: int) -> string {
var pad = ""
for d in 0 .. depth { pad = pad + " " }
var out = `{pad}{n.kind} {int(n.x)},{int(n.y)} {int(n.cw)}x{int(n.ch)} {n.text}\n`
if n.children != null {
for i in 0 .. len(n.children) { out = out + ui_dump(n.children[i], depth + 1) }
}
return out
}

View file

@ -0,0 +1,77 @@
# elem.ludic - an element made from its template: what kind of node its tag is, its id, classes
# and attributes (kept for selectors, applied after every rule), its text, and its children
const EL_BOXES: string = " col div section header footer nav main article aside ul ol li form box "
const EL_TEXTS: string = " text span p label h1 h2 h3 h4 h5 h6 strong em small b i a "
function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode {
let n = new UiNode
n.key = key
n.tag = k.tag
n.env = e
n.sheet = k.sheet
n.style = k.style
n.children = new []UiNode
n.classes = new []string
n.akeys = new []string
n.avals = new []string
n.vals = new []Val
el_kind(n, k.tag)
for i in 0 .. len(k.keys) { el_keep(n, k.keys[i], ui_eval(k.vals[i], e)) }
n.hovered = fr_was_hovered(key)
for i in 0 .. len(k.on_keys) {
if k.on_keys[i] == "press" {
n.act = k.on_acts[i]
n.env = e
} else { ui_err(`on-{k.on_keys[i]}: an element answers on-press (or onclick); a component's other events are its user's`) }
}
bd_kids(k.kids, e, key, n.children)
if k.text != null { el_text(n, ui_eval_text(k.text, e)) }
return n
}
function el_kind(n: UiNode, tag: string) -> void {
if lk_in(EL_TEXTS, tag) { n.kind = UI_TEXT } else if tag == "row" { n.dir = UI_ROW } else if tag == "button" { n.kind = UI_BUTTON } else if tag == "spacer" { n.kind = UI_SPACER } else if tag == "scroll" { n.kind = UI_SCROLL } else if tag == "img" { n.kind = UI_IMAGE } else if tag == "hr" { n.kind = UI_RULE } else if not lk_in(EL_BOXES, tag) { ui_err(`<{tag}>: there is no such element`) }
}
# a box with words in it holds them as a text of its own, as <div>words</div> does
function el_text(n: UiNode, s: string) -> void {
if n.kind != UI_BOX and n.kind != UI_SCROLL {
n.text = s
return
}
let t = new UiNode
t.kind = UI_TEXT
t.text = s
t.env = n.env
t.children = new []UiNode
push(n.children, t)
}
# id, class, hidden, disabled and src say what the element is; the rest are applied after the rules
function el_keep(n: UiNode, key: string, v: Val) -> void {
let s = vl_text(v)
if key == "id" { n.id = s } else if key == "class" {
let parts = lss_split(s, 32)
for i in 0 .. len(parts) {
if len(parts[i]) > 0 { push(n.classes, parts[i]) }
}
} else if key == "hidden" { n.hidden = vl_truthy(v) and s != "false" } else if key == "disabled" { n.enabled = not (vl_truthy(v) and s != "false") } else if key == "src" { n.src = s } else {
if key == "enabled" { n.enabled = vl_truthy(v) and s != "false" }
push(n.akeys, key)
push(n.avals, s)
push(n.vals, v)
}
}
# a colour by the renderer's name for it, else #rrggbb (or #rgb)
function ui_color(name: string) -> int {
if name == "none" or name == "" or name == "transparent" { return UI_NONE }
if name[0] != 35 {
if ui_be != null and ui_be.color != null { return ui_be.color(name) }
return 15658734
}
var c = 0
for i in 1 .. len(name) {
var d = name[i] - 48
if name[i] >= 97 { d = name[i] - 87 }
if name[i] >= 65 and name[i] < 97 { d = name[i] - 55 }
c = c * 16 + d
if len(name) == 4 { c = c * 16 + d }
}
return c
}

View file

@ -0,0 +1,65 @@
# env.ludic - what a name means where an expression is read: the loop's own names, then the
# component's props and state, then the view's model. A component sees its props, its state
# and the model - never the names of whoever used it.
export property UiEnv {
names: []string = null
vals: []Val = null
state: Val = null
parent: UiEnv = null
model: Val = null
slot: UiTpl = null
slot_env: UiEnv = null
on_names: []string = null # a component's on-* attributes, which its `emit` runs
on_acts: []UiAct = null
on_envs: []UiEnv = null
}
var ui_view: UiView = null
var ui_errs: []string = new []string
function env_new(parent: UiEnv) -> UiEnv {
let e = new UiEnv
e.names = new []string
e.vals = new []Val
e.parent = parent
if parent != null { e.model = parent.model }
return e
}
function env_bind(e: UiEnv, name: string, v: Val) -> void {
push(e.names, name)
push(e.vals, v)
}
function env_get(e: UiEnv, name: string) -> Val {
var at = e
while at != null {
var i = len(at.names) - 1
while i >= 0 {
if at.names[i] == name { return at.vals[i] }
i -= 1
}
if at.state != null and Value.has(at.state, name) != 0 { return Value.get(at.state, name) }
at = at.parent
}
if e.model != null and Value.has(e.model, name) != 0 { return Value.get(e.model, name) }
ui_err(`no {name} here: not a loop's, a prop, a state or in the view`)
return Value.null()
}
# `set name = value`: the nearest state that holds the name
function env_set(e: UiEnv, name: string, v: Val) -> void {
var at = e
while at != null {
if at.state != null and Value.has(at.state, name) != 0 {
Value.put(at.state, name, v)
return
}
at = at.parent
}
ui_err(`set {name}: no state here holds it - declare it with <state {name}="..."/>`)
}
# an error is said once, however many frames make it
function ui_err(msg: string) -> void {
for i in 0 .. len(ui_errs) {
if ui_errs[i] == msg { return }
}
push(ui_errs, msg)
print(`ui: {msg}`)
}

View file

@ -0,0 +1,52 @@
# eval.ludic - an expression's value, here
function ui_eval(x: UiExpr, e: UiEnv) -> Val {
if x.op == X_LIT { return x.v }
if x.op == X_NAME { return env_get(e, x.s) }
if x.op == X_MEMBER {
let o = ui_eval(x.a, e)
if Value.kind(o) == 6 and Value.has(o, x.s) != 0 { return Value.get(o, x.s) }
if Value.kind(o) == 5 and x.s == "length" { return Value.int(Value.count(o)) }
ui_err(`.{x.s}: there is no such field`)
return Value.null()
}
if x.op == X_INDEX { return Value.at(ui_eval(x.a, e), Value.as_int(ui_eval(x.b, e))) }
if x.op == X_CALL { return ev_call(x, e) }
if x.op == X_NOT { return vl_bool(not vl_truthy(ui_eval(x.a, e))) }
if x.op == X_NEG { return vl_arith("-", Value.int(0), ui_eval(x.a, e)) }
if x.op == X_AND {
if not vl_truthy(ui_eval(x.a, e)) { return vl_bool(false) }
return vl_bool(vl_truthy(ui_eval(x.b, e)))
}
if x.op == X_OR {
if vl_truthy(ui_eval(x.a, e)) { return vl_bool(true) }
return vl_bool(vl_truthy(ui_eval(x.b, e)))
}
if x.op == X_COND {
if vl_truthy(ui_eval(x.a, e)) { return ui_eval(x.b, e) }
return ui_eval(x.c, e)
}
if x.op == X_TEXT {
var out = ""
for i in 0 .. len(x.args) { out = out + vl_text(ui_eval(x.args[i], e)) }
return Value.str(out)
}
return vl_binary(x.s, ui_eval(x.a, e), ui_eval(x.b, e))
}
# a call: len() and range() are the template's own, anything else is the view's
function ev_call(x: UiExpr, e: UiEnv) -> Val {
let args = Value.list()
for i in 0 .. len(x.args) { Value.add(args, ui_eval(x.args[i], e)) }
if x.s == "len" and len(x.args) == 1 { return Value.int(Value.count(Value.at(args, 0))) }
if x.s == "range" and len(x.args) == 1 {
let out = Value.list()
for i in 0 .. Value.as_int(Value.at(args, 0)) { Value.add(out, Value.int(i)) }
return out
}
if ui_view == null or ui_view.call == null {
ui_err(`{x.s}(): no view is showing to ask`)
return Value.null()
}
return ui_view.call(x.s, args)
}
function ui_eval_text(x: UiExpr, e: UiEnv) -> string { return vl_text(ui_eval(x, e)) }
function ui_eval_bool(x: UiExpr, e: UiEnv) -> bool { return vl_truthy(ui_eval(x, e)) }

View file

@ -0,0 +1,82 @@
# expr.ludic - a template's expressions: `{pick == i}`, `{f.cost}`, `{placed(i) > 0 ? 'out' : ''}`.
# Read once when the template loads, evaluated against the view's model every frame.
const X_LIT: int = 0
const X_NAME: int = 1
const X_MEMBER: int = 2
const X_INDEX: int = 3
const X_CALL: int = 4
const X_NOT: int = 5
const X_NEG: int = 6
const X_BIN: int = 7
const X_AND: int = 8
const X_OR: int = 9
const X_COND: int = 10
const X_TEXT: int = 11
export property UiExpr {
op: int = 0
s: string = ""
v: Val = null
a: UiExpr = null
b: UiExpr = null
c: UiExpr = null
args: []UiExpr = null
}
# the text being read, and where the reading is
property UiRd {
s: string = ""
i: int = 0
err: string = ""
}
function ex_new(op: int) -> UiExpr {
let e = new UiExpr
e.op = op
e.args = new []UiExpr
return e
}
function ex_lit(v: Val) -> UiExpr {
let e = ex_new(X_LIT)
e.v = v
return e
}
function rd_ws(r: UiRd) -> void {
while r.i < len(r.s) and (r.s[r.i] == 32 or r.s[r.i] == 9 or r.s[r.i] == 10 or r.s[r.i] == 13) { r.i += 1 }
}
function rd_c(r: UiRd, k: int) -> int {
if r.i + k < len(r.s) { return r.s[r.i + k] }
return 0
}
# the operator `op` is next: take it
function rd_op(r: UiRd, op: string) -> bool {
rd_ws(r)
let n = len(op)
if r.i + n > len(r.s) { return false }
if r.s[r.i..r.i + n] != op { return false }
r.i += n
return true
}
function rd_is_word(c: int) -> bool { return (c >= 97 and c <= 122) or (c >= 65 and c <= 90) or c == 95 or (c >= 48 and c <= 57) }
# the word `w` is next, whole: take it
function rd_word(r: UiRd, w: string) -> bool {
rd_ws(r)
let at = r.i
if not rd_op(r, w) { return false }
if rd_is_word(rd_c(r, 0)) {
r.i = at
return false
}
return true
}
function rd_name(r: UiRd) -> string {
rd_ws(r)
let at = r.i
while r.i < len(r.s) and rd_is_word(r.s[r.i]) { r.i += 1 }
if r.i == at {
rd_fail(r, "a name")
return ""
}
return r.s[at..r.i]
}
function rd_fail(r: UiRd, want: string) -> void {
if r.err == "" { r.err = `expected {want} at {r.i + 1} in "{r.s}"` }
}

View file

@ -0,0 +1,96 @@
# expr_parse.ludic - a condition, or/and/not, a comparison, sums and products, then a name
# with its members, indexes and calls
function px_cond(r: UiRd) -> UiExpr {
let a = px_or(r)
if not rd_op(r, "?") { return a }
let e = ex_new(X_COND)
e.a = a
e.b = px_cond(r)
if not rd_op(r, ":") { rd_fail(r, "':'") }
e.c = px_cond(r)
return e
}
function px_or(r: UiRd) -> UiExpr {
var a = px_and(r)
while rd_word(r, "or") or rd_op(r, "||") {
let e = ex_new(X_OR)
e.a = a
e.b = px_and(r)
a = e
}
return a
}
function px_and(r: UiRd) -> UiExpr {
var a = px_not(r)
while rd_word(r, "and") or rd_op(r, "&&") {
let e = ex_new(X_AND)
e.a = a
e.b = px_not(r)
a = e
}
return a
}
function px_not(r: UiRd) -> UiExpr {
rd_ws(r)
if rd_word(r, "not") or (rd_c(r, 0) == 33 and rd_c(r, 1) != 61 and rd_op(r, "!")) {
let e = ex_new(X_NOT)
e.a = px_not(r)
return e
}
return px_cmp(r)
}
function px_bin(op: string, a: UiExpr, b: UiExpr) -> UiExpr {
let e = ex_new(X_BIN)
e.s = op
e.a = a
e.b = b
return e
}
function px_cmp(r: UiRd) -> UiExpr {
let a = px_sum(r)
let ops = ["==", "!=", "<=", ">=", "<", ">"]
for k in 0 .. len(ops) {
if rd_op(r, ops[k]) { return px_bin(ops[k], a, px_sum(r)) }
}
return a
}
function px_sum(r: UiRd) -> UiExpr {
var a = px_prod(r)
while true {
if rd_op(r, "+") { a = px_bin("+", a, px_prod(r)) } else if rd_op(r, "-") { a = px_bin("-", a, px_prod(r)) } else { return a }
}
return a
}
function px_prod(r: UiRd) -> UiExpr {
var a = px_unary(r)
while true {
if rd_op(r, "*") { a = px_bin("*", a, px_unary(r)) } else if rd_op(r, "/") { a = px_bin("/", a, px_unary(r)) } else if rd_op(r, "%") { a = px_bin("%", a, px_unary(r)) } else { return a }
}
return a
}
function px_unary(r: UiRd) -> UiExpr {
if rd_op(r, "-") {
let e = ex_new(X_NEG)
e.a = px_unary(r)
return e
}
return px_post(r)
}
function px_post(r: UiRd) -> UiExpr {
var a = px_primary(r)
while r.err == "" {
if rd_op(r, ".") {
let e = ex_new(X_MEMBER)
e.a = a
e.s = rd_name(r)
a = e
} else if rd_op(r, "[") {
let e = ex_new(X_INDEX)
e.a = a
e.b = px_cond(r)
if not rd_op(r, "]") { rd_fail(r, "']'") }
a = e
} else { return a }
}
return a
}

View file

@ -0,0 +1,88 @@
# expr_prim.ludic - a number, a quoted string, true/false/null, a name or a call, a bracket
function px_primary(r: UiRd) -> UiExpr {
rd_ws(r)
let c = rd_c(r, 0)
if c >= 48 and c <= 57 { return px_number(r) }
if c == 39 or c == 34 { return px_string(r, c) }
if rd_op(r, "(") {
let e = px_cond(r)
if not rd_op(r, ")") { rd_fail(r, "')'") }
return e
}
if rd_word(r, "true") { return ex_lit(Value.bool(1)) }
if rd_word(r, "false") { return ex_lit(Value.bool(0)) }
if rd_word(r, "null") { return ex_lit(Value.null()) }
let name = rd_name(r)
if not rd_op(r, "(") {
let e = ex_new(X_NAME)
e.s = name
return e
}
let e = ex_new(X_CALL)
e.s = name
if rd_op(r, ")") { return e }
while r.err == "" {
push(e.args, px_cond(r))
if rd_op(r, ")") { return e }
if not rd_op(r, ",") { rd_fail(r, "',' or ')'") }
}
return e
}
function px_number(r: UiRd) -> UiExpr {
let at = r.i
var whole = 0
while r.i < len(r.s) and r.s[r.i] >= 48 and r.s[r.i] <= 57 {
whole = whole * 10 + (r.s[r.i] - 48)
r.i += 1
}
if rd_c(r, 0) != 46 or rd_c(r, 1) < 48 or rd_c(r, 1) > 57 { return ex_lit(Value.int(whole)) }
r.i += 1
var f = float(whole)
var unit = 0.1
while r.i < len(r.s) and r.s[r.i] >= 48 and r.s[r.i] <= 57 {
f = f + float(r.s[r.i] - 48) * unit
unit = unit * 0.1
r.i += 1
}
return ex_lit(Value.float(f))
}
function px_string(r: UiRd, q: int) -> UiExpr {
r.i += 1
let at = r.i
while r.i < len(r.s) and r.s[r.i] != q { r.i += 1 }
let s: string = r.s[at..r.i]
if r.i >= len(r.s) { rd_fail(r, "the string's closing quote") } else { r.i += 1 }
return ex_lit(Value.str(s))
}
# one expression, the whole of `src`
function ex_parse(src: string, err: []string) -> UiExpr {
let r = new UiRd
r.s = src
let e = px_cond(r)
rd_ws(r)
if r.err == "" and r.i < len(r.s) { rd_fail(r, "the end") }
if r.err != "" { push(err, r.err) }
return e
}
# text with {holes}: a literal when it has none, the expression itself when it is one hole and
# nothing else (so it keeps its type), otherwise the pieces joined as text
function ex_text(src: string, err: []string) -> UiExpr {
let e = ex_new(X_TEXT)
var at = 0
var i = 0
while i < len(src) {
if src[i] == 123 {
var j = i + 1
while j < len(src) and src[j] != 125 { j += 1 }
if i > at { push(e.args, ex_lit(Value.str(src[at..i]))) }
push(e.args, ex_parse(src[i + 1..j], err))
at = j + 1
i = j
}
i += 1
}
if at < len(src) { push(e.args, ex_lit(Value.str(src[at..len(src)]))) }
if len(e.args) == 0 { return ex_lit(Value.str("")) }
if len(e.args) == 1 { return e.args[0] }
return e
}

View file

@ -0,0 +1,95 @@
# frame.ludic - a screen shown: built from the view's model, laid out, drawn, and its presses
# run once the frame is drawn, so nothing the view does can change a frame half way through it
var fr_was_down: bool = false
var fr_pressed: []UiNode = new []UiNode
export function ui_show(screen: string, view: UiView, x: float, y: float, w: float, h: float) -> void {
let root = ui_nodes(screen, view)
if root == null { return }
ui_place(root, x, y, w, h)
fr_pressed = new []UiNode
fr_pointer(root)
if ui_be != null and ui_be.text != null { fr_draw(root) }
let pressed = fr_pressed
for i in 0 .. len(pressed) { ui_press(pressed[i]) }
}
# the screen's nodes for this frame, not yet laid out - for a test, or a host that places them
export function ui_nodes(screen: string, view: UiView) -> UiNode {
let si = lib_find(lib_screen_names, screen)
if si < 0 {
ui_err(`there is no screen {screen} - ui_load the file that has it`)
return null
}
let t = lib_screens[si]
ui_view = view
let e = env_new(null)
if view != null and view.model != null { e.model = view.model() } else { e.model = Value.object() }
e.state = st_for(screen, t, e)
let root = new UiNode
root.key = screen
root.children = new []UiNode
root.env = e
root.sheet = t.sheet
root.classes = new []string
root.akeys = new []string
root.avals = new []string
root.vals = new []Val
for i in 0 .. len(t.keys) {
if t.keys[i] != "name" { el_keep(root, t.keys[i], ui_eval(t.vals[i], e)) }
}
for i in 0 .. len(root.akeys) { el_apply(root, root.akeys[i], root.vals[i]) }
bd_kids(t.kids, e, screen, root.children)
cs_tree(root)
return root
}
export function ui_place(root: UiNode, x: float, y: float, w: float, h: float) -> void {
root.w = UI_FILL
root.h = UI_FILL
var fg = 15658734
if ui_be != null and ui_be.color != null { fg = ui_be.color("text") }
ly_measure(root, w, ui_px(16.0), fg)
ly_place(root, x, y, w, h)
}
# a press, as if the pointer had made it
export function ui_press(n: UiNode) -> void {
if n.act != null and n.enabled { act_run(n.act, n.env) }
}
# the pointer: what it is over is :hover next frame (the element and all around it, as in CSS),
# and without a renderer's own button a press on the deepest pressable thing is a press
var fr_hover: []string = new []string
function fr_was_hovered(key: string) -> bool { return sel_has(fr_hover, key) }
function fr_pointer(root: UiNode) -> void {
if ui_be == null or ui_be.pointer_x == null {
fr_hover = new []string
return
}
let px = ui_be.pointer_x()
let py = ui_be.pointer_y()
let keys = new []string
fr_under(root, px, py, keys)
fr_hover = keys
if ui_be.button != null or ui_be.pointer_down == null { return }
let hit = fr_hit(root, px, py)
let down = ui_be.pointer_down()
if hit != null and down and not fr_was_down { push(fr_pressed, hit) }
fr_was_down = down
}
function fr_under(n: UiNode, px: float, py: float, keys: []string) -> void {
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return }
if n.key != "" { push(keys, n.key) }
for i in 0 .. len(n.children) { fr_under(n.children[i], px, py, keys) }
}
function fr_hit(n: UiNode, px: float, py: float) -> UiNode {
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return null }
if n.children != null {
var i = len(n.children) - 1
while i >= 0 {
let h = fr_hit(n.children[i], px, py)
if h != null { return h }
i -= 1
}
}
if n.act != null { return n }
return null
}
export function ui_hit(root: UiNode, px: float, py: float) -> UiNode { return fr_hit(root, px, py) }

View file

@ -0,0 +1,33 @@
# ludic.ui - templates: screens and components written as markup in files the program loads at
# run time, reading only what a `view` declaration lets them, and answering with its events
module ui
numbers float
import "node.ludic"
import "backend.ludic"
import "expr.ludic"
import "expr_parse.ludic"
import "expr_prim.ludic"
import "values.ludic"
import "env.ludic"
import "eval.ludic"
import "act.ludic"
import "tpl.ludic"
import "lib.ludic"
import "lib_util.ludic"
import "link.ludic"
import "state.ludic"
import "build.ludic"
import "elem.ludic"
import "attr.ludic"
import "attr_box.ludic"
import "style.ludic"
import "style_rule.ludic"
import "select.ludic"
import "select_parts.ludic"
import "select_match.ludic"
import "cascade.ludic"
import "layout.ludic"
import "layout_run.ludic"
import "layout_wrap.ludic"
import "frame.ludic"
import "draw.ludic"

View file

@ -0,0 +1,96 @@
# layout.ludic - flex layout on the CSS box model, top-down: a box is told how wide it may be,
# measures its content at that width (so a wrapping row knows how tall it is), then places each
# child. A size is the border box: padding and border inside it, margin outside it.
function ly_measure(n: UiNode, avail: float, size: float, fg: int) -> void {
n.rsize = size
if n.size > 0.0 { n.rsize = n.size }
n.rfg = fg
if n.fg != UI_INHERIT { n.rfg = n.fg }
n.mw = ly_hin(n)
n.mh = ly_vin(n)
if n.kind == UI_TEXT or n.kind == UI_BUTTON {
n.mw = n.mw + ly_text_w(n.text, n.rsize)
n.mh = n.mh + n.rsize * 1.25
}
if n.kind == UI_IMAGE {
n.mw = n.mw + n.rsize
n.mh = n.mh + n.rsize
}
var fixed_w = n.w
if n.w_pct > 0.0 { fixed_w = avail * n.w_pct / 100.0 }
if (n.kind == UI_BOX or n.kind == UI_SCROLL) and n.children != null {
var inner = avail - n.ml - n.mr
if fixed_w > 0.0 { inner = fixed_w }
inner = ly_clamp(inner, n.min_w, n.max_w) - ly_hin(n)
if n.dir == UI_ROW and n.wrap { ly_measure_wrap(n, inner) } else { ly_measure_box(n, inner) }
}
if fixed_w > 0.0 { n.mw = fixed_w }
if n.h > 0.0 { n.mh = n.h }
n.mw = ly_clamp(n.mw, n.min_w, n.max_w)
n.mh = ly_clamp(n.mh, n.min_h, n.max_h)
}
function ly_hin(n: UiNode) -> float { return n.pl + n.pr + n.border * 2.0 }
function ly_vin(n: UiNode) -> float { return n.pt + n.pb + n.border * 2.0 }
function ly_clamp(v: float, lo: float, hi: float) -> float {
var out = v
if hi > 0.0 and out > hi { out = hi }
if out < lo { out = lo }
return out
}
function ly_measure_box(n: UiNode, inner: float) -> void {
var main = 0.0
var cross = 0.0
for i in 0 .. len(n.children) {
let c = n.children[i]
ly_measure(c, inner, n.rsize, n.rfg)
main = main + ly_main(c, n.dir)
if i > 0 { main = main + n.gap }
cross = Math.max(cross, ly_cross(c, n.dir))
}
if n.dir == UI_ROW {
n.mw = n.mw + main
n.mh = n.mh + cross
} else {
n.mw = n.mw + cross
n.mh = n.mh + main
}
}
# a wrapping row: children in lines no wider than `inner`, the lines stacked `gap` apart
function ly_measure_wrap(n: UiNode, inner: float) -> void {
var line_w = 0.0
var line_h = 0.0
var widest = 0.0
var total = 0.0
var count = 0
for i in 0 .. len(n.children) {
let c = n.children[i]
ly_measure(c, inner, n.rsize, n.rfg)
let cw = ly_main(c, UI_ROW)
if count > 0 and line_w + n.gap + cw > inner {
widest = Math.max(widest, line_w)
total = total + line_h + n.gap
line_w = 0.0
line_h = 0.0
count = 0
}
if count > 0 { line_w = line_w + n.gap }
line_w = line_w + cw
line_h = Math.max(line_h, ly_cross(c, UI_ROW))
count += 1
}
n.mw = n.mw + Math.max(widest, line_w)
n.mh = n.mh + total + line_h
}
function ly_text_w(s: string, size: float) -> float {
if ui_be != null and ui_be.measure != null { return ui_be.measure(s, size) }
return float(len(s)) * size * 0.5
}
# a child's size along and across a direction, margins included
function ly_main(c: UiNode, dir: int) -> float {
if dir == UI_ROW { return c.mw + c.ml + c.mr }
return c.mh + c.mt + c.mb
}
function ly_cross(c: UiNode, dir: int) -> float {
if dir == UI_ROW { return c.mh + c.mt + c.mb }
return c.mw + c.ml + c.mr
}

View file

@ -0,0 +1,99 @@
# layout_run.ludic - a box's children given their places: along its direction by size, grow and
# justify, across it by align (or each child's own `self`), a wrapping row one line at a time
var ly_lead: float = 0.0
var ly_extra: float = 0.0
function ly_place(n: UiNode, x: float, y: float, w: float, h: float) -> void {
n.x = x
n.y = y
n.cw = w
n.ch = h
if (n.kind != UI_BOX and n.kind != UI_SCROLL) or n.children == null { return }
let inner_w = w - ly_hin(n)
var inner_h = h - ly_vin(n)
if n.kind == UI_SCROLL { inner_h = Math.max(n.mh, h) - ly_vin(n) }
let top = n.y + n.pt + n.border
if n.dir == UI_ROW and n.wrap {
ly_place_wrap(n, top, inner_w)
return
}
if n.dir == UI_ROW { ly_place_line(n, 0, len(n.children), top, inner_w, inner_h) } else { ly_place_line(n, 0, len(n.children), top, inner_h, inner_w) }
}
function ly_fills(c: UiNode, dir: int) -> bool {
if dir == UI_ROW { return c.w == UI_FILL }
return c.h == UI_FILL
}
function ly_grow(c: UiNode, dir: int) -> float {
if ly_fills(c, dir) { return Math.max(c.grow, 1.0) }
return c.grow
}
# where a child starts along the run: `fill` from nothing, a percentage from the room it is in
function ly_basis(c: UiNode, dir: int, room: float) -> float {
if ly_fills(c, dir) { return 0.0 }
if dir == UI_ROW and c.w_pct > 0.0 { return room * c.w_pct / 100.0 + c.ml + c.mr }
if dir == UI_COL and c.h_pct > 0.0 { return room * c.h_pct / 100.0 + c.mt + c.mb }
return ly_main(c, dir)
}
function ly_stretches(n: UiNode, c: UiNode) -> bool {
if ly_fills(c, 1 - n.dir) { return true }
var a = n.align
if c.self >= 0 { a = c.self }
return a == UI_STRETCH
}
# children from..to along one line starting at `top`: sizes, the spare room, then each place
function ly_place_line(n: UiNode, from: int, to: int, top: float, main_room: float, cross_room: float) -> void {
var used = 0.0
var grows = 0.0
for i in from .. to {
used = used + ly_basis(n.children[i], n.dir, main_room)
grows = grows + ly_grow(n.children[i], n.dir)
}
used = used + n.gap * float(Math.max(to - from - 1, 0))
let free = main_room - used
ly_justify(n, free, to - from, grows)
let extra = ly_extra
var at = ly_lead
for i in from .. to {
let c = n.children[i]
var m = ly_basis(c, n.dir, main_room)
if grows > 0.0 and free > 0.0 { m = m + free * ly_grow(c, n.dir) / grows }
ly_place_child(n, c, at, top, m, cross_room)
at = at + m + n.gap + extra
}
}
# one child: `m` along the run and its own size (or the whole room, stretched) across, less margins
function ly_place_child(n: UiNode, c: UiNode, at: float, top: float, m: float, cross_room: float) -> void {
var cr = ly_cross(c, n.dir)
if n.dir == UI_ROW and c.h_pct > 0.0 { cr = cross_room * c.h_pct / 100.0 + c.mt + c.mb }
if n.dir == UI_COL and c.w_pct > 0.0 { cr = cross_room * c.w_pct / 100.0 + c.ml + c.mr }
if ly_stretches(n, c) { cr = cross_room }
var a = n.align
if c.self >= 0 { a = c.self }
var off = 0.0
if a == UI_CENTER { off = (cross_room - cr) / 2.0 }
if a == UI_END { off = cross_room - cr }
let left = n.x + n.pl + n.border
if n.dir == UI_ROW {
let cw = ly_clamp(m - c.ml - c.mr, c.min_w, c.max_w)
ly_place(c, left + at + c.ml, top + off + c.mt, cw, ly_clamp(cr - c.mt - c.mb, c.min_h, c.max_h))
} else {
let chh = ly_clamp(m - c.mt - c.mb, c.min_h, c.max_h)
ly_place(c, left + off + c.ml, top + at + c.mt, ly_clamp(cr - c.ml - c.mr, c.min_w, c.max_w), chh)
}
}
# where the first child starts and what goes between, when nothing grows into the spare room
function ly_justify(n: UiNode, free: float, count: int, grows: float) -> void {
ly_lead = 0.0
ly_extra = 0.0
if grows > 0.0 or free <= 0.0 or count == 0 { return }
if n.justify == UI_CENTER { ly_lead = free / 2.0 }
if n.justify == UI_END { ly_lead = free }
if n.justify == UI_BETWEEN and count > 1 { ly_extra = free / float(count - 1) }
if n.justify == UI_AROUND {
ly_extra = free / float(count)
ly_lead = ly_extra / 2.0
}
if n.justify == UI_EVENLY {
ly_extra = free / float(count + 1)
ly_lead = ly_extra
}
}

View file

@ -0,0 +1,19 @@
# layout_wrap.ludic - a wrapping row, line by line: each line is as tall as its tallest child
function ly_place_wrap(n: UiNode, top0: float, inner_w: float) -> void {
var top = top0
var from = 0
while from < len(n.children) {
var to = from
var line_w = 0.0
var line_h = 0.0
while to < len(n.children) and (to == from or line_w + n.gap + ly_main(n.children[to], UI_ROW) <= inner_w) {
if to > from { line_w = line_w + n.gap }
line_w = line_w + ly_main(n.children[to], UI_ROW)
line_h = Math.max(line_h, ly_cross(n.children[to], UI_ROW))
to += 1
}
ly_place_line(n, from, to, top, inner_w, line_h)
top = top + line_h + n.gap
from = to
}
}

View file

@ -0,0 +1,78 @@
# lib.ludic - template files. A file's <component>s are its own unless marked export="true";
# <import src="kit.xml"/> brings a file's exported components in by their names, and
# <import src="kit.xml" as="kit"/> as <kit:Name>, so a component library is a file of them and two
# libraries never collide. A file's <style>s, and its imports' before them, style what it writes. A <screen> is the program's, by name, from whichever file has it.
export property UiFile {
path: string = ""
names: []string = null
comps: []UiTpl = null
exported: []bool = null
imp_as: []string = null
imp: []UiFile = null
own: []UiRule = null
sheet: UiSheet = null
}
var lib_files: []UiFile = new []UiFile
var lib_screen_names: []string = new []string
var lib_screens: []UiTpl = new []UiTpl
# the file a path names, read once
export function ui_load(path: string) -> bool { return lib_file(path) != null }
function lib_file(path: string) -> UiFile {
for i in 0 .. len(lib_files) {
if lib_files[i].path == path { return lib_files[i] }
}
let src = lib_text(path)
if len(src) == 0 {
ui_err(`{path}: cannot read it`)
return null
}
if lss_is(path) { return lss_file(src, path) }
return lib_read(src, path)
}
# a file's text, as if read from `path` (its imports are found beside it)
export function ui_load_text(src: string, path: string) -> void {
if lss_is(path) { lss_file(src, path) } else { lib_read(src, path) }
}
function lib_read(src: string, path: string) -> UiFile {
let f = lib_new(path)
let root = tpl_read(Xml.parse(src), path)
var tops = root.kids
if root.tag != "ui" { tops = [root] }
for i in 0 .. len(tops) { lib_top(f, tops[i]) }
f.sheet = sheet_of(f)
for i in 0 .. len(f.comps) { lib_link(f, f.comps[i]) }
for i in 0 .. len(tops) {
if tops[i].tag == "screen" { lib_link(f, tops[i]) }
}
return f
}
function lib_top(f: UiFile, t: UiTpl) -> void {
if t.tag == "style" {
sheet_read(f, t)
return
}
if t.tag == "import" {
let other = lib_file(lib_beside(f.path, bd_name(t, "src", "")))
if other != null {
push(f.imp, other)
push(f.imp_as, bd_name(t, "as", ""))
}
return
}
let name = bd_name(t, "name", "")
if name == "" or (t.tag != "component" and t.tag != "screen") {
ui_err(`{f.path}: <{t.tag}> - a file holds <import>s, <style>s, <component name=...>s and <screen name=...>s`)
return
}
if t.tag == "screen" {
let at = lib_find(lib_screen_names, name)
if at >= 0 { lib_screens[at] = t } else {
push(lib_screen_names, name)
push(lib_screens, t)
}
return
}
push(f.names, name)
push(f.comps, t)
push(f.exported, bd_name(t, "export", "") == "true")
}

View file

@ -0,0 +1,34 @@
# lib_util.ludic - a file made, found, read, and a path beside another
function lib_find(names: []string, name: string) -> int {
for i in 0 .. len(names) {
if names[i] == name { return i }
}
return -1
}
function lib_new(path: string) -> UiFile {
let f = new UiFile
f.path = path
f.names = new []string
f.comps = new []UiTpl
f.exported = new []bool
f.imp_as = new []string
f.imp = new []UiFile
f.own = new []UiRule
push(lib_files, f)
return f
}
function lib_text(path: string) -> string {
var src = ""
if ui_be != null and ui_be.read != null { src = ui_be.read(path) } else { src = Fs.read_text(path) }
if src == null { return "" }
return src
}
# `src` read from beside the file that imports it
function lib_beside(path: string, src: string) -> string {
var cut = -1
for i in 0 .. len(path) {
if path[i] == 47 { cut = i }
}
if cut < 0 or (len(src) > 0 and src[0] == 47) { return src }
return path[0..cut + 1] + src
}

View file

@ -0,0 +1,42 @@
# link.ludic - each tag in a file that is not an element names a component: the file's own, an
# unnamed import's exported one, or `as:Name` from the import called `as`
const LK_OWN: string = " row button spacer scroll img hr if else each slot state style component screen import "
function lib_link(f: UiFile, t: UiTpl) -> void {
t.sheet = f.sheet
if not lk_element(t.tag) {
t.comp = lk_find(f, t.tag, false)
if t.comp == null and lk_find(f, t.tag, true) != null { ui_err(`{f.path}: <{t.tag}>: that component is not exported - export="true" on it lets other files use it`) } else if t.comp == null { ui_err(`{f.path}: <{t.tag}>: there is no such element, and no component of that name here or exported by an import`) }
}
for i in 0 .. len(t.kids) { lib_link(f, t.kids[i]) }
}
function lk_element(tag: string) -> bool { return lk_in(LK_OWN, tag) or lk_in(EL_BOXES, tag) or lk_in(EL_TEXTS, tag) }
# a word in a list of words written " a b c "
function lk_in(words: string, w: string) -> bool {
let pat = ` {w} `
let n = len(pat)
for i in 0 .. len(words) - n + 1 {
if words[i..i + n] == pat { return true }
}
return false
}
# `any` finds an unexported one too, to say why it cannot be used
function lk_find(f: UiFile, tag: string, any: bool) -> UiTpl {
var colon = -1
for i in 0 .. len(tag) {
if tag[i] == 58 { colon = i }
}
if colon < 0 {
let own = lib_find(f.names, tag)
if own >= 0 { return f.comps[own] }
}
for i in 0 .. len(f.imp) {
var want = tag
if colon >= 0 {
if f.imp_as[i] != tag[0..colon] { continue }
want = tag[colon + 1..len(tag)]
} else if f.imp_as[i] != "" { continue }
let at = lib_find(f.imp[i].names, want)
if at >= 0 and (any or f.imp[i].exported[at]) { return f.imp[i].comps[at] }
}
return null
}

View file

@ -0,0 +1,89 @@
# node.ludic - what a template becomes each frame, the view it reads, and the renderer seam
export const UI_BOX: int = 0
export const UI_TEXT: int = 1
export const UI_SPACER: int = 2
export const UI_BUTTON: int = 3
export const UI_SCROLL: int = 4
export const UI_IMAGE: int = 5
export const UI_RULE: int = 6 # <hr>: a line across
export const UI_COL: int = 0
export const UI_ROW: int = 1
export const UI_FILL: float = -1.0
export const UI_START: int = 0
export const UI_CENTER: int = 1
export const UI_END: int = 2
export const UI_STRETCH: int = 3 # align: the whole cross axis
export const UI_BETWEEN: int = 3 # justify: the spare room between, around or evenly
export const UI_AROUND: int = 4
export const UI_EVENLY: int = 5
export const UI_INHERIT: int = -1
export const UI_NONE: int = -2
# what a program lets its templates see: `view Name { ... }` writes view_name() -> UiView
export property UiView {
name: string = ""
model: fn() -> Val = null
call: fn(string, Val) -> Val = null
}
export property UiNode {
kind: int = 0
dir: int = 0
text: string = ""
key: string = ""
tag: string = "" # what it is called in the template, and what a selector names
id: string = ""
classes: []string = null
akeys: []string = null # its attributes, as text, for [attr=value] selectors
avals: []string = null
vals: []Val = null # the same, as values, applied after every rule
style: UiRule = null # its style="..." declarations
sheet: UiSheet = null # the stylesheets of the file it was written in
parent: UiNode = null
index: int = 0 # its place among its parent's children, for :first-child and the rest
src: string = "" # an <img>'s picture
size: float = 0.0
w: float = 0.0
h: float = 0.0
w_pct: float = 0.0 # a width or height as a percentage of the parent's room
h_pct: float = 0.0
pt: float = 0.0 # padding, top right bottom left
pr: float = 0.0
pb: float = 0.0
pl: float = 0.0
mt: float = 0.0 # margin
mr: float = 0.0
mb: float = 0.0
ml: float = 0.0
border: float = 0.0
border_c: int = -2
gap: float = 0.0
align: int = 0
justify: int = 0
talign: int = 0 # text-align
self: int = -1 # align-self; -1 takes the parent's align
grow: float = 0.0 # a share of the spare room along the parent's direction
wrap: bool = false # a row breaks into lines where it runs out of room
min_w: float = 0.0
max_w: float = 0.0 # 0 is no limit
min_h: float = 0.0
max_h: float = 0.0
hidden: bool = false # display: none, or hidden
fg: int = -1
bg: int = -2
alpha: float = 1.0
enabled: bool = true
act: UiAct = null
env: UiEnv = null
children: []UiNode = null
mw: float = 0.0
mh: float = 0.0
x: float = 0.0
y: float = 0.0
cw: float = 0.0
ch: float = 0.0
rsize: float = 0.0
rfg: int = 0
hovered: bool = false
}

View file

@ -0,0 +1,5 @@
# ludic.ui — components: a UI declared as a tree of function calls, rebuilt from the program's
# state every frame, laid out, and drawn through whatever renderer the program registers.
package "ludic.ui"
version "0.1.0"
kind source

View file

@ -0,0 +1,17 @@
# screen.ludic - the 2D screen as a renderer, for a program that draws with Screen.*
function sc_rect(x: float, y: float, w: float, h: float, c: int, a: float) -> void { Screen.fill_rectangle(int(x), int(y), int(w), int(h), c) }
function sc_text(x: float, y: float, s: string, size: float, c: int) -> void { Screen.draw_text(int(x), int(y), s, c, Math.max(1, int(size / 8.0))) }
function sc_measure(s: string, size: float) -> float { return float(Screen.measure_text(s) * Math.max(1, int(size / 8.0))) }
function sc_px() -> float { return float(Input.mouse_x()) }
function sc_py() -> float { return float(Input.mouse_y()) }
function sc_down() -> bool { return Input.mouse_down(0) }
export function ui_screen_backend() -> UiBackend {
let b = new UiBackend
b.rect = fn sc_rect
b.text = fn sc_text
b.measure = fn sc_measure
b.pointer_x = fn sc_px
b.pointer_y = fn sc_py
b.pointer_down = fn sc_down
return b
}

View file

@ -0,0 +1,70 @@
# select.ludic - CSS selectors: a compound (`button.primary#buy[type=big]:hover:not(.off)`) joined
# to the one on its left by a space (anywhere inside it) or `>` (straight inside it)
export property UiSel {
tag: string = ""
id: string = ""
classes: []string = null
akeys: []string = null
avals: []string = null # "" with aops 0 is [attr]; aops 1 is [attr=value]
aops: []int = null
pseudos: []string = null
pargs: []string = null
nots: []UiSel = null
prev: UiSel = null
comb: int = 0 # 0: prev is an ancestor, 1: prev is the parent
}
function sel_new() -> UiSel {
let s = new UiSel
s.classes = new []string
s.akeys = new []string
s.avals = new []string
s.aops = new []int
s.pseudos = new []string
s.pargs = new []string
s.nots = new []UiSel
return s
}
function sel_parse(src: string, err: []string) -> UiSel {
let r = new UiRd
r.s = src
var cur: UiSel = null
var comb = 0
while r.err == "" {
rd_ws(r)
if r.i >= len(r.s) { break }
if rd_op(r, ">") {
comb = 1
continue
}
let c = sel_compound(r)
c.prev = cur
c.comb = comb
cur = c
comb = 0
}
if r.err != "" { push(err, r.err) }
if cur == null { push(err, `"{src}" selects nothing`) }
return cur
}
function sel_is_name(c: int) -> bool { return rd_is_word(c) or c == 45 }
function sel_name(r: UiRd) -> string {
let at = r.i
while r.i < len(r.s) and sel_is_name(r.s[r.i]) { r.i += 1 }
if r.i == at { rd_fail(r, "a name") }
return r.s[at..r.i]
}
function sel_compound(r: UiRd) -> UiSel {
let s = sel_new()
if rd_c(r, 0) == 42 { r.i += 1 } else if sel_is_name(rd_c(r, 0)) { s.tag = sel_name(r) }
while r.err == "" and r.i < len(r.s) {
let c = rd_c(r, 0)
if c == 35 {
r.i += 1
s.id = sel_name(r)
} else if c == 46 {
r.i += 1
push(s.classes, sel_name(r))
} else if c == 91 { sel_attr(r, s) } else if c == 58 { sel_pseudo(r, s) } else if c == 42 { r.i += 1 } else { return s }
}
return s
}

View file

@ -0,0 +1,74 @@
# select_match.ludic - whether a selector picks a node, and how specific it is (as CSS counts)
function sel_spec(s: UiSel) -> int {
var n = 0
var at = s
while at != null {
if at.id != "" { n = n + 100 }
n = n + (len(at.classes) + len(at.akeys) + len(at.pseudos)) * 10
if at.tag != "" { n = n + 1 }
for i in 0 .. len(at.nots) { n = n + sel_spec(at.nots[i]) }
at = at.prev
}
return n
}
function sel_match(s: UiSel, n: UiNode) -> bool {
if not sel_one(s, n) { return false }
if s.prev == null { return true }
if s.comb == 1 { return n.parent != null and sel_match(s.prev, n.parent) }
var a = n.parent
while a != null {
if sel_match(s.prev, a) { return true }
a = a.parent
}
return false
}
function sel_one(s: UiSel, n: UiNode) -> bool {
if s.tag != "" and s.tag != n.tag { return false }
if s.id != "" and s.id != n.id { return false }
for i in 0 .. len(s.classes) {
if not sel_has(n.classes, s.classes[i]) { return false }
}
for i in 0 .. len(s.akeys) {
let v = sel_attr_of(n, s.akeys[i])
if v == null or (s.aops[i] == 1 and v != s.avals[i]) { return false }
}
for i in 0 .. len(s.pseudos) {
if not sel_pseudo_ok(s.pseudos[i], s.pargs[i], n) { return false }
}
for i in 0 .. len(s.nots) {
if sel_one(s.nots[i], n) { return false }
}
return true
}
function sel_has(xs: []string, x: string) -> bool {
if xs == null { return false }
for i in 0 .. len(xs) {
if xs[i] == x { return true }
}
return false
}
function sel_attr_of(n: UiNode, key: string) -> string {
if n.akeys == null { return null }
for i in 0 .. len(n.akeys) {
if n.akeys[i] == key { return n.avals[i] }
}
return null
}
function sel_pseudo_ok(p: string, arg: string, n: UiNode) -> bool {
if p == "hover" { return n.hovered }
if p == "disabled" { return not n.enabled }
if p == "enabled" { return n.enabled }
if p == "first-child" { return n.index == 0 }
if p == "last-child" { return n.parent != null and n.index == len(n.parent.children) - 1 }
if p == "only-child" { return n.parent != null and len(n.parent.children) == 1 }
if p == "empty" { return len(n.children) == 0 and n.text == "" }
if p == "root" { return n.parent == null or n.parent.tag == "" }
if p == "nth-child" {
let k = n.index + 1
if arg == "odd" { return k % 2 == 1 }
if arg == "even" { return k % 2 == 0 }
return string(k) == arg
}
ui_err(`:{p}: there is no such pseudo-class here`)
return false
}

View file

@ -0,0 +1,35 @@
# select_parts.ludic - a compound's [attribute] and :pseudo-class parts
function sel_attr(r: UiRd, s: UiSel) -> void {
r.i += 1
push(s.akeys, sel_name(r))
if not rd_op(r, "=") {
push(s.aops, 0)
push(s.avals, "")
} else {
push(s.aops, 1)
rd_ws(r)
let q = rd_c(r, 0)
if q == 34 or q == 39 { push(s.avals, Value.as_str(px_string(r, q).v)) } else { push(s.avals, sel_name(r)) }
}
if not rd_op(r, "]") { rd_fail(r, "']'") }
}
function sel_pseudo(r: UiRd, s: UiSel) -> void {
r.i += 1
let name = sel_name(r)
var arg = ""
if rd_c(r, 0) == 40 {
r.i += 1
let at = r.i
while r.i < len(r.s) and r.s[r.i] != 41 { r.i += 1 }
arg = r.s[at..r.i]
r.i += 1
}
if name == "not" {
let inner = new UiRd
inner.s = tpl_words(arg)
push(s.nots, sel_compound(inner))
return
}
push(s.pseudos, name)
push(s.pargs, tpl_words(arg))
}

View file

@ -0,0 +1,34 @@
# state.ludic - a component's or a screen's own values, <state name="{first}"/>, kept from frame
# to frame by where the instance is in the tree, and set by `set name = value`
var st_keys: []string = new []string
var st_vals: []Val = new []Val
function st_for(key: string, t: UiTpl, e: UiEnv) -> Val {
var decl: UiTpl = null
for i in 0 .. len(t.kids) {
if t.kids[i].tag == "state" { decl = t.kids[i] }
}
if decl == null { return null }
for i in 0 .. len(st_keys) {
if st_keys[i] == key { return st_vals[i] }
}
let o = Value.object()
for i in 0 .. len(decl.keys) { Value.put(o, decl.keys[i], ui_eval(decl.vals[i], e)) }
push(st_keys, key)
push(st_vals, o)
return o
}
# a screen's state starts again the next time it opens
export function ui_forget(screen: string) -> void {
let keys = new []string
let vals = new []Val
for i in 0 .. len(st_keys) {
let k = st_keys[i]
let mine = len(k) >= len(screen) and k[0..len(screen)] == screen and (len(k) == len(screen) or k[len(screen)] == 47)
if not mine {
push(keys, k)
push(vals, st_vals[i])
}
}
st_keys = keys
st_vals = vals
}

View file

@ -0,0 +1,84 @@
# style.ludic - stylesheets: `.chip { bg: bg2; pad: 6 } button { h: 40 }` in a <style>, or an .lss
# file brought in by <import src="look.lss"/> (which may `@import "base.lss";` in turn). A selector is
# a tag, a .class, both, or *; a property is any attribute, {holes} and all. The more specific rule
# wins, then the later; an attribute beats them all.
export property UiRule {
sel: UiSel = null
spec: int = 0
keys: []string = null
vals: []UiExpr = null
}
export property UiSheet {
rules: []UiRule = null
}
function sheet_read(f: UiFile, t: UiTpl) -> void {
var src = t.raw
let from = bd_name(t, "src", "")
if from != "" { src = lib_text(lib_beside(f.path, from)) }
lss_text(f, src)
}
function lss_is(path: string) -> bool { return len(path) > 4 and path[len(path) - 4..len(path)] == ".lss" }
# a stylesheet file: a file with rules and imports and nothing else
function lss_file(src: string, path: string) -> UiFile {
let f = lib_new(path)
lss_text(f, src)
f.sheet = sheet_of(f)
return f
}
function lss_text(f: UiFile, src: string) -> void {
let err = new []string
let r = new UiRd
r.s = lss_strip(src)
while r.err == "" {
rd_ws(r)
if r.i >= len(r.s) { break }
if rd_op(r, "@import") {
lss_import(f, lss_upto(r, 59))
continue
}
let sels = lss_upto(r, 123)
let body = lss_upto(r, 125)
lss_rule(f, sels, body, err)
}
for i in 0 .. len(err) { ui_err(`{f.path}: <style> {err[i]}`) }
}
# @import "base.lss"; - its rules come before this file's own
function lss_import(f: UiFile, what: string) -> void {
var p = tpl_words(what)
if len(p) > 5 and p[0..4] == "url(" { p = p[4..len(p) - 1] }
if len(p) >= 2 and (p[0] == 34 or p[0] == 39) { p = p[1..len(p) - 1] }
let other = lib_file(lib_beside(f.path, p))
if other != null {
push(f.imp, other)
push(f.imp_as, "")
}
}
# the text up to the character `c`, which is taken too; a {hole} inside a body is skipped whole
function lss_upto(r: UiRd, c: int) -> string {
let at = r.i
var depth = 0
while r.i < len(r.s) and (r.s[r.i] != c or depth > 0) {
if c == 125 and r.s[r.i] == 123 { depth += 1 }
if c == 125 and r.s[r.i] == 125 { depth -= 1 }
r.i += 1
}
let out: string = r.s[at..r.i]
if r.i < len(r.s) { r.i += 1 } else { rd_fail(r, "a closing brace") }
return out
}
function lss_strip(src: string) -> string {
var out = ""
var at = 0
var i = 0
while i + 1 < len(src) {
if src[i] == 47 and src[i + 1] == 42 {
out = out + src[at..i]
var j = i + 2
while j + 1 < len(src) and not (src[j] == 42 and src[j + 1] == 47) { j += 1 }
i = j + 2
at = i
} else { i += 1 }
}
if at < len(src) { out = out + src[at..len(src)] }
return out
}

View file

@ -0,0 +1,71 @@
# style_rule.ludic - a rule read, the cascade a file sees, and a node's rules applied to it
function lss_rule(f: UiFile, sels: string, body: string, err: []string) -> void {
let decls = lss_decls(body, err)
let each = lss_split(sels, 44)
for i in 0 .. len(each) {
let sel = tpl_words(each[i])
if len(sel) == 0 { continue }
let r = new UiRule
r.keys = decls.keys
r.vals = decls.vals
r.sel = sel_parse(sel, err)
if r.sel != null {
r.spec = sel_spec(r.sel)
push(f.own, r)
}
}
}
# `key: value; key: value`, a rule's body or a style="..." attribute
function lss_decls(body: string, err: []string) -> UiRule {
let r = new UiRule
r.keys = new []string
r.vals = new []UiExpr
let decls = lss_split(body, 59)
for i in 0 .. len(decls) {
let d = decls[i]
var colon = -1
for k in 0 .. len(d) {
if colon < 0 and d[k] == 58 { colon = k }
}
if colon < 0 {
if len(tpl_words(d)) > 0 { push(err, `"{tpl_words(d)}" is not a property: value`) }
continue
}
push(r.keys, tpl_words(d[0..colon]))
push(r.vals, tpl_value(tpl_words(d[colon + 1..len(d)]), err))
}
return r
}
function lss_split(s: string, c: int) -> []string {
let out = new []string
var at = 0
for i in 0 .. len(s) {
if s[i] == c {
push(out, s[at..i])
at = i + 1
}
}
push(out, s[at..len(s)])
return out
}
# the imports' rules, then the file's own, ordered by specificity and otherwise as written
function sheet_of(f: UiFile) -> UiSheet {
let sh = new UiSheet
sh.rules = new []UiRule
for i in 0 .. len(f.imp) {
if f.imp[i].sheet != null {
for k in 0 .. len(f.imp[i].sheet.rules) { sheet_add(sh, f.imp[i].sheet.rules[k]) }
}
}
for k in 0 .. len(f.own) { sheet_add(sh, f.own[k]) }
return sh
}
function sheet_add(sh: UiSheet, r: UiRule) -> void {
push(sh.rules, r)
var i = len(sh.rules) - 1
while i > 0 and sh.rules[i - 1].spec > r.spec {
sh.rules[i] = sh.rules[i - 1]
i -= 1
}
sh.rules[i] = r
}

View file

@ -0,0 +1,84 @@
# tpl.ludic - a template file read into what the builder walks: every attribute and every
# element's text an expression, every on-* attribute an action. A file holds <component>s and
# <screen>s under one root; a component is used as a tag of its own name anywhere after.
export property UiTpl {
tag: string = ""
keys: []string = null
vals: []UiExpr = null
on_keys: []string = null
on_acts: []UiAct = null
text: UiExpr = null
kids: []UiTpl = null
comp: UiTpl = null # the component this tag names, found when its file loads
sheet: UiSheet = null # the styles of the file it was written in
style: UiRule = null # its style="..." attribute, read as declarations
raw: string = "" # a <style>'s text, which is not a template's
}
function tpl_read(x: Xml, file: string) -> UiTpl {
let t = new UiTpl
t.tag = Xml.tag(x)
t.keys = new []string
t.vals = new []UiExpr
t.on_keys = new []string
t.on_acts = new []UiAct
t.kids = new []UiTpl
let err = new []string
for i in 0 .. len(x.akeys) {
let k: string = x.akeys[i]
let v: string = x.avals[i]
if len(k) > 3 and k[0..3] == "on-" {
push(t.on_keys, tpl_event(k[3..len(k)]))
push(t.on_acts, act_parse(v, err))
} else if k == "onclick" {
push(t.on_keys, "press")
push(t.on_acts, act_parse(v, err))
} else if k == "style" { t.style = lss_decls(v, err) } else {
push(t.keys, k)
push(t.vals, tpl_value(v, err))
}
}
for i in 0 .. Xml.child_count(x) { push(t.kids, tpl_read(Xml.child(x, i), file)) }
if t.tag == "style" {
t.raw = Xml.text(x)
return t
}
let words = tpl_words(Xml.text(x))
if len(words) > 0 and len(t.kids) == 0 { t.text = ex_text(words, err) }
for i in 0 .. len(err) { ui_err(`{file}: <{t.tag}> {err[i]}`) }
return t
}
# on-click is on-press, as a browser would have it
function tpl_event(name: string) -> string {
if name == "click" { return "press" }
return name
}
# an attribute: a plain number is a number, anything else text with {holes}
function tpl_value(v: string, err: []string) -> UiExpr {
var num = len(v) > 0
for i in 0 .. len(v) {
if not ((v[i] >= 48 and v[i] <= 57) or v[i] == 46 or (i == 0 and v[i] == 45)) { num = false }
}
if num { return ex_parse(v, err) }
return ex_text(v, err)
}
# text as a reader sees it: the ends trimmed and every run of white space one space
function tpl_words(s: string) -> string {
var out = ""
var gap = false
for i in 0 .. len(s) {
let c = s[i]
if c == 32 or c == 9 or c == 10 or c == 13 { gap = len(out) > 0 } else {
if gap { out = out + " " }
gap = false
out = out + s[i..i + 1]
}
}
return out
}
function tpl_attr(t: UiTpl, key: string) -> UiExpr {
for i in 0 .. len(t.keys) {
if t.keys[i] == key { return t.vals[i] }
}
return null
}

View file

@ -0,0 +1,70 @@
# values.ludic - arithmetic, comparison, truth and text over a template's values. A sum of two
# whole numbers stays whole; anything with a fraction in it is a float; a string in a sum joins.
function vl_bool(b: bool) -> Val {
if b { return Value.bool(1) }
return Value.bool(0)
}
function vl_num(v: Val) -> bool { return Value.kind(v) == 1 or Value.kind(v) == 2 or Value.kind(v) == 7 }
function vl_truthy(v: Val) -> bool {
let k = Value.kind(v)
if k == 0 { return false }
if k == 1 or k == 3 { return Value.as_int(v) != 0 }
if k == 2 or k == 7 { return Value.as_float(v) != 0.0 }
if k == 4 { return len(Value.as_str(v)) > 0 }
return true
}
function vl_text(v: Val) -> string {
let k = Value.kind(v)
if k == 0 { return "" }
if k == 1 { return string(Value.as_int(v)) }
if k == 3 {
if Value.as_int(v) != 0 { return "true" }
return "false"
}
if k == 4 { return Value.as_str(v) }
if k == 2 or k == 7 {
let f = Value.as_float(v)
if f == float(int(f)) { return string(int(f)) }
return string(f)
}
return Json.encode(v)
}
function vl_arith(op: string, a: Val, b: Val) -> Val {
if Value.kind(a) == 1 and Value.kind(b) == 1 {
let x = Value.as_int(a)
let y = Value.as_int(b)
if op == "+" { return Value.int(x + y) }
if op == "-" { return Value.int(x - y) }
if op == "*" { return Value.int(x * y) }
if y == 0 { return Value.int(0) }
if op == "/" { return Value.int(x / y) }
return Value.int(x % y)
}
let x = Value.as_float(a)
let y = Value.as_float(b)
if op == "+" { return Value.float(x + y) }
if op == "-" { return Value.float(x - y) }
if op == "*" { return Value.float(x * y) }
if y == 0.0 { return Value.float(0.0) }
if op == "/" { return Value.float(x / y) }
return Value.float(x - y * float(int(x / y)))
}
function vl_equal(a: Val, b: Val) -> bool {
if vl_num(a) and vl_num(b) { return Value.as_float(a) == Value.as_float(b) }
if Value.kind(a) != Value.kind(b) { return false }
if Value.kind(a) == 4 { return Value.as_str(a) == Value.as_str(b) }
if Value.kind(a) == 3 { return Value.as_int(a) == Value.as_int(b) }
return Value.kind(a) == 0
}
function vl_binary(op: string, a: Val, b: Val) -> Val {
if op == "==" { return vl_bool(vl_equal(a, b)) }
if op == "!=" { return vl_bool(not vl_equal(a, b)) }
if op == "+" and (Value.kind(a) == 4 or Value.kind(b) == 4) { return Value.str(vl_text(a) + vl_text(b)) }
if op == "+" or op == "-" or op == "*" or op == "/" or op == "%" { return vl_arith(op, a, b) }
let x = Value.as_float(a)
let y = Value.as_float(b)
if op == "<" { return vl_bool(x < y) }
if op == "<=" { return vl_bool(x <= y) }
if op == ">" { return vl_bool(x > y) }
return vl_bool(x >= y)
}

View file

@ -462,6 +462,7 @@ namespace Value {
alias null() = value_null alias null() = value_null
alias int(n) = value_int alias int(n) = value_int
alias fixed(f) = value_fixed alias fixed(f) = value_fixed
alias float(x) = value_float
alias bool(b) = value_bool alias bool(b) = value_bool
alias str(s) = value_str alias str(s) = value_str
alias list() = value_list alias list() = value_list
@ -475,6 +476,7 @@ namespace Value {
alias count(value) = value_count alias count(value) = value_count
alias kind(value) = value_kind alias kind(value) = value_kind
alias as_int(value) = value_as_int alias as_int(value) = value_as_int
alias as_float(value) = value_as_float
alias as_str(value) = value_as_str alias as_str(value) = value_as_str
} }
namespace Json { namespace Json {

View file

@ -5,7 +5,7 @@
# shape a JSON save round-trips through. A node is one of: # shape a JSON save round-trips through. A node is one of:
# #
# kind 0 null kind 1 int kind 2 fixed kind 3 bool # kind 0 null kind 1 int kind 2 fixed kind 3 bool
# kind 4 str kind 5 list kind 6 object # kind 4 str kind 5 list kind 6 object kind 7 float
# #
# ludicc splices this file when it sees Value.* / Json.* / Reflect.serialize / # ludicc splices this file when it sees Value.* / Json.* / Reflect.serialize /
# Reflect.apply (parse.ludic). Everything is plain Ludic over heap records — no # Reflect.apply (parse.ludic). Everything is plain Ludic over heap records — no
@ -39,6 +39,7 @@ function value_fixed(f: int) -> Val { let v = value_new(2); v.num = f; return v
function value_bool(b: int) -> Val { let v = value_new(3); if b != 0 { v.num = 1 }; return v } function value_bool(b: int) -> Val { let v = value_new(3); if b != 0 { v.num = 1 }; return v }
function value_str(s: pointer) -> Val { let v = value_new(4); v.txt = s; return v } function value_str(s: pointer) -> Val { let v = value_new(4); v.txt = s; return v }
function value_list() -> Val { return value_new(5) } function value_list() -> Val { return value_new(5) }
function value_float(x: float) -> Val { let v = value_new(7); v.num = float_bits(x); return v } # num = the IEEE bits
function value_object() -> Val { return value_new(6) } function value_object() -> Val { return value_new(6) }
# --- builders (return the container, so calls chain) ------------------------ # --- builders (return the container, so calls chain) ------------------------
@ -53,7 +54,16 @@ function value_put(obj: Val, key: pointer, item: Val) -> Val {
# --- accessors -------------------------------------------------------------- # --- accessors --------------------------------------------------------------
function value_kind(v: Val) -> int { return v.tag } function value_kind(v: Val) -> int { return v.tag }
function value_count(v: Val) -> int { return len(v.kids) } function value_count(v: Val) -> int { return len(v.kids) }
function value_as_int(v: Val) -> int { return v.num } function value_as_int(v: Val) -> int {
if v.tag == 7 { return int(float_from_bits(v.num)) }
return v.num
}
# a number as a float, whichever kind it was written as
function value_as_float(v: Val) -> float {
if v.tag == 7 { return float_from_bits(v.num) }
if v.tag == 2 { return float(v.num) / 65536.0 }
return float(v.num)
}
function value_as_str(v: Val) -> string { if v.txt == null { return "" }; return v.txt } function value_as_str(v: Val) -> string { if v.txt == null { return "" }; return v.txt }
function value_at(list: Val, i: int) -> Val { function value_at(list: Val, i: int) -> Val {
if i < 0 or i >= len(list.kids) { return value_null() } if i < 0 or i >= len(list.kids) { return value_null() }
@ -118,6 +128,7 @@ function json_encode(v: Val) -> string {
if v.tag == 2 { return json_fixed_str(v.num) } if v.tag == 2 { return json_fixed_str(v.num) }
if v.tag == 3 { if v.num != 0 { return "true" }; return "false" } if v.tag == 3 { if v.num != 0 { return "true" }; return "false" }
if v.tag == 4 { return json_quote(v.txt) } if v.tag == 4 { return json_quote(v.txt) }
if v.tag == 7 { return string(float_from_bits(v.num)) }
if v.tag == 5 { if v.tag == 5 {
var out = "[" var out = "["
var i = 0 var i = 0

View file

@ -50,13 +50,16 @@ function emit_index_addr(e: Node) -> pointer {
return r return r
} }
let el = slice_elem(base.ty) let el = slice_elem(base.ty)
# the index is evaluated BEFORE the elements' address is read: evaluating it may grow this
# very slice (xs[slot_of(k)] where slot_of pushes), and a push can move the elements
let ix = emit_expr(e.b)
# load the data pointer from the slice header (field 0) # load the data pointer from the slice header (field 0)
let dp = nreg() let dp = nreg()
emit(" "); emit(dp); emit(" = getelementptr inbounds %LSlice, ptr ") emit(" "); emit(dp); emit(" = getelementptr inbounds %LSlice, ptr ")
emit(base.code); emit(", i32 0, i32 0\n") emit(base.code); emit(", i32 0, i32 0\n")
let data = nreg() let data = nreg()
emit(" "); emit(data); emit(" = load ptr, ptr "); emit(dp); emit("\n") emit(" "); emit(data); emit(" = load ptr, ptr "); emit(dp); emit("\n")
let ix = emit_expr(e.b) # (evaluate index BEFORE setting # (the index was evaluated first, above, and before g_addr_ty is set
# THE INDEX IS CHECKED AGAINST THE LENGTH. A slice has carried { ptr, len, cap } since it # THE INDEX IS CHECKED AGAINST THE LENGTH. A slice has carried { ptr, len, cap } since it
# existed, and nothing ever read the len: every index was a bare getelementptr, so running # existed, and nothing ever read the len: every index was a bare getelementptr, so running
# off the end of one wrote into whatever the allocator had put next and the program died # off the end of one wrote into whatever the allocator had put next and the program died
@ -73,7 +76,7 @@ function emit_index_addr(e: Node) -> pointer {
let lbad = lbl("ixbad") let lbad = lbl("ixbad")
emit(` br i1 {okc}, label %{lok}, label %{lbad}\n`) emit(` br i1 {okc}, label %{lok}, label %{lbad}\n`)
emit(`{lbad}:\n`) emit(`{lbad}:\n`)
let bmsg = emit_str_const(`{g_src_name}:{itoa(e.line)}: index out of range: `) let bmsg = emit_str_const(`{src_name_of(e)}:{itoa(e.line)}: index out of range: `)
let bse = emit_bind(stdstream_rhs(2)) let bse = emit_bind(stdstream_rhs(2))
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {bse}, ptr @.fmt_bounds, ptr {bmsg}, i32 {ix.code}, i32 {lnv})\n`) emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {bse}, ptr @.fmt_bounds, ptr {bmsg}, i32 {ix.code}, i32 {lnv})\n`)
emit(" call void @exit(i32 1)\n unreachable\n") emit(" call void @exit(i32 1)\n unreachable\n")

View file

@ -523,7 +523,7 @@ function emit_call(e: Node) -> Val {
if (name == "self") { if nself == 0 { return val("0", "entity") }; return val(emit_bind(`load i32, ptr {self_stk[nself - 1]}`), "entity") } if (name == "self") { if nself == 0 { return val("0", "entity") }; return val(emit_bind(`load i32, ptr {self_stk[nself - 1]}`), "entity") }
if (name == "key") { return val(emit_bind("load i32, ptr @L_key"), "int") } if (name == "key") { return val(emit_bind("load i32, ptr @L_key"), "int") }
if (name == "save") { emit(" call void @L_save()\n"); return val("0", "void") } if (name == "save") { emit(" call void @L_save()\n"); return val("0", "void") }
if (name == "ui_build") { emit(" call void @ui_build()\n"); return val("0", "void") } if (name == "ui_build") and (find_fn(name) == null) { emit(" call void @ui_build()\n"); return val("0", "void") }
if (name == "load") { return val(emit_bind("call i32 @L_load()"), "bool") } if (name == "load") { return val(emit_bind("call i32 @L_load()"), "bool") }
if (name == "world_size") { return val(emit_bind("call i32 @L_world_size()"), "int") } if (name == "world_size") { return val(emit_bind("call i32 @L_world_size()"), "int") }
if (name == "world_save") { # world_save(buf) -> bytes written if (name == "world_save") { # world_save(buf) -> bytes written
@ -906,6 +906,7 @@ function emit_call(e: Node) -> Val {
cname = rtname cname = rtname
} }
vis_check(fn2, name) vis_check(fn2, name)
call_fill_defaults(e, fn2) # L11 - for code the checker does not walk
reorder_named(e, param_labels(fn2)) reorder_named(e, param_labels(fn2))
# evaluate args first (their IR is emitted before the call instruction), coercing # evaluate args first (their IR is emitted before the call instruction), coercing
# each to the parameter's declared type so an int passed for a `long` widens. # each to the parameter's declared type so an int passed for a `long` widens.

View file

@ -70,6 +70,11 @@ function emit_sized_slice(el: pointer, n: Val) -> Val {
emit(` store i32 {n.code}, ptr {slice_field(h.code, 2)}\n`) emit(` store i32 {n.code}, ptr {slice_field(h.code, 2)}\n`)
return h return h
} }
# the file a runtime error names: the one the expression is written in, not the program's own
function src_name_of(e: Node) -> pointer {
if e == null or e.file == null { return g_src_name }
return e.file
}
function emit_view(e: Node) -> Val { function emit_view(e: Node) -> Val {
let xs = emit_expr(e.kids[0]) let xs = emit_expr(e.kids[0])
if not is_slice_ty(xs.ty) { perr(`view takes a slice, and this is {xs.ty}`) } if not is_slice_ty(xs.ty) { perr(`view takes a slice, and this is {xs.ty}`) }
@ -88,7 +93,7 @@ function emit_view(e: Node) -> Val {
emit(` br i1 {ok}, label %{lok}, label %{lbad}\n`) emit(` br i1 {ok}, label %{lok}, label %{lbad}\n`)
emit(`{lbad}:\n`) emit(`{lbad}:\n`)
g_uses_bounds = true g_uses_bounds = true
let bmsg = emit_str_const(`{g_src_name}:{itoa(e.line)}: view past the end: from `) let bmsg = emit_str_const(`{src_name_of(e)}:{itoa(e.line)}: view past the end: from `)
let bse = emit_bind(stdstream_rhs(2)) let bse = emit_bind(stdstream_rhs(2))
emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {bse}, ptr @.fmt_bounds, ptr {bmsg}, i32 {endv}, i32 {ln})\n`) emit(` call i32 (ptr, ptr, ...) @fprintf(ptr {bse}, ptr @.fmt_bounds, ptr {bmsg}, i32 {endv}, i32 {ln})\n`)
emit(" call void @exit(i32 1)\n unreachable\n") emit(" call void @exit(i32 1)\n unreachable\n")

View file

@ -13,6 +13,7 @@ function ck_params(f: Node, labels: []pointer, tys: []pointer) -> void {
} }
} }
function ck_call_fn(e: Node, name: pointer, f: Node) -> pointer { function ck_call_fn(e: Node, name: pointer, f: Node) -> pointer {
call_fill_defaults(e, f)
let labels = new []pointer let labels = new []pointer
let tys = new []pointer let tys = new []pointer
ck_params(f, labels, tys) ck_params(f, labels, tys)
@ -21,6 +22,7 @@ function ck_call_fn(e: Node, name: pointer, f: Node) -> pointer {
return f.ty return f.ty
} }
function ck_call_alias(e: Node, name: pointer, al: int, f: Node) -> pointer { function ck_call_alias(e: Node, name: pointer, al: int, f: Node) -> pointer {
if g_al_labels[al] == null { call_fill_defaults(e, f) } else { call_mixed_to_named(e, ns_alias_labels(al)) }
let labels = new []pointer let labels = new []pointer
let tys = new []pointer let tys = new []pointer
ck_params(f, labels, tys) ck_params(f, labels, tys)
@ -50,11 +52,18 @@ function ck_args(e: Node, name: pointer, labels: []pointer, tys: []pointer) -> v
let vt = ck_expr(fi.a) let vt = ck_expr(fi.a)
let at = ck_label_at(labels, fi.s) let at = ck_label_at(labels, fi.s)
if at >= 0 { ck_give(tys[at], vt, fi.a, `argument {fi.s} of {name}`) } if at >= 0 { ck_give(tys[at], vt, fi.a, `argument {fi.s} of {name}`) }
if at < 0 and len(labels) > 0 { ck_err("arity", fi.a, `{name} has no parameter {fi.s}`) }
j += 1 j += 1
} }
var m = 0
while m < len(labels) {
if not ck_named_has(e, labels[m]) { ck_err("arity", e, `this call to {name} leaves out {labels[m]}, which has no default`) }
m += 1
}
return return
} }
if n != len(tys) { ck_err("arity", e, `{name} takes {itoa(len(tys))} argument(s) and this call gives {itoa(n)}`) } if n < len(tys) and len(labels) == len(tys) { ck_err("arity", e, `this call to {name} leaves out {labels[n]}, which has no default`) }
else { if n != len(tys) { ck_err("arity", e, `{name} takes {itoa(len(tys))} argument(s) and this call gives {itoa(n)}`) } }
var i = 0 var i = 0
while i < n { while i < n {
if i < len(tys) { ck_expect = tys[i] } if i < len(tys) { ck_expect = tys[i] }
@ -63,6 +72,14 @@ function ck_args(e: Node, name: pointer, labels: []pointer, tys: []pointer) -> v
i += 1 i += 1
} }
} }
function ck_named_has(e: Node, label: pointer) -> bool {
var i = 0
while i < len(e.kids) {
if (e.kids[i].s == label) { return true }
i += 1
}
return false
}
function ck_walk_args(e: Node) -> void { function ck_walk_args(e: Node) -> void {
var i = 0 var i = 0
while i < len(e.kids) { while i < len(e.kids) {

View file

@ -45,6 +45,7 @@ function gen_arg_nodes(e: Node, labels: []pointer) -> []Node {
} }
function gen_call(e: Node, name: pointer, t: Node) -> pointer { function gen_call(e: Node, name: pointer, t: Node) -> pointer {
let expect = ck_call_expect let expect = ck_call_expect
call_fill_defaults(e, t)
let tps = gen_split(t.tps) let tps = gen_split(t.tps)
let labels = new []pointer let labels = new []pointer
let ptys = new []pointer let ptys = new []pointer

View file

@ -147,10 +147,24 @@ function ck_listener(l: Node) -> void {
ck_block(l.a) ck_block(l.a)
ck_pop(m) ck_pop(m)
} }
# a program's function named like one of the runtime's takes every call the runtime makes to its
# own - the runtime's ui_measure started answering to a package's - so the name is refused
function ck_runtime_shadows() -> void {
var r = g_prog_user_end
while r < len(prog) {
let d = prog[r]
if d.kind == N_FN {
let u = ck_fn(d.s)
if u != null and u != d { ck_err("shadow", u, `{d.s} is also a function of the engine runtime ({d.file}); choose another name`) }
}
r += 1
}
}
# the pass also makes the generics real (check_gen.ludic), so it always runs # the pass also makes the generics real (check_gen.ludic), so it always runs
function check_program() -> void { function check_program() -> void {
gen_collect() gen_collect()
ck_index() ck_index()
ck_runtime_shadows()
var i = 0 var i = 0
while i < len(prog) { while i < len(prog) {
let d = prog[i] let d = prog[i]

View file

@ -64,6 +64,7 @@ function ns_alias_labels(i: int) -> []pointer {
return out return out
} }
function emit_alias_call(i: int, e: Node) -> Val { 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)) reorder_named(e, ns_alias_labels(i))
let id = node(E_ID) let id = node(E_ID)
id.s = g_al_target[i] id.s = g_al_target[i]

View 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
}

View file

@ -577,7 +577,9 @@ function parse_fn() -> Node {
eat_op("(") eat_op("(")
if is_reserved_word(n.s) { perr(`'{n.s}' is a reserved word and cannot name a function`) } if is_reserved_word(n.s) { perr(`'{n.s}' is a reserved word and cannot name a function`) }
while not is_op(")") { 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 } if is_op(",") { pi += 1 }
} }
eat_op(")") eat_op(")")
@ -1014,6 +1016,7 @@ function parse_one_decl() -> void {
if is_id("namespace") { parse_namespace(); return } # #76 namespace block 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("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("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("var") { push(prog, parse_var()); return }
if is_id("const") { push(prog, parse_const()); return } if is_id("const") { push(prog, parse_const()); return }
var is_unsafe = false var is_unsafe = false
@ -1510,4 +1513,5 @@ function parse_program() -> void {
parse_one_decl() parse_one_decl()
} }
registries_finish() # L8: each registry gets its defs registries_finish() # L8: each registry gets its defs
views_finish() # L11: each view gets its model and call
} }

View 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
}

View 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
}

View 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 ""
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,10 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/frontend/registry.ludic") push(f, "selfhost/frontend/registry.ludic")
push(f, "selfhost/frontend/registry_finish.ludic") push(f, "selfhost/frontend/registry_finish.ludic")
push(f, "selfhost/frontend/resource.ludic") push(f, "selfhost/frontend/resource.ludic")
push(f, "selfhost/frontend/defaults.ludic")
push(f, "selfhost/frontend/view.ludic")
push(f, "selfhost/frontend/view_conv.ludic")
push(f, "selfhost/frontend/view_finish.ludic")
push(f, "selfhost/backend/emit_core.ludic") push(f, "selfhost/backend/emit_core.ludic")
push(f, "selfhost/backend/emit_head.ludic") push(f, "selfhost/backend/emit_head.ludic")
push(f, "selfhost/backend/emit_addr.ludic") push(f, "selfhost/backend/emit_addr.ludic")

View file

@ -724,7 +724,7 @@ function cmd_dev_test() -> int {
feat_case("modules/visible", "", "42 42", "visible.ludic (L3: a module's exported function and event used from outside it)") feat_case("modules/visible", "", "42 42", "visible.ludic (L3: a module's exported function and event used from outside it)")
reject_case("rejected/private_module", "add is private to module bank", "a private function of another module is refused") reject_case("rejected/private_module", "add is private to module bank", "a private function of another module is refused")
feat_case("lang/checked", "", "7.5 3 12 hi 2 ok", "checked.ludic (L4: literals take their slot's kind, string(p), []string as []pointer, null, named args)") feat_case("lang/checked", "", "7.5 3 12 hi 2 ok", "checked.ludic (L4: literals take their slot's kind, string(p), []string as []pointer, null, named args)")
reject_case("rejected/wrong_arity", "area takes 2 argument(s) and this call gives 1", "a call with the wrong number of arguments is refused") reject_case("rejected/wrong_arity", "this call to area leaves out h, which has no default", "a call with the wrong number of arguments is refused")
reject_case("rejected/float_into_int", "metres wants an int and this is a float", "a float computed into an int is refused") reject_case("rejected/float_into_int", "metres wants an int and this is a float", "a float computed into an int is refused")
reject_case("rejected/record_mixup", "d wants a Door and this is a Key", "one record type is not another") reject_case("rejected/record_mixup", "d wants a Door and this is a Key", "one record type is not another")
reject_case("rejected/record_as_text", "argument 1 of sign wants a string and this is a Trail", "a record is not text") reject_case("rejected/record_as_text", "argument 1 of sign wants a string and this is a Trail", "a record is not text")
@ -736,7 +736,7 @@ function cmd_dev_test() -> int {
reject_case("rejected/generic_arity", "Pool takes 1 type argument(s) and Pool<int, string> gives 2", "a generic takes as many type arguments as it has parameters") reject_case("rejected/generic_arity", "Pool takes 1 type argument(s) and Pool<int, string> gives 2", "a generic takes as many type arguments as it has parameters")
reject_case("rejected/private_generic", "grow is private to module kit", "an instance of a private generic is private to its module") reject_case("rejected/private_generic", "grow is private to module kit", "an instance of a private generic is private to its module")
feat_case("lang/aliases", "", "10 5 3", "aliases.ludic (L6: a namespace method declared as an alias of a function, labelled or by its parameters)") feat_case("lang/aliases", "", "10 5 3", "aliases.ludic (L6: a namespace method declared as an alias of a function, labelled or by its parameters)")
reject_case("rejected/alias_arity", "Trail.length takes 2 argument(s) and this call gives 1", "an alias's arguments are checked against its target") reject_case("rejected/alias_arity", "this call to Trail.length leaves out to, which has no default", "an alias's arguments are checked against its target")
reject_case("rejected/alias_twice", "Trail.length is declared twice", "a namespace method is declared once") reject_case("rejected/alias_twice", "Trail.length is declared twice", "a namespace method is declared once")
feat_case("lang/registry", "", "3 Crate true 2 fire_ring -1", "registry.ludic (L8: a registry filled by def declarations - index constants, a count, the key, a lookup)") feat_case("lang/registry", "", "3 Crate true 2 fire_ring -1", "registry.ludic (L8: a registry filled by def declarations - index constants, a count, the key, a lookup)")
reject_case("rejected/registry_unknown", "there is no registry Tables", "a def names a registry that exists") reject_case("rejected/registry_unknown", "there is no registry Tables", "a def names a registry that exists")
@ -745,6 +745,15 @@ function cmd_dev_test() -> int {
feat_case("lang/resources", "", "2 Axe 2 Split 10 0 1", "resources.ludic (L9: a registry read from a resource file at compile time, nested records typed by the schema)") feat_case("lang/resources", "", "2 Axe 2 Split 10 0 1", "resources.ludic (L9: a registry read from a resource file at compile time, nested records typed by the schema)")
reject_case("rejected/resource_field", "bad_tools.lres:3: error: field weight of Tool wants a float", "a resource file's entry is checked at its own line") reject_case("rejected/resource_field", "bad_tools.lres:3: error: field weight of Tool wants a float", "a resource file's entry is checked at its own line")
reject_case("rejected/resource_missing", "cannot read the resource file data/no_such.lres", "a registry's resource file must exist") reject_case("rejected/resource_missing", "cannot read the resource file data/no_such.lres", "a registry's resource file must exist")
feat_case("lang/defaults", "", "a 300.0 8.0 / b 120.0 8.0 / c! 300.0 8.0 / d 300.0 2.0 / x / y", "defaults.ludic (L11: default parameters, positional then named arguments, in a generic too)")
reject_case("rejected/default_missing", "this call to f leaves out a, which has no default", "a call leaves out only what has a default")
reject_case("rejected/default_unknown", "f has no parameter c", "a named argument names a parameter the function has")
reject_case("rejected/positional_after_named", "a positional argument after a named one", "positional arguments come first")
controller_case("library/ui_templates", "", "ui: ui/peek.xml: <kit:Price>: that component is not exported - export=\"true\" on it lets other files use it purse 70, bought 1; the lantern is enabled: 0", "ui_templates.ludic (L11: a template reads a view, a press sends its event, component state and emit)")
controller_case("library/ui_flex", "", "grow 97:282; between at 197; wrap lines at 51, 67; red 16711680; stretched 40; own size 12", "ui_flex.ludic (L11: flex layout - grow, justify, wrap, stretch - and stylesheets with imports and specificity)")
controller_case("library/ui_html", "", "id beats class: 40; card 200 wide at 3,53 pad 4 8 12 16 border 2 inside 65280, outside -1; warn bg 16776960; items 3 of 30: 30; colours 16711680 255 -1; hr 1; children 7 clicks 1; the button now disabled 1 at opacity 0.5; text-align 1", "ui_html.ludic (L11: HTML elements, ids, classes, attributes, style=, CSS selectors and properties, the box model)")
reject_case("rejected/view_untyped", "view Sums: say what type total is", "a view field that is not a bare global says its type")
reject_case("rejected/view_without_ui", "a view is shown by ludic.ui", "a view needs ludic.ui to show it")
# EV2 the world table: the mod reflection ABI, callable from Ludic by name. # EV2 the world table: the mod reflection ABI, callable from Ludic by name.
net_case("ecs/world_get", "50 1 7") net_case("ecs/world_get", "50 1 7")