feat(ui): popovers, tooltips, progress; the render3d backend installs itself; duplicate names caught first

- `popover`: a top layer that keeps the pointer and the keyboard, closed by a press outside or Esc.
- `title` tooltips after half a second's rest, styled by .ui-tooltip.
- `<progress>` and `<meter>`.
- Atlases take rows and number cells, and importing ludic.ui/render3d.ludic is enough to draw with
  render3d.
- The compiler reports two declarations of one name before it type-checks, so a package global
  clashing with a program's reads as that, not as 29 type errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 17:41:02 +03:00
parent 1dad96102a
commit 0a7525b125
24 changed files with 243 additions and 21 deletions

View file

@ -446,14 +446,20 @@ applies to both:
Each reports with `on-change` and `event.value`, and plays `sound="..."` (or "click") through
the program's `ui_sounds`.
- **Popovers, tooltips and bars.**
- `<div popover on-close="...">` sits absolutely in its parent and draws on the top layer. While
it is up, the pointer and Tab stay inside it, and a press outside it or Esc closes it.
- `title="..."` shows a tooltip (`.ui-tooltip`) after the pointer rests for half a second.
- `<progress value max>` and `<meter value min max>` fill (`.ui-fill`) as far as their value.
- **Natives** are for what markup cannot say. `ui_native("minimap", measure, draw)` makes
`<minimap>` an element the program draws itself, laid out and styled like any other. It reports
with `ui_fire(n, "change", value)` and reads its attributes with `ui_attr` / `ui_attr_on`.
- **Renderers.** `ui_backend(b)` takes any renderer with `rect`, `text` and `measure`, and uses
`round`, `ring`, `image`, `nine`, `clip`, `scale` and `now` when it has them.
`import "ludic.ui/render3d.ludic"` and `ui_render3d()` draw with render3d's overlay:
- images by path, cells of an atlas named with `ui_atlas(prefix, texture, cols, names)` as
`<img src="prefix:name">`, and nine-slices for `border-image`;
Importing `ludic.ui/render3d.ludic` makes render3d's overlay the renderer:
- images by path, and cells of an atlas named with `ui_atlas(prefix, texture, cols, rows,
names)` as `<img src="prefix:name">` (or `prefix:12`);
- nine-slices for `border-image`;
- a scale from the screen's height (`ui_render3d_scale` for the player's interface size).
`ui_translator(fn)`, `ui_sounds(fn)` and `ui_clock(fn)` hand the runtime the program's

View file

@ -48,6 +48,9 @@ The runtime is a UI framework, not only a template engine:
- more CSS: `rgba()`/`#rrggbbaa`, `border-radius`, `outline`, `box-shadow`, `background-image`,
`border-image`, group `opacity`, `@keyframes` / `animation` / `transition`;
- HTML mixed content, and boolean attributes;
- `popover` (a top layer that keeps the pointer and keys, with light dismissal), `title` tooltips,
and `<progress>` / `<meter>`;
- importing `ludic.ui/render3d.ludic` installs the backend, and atlases take rows;
- hooks for the program's language, sounds and clock.
render3d gains `tex_width` / `tex_height`, and the XML reader keeps text runs among elements in
@ -63,3 +66,6 @@ Also:
- 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.
Two declarations with one name (a package's private global and a program's, say) are reported as
such before type checking. They used to surface as a page of type errors about the wrong type.

View file

