Complete nullable page and typed lifecycle resource method contracts

This commit is contained in:
2026-10-22 14:01:01 +08:00
parent 543c909d53
commit d9fd3f2f65
10 changed files with 193 additions and 24 deletions
+2 -2
View File
@@ -422,9 +422,9 @@ static effect typing and complete native signature coverage remain pending. Nati
Native methods publish positional argument intervals through Aster CallArity. The VM checks counts before invoking them, and trait registration/descendant updates verify that each source or native method accepts the canonical argument count. Optional/default parameters are included; bound receiver counts are removed for callback queries. Generic Type construction still uses its host constructor rules. CallSignature adds optional resolved parameter/return types and host effect metadata, as described below.
Native typed contracts now cover measure(Block, Mount, Constraints): MeasureResult, arrange(Block, Mount, Bounds): Arrangement, paint(Block, Mount, Bounds): DisplayList and hitTest(Block, Mount, Point): Bool, plus Point geometry queries, Window methods and the representable Block/Container/Page methods. The explicit receiver is checked before binding dispatch. Aster checks types before invoking the implementation and checks actual returns, including final results delegated to Endor functions. Signature definitions are traced managed edges; pending delegated return checks retain execution roots and use the existing VM frame/budget machinery. Wrong returns release temporary results but do not roll back earlier writes.
Native typed contracts now cover measure(Block, Mount, Constraints): MeasureResult, arrange(Block, Mount, Bounds): Arrangement, paint(Block, Mount, Bounds): DisplayList and hitTest(Block, Mount, Point): Bool, plus Point geometry queries and Window, Block/Container/Page, Mount, Task, Event, Subscription and ObjectList.at methods. The explicit receiver is checked before binding dispatch. Aster checks types before invoking the implementation and checks actual returns, including final results delegated to Endor functions. Signature definitions are traced managed edges; pending delegated return checks retain execution roots and use the existing VM frame/budget machinery. Wrong returns release temporary results but do not roll back earlier writes.
Host CallEffect distinguishes ReadOnly, Mutating and External. A declared mutating/external call is rejected before invocation during read-only rendering; declaring ReadOnly does not bypass the existing heap write barrier. Legacy missing metadata remains unknown, and trusted native code still must obey its I/O contract. Constructors and other management APIs are not yet fully covered; Page.open/window and Block.on keep absent type metadata until nullable/union types can represent their existing forms. Source override variance, trait type/effect compatibility and static effect inference remain pending. Existing [visual-protocols.endor](examples/visual-protocols.endor) and [layout-negotiation.endor](examples/layout-negotiation.endor) exercise the typed visual methods with actual Endor code.
Host CallEffect distinguishes ReadOnly, Mutating and External. A declared mutating/external call is rejected before invocation during read-only rendering; declaring ReadOnly does not bypass the existing heap write barrier. Legacy missing metadata remains unknown, and trusted native code still must obey its I/O contract. CallType supports single types and flat nonempty unions, retaining every branch definition. Page.open accepts Window or nil, Page.window returns Window or nil, and Block.on accepts Bool or String for its optional phase. Mount parent/child/sibling/Page queries have nullable results; bounds() returns an actual Bounds object so Point.inside(mount.bounds()) composes directly. Scope/resource eligibility, numeric ranges and phase names keep their domain checks. This adds native contract metadata, not Endor union syntax. Constructors and other management APIs are not yet fully covered. Source override variance, trait type/effect compatibility and static effect inference remain pending. Existing [visual-protocols.endor](examples/visual-protocols.endor) and [layout-negotiation.endor](examples/layout-negotiation.endor) exercise the typed visual methods with actual Endor code.
Event listeners and onClick callbacks must be Functions with known metadata accepting zero arguments or one Event. If both forms work, the dispatcher passes Event. Binding computes and Task callbacks must accept zero arguments, checked before replacing a binding, adding a connection or scheduling work. Native and source callbacks follow the same rule. See [native-signatures.endor](examples/native-signatures.endor) for real native one-argument and zero-argument handlers. Event and ObjectList subclass allocators also check declared field suffixes before allocation.
+5 -5
View File
@@ -7,16 +7,16 @@
| activity.rs | Bounded in-memory HTTP exchanges with marked previews and allowlisted request headers |
| endor_host.rs | Aster compilation, fresh PUT candidates retained after successful publication, shared Console/program VM, asynchronous completion/fault boundaries, bounded execution, root cleanup and AST developer tree for function/class/member assignment nodes |
| endor_script.rs | Batch complete inputs for Aster's Interactive parser/compiler, retain Module entry, and rebase Aster spans to the original snapshot |
| ui_bridge.rs | Inheritable native nominal constructors with source fields, shared type method tables and bound receiver dispatch, configured unified Function classification and source/field annotation checks with native constructor preflight and shared native arity intervals plus resolved types/effects on representable methods, tab registry, stable-slot insertion, source-dispatched tab reorder/drop and callback registration checks, Page lifecycle/navigation and immutable frame extraction |
| ui_bridge.rs | Inheritable native nominal constructors with source fields, shared type method tables and bound receiver dispatch, configured unified Function classification and source/field annotation checks with native constructor preflight and shared native arity intervals plus resolved types/unions/effects on methods, tab registry, stable-slot insertion, source-dispatched tab reorder/drop and callback registration checks, Page lifecycle/navigation and immutable frame extraction |
| binding.rs | Target-owned managed property bindings, weak Subscription tokens, zero-argument callback preflight, read-only compute and borrowed dependency/output caches |
| block.rs | Aster-managed Block/Cell/data graph, stable child slots, content-owned field storage, shared assignment, cycle validation and explicit text bindings |
| snapshot.rs | Property and visual-method identity dependencies across shared field-storage paths, reverse parent links and cached immutable visual derivation |
| mount.rs | Managed per-occurrence contexts, weak reverse links, ordered mount/unmount hooks, geometry and focus queries |
| mount.rs | Typed per-occurrence methods and nominal Bounds results, managed contexts, weak reverse links, ordered mount/unmount hooks, geometry and focus queries |
| events.rs | Bounded native input transport with kind-specific tab-drop/desktop-position metadata to the owning VM |
| event_dispatch.rs | Managed Event, weak Subscription tokens, Block-owned connections and occurrence-specific capture/target/bubble through a budgeted VM driver |
| event_dispatch.rs | Typed managed Event/Subscription methods, weak Subscription tokens, Block-owned connections and occurrence-specific capture/target/bubble through a budgeted VM driver |
| protocols.rs | Register explicit Visual/EventTarget/Children (including insert)/Presentable managed traits and native type declarations |
| presentation.rs | Per-window layout, Mount/focus/tab state and read dependencies; VM changes fan out before any view consumes work |
| tasks.rs | Managed owner scopes and callbacks, weak timer index, request-key replacement, lifecycle cancellation and budgeted worker delivery |
| tasks.rs | Typed Task/ui.after methods, managed owner scopes and callbacks, weak timer index, request-key replacement, lifecycle cancellation and budgeted worker delivery |
| tabs.rs | Immutable tab chrome, shared draw/hit geometry, overflow navigation, drag gaps/anchors/insertion markers and opaque published Page identities |
| window.rs | Typed native Window queries/effects, construction/close, context ownership, ordered Page roots, read-only guards, Page reordering, physical desktop placement and coalesced GUI projections |
| vm_sequence.rs | Shared validated call-sequence builder and EXTENDED_ARG-aware same-frame repetition for layout, event and lifecycle work |
@@ -24,7 +24,7 @@
| source_hit.rs | Read-only source/native pointer target selectors, reverse paint order, strict Bool results and matching press/release targets |
| source_paint.rs | Read-only source paint dispatch, shared VM budget, paired text-wrap bounds and checked command conversion and versioned object dependencies |
| native_visual.rs | Managed native visual protocol queries with resolved type/read-only signatures, occurrence validation, value conversion and bounded results |
| object_list.rs | Traced indexed results shared by Mount queries and visual protocol results |
| object_list.rs | Typed read-only indexed access and traced results shared by Mount queries and visual protocol results |
| layout.rs | Shared native geometry/paint functions, iterative measurement/arrangement, stable occurrence IDs, measurement cache, solve-pass counters, geometry budgets and width-sensitive native text measurement |
| document.rs | Published source/AST/UI snapshot, revisions, invalidation, frame validation, single-line fault banner (full cause retained in Frame), validated optional text-wrap bounds and display commands |
| app.rs | Window events, logical viewport publication, redraw, native multi-window tab dragging and system widget pointer/keyboard/IME routing |
+1 -1
View File
@@ -1103,7 +1103,7 @@ Declared fields are checked at construction and set_attr writes through the same
Native methods now publish positional CallArity intervals, checked by the VM before invocation. Source defaults and bound receivers participate in the same query. Traits validate acceptance of their canonical count including self and reject native methods without metadata. Event listeners/onClick require Function metadata accepting zero arguments or one Event, preferring Event if both work. Binding computes and Task callbacks require zero-argument acceptance before registration/replacement. Generic Type constructor reflection and complete native signature coverage remain separate gaps. See examples/native-signatures.endor.
Native CallSignature checks resolved parameter/return types for visual measure/arrange/paint/hitTest, Point queries, Window methods and representable Block/Container/Page methods. Signature definitions are traced managed edges. Parameters fail before dispatch; final results delegated to source/native functions must satisfy all pending native return contracts, with rooted definitions and the same frame/instruction budgets. Wrong returns release scoped results but do not undo earlier writes. CallEffect ReadOnly allows result allocation; Mutating/External calls are rejected before invocation during read-only execution. Existing heap barriers still apply, and this does not sandbox trusted native I/O. Constructors/other API coverage, nullable/union signatures for Page.open/window and Block.on, override variance and static effect inference remain pending. Unknown legacy metadata remains unknown. See examples/visual-protocols.endor and examples/layout-negotiation.endor.
Native CallSignature checks resolved parameter/return types for visual measure/arrange/paint/hitTest, Point queries, Window/Block/Container/Page/Mount/Task/Event/Subscription methods and ObjectList.at. Signature definitions are traced managed edges. Parameters fail before dispatch; final results delegated to source/native functions must satisfy all pending native return contracts, with rooted definitions and the same frame/instruction budgets. Wrong returns release scoped results but do not undo earlier writes. CallEffect ReadOnly allows result allocation; Mutating/External calls are rejected before invocation during read-only execution. Existing heap barriers still apply, and this does not sandbox trusted native I/O. CallType unions preserve every branch definition. Page.open accepts Window or nil; page.window returns Window or nil; Block.on accepts Bool or String for its optional phase, while phase names retain domain validation. Mount relation queries are nullable, and bounds() is a real Bounds object usable by Point.inside. Native union metadata does not add Endor union syntax. Constructors/other API coverage, override variance and static effect inference remain pending. Unknown legacy metadata remains unknown. See examples/visual-protocols.endor and examples/layout-negotiation.endor.
Trait definitions Visual, EventTarget, Children and Presentable are managed nominal capabilities. ui.implement(type,capability) explicitly declares an implementation after required-method/positional-arity checks; ui.implements(type,capability) and ui.supports(value,capability) query it. Block declares Visual/EventTarget; ContainerBlock declares Children, inherited by Page. Same-name methods alone never declare an implementation. Source subclass construction and host method replacement validate inherited/affected descendant contracts. Source/native positional intervals are checked, and missing native arity metadata prevents a capability declaration; source parameter/return annotations are checked at invocation. Trait type/effect compatibility and formal trait syntax remain pending. EventTarget requires canonical on/dispatch/onEvent arities 3/2/1 after self; native optional convenience forms remain.
+37
View File
@@ -2951,6 +2951,43 @@ page.render(block)
assert!(session.refresh_document().unwrap().is_none());
}
#[test]
fn nullable_page_methods_union_event_options_and_nominal_mount_bounds_compose() {
let (mut session, _) = ReplSession::load(
include_str!("../examples/visual-protocols.endor"),
"app.main",
163,
1,
)
.unwrap();
for (expression, expected) in [
("context = rect.mounts().at(0)", None),
("ui.isInstance(context.bounds(), Bounds)", Some("true")),
("Point(0, 0).inside(context.bounds())", Some("false")),
(
"listener = root.on(\"click\", page.close, Point(0, 0).inCircle(0, 0, 1))",
None,
),
("listener.isConnected()", Some("true")),
("listener.disconnect()", Some("nil")),
("listener.isConnected()", Some("false")),
("page.close()", Some("nil")),
("page.window()", Some("nil")),
("page.open(page.window())", None),
("ui.isInstance(page.window(), Window)", Some("true")),
] {
let result = session.execute(expression, "app.main").unwrap();
if let Some(expected) = expected {
assert_eq!(result.value, expected, "{expression}");
}
}
let error = session
.execute("root.on(\"click\", page.close, 7)", "app.main")
.err()
.expect("invalid union input");
assert!(error.to_string().contains("Bool | String"), "{error:#}");
}
#[test]
fn native_contracts_reject_wrong_types_before_geometry_or_window_effects() {
let (mut session, _) = ReplSession::load(
+35 -1
View File
@@ -225,6 +225,20 @@ pub fn new_event(
Ok(event)
}
#[derive(Debug)]
struct TypedMethod {
method: Method,
signature: endor_core::CallSignature,
}
impl NativeCallable for TypedMethod {
fn signature(&self) -> Option<endor_core::CallSignature> {
Some(self.signature.clone())
}
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
self.method.invoke(cx, args)
}
}
pub fn install(
vm: &mut Vm,
roots: &mut Vec<ObjectRef>,
@@ -244,7 +258,27 @@ pub fn install(
(token_type, "disconnect", Method::Disconnect),
(token_type, "isConnected", Method::Connected),
] {
let function = vm.allocate(ContextNativeFunction::new(name, method, vec![]))?;
let query = matches!(method, Method::Prevented | Method::Connected);
let function = vm.allocate(ContextNativeFunction::new(
name,
TypedMethod {
method,
signature: endor_core::CallSignature::exact(
vec![endor_core::AnnotationType::Nominal(definition)],
if query {
endor_core::AnnotationType::Bool
} else {
endor_core::AnnotationType::Unit
},
if query {
endor_core::CallEffect::ReadOnly
} else {
endor_core::CallEffect::Mutating
},
),
},
vec![],
))?;
roots.push(function);
vm.attach_type(function, function_type)?;
vm.define_method(definition, name, function)?;
+46
View File
@@ -65,12 +65,16 @@ enum Query {
#[derive(Debug)]
struct Method {
signature: endor_core::CallSignature,
owner: ObjectRef,
desktop: WeakObjectRef,
query: Query,
}
impl NativeCallable for Method {
fn signature(&self) -> Option<endor_core::CallSignature> {
Some(self.signature.clone())
}
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(
if matches!(
@@ -158,6 +162,12 @@ impl NativeCallable for Method {
]
};
let bounds = cx.new_object()?;
let endor_core::CallType::Single(endor_core::AnnotationType::Nominal(definition)) =
&self.signature.returns
else {
return Err(VmError::InvalidArgument("invalid Mount bounds signature"));
};
cx.attach_type(bounds, *definition)?;
for (name, value) in [
("x", geometry[0]),
("y", geometry[1]),
@@ -234,6 +244,41 @@ impl NativeCallable for Method {
}
}
fn mount_signature(
store: &endor_core::ObjectStore,
desktop: ObjectRef,
query: Query,
) -> Result<endor_core::CallSignature, VmError> {
use endor_core::{AnnotationType as T, CallEffect, CallSignature, CallType};
let nominal = |name| crate::ui_bridge::desktop_type(store, name, desktop).map(T::Nominal);
let returns = match query {
Query::Bounds => nominal("Bounds")?.into(),
Query::Parent | Query::FirstChild | Query::NextSibling => {
CallType::nullable(nominal("Mount")?)
}
Query::Page => CallType::nullable(nominal("Page")?),
Query::HitTest => T::Bool.into(),
Query::ScrollPosition => T::Any.into(),
_ => T::Unit.into(),
};
let effect = if matches!(
query,
Query::ScrollTo | Query::ScrollBy | Query::ScrollIntoView | Query::Focus
) {
CallEffect::Mutating
} else {
CallEffect::ReadOnly
};
let parameters = if matches!(query, Query::HitTest | Query::ScrollTo | Query::ScrollBy) {
vec![T::Number, T::Number]
} else {
vec![]
};
let mut signature = CallSignature::exact(parameters, T::Any, effect);
signature.returns = returns;
Ok(signature)
}
#[derive(Debug, Default, Clone)]
pub struct MountTable {
pub active: HashMap<u64, WeakObjectRef>,
@@ -365,6 +410,7 @@ impl NativeCallable for Sync {
let function = cx.allocate(ContextNativeFunction::new(
name,
Method {
signature: mount_signature(cx.store(), self.desktop, query)?,
owner: object,
desktop: cx.store().downgrade(self.desktop)?,
query,
+7
View File
@@ -28,6 +28,13 @@ impl ObjectPayload for ObjectList {
#[derive(Debug)]
struct At(ObjectRef);
impl NativeCallable for At {
fn signature(&self) -> Option<endor_core::CallSignature> {
Some(endor_core::CallSignature::exact(
vec![endor_core::AnnotationType::Number],
endor_core::AnnotationType::Any,
endor_core::CallEffect::ReadOnly,
))
}
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(1))
}
+35 -1
View File
@@ -192,10 +192,14 @@ pub fn cancel_owner(cx: &mut NativeContext<'_>, owner: ObjectRef) -> Result<(),
#[derive(Debug)]
struct After {
signature: endor_core::CallSignature,
index: Index,
desktop: ObjectRef,
}
impl NativeCallable for After {
fn signature(&self) -> Option<endor_core::CallSignature> {
Some(self.signature.clone())
}
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(4))
}
@@ -324,10 +328,14 @@ enum Query {
}
#[derive(Debug)]
struct Method {
signature: endor_core::CallSignature,
query: Query,
desktop: ObjectRef,
}
impl NativeCallable for Method {
fn signature(&self) -> Option<endor_core::CallSignature> {
Some(self.signature.clone())
}
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(1))
}
@@ -551,6 +559,16 @@ pub fn install(
let after = vm.allocate(ContextNativeFunction::new(
"after",
After {
signature: endor_core::CallSignature::exact(
vec![
endor_core::AnnotationType::Any,
endor_core::AnnotationType::String,
endor_core::AnnotationType::Number,
endor_core::AnnotationType::Function,
],
endor_core::AnnotationType::Nominal(definition),
endor_core::CallEffect::Mutating,
),
index: index.clone(),
desktop,
},
@@ -565,7 +583,23 @@ pub fn install(
] {
let method = vm.allocate(ContextNativeFunction::new(
name,
Method { query, desktop },
Method {
query,
desktop,
signature: endor_core::CallSignature::exact(
vec![endor_core::AnnotationType::Nominal(definition)],
if matches!(query, Query::Status) {
endor_core::AnnotationType::String
} else {
endor_core::AnnotationType::Bool
},
if matches!(query, Query::Cancel) {
endor_core::CallEffect::Mutating
} else {
endor_core::CallEffect::ReadOnly
},
),
},
vec![desktop],
))?;
roots.push(method);
+24 -13
View File
@@ -3,9 +3,9 @@ use std::sync::{Arc, Mutex};
use anyhow::{Result, bail};
use endor_core::{
AnnotationType, CallEffect, CallSignature, ContextNativeFunction, EndorReal, EndorTypeObject,
NativeCallable, NativeContext, NativeResult, ObjectRef, TypeConstructor, Value, Vm, VmError,
WeakObjectRef,
AnnotationType, CallEffect, CallSignature, CallType, ContextNativeFunction, EndorReal,
EndorTypeObject, NativeCallable, NativeContext, NativeResult, ObjectRef, TypeConstructor,
Value, Vm, VmError, WeakObjectRef,
};
use crate::{
@@ -326,7 +326,7 @@ fn method_signature(
desktop: ObjectRef,
receiver: ObjectRef,
operation: Operation,
) -> Result<Option<CallSignature>, VmError> {
) -> Result<CallSignature, VmError> {
use AnnotationType::{Any, Function, Nominal, Number, String as TextType, Unit};
use Operation::*;
let nominal = |name| desktop_type(store, name, desktop).map(Nominal);
@@ -368,21 +368,32 @@ fn method_signature(
Activate => (vec![], Nominal(receiver), CallEffect::Mutating),
Detach => (vec![], nominal("Window")?, CallEffect::Mutating),
Unmount | Close | CloseWindow | Back | Forward => (vec![], Unit, CallEffect::Mutating),
// These accept nullable/union values not yet expressible in AnnotationType.
// Keep absent metadata explicit instead of narrowing existing calls.
Open | PageWindow | On => return Ok(None),
Open => (vec![Any], Nominal(receiver), CallEffect::Mutating),
PageWindow => (vec![], Any, CallEffect::ReadOnly),
On => (
vec![TextType, Function, Any],
nominal("Subscription")?,
CallEffect::Mutating,
),
_ => return Err(invalid("unsupported native method signature")),
};
let arity = operation.arity();
let mut parameters = vec![Nominal(receiver)];
parameters.extend(arguments);
Ok(Some(CallSignature {
let mut signature = CallSignature {
arity: endor_core::CallArity::range(arity.required + 1, arity.maximum + 1),
parameters,
parameters: parameters.into_iter().map(CallType::from).collect(),
variadic: None,
returns,
returns: returns.into(),
effect,
}))
};
match operation {
Open => signature.parameters[1] = CallType::nullable(nominal("Window")?),
PageWindow => signature.returns = CallType::nullable(nominal("Window")?),
On => signature.parameters[3] = CallType::Union(vec![AnnotationType::Bool, TextType]),
_ => {}
}
Ok(signature)
}
#[derive(Debug)]
@@ -397,7 +408,7 @@ struct Function {
/// individual Blocks no longer allocate owner-capturing method closures.
#[derive(Debug)]
struct TypeMethod {
signature: Option<CallSignature>,
signature: CallSignature,
operation: Operation,
expected: ObjectRef,
session: Shared,
@@ -405,7 +416,7 @@ struct TypeMethod {
impl NativeCallable for TypeMethod {
fn signature(&self) -> Option<CallSignature> {
self.signature.clone()
Some(self.signature.clone())
}
fn arity(&self) -> Option<endor_core::CallArity> {
let arity = self.operation.arity();
+1 -1