Summary
While auditing Docs/04-advanced-features/containers-oop.md against the implementation (branch claude/containers-md-wiring-cleanup-8k19ys, which wired up interface contracts), I found that the container event system is only half-wired. Events parse and trigger executes, but the handler side is unreachable end to end — so event/trigger are effectively no-ops that only validate the event exists. Events are not covered by the containers doc, so this was left out of that change as a follow-up.
What works today
event on_click inside a container body parses (parse_event_definition_full), including needs parameters, and is stored in ContainerDefinitionValue.events.
trigger on_click inside a container action parses and executes: method calls inject the definition's events into the method environment, and the EventTrigger arm runs all registered handlers.
TestPrograms/containers_comprehensive.wfl exercises exactly this much — its triggers succeed because running zero handlers is a no-op, so the test never proves a handler fires.
What is broken
-
The on <source> <event> handler statement cannot be written. parse_event_handler (src/parser/stmt/containers.rs) calls parse_expression() for the event source, which consumes both the source and the event name as one natural-language expression, then fails with Expected identifier for event name, found Eol:
create container Button:
property label: Text
event on_click
action click:
display "clicking"
trigger on_click
end
end
create new Button as btn:
label is "Submit"
end
on btn on_click
display "handler ran"
btn.click()
error[ERROR]: Expected identifier for event name, found Eol
-
Even if it parsed, the handler body is always dropped. parse_event_handler is a stub that returns handler_body: Vec::new() — the statements under the handler are never attached to it.
-
Statement-level trigger never parses arguments (parse_event_trigger stub returns arguments: Vec::new()), so an event declared with needs parameters can never receive values; runtime binds parameters to Null.
-
Handler registration and triggering don't share state. The runtime EventHandler arm clones the event, appends the handler, and defines the result as a plain environment binding under the event name (src/interpreter/mod.rs, Statement::EventHandler). But trigger inside a method looks up events freshly injected from ContainerDefinitionValue.events — whose handlers list is always empty (Rc<ContainerEventValue> is immutable, handlers: Vec::new() at definition). So even with 1–3 fixed, a registered handler would never be seen by a trigger inside a method.
Expected
Either events work end to end — define, register a handler with a body, trigger with arguments, handler runs with bound parameters — or the surface is removed/documented as unimplemented. Half-parsed syntax that silently does nothing is the worst of both (a program can trigger all day and look successful).
Suggested direction
- Decide the handler grammar (e.g.
on <instance> <event>: ... end on) and parse the body properly.
- Store handlers somewhere shared and mutable (e.g.
RefCell on the event value in the container definition, or per-instance), so registration and triggering agree.
- Parse trigger arguments and bind event parameters.
- Add red-first tests that assert a handler actually runs (current tests only prove triggers don't crash), including the
needs-parameter path.
- Update docs (containers doc or a dedicated events page) only once behavior is real, per the docs-honesty policy.
Code pointers
src/parser/stmt/containers.rs — parse_event_handler, parse_event_trigger, parse_event_definition stubs ("For now, just create a simple ...").
src/interpreter/mod.rs — Statement::EventTrigger, Statement::EventHandler arms; event injection in the MethodCall path.
src/interpreter/value.rs — ContainerEventValue / EventHandler.
Summary
While auditing
Docs/04-advanced-features/containers-oop.mdagainst the implementation (branchclaude/containers-md-wiring-cleanup-8k19ys, which wired up interface contracts), I found that the container event system is only half-wired. Events parse andtriggerexecutes, but the handler side is unreachable end to end — soevent/triggerare effectively no-ops that only validate the event exists. Events are not covered by the containers doc, so this was left out of that change as a follow-up.What works today
event on_clickinside a container body parses (parse_event_definition_full), includingneedsparameters, and is stored inContainerDefinitionValue.events.trigger on_clickinside a container action parses and executes: method calls inject the definition's events into the method environment, and theEventTriggerarm runs all registered handlers.TestPrograms/containers_comprehensive.wflexercises exactly this much — its triggers succeed because running zero handlers is a no-op, so the test never proves a handler fires.What is broken
The
on <source> <event>handler statement cannot be written.parse_event_handler(src/parser/stmt/containers.rs) callsparse_expression()for the event source, which consumes both the source and the event name as one natural-language expression, then fails withExpected identifier for event name, found Eol:Even if it parsed, the handler body is always dropped.
parse_event_handleris a stub that returnshandler_body: Vec::new()— the statements under the handler are never attached to it.Statement-level
triggernever parses arguments (parse_event_triggerstub returnsarguments: Vec::new()), so an event declared withneedsparameters can never receive values; runtime binds parameters toNull.Handler registration and triggering don't share state. The runtime
EventHandlerarm clones the event, appends the handler, and defines the result as a plain environment binding under the event name (src/interpreter/mod.rs,Statement::EventHandler). Buttriggerinside a method looks up events freshly injected fromContainerDefinitionValue.events— whosehandlerslist is always empty (Rc<ContainerEventValue>is immutable,handlers: Vec::new()at definition). So even with 1–3 fixed, a registered handler would never be seen by a trigger inside a method.Expected
Either events work end to end — define, register a handler with a body, trigger with arguments, handler runs with bound parameters — or the surface is removed/documented as unimplemented. Half-parsed syntax that silently does nothing is the worst of both (a program can
triggerall day and look successful).Suggested direction
on <instance> <event>: ... end on) and parse the body properly.RefCellon the event value in the container definition, or per-instance), so registration and triggering agree.needs-parameter path.Code pointers
src/parser/stmt/containers.rs—parse_event_handler,parse_event_trigger,parse_event_definitionstubs ("For now, just create a simple ...").src/interpreter/mod.rs—Statement::EventTrigger,Statement::EventHandlerarms; event injection in theMethodCallpath.src/interpreter/value.rs—ContainerEventValue/EventHandler.