@ -0,0 +1,59 @@
# ui_popover.ludic — HTML's popover and progress in ludic.ui: a button opens a popover of verbs, the
# keyboard stays inside it, a press outside or Esc closes it, and a progress bar fills to its value.
import "ludic.ui"
import "ui_popover_parts/Verbs.ludic"
program UiPopover {
numbers float
function frame(i: UiInput) -> UiNode {
ui_input(i)
ui_show("Verbs", null, 0.0, 0.0, 400.0, 400.0)
let root: UiNode = ui_nodes("Verbs", null)
ui_place(root, 0.0, 0.0, 400.0, 400.0)
return root
}
function click(x: float, y: float) -> void {
let d = new UiInput
d.x = x
d.y = y
d.down = true
frame(d)
let u = new UiInput
u.x = x
u.y = y
frame(u)
}
function said(root: UiNode) -> string {
let page = root.children[0]
return page.children[len(page.children) - 1].text
}
entry {
ui_backend(new UiBackend)
var root = frame(new UiInput)
let cell = root.children[0].children[0]
click(cell.x + 2.0, cell.y + 2.0)
root = frame(new UiInput)
let tab = new UiInput
tab.tab = true
frame(tab)
frame(tab)
root = frame(tab)
let enter = new UiInput
enter.enter = true
root = frame(enter)
root = frame(new UiInput)
let first = said(root)
click(cell.x + 2.0, cell.y + 2.0)
root = frame(new UiInput)
let opened = said(root)
click(390.0, 390.0)
root = frame(new UiInput)
let outside = said(root)
click(cell.x + 2.0, cell.y + 2.0)
let esc = new UiInput
esc.escape = true
frame(esc)
root = frame(new UiInput)
let bar = root.children[0].children[1]
print(`{first} | {opened} | {outside} | {said(root)} | fill {int(bar.children[0].cw)} of {int(bar.cw)}`)
}
}

View file

@ -0,0 +1,3 @@
.page { width: 300px; height: 200px; gap: 4px }
.verbs { top: 40px; left: 10px; width: 120px }
progress { width: 200px }

View file

@ -0,0 +1,6 @@
# Verbs.ludic - a grid cell that opens a popover of verbs
component Verbs {
state open: bool = false
state used: string = "none"
state health: int = 30
}

View file

@ -0,0 +1,11 @@
<div class="page">
<button id="cell" on-click="set open = true">Axe</button>
<if test="{open}">
<div popover class="verbs" on-close="set open = false">
<button on-click="set used = 'drop'; set open = false">Drop</button>
<button on-click="set used = 'give'; set open = false">Give</button>
</div>
</if>
<progress value="{health}" max="{100}"/>
<p title="how much is left">{used} {open}</p>
</div>

View file

