Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@ version number before tagging the release.

### Fixed

- **E0200 "block setter called as a node builder" rejected every container
of a widget DSL that also declares a `builder`.** The rule from 0.667
used "the callee's module also defines a `builder`" as its whole
discriminator between a block setter and a DSL container, on the premise
that a widget-style module has no builders. aether-ui's `ui` module has
four (`window`, `render_to`, `record`, `nav_page`) beside some fifty
`_ctx`-first containers, so `ui.vstack(10) { ... }` — every app in that
repository — became a compile error the moment it moved past 0.666
(aether-ui#147). The rule now also requires that the callee yield no
value: a container RETURNS the handle its block runs inside (that value
is the block's `_ctx`), while a block setter records config and returns
nothing, which is exactly why its own trailing block can never be
entered. aeb's setters are all void, so the misuse it was written for
is still caught. `tests/integration/setter_in_builder_position` gains
`uimod` (a builder beside handle-returning containers and one void
setter): the container compiles, the setter still errors.

- **`os.run_full` and `os.run_capture` are now tested on Windows.** Both
have had CreateProcessW backends for some time, but their regression
tests still skipped Windows with comments calling them POSIX-only, so
Expand Down
41 changes: 30 additions & 11 deletions compiler/analysis/typechecker.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions tests/integration/setter_in_builder_position/legit_c.ae
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// LEGIT C (must COMPILE, NO false positive): a widget-style DSL container in
// a module that ALSO has a builder.
//
// `vstack` is `_ctx`-first and takes a trailing block, and its module defines
// `builder window` — conditions 1-3 all hold, which is why this was rejected
// before condition 4. It RETURNS the container handle its block runs inside,
// so it is a container, not a setter. This is every aether-ui app.

import uimod

main() {
uimod.window("app") {
vstack(10) {
label("hello")
}
}
}
11 changes: 11 additions & 0 deletions tests/integration/setter_in_builder_position/misuse_b.ae
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// MISUSE B (must ERROR): the void block setter of a widget module, called as
// a node with its own trailing block. Condition 4 must not let a genuine
// setter through just because its module also has containers.

import uimod

main() {
uimod.font_size(14) {
label("never attached")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
#
# The check is deliberately NARROW to stay false-positive-free. It fires
# ONLY when the callee is a plain `_ctx`-first function, the call carries
# its own trailing block, AND the callee's module also defines a `builder`.
# A widget-style DSL container (`panel(_ctx, title) { button() }`) in a
# module with NO builders is structurally identical but must NOT be flagged.
# its own trailing block, the callee's module also defines a `builder`,
# AND the callee yields no value. A widget-style DSL container
# (`panel(_ctx, title) { button() }`) in a module with NO builders is
# structurally identical but must NOT be flagged — and neither is one in a
# module WITH builders when it returns the handle its block runs inside
# (aether-ui's `ui` module: `builder window` beside `vstack(_ctx, spacing)`,
# which rejected every app under the first cut of this rule).

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
Expand Down Expand Up @@ -86,5 +90,7 @@ expect_ok() {
expect_error "$SCRIPT_DIR/misuse.ae" "MISUSE: setter called as top-level node builder must error"
expect_ok "$SCRIPT_DIR/legit_a.ae" "LEGIT A: same setter called inside the builder's block compiles"
expect_ok "$SCRIPT_DIR/legit_b.ae" "LEGIT B: widget-style DSL container (no builders) not flagged"
expect_ok "$SCRIPT_DIR/legit_c.ae" "LEGIT C: handle-returning container in a module WITH a builder not flagged"
expect_error "$SCRIPT_DIR/misuse_b.ae" "MISUSE B: void setter of that same widget module still errors"

exit $fail
32 changes: 32 additions & 0 deletions tests/integration/setter_in_builder_position/uimod/module.ae
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Widget-style DSL module that ALSO defines a builder — aether-ui's shape.
//
// `window` is a real `builder`. `vstack` is a `_ctx`-first plain function
// that LEGITIMATELY takes a trailing block: it creates a container and
// RETURNS its handle, which is the `_ctx` the block's children attach to.
// Under conditions 1-3 alone this module looks exactly like rubymod, and
// `uimod.vstack(10) { ... }` was rejected as a misused setter. Condition 4
// tells them apart: a container yields a value, a block setter yields none.
//
// `font_size` is the module's genuine block setter: `_ctx`-first, void,
// records config on the container. Called with a trailing block of its own
// it must still be flagged, builder-module or not.

exports(window, vstack, label, font_size)

builder window(title: string) {
println("window ${title}")
}

vstack(_ctx: ptr, spacing: int) {
println("vstack ${spacing}")
return 42
}

label(_ctx: ptr, text: string) {
println("label ${text}")
return 43
}

font_size(_ctx: ptr, px: int) {
println("font_size ${px}")
}
Loading