feature.bend

bendvy-ecs-core@0.1.0.3 · 180 lines · 40 definitions

1import Base
2import bendvy-ecs-core@0.1.0.3/schema_fragments.bend as SF
3import bendvy-ecs-core@0.1.0.3/execution_composer_layout.bend as Layout
4import bendvy-ecs-core@0.1.0.3/execution_composer_schema.bend as Schema
5import bendvy-ecs-core@0.1.0.3/execution_registry.bend as Registry
6import bendvy-ecs-core@0.1.0.3/dependency_registration.bend as Reg
7import bendvy-ecs-core@0.1.0.3/dependency_executor.bend as Exec
8import bendvy-ecs-core@0.1.0.3/dependency_composer_program.bend as Scoped
9import bendvy-ecs-core@0.1.0.3/dependency_composer.bend as Compose
10import bendvy-ecs-core@0.1.0.3/dependency_plan.bend as Plan
11
12# Recursive author metadata contains no payload owners or gameplay authority.
13# Detached feature selection metadata with named dependencies and schema
14# entries. A dependency reference does not automatically select/build it;
15# selected features must include required names. Owns no live world or payload.
16type Tree<S:Data> is Data:
17  Empty{}
18  Feature{name:String,entries:List<&2,SF.Entry<S>>,dependencies:Tree<S>}
19  Sequence{left:Tree<S>,right:Tree<S>}
20# Feature composition refusal: duplicate selected feature, absent dependency
21# name or schema descriptor conflict. No builder has run on validation refusal.
22type Error is Data:
23  DuplicateFeature{name:String}
24  MissingDependency{name:String}
25  SchemaConflict{error:SF.Error}
26# Pure feature validation result; owns no application payload or context.
27type Validation is Data:
28  Valid{}
29  Invalid{error:Error}
30# Validated composition outcome. Built owns build output; Rejected returns
31# the original affine Owners with the validation error for recovery or retry.
32type Composition<S:Data,Owners:Type,Output:Type> is Type:
33  Built{output:Output}
34  Rejected{error:Error,owners:Owners}
35# List selected feature names in authored sequence order, without recursively
36# treating dependency references as additional selected features.
37def names(~S:Data,tree:Tree<S>) -> List<&2,String>:
38  match tree:
39    case Empty{}: []
40    case Feature{name,_,_}: [name]
41    case Sequence{left,right}: List.append(&2,String,names(~S,left),names(~S,right))
42# List entries from selected features in authored sequence order. Dependency
43# metadata does not implicitly add those features or their schema entries.
44def entries(~S:Data,tree:Tree<S>) -> List<&2,SF.Entry<S>>:
45  match tree:
46    case Empty{}: []
47    case Feature{_,entries,_}: entries
48    case Sequence{left,right}: List.append(&2,SF.Entry<S>,entries(~S,left),entries(~S,right))
49# Collect own and referenced dependency entries recursively for visibility
50# description. This does not select/build dependencies or validate composition.
51def visibility(~S:Data,tree:Tree<S>) -> List<&2,SF.Entry<S>>:
52  match tree:
53    case Empty{}: []
54    case Feature{_,own,dependencies}: List.append(&2,SF.Entry<S>,own,visibility(~S,dependencies))
55    case Sequence{left,right}: List.append(&2,SF.Entry<S>,visibility(~S,left),visibility(~S,right))
56def contains(items:List<&2,String>,+name:String) -> Bool:
57  match items:
58    case []: False{}
59    case head <> rest: Bool.or(String.eq(head,name),contains(rest,name))
60def repeated(present:Bool,name:String,next:Unit -> Validation) -> Validation:
61  match present:
62    case True{}: Invalid{DuplicateFeature{name}}
63    case False{}: next(Unit{})
64def duplicates(items:List<&2,String>,+seen:List<&2,String>) -> Validation:
65  match items:
66    case []: Valid{}
67    case +name <> rest: repeated(contains(seen,name),name,_ => duplicates(rest,name <> seen))
68def missing(present:Bool,name:String,next:Unit -> Validation) -> Validation:
69  match present:
70    case False{}: Invalid{MissingDependency{name}}
71    case True{}: next(Unit{})
72def required(items:List<&2,String>,+selected:List<&2,String>) -> Validation:
73  match items:
74    case []: Valid{}
75    case +name <> rest: missing(contains(selected,name),name,_ => required(rest,selected))
76def continue_validation(result:Validation,next:Unit -> Validation) -> Validation:
77  match result:
78    case Invalid{error}: Invalid{error}
79    case Valid{}: next(Unit{})
80def dependencies(~S:Data,tree:Tree<S>,+selected:List<&2,String>) -> Validation:
81  match tree:
82    case Empty{}: Valid{}
83    case Feature{_,_,deps}: required(names(~S,deps),selected)
84    case Sequence{left,right}: continue_validation(dependencies(~S,left,selected),_ => dependencies(~S,right,selected))
85def schema_validation(result:SF.Validation) -> Validation:
86  match result:
87    case SF.Valid{}: Valid{}
88    case SF.Invalid{error}: Invalid{SchemaConflict{error}}
89# Check selected feature names, required dependency names and schema entries
90# before any owned builder runs. Dependencies are name requirements, not an
91# automatic dependency resolver or topological reorder of authored selection.
92def validate(~S:Data,+tree:Tree<S>) -> Validation:
93  continue_validation(duplicates(names(~S,tree),[]),_ => continue_validation(dependencies(~S,tree,names(~S,tree)),_ => schema_validation(SF.validate(~S,entries(~S,tree)))))
94def compose_checked(~S:Data,~Owners:Type,~Output:Type,~build:Owners -> Output,result:Validation,owners:Owners) -> Composition<S,Owners,Output>:
95  match result:
96    case Invalid{error}: Rejected{error,owners}
97    case Valid{}: Built{build(owners)}
98# Validate the metadata tree before passing affine Owners to the trusted
99# build callback. Invalid returns Rejected with original owners; valid invokes
100# build once. Does not make arbitrary builder effects transactional.
101# Contract and accepted consumers: docs/parity/features.md.
102def compose(~S:Data,~Owners:Type,~Output:Type,~build:Owners -> Output,tree:Tree<S>,owners:Owners) -> Composition<S,Owners,Output>:
103  compose_checked(~S,~Owners,~Output,~build,validate(~S,tree),owners)
104
105# Heterogeneous affine head/tail feature owners or outputs, preserving order.
106# Nest this product for additional features; payloads need not be Data.
107type Pack<Head:Type,Tail:Type> is Type:
108  Pack{head:Head,tail:Tail}
109
110type Definition<S:Data> is Data:
111  Definition{name:String,entries:List<&2,SF.Entry<S>>,dependencies:Tree<S>}
112def definition_tree(~S:Data,definition:Definition<S>) -> Tree<S>:
113  match definition:
114    case Definition{name,entries,dependencies}:Feature{name,entries,dependencies}
115
116# Entries and node IDs come from the same accepted declarations that execute.
117# Scope paths retain feature boundaries before dependency edges are flattened.
118type Assembly<-S:Data,-ActualRegistry:Type> is Type:
119  Assembly{selected:Tree<S>,facts:Registry.Facts<ActualRegistry>,scope:Scoped.Scope}
120type AssemblyError is Data:
121  InvalidFeatures{error:Error}
122  InvalidComposition{error:Compose.Error}
123type Lowered<-ActualRegistry:Type> is Type:
124  Lowered{facts:Registry.Facts<ActualRegistry>,edges:List<&2,Plan.Edge>,invalid:Maybe<&2,AssemblyError>}
125def needs_key(nodes:List<&2,Plan.Node<String>>,+key:String) -> Bool:
126  match nodes:
127    case []:False{}
128    case Plan.Node{_,needs} <> rest:Bool.or(contains(needs,key),needs_key(rest,key))
129def selected_entry(~S:Data,selected:Bool,entry:SF.Entry<S>,rest:List<&2,SF.Entry<S>>) -> List<&2,SF.Entry<S>>:
130  match selected:
131    case True{}:entry <> rest
132    case False{}:rest
133def own_entries(~S:Data,entries:List<&2,SF.Entry<S>>,+nodes:List<&2,Plan.Node<String>>) -> List<&2,SF.Entry<S>>:
134  match entries:
135    case []:[]
136    case +entry <> rest:selected_entry(~S,needs_key(nodes,Compose.entry_key(~S,entry)),entry,own_entries(~S,rest,nodes))
137def from_facts(~S:Data,~components:Layout.Layout,~resources:Layout.Layout,~World:Type,~Nodes:Type,~Error:Data,~Out:Data,~dispatch:Nodes -> World -> U32 -> Nodes & (World & Exec.Invocation<Error,Out>),name:String,dependencies:Tree<S>,facts:Registry.Facts<Reg.Registry<World,Nodes,Error,Out,dispatch>>) -> Assembly<S,Reg.Registry<World,Nodes,Error,Out,dispatch>>:
138  match facts:
139    case Registry.Facts{Reg.Registry{nodes,+planNodes},deferred}:Assembly{Feature{name,own_entries(~S,Schema.inventory(~S,~components,~resources),planNodes),dependencies},Registry.Facts{Reg.Registry{nodes,planNodes},deferred},Scoped.Scope{Scoped.ids(planNodes),[]}}
140def scope_members(scope:Scoped.Scope) -> List<&2,U32>:
141  match scope:
142    case Scoped.Scope{members,_}:members
143def pair(~S:Data,~World:Type,~Left:Type,~Right:Type,~Error:Data,~Out:Data,~leftDispatch:Left -> World -> U32 -> Left & (World & Exec.Invocation<Error,Out>),~rightDispatch:Right -> World -> U32 -> Right & (World & Exec.Invocation<Error,Out>),left:Assembly<S,Reg.Registry<World,Left,Error,Out,leftDispatch>>,right:Assembly<S,Reg.Registry<World,Right,Error,Out,rightDispatch>>) -> Assembly<S,Reg.Registry<World,Reg.Product<Left,Right>,Error,Out,Reg.paired_dispatch(~World,~Left,~Right,~Error,~Out,~leftDispatch,~rightDispatch)>>:
144  match left right:
145    case Assembly{left,lf,+ls} Assembly{right,rf,+rs}:Assembly{Sequence{left,right},Registry.paired(~World,~Left,~Right,~Error,~Out,~leftDispatch,~rightDispatch,lf,rf),Scoped.Scope{List.append(&2,U32,scope_members(ls),scope_members(rs)),[ls,rs]}}
146def feature_error(validation:Validation) -> Maybe<&2,AssemblyError>:
147  match validation:
148    case Valid{}:None{}
149    case Invalid{error}:Some{InvalidFeatures{error}}
150def composition_error(error:Maybe<&2,Compose.Error>) -> Maybe<&2,AssemblyError>:
151  match error:
152    case None{}:None{}
153    case Some{error}:Some{InvalidComposition{error}}
154def first_error(first:Maybe<&2,AssemblyError>,second:Maybe<&2,AssemblyError>) -> Maybe<&2,AssemblyError>:
155  match first:
156    case Some{error}:Some{error}
157    case None{}:second
158def nesting_error(error:Maybe<&2,Scoped.NestError>) -> Maybe<&2,Compose.Error>:
159  match error:
160    case None{}:None{}
161    case Some{error}:Some{Compose.Nesting{error}}
162def lower_nested(~S:Data,~components:Layout.Layout,~resources:Layout.Layout,~World:Type,~Nodes:Type,~Error:Data,~Out:Data,~dispatch:Nodes -> World -> U32 -> Nodes & (World & Exec.Invocation<Error,Out>),selected:Tree<S>,facts:Registry.Facts<Reg.Registry<World,Nodes,Error,Out,dispatch>>,nested:Scoped.Nesting) -> Lowered<Reg.Registry<World,Nodes,Error,Out,dispatch>>:
163  match facts nested:
164    case Registry.Facts{Reg.Registry{nodes,+planNodes},deferred} Scoped.Nesting{edges,invalid}:Lowered{Registry.Facts{Reg.Registry{nodes,planNodes},deferred},edges,first_error(feature_error(validate(~S,selected)),composition_error(Compose.first_error(nesting_error(invalid),Compose.first_error(Compose.schema_error(Schema.validate(~S,~components,~resources)),Compose.schedule_error(Compose.missing(~S,planNodes,Schema.inventory(~S,~components,~resources)))))))}
165def lower(~S:Data,~components:Layout.Layout,~resources:Layout.Layout,~World:Type,~Nodes:Type,~Error:Data,~Out:Data,~dispatch:Nodes -> World -> U32 -> Nodes & (World & Exec.Invocation<Error,Out>),groups:List<&2,Scoped.Nested>,assembly:Assembly<S,Reg.Registry<World,Nodes,Error,Out,dispatch>>) -> Lowered<Reg.Registry<World,Nodes,Error,Out,dispatch>>:
166  match assembly:
167    case Assembly{selected,facts,scope}:lower_nested(~S,~components,~resources,~World,~Nodes,~Error,~Out,~dispatch,selected,facts,Scoped.nested(groups,scope))
168
169type Application<-Root:Type,-Provisions:Type> is Type:
170  Application{root:Root,provisions:Provisions,invalid:Maybe<&2,AssemblyError>}
171type Report<Out:Data> is Data:
172  Executed{report:Out}
173  Refused{error:AssemblyError}
174def returned(-Root:Type,-Provisions:Type,-Out:Data,provisions:Provisions,result:Root & Out) -> Application<Root,Provisions> & Report<Out>:
175  match result:
176    case (root,report):(Application{root,provisions,None{}},Executed{report})
177def run(-Root:Type,-Provisions:Type,-Out:Data,invoke:Root -> Root & Out,application:Application<Root,Provisions>) -> Application<Root,Provisions> & Report<Out>:
178  match application:
179    case Application{root,provisions,Some{+error}}:(Application{root,provisions,Some{error}},Refused{error})
180    case Application{root,provisions,None{}}:returned(Root,Provisions,Out,provisions,invoke(root))