@ -21,7 +21,21 @@ function ct_part(n: UiNode, cls: string, text: string) -> UiNode {
}
return p
}
# <progress value max> and <meter value min max>: a track with a fill as far as the value
function ct_build_bar(n: UiNode) -> void {
n.dir = UI_ROW
let lo = Value.as_float(ui_attr(n, "min"))
var hi = Value.as_float(ui_attr(n, "max"))
if hi <= lo { hi = lo + 1.0 }
let fill = ct_part(n, "ui-fill", "")
fill.w_pct = Math.clamp((Value.as_float(ui_attr(n, "value")) - lo) / (hi - lo), 0.0, 1.0) * 100.0
push(n.children, fill)
}
function ct_build(n: UiNode) -> void {
if n.tag == "progress" or n.tag == "meter" {
ct_build_bar(n)
return
}
n.dir = UI_ROW
n.focusable = true
let t = ui_attr_text(n, "type")

View file

@ -29,20 +29,20 @@ function fr_clip_pop() -> void {
function fr_children(n: UiNode) -> void {
var positioned = 0
for i in 0 .. len(n.children) {
if n.children[i].pos == 0 { fr_draw(n.children[i]) } else { positioned += 1 }
if n.children[i].popover { push(pop_list, n.children[i]) } else if n.children[i].pos == 0 { fr_draw(n.children[i]) } else { positioned += 1 }
}
if positioned == 0 { return }
var lo = 1000000
var hi = -1000000
for i in 0 .. len(n.children) {
if n.children[i].pos != 0 {
if n.children[i].pos != 0 and not n.children[i].popover {
lo = Math.min(lo, n.children[i].z)
hi = Math.max(hi, n.children[i].z)
}
}
for z in lo .. hi + 1 {
for i in 0 .. len(n.children) {
if n.children[i].pos != 0 and n.children[i].z == z { fr_draw(n.children[i]) }
if n.children[i].pos != 0 and not n.children[i].popover and n.children[i].z == z { fr_draw(n.children[i]) }
}
}
}

View file

@ -1,6 +1,6 @@
# 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_BOXES: string = " col div progress meter 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 option "
function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode {
ui_at = k.where
@ -27,7 +27,7 @@ function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode {
bd_kids(k.kids, e, key, n.children)
if k.text != null { el_text(n, ui_tr(ui_eval_text(k.text, e))) }
el_inline(n)
if k.tag == "input" or k.tag == "select" { ct_build(n) }
if k.tag == "input" or k.tag == "select" or k.tag == "progress" or k.tag == "meter" { ct_build(n) }
return n
}
function el_kind(n: UiNode, tag: string) -> void {
@ -61,6 +61,12 @@ function el_keep(n: UiNode, key: string, v: Val) -> void {
for i in 0 .. len(parts) {
if len(parts[i]) > 0 { push(n.classes, parts[i]) }
}
} else if key == "popover" {
n.popover = true
n.pos = 2
push(n.akeys, key)
push(n.avals, s)
push(n.vals, v)
} else if key == "tabindex" { n.focusable = s != "-1" } else if key == "hidden" { n.hidden = el_flag(v) } else if key == "disabled" { n.enabled = not el_flag(v) } else if key == "src" { n.src = s } else {
if key == "enabled" { n.enabled = el_flag(v) }
push(n.akeys, key)

View file

@ -27,7 +27,9 @@ function fc_move(step: int) -> void {
# the keys, after the tree is laid out: moving, then acting on what has the focus
function fc_keys(root: UiNode) -> void {
fc_list = new []UiNode
fc_collect(root)
let top = pp_top(root)
if top != null and in_now.escape { ui_fire(top, "close", Value.null()) }
if top != null { fc_collect(top) } else { fc_collect(root) }
let i = in_now
if fc_find() < 0 { fc_autofocus() }
let at = fc_find()

View file

@ -14,7 +14,11 @@ export function ui_show(screen: string, view: UiView, x: float, y: float, w: flo
sc_update(root)
fr_pointer(root)
fc_keys(root)
if ui_be != null and ui_be.text != null { fr_draw(root) }
if ui_be != null and ui_be.text != null {
fr_draw(root)
pp_draw_top()
tt_draw(root)
}
dv_after(screen, root)
lc_run(screen)
nt_fired_run()

View file

@ -19,6 +19,8 @@ import "controls_build.ludic"
import "input.ludic"
import "focus.ludic"
import "scroll.ludic"
import "popover.ludic"
import "tooltip.ludic"
import "life.ludic"
import "tpl.ludic"
import "tpl_lines.ludic"

View file

@ -1,6 +1,6 @@
# 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 input select textarea spacer scroll img hr if else each slot state style component screen import let fragment template provide #text "
const LK_OWN: string = " row button input select textarea progress meter spacer scroll img hr if else each slot state style component screen import let fragment template provide #text "
function lib_link(f: UiFile, t: UiTpl) -> void {
t.sheet = f.sheet
if not lk_element(t.tag) {

View file

@ -103,5 +103,6 @@ export property UiNode {
anim_loop: bool = false
anim_alt: bool = false
tr_dur: float = 0.0 # transition: how long a change of opacity takes
popover: bool = false # drawn on the top layer; a press outside it or Esc closes it
}

View file

@ -9,9 +9,14 @@ function fr_pointer(root: UiNode) -> void {
let i = in_now
fr_down_now = i.down
let keys = new []string
fr_under(root, i.x, i.y, keys)
let top = pp_top(root)
var scope = root
if top != null { scope = top }
fr_under(scope, i.x, i.y, keys)
fr_hover = keys
let hit = fr_hit(root, i.x, i.y)
tt_track(scope, i.x, i.y)
if top != null and pp_pointer(top) { return }
let hit = fr_hit(scope, i.x, i.y)
if i.down and not in_was_down {
fr_press_key = ""
if hit != null {

View file

@ -0,0 +1,32 @@
# popover.ludic - HTML's popover: `<div popover on-close="...">` sits where its position puts it
# (absolutely, in its parent) and is drawn on the top layer, over everything. While one is up the
# pointer and the keyboard are its: a press outside it, or Esc, closes it (on-close), and Tab
# walks only its own controls.
var pop_list: []UiNode = new []UiNode
function pp_find(n: UiNode, out: []UiNode) -> void {
if n.popover {
push(out, n)
return
}
for i in 0 .. len(n.children) { pp_find(n.children[i], out) }
}
# the topmost popover open in this tree, or null
function pp_top(root: UiNode) -> UiNode {
let all = new []UiNode
pp_find(root, all)
if len(all) == 0 { return null }
return all[len(all) - 1]
}
# the pointer against the popover: inside it is as usual; a press outside closes it and is spent
function pp_pointer(top: UiNode) -> bool {
let i = in_now
if not (i.down and not in_was_down) { return false }
if i.x >= top.x and i.y >= top.y and i.x < top.x + top.cw and i.y < top.y + top.ch { return false }
ui_fire(top, "close", Value.null())
return true
}
function pp_draw_top() -> void {
let list = pop_list
pop_list = new []UiNode
for i in 0 .. len(list) { fr_draw(list[i]) }
}

View file

@ -1,5 +1,5 @@
# render3d.ludic - ludic.ui drawn with ludic.render3d's overlay. A program that draws with render3d
# imports this and calls ui_render3d() once; its templates then draw in the overlay pass, sized in
# imports this, and its templates draw in the overlay pass (ui_render3d() installs it again), sized in
# design pixels of a 1080-line screen (ui_render3d_scale sets the player's own interface size).
# <img src="path/to/picture.png"> a texture, loaded once
# <img src="icon:tent"> a cell of an atlas the program named with ui_atlas
@ -12,7 +12,14 @@ var r3u_tex_ids: []int = new []int
var r3u_atlas: []string = new []string
var r3u_atlas_tex: []int = new []int
var r3u_atlas_cols: []int = new []int
var r3u_atlas_rows: []int = new []int
var r3u_atlas_names: [][]string = new [][]string
var r3u_installed: bool = r3u_install()
# importing this file is enough: it is the renderer from the program's first frame
function r3u_install() -> bool {
ui_render3d()
return true
}
export function ui_render3d() -> void {
let b = new UiBackend
b.rect = fn r3u_rect
@ -30,11 +37,13 @@ export function ui_render3d() -> void {
}
# the player's interface size, 1.0 as designed
export function ui_render3d_scale(s: float) -> void { r3u_user = s }
# a grid atlas: `names` are its cells, row by row, `cols` to a row; <img src="prefix:name"> draws one
export function ui_atlas(prefix: string, tex: int, cols: int, names: []string) -> void {
# a grid atlas: `names` are its cells, row by row, `cols` across and `rows` down; <img
# src="prefix:name"> draws one (by its name, or by its number: "item:12")
export function ui_atlas(prefix: string, tex: int, cols: int, rows: int, names: []string) -> void {
push(r3u_atlas, prefix)
push(r3u_atlas_tex, tex)
push(r3u_atlas_cols, cols)
push(r3u_atlas_rows, Math.max(rows, 1))
push(r3u_atlas_names, names)
}
function r3u_now() -> float { return float(Time.now_us() / 1000) / 1000.0 }

View file

@ -37,17 +37,19 @@ function r3u_cell(k: int, name: string, x: float, y: float, w: float, h: float,
for i in 0 .. len(names) {
if names[i] == name { idx = i }
}
if idx < 0 and len(name) > 0 and name[0] >= 48 and name[0] <= 57 { idx = int(el_num(name)) }
if idx < 0 {
ui_err(`{r3u_atlas[k]}:{name}: the atlas has no such picture`)
return
}
let cols = r3u_atlas_cols[k]
let rows = r3u_atlas_rows[k]
let cx = idx % cols
let cy = idx / cols
let u0 = float(cx) / float(cols)
let v0 = float(cy) / float(cols)
let v0 = float(cy) / float(rows)
let s = Math.min(w, h)
ov_sub(r3u_atlas_tex[k], int(x + (w - s) / 2.0), int(y + (h - s) / 2.0), int(s), int(s), u0, v0, u0 + 1.0 / float(cols), v0 + 1.0 / float(cols), r3u_r(c), r3u_g(c), r3u_b(c), a)
ov_sub(r3u_atlas_tex[k], int(x + (w - s) / 2.0), int(y + (h - s) / 2.0), int(s), int(s), u0, v0, u0 + 1.0 / float(cols), v0 + 1.0 / float(rows), r3u_r(c), r3u_g(c), r3u_b(c), a)
}
# border-image: the texture's `slice`-pixel corners kept, the rest stretched, drawn `dst` wide
function r3u_nine(src: string, slice: float, x: float, y: float, w: float, h: float, dst: float, c: int, a: float) -> void {

View file

@ -0,0 +1,47 @@
# tooltip.ludic - an element's title="..." shown beside the pointer once it has rested on the
# element for half a second, in a box styled by .ui-tooltip (the default sheet's, or a theme's)
var tt_key: string = ""
var tt_since: float = 0.0
var tt_text: string = ""
var tt_x: float = 0.0
var tt_y: float = 0.0
function tt_track(scope: UiNode, x: float, y: float) -> void {
let n = tt_under(scope, x, y)
if n == null {
tt_key = ""
return
}
if n.key != tt_key {
tt_key = n.key
tt_since = an_now()
}
tt_text = ui_tr(ui_attr_text(n, "title"))
tt_x = x
tt_y = y
}
# the deepest element under the point that has a title
function tt_under(n: UiNode, x: float, y: float) -> UiNode {
if x < n.x or y < n.y or x >= n.x + n.cw or y >= n.y + n.ch { return null }
var i = len(n.children) - 1
while i >= 0 {
let t = tt_under(n.children[i], x, y)
if t != null { return t }
i -= 1
}
if n.akeys != null and ui_attr_text(n, "title") != "" { return n }
return null
}
function tt_draw(root: UiNode) -> void {
if tt_key == "" or tt_text == "" or an_now() - tt_since < 0.5 { return }
let n = ct_part(root, "ui-tooltip", "")
n.tag = "div"
n.parent = root
n.children = new []UiNode
push(n.children, ct_part(root, "ui-tooltip-text", tt_text))
cs_tree(n)
ly_measure(n, ly_vp_w * 0.4, ui_px(16.0), root.rfg)
let x = Math.min(tt_x + ui_px(16.0), ly_vp_x + ly_vp_w - n.mw)
let y = Math.min(tt_y + ui_px(20.0), ly_vp_y + ly_vp_h - n.mh)
ly_place(n, x, y, n.mw, n.mh)
fr_draw(n)
}

View file

@ -1,4 +1,4 @@
# ua.ludic - what every element looks like before a program styles it, as a browser's own sheet
# does: headings sized, buttons and controls usable and plain, a focus ring. A theme overrides any
# of it, with the same selectors or with the variables it reads.
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 } hr { height: 1; width: fill; background: #808080; margin: 4 0 } scroll { width: fill; height: fill } img { width: 24; height: 24 } button { align-items: center; justify-content: center; gap: 8; padding: 6 14; background: #3a3f46; border-radius: 4 } button:hover { background: #4a5058 } button:disabled { opacity: 0.4 } :focus-visible { outline: 2px solid #ffcc55; outline-offset: 2 } input, select { align-items: center; gap: 10; padding: 6 10 } .ui-label { flex-grow: 1 } .ui-track { width: 44; height: 22; padding: 3; background: #555a60; border-radius: 11 } .ui-knob { width: 16; height: 16; background: #eeeeee; border-radius: 8 } input:checked .ui-track { justify-content: end; background: #d9a54a } input[type=range] .ui-track { width: 200; height: 8; padding: 0; border-radius: 4 } .ui-fill { height: fill; background: #d9a54a; justify-content: end; align-items: center; border-radius: 4 } .ui-thumb { width: 14; height: 14; background: #eeeeee; border-radius: 7; margin-right: -7 } .ui-value { min-width: 40 } .ui-field { width: 200; padding: 4 8; background: #202428; border: 1px solid #555a60 } .ui-caret { width: 2; height: 1.2em; background: #eeeeee } .ui-prev, .ui-next { color: #aaaaaa }"
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 } hr { height: 1; width: fill; background: #808080; margin: 4 0 } scroll { width: fill; height: fill } img { width: 24; height: 24 } button { align-items: center; justify-content: center; gap: 8; padding: 6 14; background: #3a3f46; border-radius: 4 } button:hover { background: #4a5058 } button:disabled { opacity: 0.4 } :focus-visible { outline: 2px solid #ffcc55; outline-offset: 2 } input, select { align-items: center; gap: 10; padding: 6 10 } .ui-label { flex-grow: 1 } .ui-track { width: 44; height: 22; padding: 3; background: #555a60; border-radius: 11 } .ui-knob { width: 16; height: 16; background: #eeeeee; border-radius: 8 } input:checked .ui-track { justify-content: end; background: #d9a54a } input[type=range] .ui-track { width: 200; height: 8; padding: 0; border-radius: 4 } .ui-fill { height: fill; background: #d9a54a; justify-content: end; align-items: center; border-radius: 4 } .ui-thumb { width: 14; height: 14; background: #eeeeee; border-radius: 7; margin-right: -7 } .ui-value { min-width: 40 } .ui-field { width: 200; padding: 4 8; background: #202428; border: 1px solid #555a60 } .ui-caret { width: 2; height: 1.2em; background: #eeeeee } .ui-prev, .ui-next { color: #aaaaaa } progress, meter { width: 160; height: 10; background: #202428; border-radius: 5 } progress .ui-fill, meter .ui-fill { justify-content: start; border-radius: 5 } [popover] { position: absolute; z-index: 900 } .ui-tooltip { position: fixed; padding: 6 10; background: #202428; border: 1px solid #555a60; font-size: 15; z-index: 1000 }"

View file

@ -12771,7 +12771,7 @@ declare void @hs_free(i32)
@.str12493 = private unnamed_addr constant [2 x i8] c" \00"
@.str12494 = private unnamed_addr constant [50 x i8] c" -Wl,/subsystem:windows -Wl,/entry:mainCRTStartup\00"
@.str12495 = private unnamed_addr constant [3 x i8] c" \22\00"
@.str12496 = private unnamed_addr constant [55 x i8] c"build/cfree/selfhost.ludic:17806: index out of range: \00"
@.str12496 = private unnamed_addr constant [55 x i8] c"build/cfree/selfhost.ludic:17808: index out of range: \00"
@.str12497 = private unnamed_addr constant [2 x i8] c"\22\00"
@.str12498 = private unnamed_addr constant [5 x i8] c" -o \00"
@.str12499 = private unnamed_addr constant [2 x i8] c"\22\00"
@ -134235,6 +134235,8 @@ ifend89:
br label %ifend86
ifend86:
store i32 0, ptr @g_g_parsing
call void @fn_check_duplicate_fns()
call void @fn_check_duplicate_decls()
call void @fn_check_program()
call void @fn_emit_program()
%t230 = load i32, ptr %t3

View file

@ -13088,7 +13088,7 @@ entry:
@.str12493 = private unnamed_addr constant [2 x i8] c" \00"
@.str12494 = private unnamed_addr constant [50 x i8] c" -Wl,/subsystem:windows -Wl,/entry:mainCRTStartup\00"
@.str12495 = private unnamed_addr constant [3 x i8] c" \22\00"
@.str12496 = private unnamed_addr constant [55 x i8] c"build/cfree/selfhost.ludic:17806: index out of range: \00"
@.str12496 = private unnamed_addr constant [55 x i8] c"build/cfree/selfhost.ludic:17808: index out of range: \00"
@.str12497 = private unnamed_addr constant [2 x i8] c"\22\00"
@.str12498 = private unnamed_addr constant [5 x i8] c" -o \00"
@.str12499 = private unnamed_addr constant [2 x i8] c"\22\00"
@ -134538,6 +134538,8 @@ ifend89:
br label %ifend86
ifend86:
store i32 0, ptr @g_g_parsing
call void @fn_check_duplicate_fns()
call void @fn_check_duplicate_decls()
call void @fn_check_program()
call void @fn_emit_program()
%t230 = load i32, ptr %t3

View file

@ -203,6 +203,8 @@ entry {
else { g_windowed = has_target and has_systems() }
g_parsing = false # lowering errors now locate by statement, not token
check_duplicate_fns() # two declarations of one name: the cause, before its symptoms
check_duplicate_decls()
check_program() # L4: the types agree before anything is emitted
emit_program()

View file

@ -759,6 +759,7 @@ function cmd_dev_test() -> int {
reject_case("rejected/component_no_template", "there is no Lonely.xml beside it", "a component needs its template beside it")
controller_case("library/ui_controls", "", "1 false 7 2 Adalove 70 1 false 8 2 Adalove 70; scrolled 40", "ui_controls.ludic (L11: focus and keys, the built-in controls, a range dragged, a box scrolled)")
controller_case("library/ui_anim", "", "0 100 100 | 49 50 100 | 100 100 61", "ui_anim.ludic (L11: @keyframes and animation, looping and alternating, and a transition)")
controller_case("library/ui_popover", "", "drop false | drop true | drop false | drop false | fill 60 of 200", "ui_popover.ludic (L11: a popover keeps the keys and closes on a press outside or Esc; progress fills)")
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")