1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
31
32use logicaffeine_base::{Interner, Symbol};
33
34use crate::ast::component::{ComponentDef, Trigger};
35use crate::ast::stmt::{Block, ClosureBody, Expr, Pattern, Stmt, StringPart};
36use crate::ast::view::{Attr, ViewNode};
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum DepError {
41 DerivedCycle(Vec<String>),
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum SiteKind {
52 Text,
54 Attr(Symbol),
56 Class(Symbol),
58 If,
60 Each,
62 Bind(Symbol),
64 Inspect,
66}
67
68#[derive(Debug, Clone)]
70pub struct SiteDeps {
71 pub path: String,
73 pub kind: SiteKind,
74 pub dynamic: bool,
82 pub reads: BTreeSet<Symbol>,
84 pub prop_reads: BTreeSet<Symbol>,
87}
88
89#[derive(Debug, Clone)]
91pub struct ComponentDeps {
92 pub sites: Vec<SiteDeps>,
94 pub derived: BTreeMap<Symbol, BTreeSet<Symbol>>,
96 pub writes: BTreeMap<String, BTreeSet<Symbol>>,
98}
99
100impl SiteDeps {
101 pub fn key(&self, interner: &Interner) -> String {
108 let name = |s: &Symbol| interner.resolve(*s).to_string();
109 let kind = match &self.kind {
110 SiteKind::Text => "text".to_string(),
111 SiteKind::Attr(a) => format!("attr:{}", name(a)),
112 SiteKind::Class(c) => format!("class:{}", name(c)),
113 SiteKind::If => "if".to_string(),
114 SiteKind::Each => "each".to_string(),
115 SiteKind::Bind(f) => format!("bind:{}", name(f)),
116 SiteKind::Inspect => "inspect".to_string(),
117 };
118 format!("{kind}@{}", self.path)
119 }
120
121 pub fn patchable(&self) -> bool {
128 !self.dynamic && matches!(self.kind, SiteKind::Text | SiteKind::Bind(_))
138 }
139}
140
141impl ComponentDeps {
142 pub fn sites_reading(&self, field: Symbol) -> impl Iterator<Item = &SiteDeps> {
144 self.sites.iter().filter(move |s| s.reads.contains(&field))
145 }
146
147 pub fn describe(&self, interner: &Interner) -> Vec<String> {
151 let name = |s: &Symbol| interner.resolve(*s).to_string();
152 let list = |set: &BTreeSet<Symbol>| {
153 let mut v: Vec<String> = set.iter().map(name).collect();
154 v.sort();
155 v
156 };
157 let mut out = Vec::new();
158 for site in &self.sites {
159 let key = site.key(interner);
160 let mut parts: Vec<String> = Vec::new();
161 if !site.reads.is_empty() {
162 parts.push(list(&site.reads).join(", "));
163 }
164 if !site.prop_reads.is_empty() {
165 parts.push(format!("props {}", list(&site.prop_reads).join(", ")));
166 }
167 let reads = if parts.is_empty() {
168 "—".to_string()
169 } else {
170 parts.join(" ")
171 };
172 out.push(format!("{key} reads {reads}"));
173 }
174 for (field, from) in &self.derived {
175 let from = if from.is_empty() {
176 "—".to_string()
177 } else {
178 list(from).join(", ")
179 };
180 out.push(format!("derived {} <- {from}", name(field)));
181 }
182 for (turn, fields) in &self.writes {
183 let f = if fields.is_empty() {
184 "—".to_string()
185 } else {
186 list(fields).join(", ")
187 };
188 out.push(format!("writes {turn} -> {f}"));
189 }
190 out
191 }
192}
193
194#[derive(Debug, Clone, Default)]
196struct Resolved {
197 state: BTreeSet<Symbol>,
198 props: BTreeSet<Symbol>,
199}
200
201impl Resolved {
202 fn merge(&mut self, other: &Resolved) {
203 self.state.extend(other.state.iter().copied());
204 self.props.extend(other.props.iter().copied());
205 }
206}
207
208pub fn component_deps(
210 def: &ComponentDef<'_>,
211 interner: &Interner,
212) -> Result<ComponentDeps, DepError> {
213 let plain: HashSet<Symbol> = def
214 .state
215 .iter()
216 .filter(|f| !f.derived)
217 .map(|f| f.name)
218 .collect();
219 let derived_fields: Vec<Symbol> = def
220 .state
221 .iter()
222 .filter(|f| f.derived)
223 .map(|f| f.name)
224 .collect();
225 let derived_set: HashSet<Symbol> = derived_fields.iter().copied().collect();
226 let props: HashSet<Symbol> = def.props.iter().map(|(n, _)| *n).collect();
227
228 let mut direct: HashMap<Symbol, Vec<Symbol>> = HashMap::new();
230 for field in def.state.iter().filter(|f| f.derived) {
231 let mut names = Vec::new();
232 collect_names(field.init, &mut names);
233 let deps: Vec<Symbol> = names
234 .into_iter()
235 .filter(|n| plain.contains(n) || derived_set.contains(n))
236 .collect();
237 direct.insert(field.name, deps);
238 }
239
240 let mut base: HashMap<Symbol, BTreeSet<Symbol>> = HashMap::new();
242 for field in &derived_fields {
243 let mut stack = Vec::new();
244 let mut visiting = HashSet::new();
245 close_derived(
246 *field,
247 &direct,
248 &plain,
249 &mut base,
250 &mut visiting,
251 &mut stack,
252 interner,
253 )?;
254 }
255
256 let ctx = Ctx {
257 plain,
258 derived: derived_set,
259 props,
260 base,
261 };
262
263 let mut sites = Vec::new();
265 if let Some(view) = &def.view {
266 let mut env: HashMap<Symbol, Resolved> = HashMap::new();
267 walk_nodes(&view.nodes, "", &ctx, &mut env, &mut sites, false);
268 }
269
270 let mut writes: BTreeMap<String, BTreeSet<Symbol>> = BTreeMap::new();
272 for action in &def.actions {
273 let mut shadowed: HashSet<Symbol> = action.params.iter().map(|(n, _)| *n).collect();
274 let mut set = BTreeSet::new();
275 writes_of_block(&action.body, &ctx, &mut shadowed, &mut set);
276 set.retain(|n| ctx.plain.contains(n));
277 writes.insert(interner.resolve(action.name).to_string(), set);
278 }
279 for handler in &def.events {
280 let mut shadowed = HashSet::new();
281 let mut set = BTreeSet::new();
282 writes_of_block(&handler.body, &ctx, &mut shadowed, &mut set);
283 set.retain(|n| ctx.plain.contains(n));
284 writes.insert(trigger_label(&handler.trigger, interner), set);
285 }
286
287 let derived_out: BTreeMap<Symbol, BTreeSet<Symbol>> = derived_fields
288 .iter()
289 .map(|f| (*f, ctx.base.get(f).cloned().unwrap_or_default()))
290 .collect();
291
292 Ok(ComponentDeps {
293 sites,
294 derived: derived_out,
295 writes,
296 })
297}
298
299pub fn block_writes(def: &ComponentDef<'_>, block: Block<'_>, _interner: &Interner) -> Vec<Symbol> {
306 let plain: HashSet<Symbol> = def
307 .state
308 .iter()
309 .filter(|f| !f.derived)
310 .map(|f| f.name)
311 .collect();
312 let ctx = Ctx {
313 plain,
314 derived: def
315 .state
316 .iter()
317 .filter(|f| f.derived)
318 .map(|f| f.name)
319 .collect(),
320 props: def.props.iter().map(|(n, _)| *n).collect(),
321 base: HashMap::new(),
322 };
323 let mut shadowed = HashSet::new();
324 let mut out = BTreeSet::new();
325 writes_of_block(block, &ctx, &mut shadowed, &mut out);
326 out.into_iter().filter(|n| ctx.plain.contains(n)).collect()
327}
328
329pub fn block_prop_writes(
334 def: &ComponentDef<'_>,
335 block: Block<'_>,
336 bound: &[Symbol],
337) -> Vec<Symbol> {
338 let ctx = Ctx {
339 plain: def
340 .state
341 .iter()
342 .filter(|f| !f.derived)
343 .map(|f| f.name)
344 .collect(),
345 derived: def
346 .state
347 .iter()
348 .filter(|f| f.derived)
349 .map(|f| f.name)
350 .collect(),
351 props: def.props.iter().map(|(n, _)| *n).collect(),
352 base: HashMap::new(),
353 };
354 let mut shadowed: HashSet<Symbol> = bound.iter().copied().collect();
355 let mut out = BTreeSet::new();
356 writes_of_block(block, &ctx, &mut shadowed, &mut out);
357 out.into_iter().filter(|n| ctx.props.contains(n)).collect()
358}
359
360struct Ctx {
362 plain: HashSet<Symbol>,
363 derived: HashSet<Symbol>,
364 props: HashSet<Symbol>,
365 base: HashMap<Symbol, BTreeSet<Symbol>>,
366}
367
368impl Ctx {
369 fn resolve_name(&self, name: Symbol, env: &HashMap<Symbol, Resolved>) -> Resolved {
371 if let Some(found) = env.get(&name) {
374 return found.clone();
375 }
376 if self.derived.contains(&name) {
377 let state = self.base.get(&name).cloned().unwrap_or_default();
378 return Resolved {
379 state,
380 props: BTreeSet::new(),
381 };
382 }
383 if self.plain.contains(&name) {
384 return Resolved {
385 state: [name].into_iter().collect(),
386 props: BTreeSet::new(),
387 };
388 }
389 if self.props.contains(&name) {
390 return Resolved {
391 state: BTreeSet::new(),
392 props: [name].into_iter().collect(),
393 };
394 }
395 Resolved::default()
396 }
397
398 fn resolve_expr(&self, expr: &Expr<'_>, env: &HashMap<Symbol, Resolved>) -> Resolved {
400 let mut names = Vec::new();
401 collect_names(expr, &mut names);
402 let mut out = Resolved::default();
403 for n in names {
404 let r = self.resolve_name(n, env);
405 out.merge(&r);
406 }
407 out
408 }
409}
410
411fn close_derived(
413 field: Symbol,
414 direct: &HashMap<Symbol, Vec<Symbol>>,
415 plain: &HashSet<Symbol>,
416 base: &mut HashMap<Symbol, BTreeSet<Symbol>>,
417 visiting: &mut HashSet<Symbol>,
418 stack: &mut Vec<Symbol>,
419 interner: &Interner,
420) -> Result<BTreeSet<Symbol>, DepError> {
421 if let Some(done) = base.get(&field) {
422 return Ok(done.clone());
423 }
424 if !visiting.insert(field) {
425 let mut names: Vec<String> = stack
428 .iter()
429 .map(|s| interner.resolve(*s).to_string())
430 .collect();
431 names.push(interner.resolve(field).to_string());
432 return Err(DepError::DerivedCycle(names));
433 }
434 stack.push(field);
435 let mut out = BTreeSet::new();
436 for dep in direct.get(&field).map(|v| v.as_slice()).unwrap_or(&[]) {
437 if plain.contains(dep) {
438 out.insert(*dep);
439 } else {
440 let inner = close_derived(*dep, direct, plain, base, visiting, stack, interner)?;
441 out.extend(inner);
442 }
443 }
444 stack.pop();
445 visiting.remove(&field);
446 base.insert(field, out.clone());
447 Ok(out)
448}
449
450fn collect_names(expr: &Expr<'_>, out: &mut Vec<Symbol>) {
456 match expr {
457 Expr::Identifier(s) => out.push(*s),
458 Expr::Literal(_) | Expr::OptionNone | Expr::Escape { .. } => {}
459 Expr::BinaryOp { left, right, .. }
460 | Expr::Contains {
461 collection: left,
462 value: right,
463 }
464 | Expr::Union { left, right }
465 | Expr::Intersection { left, right }
466 | Expr::Range {
467 start: left,
468 end: right,
469 }
470 | Expr::Index {
471 collection: left,
472 index: right,
473 }
474 | Expr::WithCapacity {
475 value: left,
476 capacity: right,
477 }
478 | Expr::ChunkAt {
479 index: left,
480 zone: right,
481 } => {
482 collect_names(left, out);
483 collect_names(right, out);
484 }
485 Expr::Not { operand }
486 | Expr::Copy { expr: operand }
487 | Expr::Give { value: operand }
488 | Expr::Length {
489 collection: operand,
490 }
491 | Expr::ManifestOf { zone: operand }
492 | Expr::OptionSome { value: operand } => collect_names(operand, out),
493 Expr::Slice {
494 collection,
495 start,
496 end,
497 } => {
498 collect_names(collection, out);
499 collect_names(start, out);
500 collect_names(end, out);
501 }
502 Expr::Call { args, .. } => args.iter().for_each(|a| collect_names(a, out)),
504 Expr::CallExpr { callee, args } => {
505 collect_names(callee, out);
506 args.iter().for_each(|a| collect_names(a, out));
507 }
508 Expr::List(items) | Expr::Tuple(items) => items.iter().for_each(|i| collect_names(i, out)),
509 Expr::FieldAccess { object, .. } => collect_names(object, out),
511 Expr::New { init_fields, .. } => {
512 init_fields.iter().for_each(|(_, e)| collect_names(e, out))
513 }
514 Expr::NewVariant { fields, .. } => fields.iter().for_each(|(_, e)| collect_names(e, out)),
515 Expr::Closure { body, .. } => match body {
516 ClosureBody::Expression(e) => collect_names(e, out),
517 ClosureBody::Block(b) => b.iter().for_each(|s| collect_names_stmt(s, out)),
518 },
519 Expr::InterpolatedString(parts) => {
520 for part in parts {
521 if let StringPart::Expr { value, .. } = part {
522 collect_names(value, out);
523 }
524 }
525 }
526 Expr::View(_) => {}
528 Expr::Dom(q) => {
531 if let Some(sel) = q.selector() {
532 collect_names(sel, out);
533 }
534 }
535 }
536}
537
538fn collect_names_stmt(stmt: &Stmt<'_>, out: &mut Vec<Symbol>) {
540 let mut sink = |e: &Expr<'_>| collect_names(e, out);
541 match stmt {
542 Stmt::Let { value, .. } => sink(value),
543 Stmt::Show { object, recipient } => {
544 sink(object);
545 sink(recipient);
546 }
547 Stmt::Set { target, value } => {
548 out.push(*target);
549 collect_names(value, out);
550 }
551 Stmt::Return { value } => {
552 if let Some(v) = value {
553 collect_names(v, out);
554 }
555 }
556 Stmt::If {
557 cond,
558 then_block,
559 else_block,
560 } => {
561 collect_names(cond, out);
562 then_block.iter().for_each(|s| collect_names_stmt(s, out));
563 if let Some(b) = else_block {
564 b.iter().for_each(|s| collect_names_stmt(s, out));
565 }
566 }
567 Stmt::While { cond, body, .. } => {
568 collect_names(cond, out);
569 body.iter().for_each(|s| collect_names_stmt(s, out));
570 }
571 Stmt::Repeat { iterable, body, .. } => {
572 collect_names(iterable, out);
573 body.iter().for_each(|s| collect_names_stmt(s, out));
574 }
575 _ => {}
576 }
577}
578
579fn writes_of_block(
581 block: Block<'_>,
582 ctx: &Ctx,
583 shadowed: &mut HashSet<Symbol>,
584 out: &mut BTreeSet<Symbol>,
585) {
586 for stmt in block {
587 writes_of_stmt(stmt, ctx, shadowed, out);
588 }
589}
590
591fn writes_of_stmt(
597 stmt: &Stmt<'_>,
598 ctx: &Ctx,
599 shadowed: &mut HashSet<Symbol>,
600 out: &mut BTreeSet<Symbol>,
601) {
602 let note = |name: Symbol, shadowed: &HashSet<Symbol>, out: &mut BTreeSet<Symbol>| {
603 if !shadowed.contains(&name) {
604 out.insert(name);
605 }
606 };
607 match stmt {
608 Stmt::Let { var, .. } => {
609 shadowed.insert(*var);
610 }
611 Stmt::Set { target, .. } => note(*target, shadowed, out),
612 Stmt::Swap { a, b } => {
613 note(*a, shadowed, out);
614 note(*b, shadowed, out);
615 }
616 Stmt::Push { collection, .. }
619 | Stmt::Add { collection, .. }
620 | Stmt::Remove { collection, .. }
621 | Stmt::SetIndex { collection, .. } => {
622 for n in base_identifiers(collection) {
623 note(n, shadowed, out);
624 }
625 }
626 Stmt::Pop { collection, into } => {
627 for n in base_identifiers(collection) {
628 note(n, shadowed, out);
629 }
630 if let Some(v) = into {
631 note(*v, shadowed, out);
632 }
633 }
634 Stmt::SetField { object, .. }
635 | Stmt::IncreaseCrdt { object, .. }
636 | Stmt::DecreaseCrdt { object, .. } => {
637 for n in base_identifiers(object) {
638 note(n, shadowed, out);
639 }
640 }
641 Stmt::Ui(ui) => {
650 if let Some(name) = ui.writes() {
651 note(name, shadowed, out);
652 }
653 }
654 Stmt::If {
658 then_block,
659 else_block,
660 ..
661 } => {
662 let mut inner = shadowed.clone();
663 writes_of_block(then_block, ctx, &mut inner, out);
664 if let Some(b) = else_block {
665 let mut inner = shadowed.clone();
666 writes_of_block(b, ctx, &mut inner, out);
667 }
668 }
669 Stmt::While { body, .. } => {
670 let mut inner = shadowed.clone();
671 writes_of_block(body, ctx, &mut inner, out);
672 }
673 Stmt::Repeat { pattern, body, .. } => {
674 let mut inner = shadowed.clone();
675 bind_pattern(pattern, &mut inner);
676 writes_of_block(body, ctx, &mut inner, out);
677 }
678 Stmt::Inspect { arms, .. } => {
679 for arm in arms {
680 let mut inner = shadowed.clone();
681 for (_, binding) in &arm.bindings {
682 inner.insert(*binding);
683 }
684 writes_of_block(arm.body, ctx, &mut inner, out);
685 }
686 }
687 Stmt::Splice { body } => writes_of_block(body, ctx, shadowed, out),
690 _ => {}
691 }
692}
693
694fn bind_pattern(pattern: &Pattern, shadowed: &mut HashSet<Symbol>) {
696 match pattern {
697 Pattern::Identifier(s) => {
698 shadowed.insert(*s);
699 }
700 Pattern::Tuple(parts) => {
701 for p in parts {
702 shadowed.insert(*p);
703 }
704 }
705 }
706}
707
708fn base_identifiers(expr: &Expr<'_>) -> Vec<Symbol> {
710 let mut out = Vec::new();
711 collect_names(expr, &mut out);
712 out
713}
714
715fn trigger_label(trigger: &Trigger<'_>, interner: &Interner) -> String {
717 let text_of = |e: &Expr<'_>| match e {
718 Expr::Literal(crate::ast::stmt::Literal::Text(s)) => interner.resolve(*s).to_string(),
719 _ => "<computed>".to_string(),
720 };
721 match trigger {
722 Trigger::PageLoads => "when:page-loads".to_string(),
723 Trigger::Appears => "when:appears".to_string(),
724 Trigger::Disappears => "when:disappears".to_string(),
725 Trigger::RouteChanges => "when:route-changes".to_string(),
726 Trigger::Online => "when:online".to_string(),
727 Trigger::Offline => "when:offline".to_string(),
728 Trigger::Selector {
729 selector, event, ..
730 } => {
731 format!("when:{}:{}", text_of(selector), interner.resolve(*event))
732 }
733 Trigger::Key { key } => format!("when:key:{}", interner.resolve(*key)),
734 Trigger::FieldChanges { store, field } => match store {
735 Some(s) => format!(
736 "when:{}.{}:changes",
737 interner.resolve(*s),
738 interner.resolve(*field)
739 ),
740 None => format!("when:{}:changes", interner.resolve(*field)),
741 },
742 Trigger::Every { .. } => "when:every".to_string(),
743 Trigger::After { .. } => "when:after".to_string(),
744 Trigger::Message { topic, .. } => format!("when:message:{}", text_of(topic)),
745 Trigger::ActionFails { .. } => "when:action-fails".to_string(),
746 Trigger::FetchFails { .. } => "when:fetch-fails".to_string(),
747 Trigger::WindowResizes => "when:resize".to_string(),
748 Trigger::ScrollsToBottom { .. } => "when:scroll-bottom".to_string(),
749 Trigger::FileDropped { .. } => "when:file-drop".to_string(),
750 }
751}
752
753fn walk_nodes(
755 nodes: &[ViewNode<'_>],
756 prefix: &str,
757 ctx: &Ctx,
758 env: &mut HashMap<Symbol, Resolved>,
759 out: &mut Vec<SiteDeps>,
760 dynamic: bool,
761) {
762 for (i, node) in nodes.iter().enumerate() {
763 let path = format!("{prefix}{i}");
764 walk_node(node, &path, ctx, env, out, dynamic);
765 }
766}
767
768fn push_site(
769 out: &mut Vec<SiteDeps>,
770 path: &str,
771 kind: SiteKind,
772 resolved: Resolved,
773 dynamic: bool,
774) {
775 out.push(SiteDeps {
776 path: path.to_string(),
777 kind,
778 dynamic,
779 reads: resolved.state,
780 prop_reads: resolved.props,
781 });
782}
783
784fn walk_node(
785 node: &ViewNode<'_>,
786 path: &str,
787 ctx: &Ctx,
788 env: &mut HashMap<Symbol, Resolved>,
789 out: &mut Vec<SiteDeps>,
790 dynamic: bool,
791) {
792 match node {
793 ViewNode::Element {
794 attrs, children, ..
795 } => {
796 for attr in attrs {
797 match attr {
798 Attr::Plain { name, value } => {
802 if !is_static(value) {
803 push_site(
804 out,
805 path,
806 SiteKind::Attr(*name),
807 ctx.resolve_expr(value, env),
808 dynamic,
809 );
810 }
811 }
812 Attr::Class { name, cond } => {
813 push_site(
814 out,
815 path,
816 SiteKind::Class(*name),
817 ctx.resolve_expr(cond, env),
818 dynamic,
819 );
820 }
821 Attr::Bind { field } => {
822 let r = ctx.resolve_name(*field, env);
823 push_site(out, path, SiteKind::Bind(*field), r, dynamic);
824 }
825 Attr::Directive { name, value } => {
826 push_site(
827 out,
828 path,
829 SiteKind::Attr(*name),
830 ctx.resolve_expr(value, env),
831 dynamic,
832 );
833 }
834 }
835 }
836 walk_nodes(children, &format!("{path}."), ctx, env, out, dynamic);
837 }
838 ViewNode::Text { text, .. } => {
839 push_site(
840 out,
841 path,
842 SiteKind::Text,
843 ctx.resolve_expr(text, env),
844 dynamic,
845 );
846 }
847 ViewNode::Component {
848 props, children, ..
849 } => {
850 for (name, value) in props {
853 push_site(
854 out,
855 path,
856 SiteKind::Attr(*name),
857 ctx.resolve_expr(value, env),
858 dynamic,
859 );
860 }
861 walk_nodes(children, &format!("{path}."), ctx, env, out, dynamic);
862 }
863 ViewNode::Each {
864 pattern,
865 iterable,
866 key,
867 body,
868 empty,
869 ..
870 } => {
871 let over = ctx.resolve_expr(iterable, env);
872 push_site(out, path, SiteKind::Each, over.clone(), dynamic);
873 let mut inner = env.clone();
876 bind_pattern_env(pattern, &over, &mut inner);
877 if let Some(k) = key {
878 let _ = ctx.resolve_expr(k, &inner);
879 }
880 walk_nodes(body, &format!("{path}."), ctx, &mut inner, out, true);
881 walk_nodes(empty, &format!("{path}.e"), ctx, env, out, true);
882 }
883 ViewNode::If {
884 arms, otherwise, ..
885 } => {
886 let mut conds = Resolved::default();
889 for (cond, _) in arms {
890 let r = ctx.resolve_expr(cond, env);
891 conds.merge(&r);
892 }
893 push_site(out, path, SiteKind::If, conds, dynamic);
894 for (k, (_, body)) in arms.iter().enumerate() {
895 let mut inner = env.clone();
896 walk_nodes(body, &format!("{path}.{k}."), ctx, &mut inner, out, true);
897 }
898 if let Some(body) = otherwise {
899 let mut inner = env.clone();
900 walk_nodes(body, &format!("{path}.o"), ctx, &mut inner, out, true);
901 }
902 }
903 ViewNode::Inspect {
904 scrutinee, arms, ..
905 } => {
906 let over = ctx.resolve_expr(scrutinee, env);
907 push_site(out, path, SiteKind::Inspect, over.clone(), dynamic);
908 for (k, arm) in arms.iter().enumerate() {
909 let mut inner = env.clone();
910 for (_, binding) in &arm.bindings {
913 inner.insert(*binding, over.clone());
914 }
915 walk_nodes(
916 &arm.body,
917 &format!("{path}.{k}."),
918 ctx,
919 &mut inner,
920 out,
921 true,
922 );
923 }
924 }
925 ViewNode::Let { var, value, .. } => {
926 let r = ctx.resolve_expr(value, env);
927 env.insert(*var, r);
928 }
929 ViewNode::Raw { html, .. } => {
930 push_site(
931 out,
932 path,
933 SiteKind::Text,
934 ctx.resolve_expr(html, env),
935 dynamic,
936 );
937 }
938 ViewNode::Block { default, .. } => {
939 if let Some(body) = default {
940 walk_nodes(body, &format!("{path}."), ctx, env, out, dynamic);
941 }
942 }
943 ViewNode::Handler { .. } | ViewNode::Extends { .. } | ViewNode::Children { .. } => {}
946 }
947}
948
949fn bind_pattern_env(pattern: &Pattern, over: &Resolved, env: &mut HashMap<Symbol, Resolved>) {
951 match pattern {
952 Pattern::Identifier(s) => {
953 env.insert(*s, over.clone());
954 }
955 Pattern::Tuple(parts) => {
956 for p in parts {
957 env.insert(*p, over.clone());
958 }
959 }
960 }
961}
962
963fn is_static(expr: &Expr<'_>) -> bool {
965 match expr {
966 Expr::Literal(_) => true,
967 Expr::InterpolatedString(parts) => {
968 parts.iter().all(|p| matches!(p, StringPart::Literal(_)))
969 }
970 _ => false,
971 }
972}