1use crate::error::SeqError;
2use crate::ir::{
3 BinOp, ComponentDef, MatchPat, SeqApp, SeqExpr, SeqExprKind, SeqFn, SeqNode, SeqStmt, SeqValue,
4 StyleDecl, UnOp, Visibility,
5};
6
7const COMPONENTS: &[&str] = &[
8 "Column",
9 "Row",
10 "Container",
11 "Panel",
12 "ScrollView",
13 "Text",
14 "Button",
15 "Spacer",
16 "Icon",
17 "Image",
18 "List",
19 "VirtualList",
20 "Canvas",
21 "Stack",
22 "Grid",
23 "Wrap",
24 "SplitPanel",
25 "Checkbox",
26 "Switch",
27 "TextField",
28 "TextArea",
29 "Field",
30 "NumberField",
31 "DateField",
32 "MultiSelect",
33 "Slider",
34 "DragValue",
35 "Dropdown",
36 "Progress",
37 "If",
38 "Width",
39 "Height",
40 "Opacity",
41 "Scale",
42 "Rotate",
43 "Translate",
44 "Pulse",
45 "Shake",
46 "Async",
47 "Loading",
48 "Data",
49 "Error",
50 "Idle",
51 "Cancelled",
52 "Effect",
53 "Rect",
54 "Circle",
55 "Ellipse",
56 "Ring",
57 "Line",
58 "Arrow",
59 "Capsule",
60 "Polygon",
61 "Polyline",
62 "Spline",
63 "Star",
64 "Ngon",
65 "Cross",
66 "Text2d",
67 "Svg",
68 "Dots",
69 "Axes",
70 "Frame",
71 "Marquee",
72 "Handle",
73 "Image2d",
74 "Clip",
75 "Group",
76];
77
78const SHAPES: &[&str] = &[
79 "Grid", "Rect", "Circle", "Ellipse", "Ring", "Line", "Arrow", "Capsule", "Polygon", "Polyline",
80 "Spline", "Star", "Ngon", "Cross", "Text2d", "Svg", "Dots", "Axes", "Frame", "Marquee",
81 "Handle", "Image2d", "Clip", "Group",
82];
83
84const LAYOUT_PROPS: &[&str] = &[
85 "width",
86 "height",
87 "padding",
88 "padding_x",
89 "padding_y",
90 "background",
91 "flex",
92];
93
94pub fn check(file: &str, app: &SeqApp) -> Result<(), SeqError> {
95 check_styles(file, app)?;
96 let comps = check_components(file, &app.components, &app.styles)?;
97 let fns = check_fns(file, app)?;
98 check_anims(file, app)?;
99 check_machines(file, app)?;
100 check_springs(file, app)?;
101 check_seq_calls(file, app, &fns)?;
102 let ctx = CheckCtx {
103 styles: &app.styles,
104 components: &app.components,
105 comps: &comps,
106 in_comp: false,
107 };
108 for w in &app.windows {
109 for n in &w.nodes {
110 check_node(file, n, None, false, &ctx)?;
111 }
112 }
113 Ok(())
114}
115
116const COMP_PARAM_TYPES: &[&str] = &["String", "Bool", "Int", "Float", "Color"];
117
118type CompTable = std::collections::HashMap<String, usize>;
119
120struct CheckCtx<'a> {
121 styles: &'a [StyleDecl],
122 components: &'a [ComponentDef],
123 comps: &'a CompTable,
124
125 in_comp: bool,
126}
127
128fn check_components(
129 file: &str,
130 components: &[ComponentDef],
131 styles: &[StyleDecl],
132) -> Result<CompTable, SeqError> {
133 let mut comps: CompTable = std::collections::HashMap::new();
134 for (i, c) in components.iter().enumerate() {
135 if let Some(&prev) = comps.get(&c.name) {
136 let first = &components[prev];
137 return Err(SeqError::new(
138 &c.file,
139 c.line,
140 1,
141 format!(
142 "komponent '{}' zdefiniowany dwa razy (pierwszy w {}:{})",
143 c.name, first.file, first.line
144 ),
145 ));
146 }
147 if COMPONENTS.contains(&c.name.as_str()) {
148 return Err(SeqError::new(
149 &c.file,
150 c.line,
151 1,
152 format!("komponent '{}' koliduje z wbudowanym", c.name),
153 ));
154 }
155 if !c.name.starts_with(|ch: char| ch.is_ascii_uppercase()) {
156 return Err(SeqError::new(
157 &c.file,
158 c.line,
159 1,
160 format!(
161 "component name '{name}' capitalized (like Column)",
162 name = c.name
163 ),
164 ));
165 }
166 let mut seen: Vec<&str> = Vec::new();
167 for p in &c.params {
168 if seen.contains(&p.name.as_str()) {
169 return Err(SeqError::new(
170 &c.file,
171 p.line,
172 1,
173 format!("parametr '{}' dwa razy w {}", p.name, c.name),
174 ));
175 }
176 seen.push(p.name.as_str());
177 if !COMP_PARAM_TYPES.contains(&p.ty.as_str()) {
178 return Err(SeqError::new(
179 &c.file,
180 p.line,
181 1,
182 format!(
183 "nieznany typ parametru '{}' (String/Bool/Int/Float/Color)",
184 p.ty
185 ),
186 ));
187 }
188 }
189 comps.insert(c.name.clone(), i);
190 }
191 for c in components {
192 if count_placeholders(&c.body) > 1 {
193 return Err(SeqError::new(
194 &c.file,
195 c.line,
196 1,
197 format!("jedno `children` w ciele {}", c.name),
198 ));
199 }
200 }
201
202 let body_ctx = CheckCtx {
203 styles,
204 components,
205 comps: &comps,
206 in_comp: true,
207 };
208 for c in components {
209 for n in &c.body {
210 check_node(&c.file, n, None, false, &body_ctx)?;
211 }
212 }
213
214 let mut edges: std::collections::HashMap<String, Vec<String>> =
215 std::collections::HashMap::new();
216 for c in components {
217 edges.insert(c.name.clone(), called_components(&c.body, &comps));
218 }
219 if let Some(cycle) = find_cycle(&edges) {
220 let start = cycle.first().cloned().unwrap_or_default();
221 let line = components
222 .iter()
223 .find(|c| c.name == start)
224 .map(|c| c.line)
225 .unwrap_or(1);
226 let cycle_file = components
227 .iter()
228 .find(|c| c.name == start)
229 .map_or(file, |c| c.file.as_str());
230 let mut path = cycle.join(" -> ");
231 path.push_str(&format!(" -> {start}"));
232 return Err(SeqError::new(
233 cycle_file,
234 line,
235 1,
236 format!("component recursion: {path}"),
237 ));
238 }
239 Ok(comps)
240}
241
242fn count_placeholders(nodes: &[SeqNode]) -> usize {
243 nodes
244 .iter()
245 .map(|n| (usize::from(n.kind == "children")) + count_placeholders(&n.children))
246 .sum()
247}
248
249fn called_components(nodes: &[SeqNode], comps: &CompTable) -> Vec<String> {
250 let mut out = Vec::new();
251 for n in nodes {
252 if comps.contains_key(n.kind.as_str()) && !out.contains(&n.kind) {
253 out.push(n.kind.clone());
254 }
255 for sub in called_components(&n.children, comps) {
256 if !out.contains(&sub) {
257 out.push(sub);
258 }
259 }
260 }
261 out
262}
263
264fn find_cycle(edges: &std::collections::HashMap<String, Vec<String>>) -> Option<Vec<String>> {
265 fn visit(
266 node: &str,
267 edges: &std::collections::HashMap<String, Vec<String>>,
268 state: &mut std::collections::HashMap<String, u8>,
269 path: &mut Vec<String>,
270 ) -> Option<Vec<String>> {
271 state.insert(node.to_string(), 1);
272 path.push(node.to_string());
273 if let Some(next) = edges.get(node) {
274 for n in next {
275 match state.get(n.as_str()).copied().unwrap_or(0) {
276 1 => {
277 let pos = path.iter().position(|s| s == n).unwrap_or(0);
278 return Some(path[pos..].to_vec());
279 }
280 0 => {
281 if let Some(c) = visit(n, edges, state, path) {
282 return Some(c);
283 }
284 }
285 _ => {}
286 }
287 }
288 }
289 path.pop();
290 state.insert(node.to_string(), 2);
291 None
292 }
293 let mut names: Vec<&String> = edges.keys().collect();
294 names.sort();
295 let mut state: std::collections::HashMap<String, u8> = std::collections::HashMap::new();
296 for start in names {
297 if state.get(start).copied().unwrap_or(0) == 0 {
298 let mut path = Vec::new();
299 if let Some(c) = visit(start, edges, &mut state, &mut path) {
300 return Some(c);
301 }
302 }
303 }
304 None
305}
306
307fn has_placeholder(def: &ComponentDef) -> bool {
308 count_placeholders(&def.body) > 0
309}
310
311const BOX_STYLE_FIELDS: &[&str] = &[
312 "background",
313 "radius",
314 "border",
315 "border_left",
316 "border_right",
317 "border_top",
318 "border_bottom",
319 "border_x",
320 "border_y",
321 "shadow",
322 "inset_shadow",
323];
324
325const BUTTON_STYLE_FIELDS: &[&str] = &[
326 "background",
327 "hover",
328 "pressed",
329 "disabled",
330 "text",
331 "text_disabled",
332 "font_size",
333 "font_weight",
334 "radius",
335 "padding",
336 "border",
337 "border_width",
338 "border_color",
339];
340
341const TEXT_STYLE_FIELDS: &[&str] = &[
342 "size", "color", "bold", "semi", "medium", "normal", "light", "black",
343];
344
345const TEXT_WEIGHT_FLAGS: &[&str] = &["bold", "semi", "medium", "normal", "light", "black"];
346
347const BUTTON_PRESETS: &[&str] = &[
348 "primary",
349 "secondary",
350 "danger",
351 "ghost",
352 "dark_filled",
353 "outline_rose",
354];
355
356fn check_styles(file: &str, app: &SeqApp) -> Result<(), SeqError> {
357 for (i, s) in app.styles.iter().enumerate() {
358 if app.styles[..i].iter().any(|o| o.name == s.name) {
359 return Err(SeqError::new(
360 file,
361 s.line,
362 1,
363 format!("styl '{}' zdefiniowany dwa razy", s.name),
364 ));
365 }
366 let fields: &[&str] = match s.target.as_str() {
367 "Box" => BOX_STYLE_FIELDS,
368 "Button" => BUTTON_STYLE_FIELDS,
369 "Text" => TEXT_STYLE_FIELDS,
370 other => {
371 return Err(SeqError::new(
372 file,
373 s.line,
374 1,
375 format!(
376 "style '{}': nieznany typ '{other}' (Box/Button/Text)",
377 s.name
378 ),
379 ));
380 }
381 };
382 let mut weights = 0;
383 for a in &s.args {
384 if !fields.contains(&a.name.as_str()) {
385 return Err(SeqError::new(
386 file,
387 a.line,
388 1,
389 format!(
390 "nieznane pole '{}' w style {}:{} (dozwolone: {})",
391 a.name,
392 s.name,
393 s.target,
394 fields.join(", ")
395 ),
396 ));
397 }
398 if s.target == "Text" && TEXT_WEIGHT_FLAGS.contains(&a.name.as_str()) {
399 weights += 1;
400 }
401 }
402 if s.target == "Text" && weights > 1 {
403 return Err(SeqError::new(
404 file,
405 s.line,
406 1,
407 format!(
408 "style '{}': jedna flaga wagi (bold/semi/medium/normal/light/black)",
409 s.name
410 ),
411 ));
412 }
413 if let Some(base) = &s.base {
414 if s.target == "Button" && BUTTON_PRESETS.contains(&base.as_str()) {
415 continue;
416 }
417 let mut cur = base.as_str();
418 let mut seen = vec![s.name.as_str()];
419 loop {
420 if seen.contains(&cur) {
421 return Err(SeqError::new(
422 file,
423 s.line,
424 1,
425 format!("style '{}': cykliczne extends", s.name),
426 ));
427 }
428 seen.push(cur);
429 let Some(parent) = app.styles.iter().find(|o| o.name == cur) else {
430 return Err(SeqError::new(
431 file,
432 s.line,
433 1,
434 format!(
435 "style '{}': nieznana baza '{cur}' (preset albo styl)",
436 s.name
437 ),
438 ));
439 };
440 if parent.target != s.target {
441 return Err(SeqError::new(
442 file,
443 s.line,
444 1,
445 format!(
446 "style '{}': baza '{}' to {}, a styl to {}",
447 s.name, cur, parent.target, s.target
448 ),
449 ));
450 }
451 if let Some(next) = &parent.base {
452 if s.target == "Button" && BUTTON_PRESETS.contains(&next.as_str()) {
453 break;
454 }
455 cur = next;
456 } else {
457 break;
458 }
459 }
460 }
461 }
462 Ok(())
463}
464
465fn find_style<'s>(styles: &'s [StyleDecl], name: &str) -> Option<&'s StyleDecl> {
466 styles.iter().find(|s| s.name == name)
467}
468
469fn check_node(
470 file: &str,
471 node: &SeqNode,
472 parent: Option<&str>,
473 in_canvas: bool,
474 ctx: &CheckCtx<'_>,
475) -> Result<(), SeqError> {
476 let in_canvas = in_canvas || matches!(parent, Some("Canvas" | "Clip" | "Group"));
477 if node.kind == "children" {
478 if !ctx.in_comp {
479 return Err(SeqError::new(
480 file,
481 node.line,
482 1,
483 "znacznik `children` tylko w ciele component".to_string(),
484 ));
485 }
486 if in_canvas {
487 return Err(SeqError::new(
488 file,
489 node.line,
490 1,
491 "inside Canvas is shapes or List, not 'children' (UI views belong outside Canvas)"
492 .to_string(),
493 ));
494 }
495 if node.label.is_some() || !node.args.is_empty() || !node.children.is_empty() {
496 return Err(SeqError::new(
497 file,
498 node.line,
499 1,
500 "`children` is a bare tag (no props or children)".to_string(),
501 ));
502 }
503 return Ok(());
504 }
505 if let Some(&idx) = ctx.comps.get(node.kind.as_str()) {
506 if in_canvas {
507 return Err(SeqError::new(
508 file,
509 node.line,
510 1,
511 format!(
512 "inside Canvas is shapes or List, not '{}' (UI views belong outside Canvas)",
513 node.kind
514 ),
515 ));
516 }
517 check_call(file, node, ctx, idx)?;
518 return Ok(());
519 }
520 if !COMPONENTS.contains(&node.kind.as_str()) {
521 return Err(SeqError::new(
522 file,
523 node.line,
524 1,
525 format!(
526 "unknown component '{}' (Column/Row/Container/Panel/ScrollView/Text/Button/Spacer/Icon/Image/List/VirtualList/Canvas/Stack/Grid/Wrap/SplitPanel/Checkbox/Switch/TextFiel d/TextArea/Field/NumberField/DateField/MultiSelect/Slider/DragValue/Dropdown/Progress/If/Opacity/Scale/Rotate/Translate/Pulse/Shake/Async/Effect + shapes)",
527 node.kind
528 ),
529 ));
530 }
531
532 if matches!(
533 node.kind.as_str(),
534 "Loading" | "Data" | "Error" | "Idle" | "Cancelled"
535 ) && parent != Some("Async")
536 {
537 return Err(SeqError::new(
538 file,
539 node.line,
540 1,
541 format!("block '{}' only inside Async", node.kind),
542 ));
543 }
544
545 if in_canvas && node.kind != "List" && !SHAPES.contains(&node.kind.as_str()) {
546 return Err(SeqError::new(
547 file,
548 node.line,
549 1,
550 format!(
551 "inside Canvas is shapes or List, not '{}' (UI views belong outside Canvas)",
552 node.kind
553 ),
554 ));
555 }
556 if !in_canvas
557 && node.kind != "Canvas"
558 && node.kind != "Grid"
559 && SHAPES.contains(&node.kind.as_str())
560 {
561 return Err(SeqError::new(
562 file,
563 node.line,
564 1,
565 format!("shape '{}' must lie in Canvas", node.kind),
566 ));
567 }
568 if node.label.is_some() && node.kind != "Text" && node.kind != "Button" && node.kind != "Image"
569 {
570 return Err(SeqError::new(
571 file,
572 node.line,
573 1,
574 format!(
575 "etykieta pozycyjna tylko w Text/Button/Image, nie w {}",
576 node.kind
577 ),
578 ));
579 }
580
581 if node.kind == "Text" {
582 let weights = node
583 .args
584 .iter()
585 .filter(|a| {
586 matches!(
587 a.name.as_str(),
588 "bold" | "semi" | "medium" | "normal" | "light" | "black"
589 )
590 })
591 .count();
592 if weights > 1 {
593 return Err(SeqError::new(
594 file,
595 node.line,
596 1,
597 "Text: jedna flaga wagi (bold/semi/medium/normal/light/black)".to_string(),
598 ));
599 }
600 }
601 for arg in &node.args {
602 check_arg(file, node, arg, parent, in_canvas, ctx.styles)?;
603 }
604 for child in &node.children {
605 check_node(file, child, Some(node.kind.as_str()), in_canvas, ctx)?;
606 }
607 Ok(())
608}
609
610fn check_call(file: &str, node: &SeqNode, ctx: &CheckCtx<'_>, idx: usize) -> Result<(), SeqError> {
611 let def = &ctx.components[idx];
612 if def.visibility == Visibility::Priv && def.file != file {
613 return Err(SeqError::new(
614 file,
615 node.line,
616 1,
617 format!(
618 "komponent '{}' jest prywatny (zdefiniowany w {})",
619 def.name, def.file
620 ),
621 ));
622 }
623 if node.label.is_some() {
624 return Err(SeqError::new(
625 file,
626 node.line,
627 1,
628 format!(
629 "parametry {} tylko nazwane (bez etykiety pozycyjnej)",
630 def.name
631 ),
632 ));
633 }
634 let mut seen: Vec<&str> = Vec::new();
635 for arg in &node.args {
636 if arg.name == "flex" {
637 continue;
638 }
639 let Some(param) = def.params.iter().find(|p| p.name == arg.name) else {
640 let want = if def.params.is_empty() {
641 "(no parameters)".to_string()
642 } else {
643 def.params
644 .iter()
645 .map(|p| p.name.as_str())
646 .collect::<Vec<_>>()
647 .join(", ")
648 };
649 return Err(SeqError::new(
650 file,
651 arg.line,
652 1,
653 format!(
654 "nieznany parametr '{}' w {} (oczekiwano: {want})",
655 arg.name, def.name
656 ),
657 ));
658 };
659 if seen.contains(&arg.name.as_str()) {
660 return Err(SeqError::new(
661 file,
662 arg.line,
663 1,
664 format!("parametr '{}' dwa razy w {}", arg.name, def.name),
665 ));
666 }
667 seen.push(arg.name.as_str());
668 check_comp_value(file, &def.name, param, arg)?;
669 }
670 for p in &def.params {
671 if !seen.contains(&p.name.as_str()) {
672 return Err(SeqError::new(
673 file,
674 node.line,
675 1,
676 format!("brakuje parametru '{}' w {}", p.name, def.name),
677 ));
678 }
679 }
680 if !node.children.is_empty() && !has_placeholder(def) {
681 return Err(SeqError::new(
682 file,
683 node.line,
684 1,
685 format!(
686 "{} nie przyjmuje dzieci (brak `children` w ciele)",
687 def.name
688 ),
689 ));
690 }
691 for child in &node.children {
692 check_node(file, child, Some(node.kind.as_str()), false, ctx)?;
693 }
694 Ok(())
695}
696
697fn check_comp_value(
698 file: &str,
699 comp: &str,
700 param: &crate::ir::CompParam,
701 arg: &crate::ir::SeqArg,
702) -> Result<(), SeqError> {
703 let ok = match &arg.value {
704 SeqValue::Str(_) => param.ty == "String",
705 SeqValue::Bool(_) => param.ty == "Bool",
706 SeqValue::Num(n) => match param.ty.as_str() {
707 "Float" => n.is_finite(),
708 "Int" => n.is_finite() && n.fract() == 0.0,
709 _ => false,
710 },
711 SeqValue::Color(_) | SeqValue::Theme(_) => param.ty == "Color",
712 SeqValue::Ident(_) | SeqValue::Dynamic(_) | SeqValue::Env(_) | SeqValue::ItemField(_) => {
713 true
714 }
715 SeqValue::Length(_) | SeqValue::Tuple(_) | SeqValue::Array(_) => false,
716 SeqValue::Callback { .. } => false,
717
718 SeqValue::CallSeq { .. } => true,
719 };
720 if !ok {
721 return Err(SeqError::new(
722 file,
723 arg.line,
724 1,
725 format!(
726 "bad type of parameter '{}' in {comp} (expected {})",
727 param.name, param.ty
728 ),
729 ));
730 }
731 Ok(())
732}
733
734fn check_arg(
735 file: &str,
736 node: &SeqNode,
737 arg: &crate::ir::SeqArg,
738 parent: Option<&str>,
739 in_canvas: bool,
740 styles: &[StyleDecl],
741) -> Result<(), SeqError> {
742 let name = arg.name.as_str();
743
744 if matches!(
745 name,
746 "indent"
747 | "indent_x"
748 | "indent_y"
749 | "indent_left"
750 | "indent_right"
751 | "indent_top"
752 | "indent_bottom"
753 ) && matches!(
754 node.kind.as_str(),
755 "Column"
756 | "Row"
757 | "Container"
758 | "Panel"
759 | "Stack"
760 | "Grid"
761 | "Wrap"
762 | "ScrollView"
763 | "Text"
764 | "Button"
765 | "Spacer"
766 | "Icon"
767 | "Image"
768 | "List"
769 | "VirtualList"
770 | "Canvas"
771 | "SplitPanel"
772 | "Checkbox"
773 | "Switch"
774 | "TextField"
775 | "Slider"
776 | "Dropdown"
777 | "Progress"
778 | "If"
779 | "Async"
780 | "Effect"
781 ) && !in_canvas
782 {
783 return Ok(());
784 }
785
786 if name == "hit"
787 && matches!(
788 node.kind.as_str(),
789 "Column"
790 | "Row"
791 | "Container"
792 | "Panel"
793 | "Stack"
794 | "Wrap"
795 | "SplitPanel"
796 | "Text"
797 | "Icon"
798 | "Spacer"
799 )
800 && !in_canvas
801 {
802 return Ok(());
803 }
804 if parent == Some("Stack") && matches!(name, "at" | "offset" | "z") {
805 return Ok(());
806 }
807 if parent == Some("Grid") && matches!(name, "col" | "row" | "col_span" | "row_span") {
808 return Ok(());
809 }
810
811 if node.kind == "Grid" && in_canvas {
812 let allowed: &[&str] = &["step", "color", "visible"];
813 if !allowed.contains(&name) {
814 return Err(SeqError::new(
815 file,
816 arg.line,
817 1,
818 format!(
819 "unknown prop '{name}' in Grid (Canvas shape; allowed: {})",
820 allowed.join(", ")
821 ),
822 ));
823 }
824 return Ok(());
825 }
826 let allowed: &[&str] = match node.kind.as_str() {
827 "Column" | "Row" => &[
828 "width",
829 "height",
830 "padding",
831 "padding_x",
832 "padding_y",
833 "background",
834 "flex",
835 "gap",
836 "main_gap",
837 "cross_gap",
838 "wrap",
839 "align_content",
840 "align",
841 "justify",
842 "min_width",
843 "max_width",
844 "min_height",
845 "max_height",
846 "margin",
847 "margin_x",
848 "margin_y",
849 "margin_left",
850 "margin_right",
851 "margin_top",
852 "margin_bottom",
853 "overflow",
854 "on_click",
855 "hover",
856 ],
857 "Container" => &[
858 "width",
859 "height",
860 "padding",
861 "padding_x",
862 "padding_y",
863 "background",
864 "flex",
865 "radius",
866 "border",
867 "border_left",
868 "border_right",
869 "border_top",
870 "border_bottom",
871 "border_x",
872 "border_y",
873 "style",
874 "min_width",
875 "max_width",
876 "min_height",
877 "max_height",
878 "margin",
879 "margin_x",
880 "margin_y",
881 "margin_left",
882 "margin_right",
883 "margin_top",
884 "margin_bottom",
885 "overflow",
886 "on_click",
887 "hover",
888 ],
889 "Panel" => &[
890 "width",
891 "height",
892 "padding",
893 "padding_x",
894 "padding_y",
895 "background",
896 "flex",
897 "radius",
898 "border",
899 "border_left",
900 "border_right",
901 "border_top",
902 "border_bottom",
903 "border_x",
904 "border_y",
905 "style",
906 "min_width",
907 "max_width",
908 "min_height",
909 "max_height",
910 "margin",
911 "margin_x",
912 "margin_y",
913 "margin_left",
914 "margin_right",
915 "margin_top",
916 "margin_bottom",
917 "overflow",
918 ],
919 "Stack" => &[
920 "width",
921 "height",
922 "padding",
923 "padding_x",
924 "padding_y",
925 "background",
926 "flex",
927 "radius",
928 "border",
929 "min_width",
930 "max_width",
931 "min_height",
932 "max_height",
933 "margin",
934 "margin_x",
935 "margin_y",
936 "margin_left",
937 "margin_right",
938 "margin_top",
939 "margin_bottom",
940 "overflow",
941 ],
942 "Grid" => &[
943 "width",
944 "height",
945 "padding",
946 "padding_x",
947 "padding_y",
948 "background",
949 "flex",
950 "radius",
951 "border",
952 "columns",
953 "rows",
954 "gap",
955 "column_gap",
956 "row_gap",
957 "auto_flow",
958 "align",
959 "justify",
960 "min_width",
961 "max_width",
962 "min_height",
963 "max_height",
964 "margin",
965 "margin_x",
966 "margin_y",
967 "margin_left",
968 "margin_right",
969 "margin_top",
970 "margin_bottom",
971 "overflow",
972 ],
973 "Wrap" => &[
974 "width",
975 "height",
976 "padding",
977 "padding_x",
978 "padding_y",
979 "background",
980 "flex",
981 "axis",
982 "gap",
983 "column_gap",
984 "row_gap",
985 "align",
986 "min_width",
987 "max_width",
988 "min_height",
989 "max_height",
990 "margin",
991 "margin_x",
992 "margin_y",
993 "margin_left",
994 "margin_right",
995 "margin_top",
996 "margin_bottom",
997 "overflow",
998 ],
999 "ScrollView" => &["axis"],
1000 "Text" => &[
1001 "size",
1002 "color",
1003 "bold",
1004 "semi",
1005 "medium",
1006 "normal",
1007 "light",
1008 "black",
1009 "truncate",
1010 "wrap",
1011 "text",
1012 "dynamic",
1013 "t",
1014 "flex",
1015 "style",
1016 "width",
1017 "height",
1018 "min_width",
1019 "max_width",
1020 "min_height",
1021 "max_height",
1022 "margin",
1023 "margin_x",
1024 "margin_y",
1025 "margin_left",
1026 "margin_right",
1027 "margin_top",
1028 "margin_bottom",
1029 ],
1030 "Button" => &[
1031 "style",
1032 "enabled",
1033 "font_size",
1034 "on_click",
1035 "flex",
1036 "width",
1037 "height",
1038 "min_width",
1039 "max_width",
1040 "min_height",
1041 "max_height",
1042 "margin",
1043 "margin_x",
1044 "margin_y",
1045 "margin_left",
1046 "margin_right",
1047 "margin_top",
1048 "margin_bottom",
1049 ],
1050 "Spacer" => &[
1051 "flex",
1052 "width",
1053 "height",
1054 "min_width",
1055 "max_width",
1056 "min_height",
1057 "max_height",
1058 ],
1059 "Icon" => &[
1060 "name",
1061 "size",
1062 "color",
1063 "flex",
1064 "width",
1065 "height",
1066 "min_width",
1067 "max_width",
1068 "min_height",
1069 "max_height",
1070 "margin",
1071 "margin_x",
1072 "margin_y",
1073 "margin_left",
1074 "margin_right",
1075 "margin_top",
1076 "margin_bottom",
1077 ],
1078 "Image" => &[
1079 "path",
1080 "width",
1081 "height",
1082 "flex",
1083 "min_width",
1084 "max_width",
1085 "min_height",
1086 "max_height",
1087 "margin",
1088 "margin_x",
1089 "margin_y",
1090 "margin_left",
1091 "margin_right",
1092 "margin_top",
1093 "margin_bottom",
1094 ],
1095 "List" => &["items", "key"],
1096 "VirtualList" => &["items", "key", "item_height", "overscan"],
1097 "SplitPanel" => &[
1098 "value",
1099 "axis",
1100 "handle",
1101 "min_first",
1102 "min_second",
1103 "handle_color",
1104 "width",
1105 "height",
1106 "padding",
1107 "padding_x",
1108 "padding_y",
1109 "background",
1110 "flex",
1111 "min_width",
1112 "max_width",
1113 "min_height",
1114 "max_height",
1115 "margin",
1116 "margin_x",
1117 "margin_y",
1118 "margin_left",
1119 "margin_right",
1120 "margin_top",
1121 "margin_bottom",
1122 "overflow",
1123 ],
1124 "If" => &["when", "enter", "exit"],
1125 "Width" => &[
1126 "value",
1127 "animate",
1128 "width",
1129 "height",
1130 "flex",
1131 "margin",
1132 "margin_x",
1133 "margin_y",
1134 "margin_left",
1135 "margin_right",
1136 "margin_top",
1137 "margin_bottom",
1138 ],
1139 "Height" => &[
1140 "value",
1141 "animate",
1142 "width",
1143 "height",
1144 "flex",
1145 "margin",
1146 "margin_x",
1147 "margin_y",
1148 "margin_left",
1149 "margin_right",
1150 "margin_top",
1151 "margin_bottom",
1152 ],
1153 "Opacity" => &[
1154 "value",
1155 "animate",
1156 "width",
1157 "height",
1158 "flex",
1159 "margin",
1160 "margin_x",
1161 "margin_y",
1162 "margin_left",
1163 "margin_right",
1164 "margin_top",
1165 "margin_bottom",
1166 ],
1167 "Scale" => &[
1168 "value",
1169 "animate",
1170 "width",
1171 "height",
1172 "flex",
1173 "margin",
1174 "margin_x",
1175 "margin_y",
1176 "margin_left",
1177 "margin_right",
1178 "margin_top",
1179 "margin_bottom",
1180 ],
1181 "Rotate" => &[
1182 "value",
1183 "animate",
1184 "width",
1185 "height",
1186 "flex",
1187 "margin",
1188 "margin_x",
1189 "margin_y",
1190 "margin_left",
1191 "margin_right",
1192 "margin_top",
1193 "margin_bottom",
1194 ],
1195 "Translate" => &[
1196 "x",
1197 "y",
1198 "animate",
1199 "width",
1200 "height",
1201 "flex",
1202 "margin",
1203 "margin_x",
1204 "margin_y",
1205 "margin_left",
1206 "margin_right",
1207 "margin_top",
1208 "margin_bottom",
1209 ],
1210 "Pulse" => &[
1211 "min",
1212 "period",
1213 "width",
1214 "height",
1215 "flex",
1216 "margin",
1217 "margin_x",
1218 "margin_y",
1219 "margin_left",
1220 "margin_right",
1221 "margin_top",
1222 "margin_bottom",
1223 ],
1224 "Shake" => &[
1225 "amplitude",
1226 "period",
1227 "duration",
1228 "delay",
1229 "width",
1230 "height",
1231 "flex",
1232 "margin",
1233 "margin_x",
1234 "margin_y",
1235 "margin_left",
1236 "margin_right",
1237 "margin_top",
1238 "margin_bottom",
1239 ],
1240 "Async" => &["source"],
1241 "Loading" | "Data" | "Error" | "Idle" | "Cancelled" => &[],
1242 "Effect" => &["track", "on_run"],
1243 "Canvas" => &[
1244 "width",
1245 "height",
1246 "flex",
1247 "background",
1248 "transparent",
1249 "cursor",
1250 "focusable",
1251 "grid",
1252 "grid_color",
1253 "axes",
1254 "axes_color",
1255 "clip",
1256 "allow_overflow",
1257 "camera",
1258 "capture",
1259 "on_click_at",
1260 "on_drag",
1261 "on_wheel",
1262 "on_press",
1263 "on_pinch",
1264 "tick",
1265 "speed",
1266 "running",
1267 "min_width",
1268 "max_width",
1269 "min_height",
1270 "max_height",
1271 "margin",
1272 "margin_x",
1273 "margin_y",
1274 ],
1275 "Checkbox" | "Switch" => &[
1276 "checked",
1277 "label",
1278 "font_size",
1279 "enabled",
1280 "flex",
1281 "margin",
1282 "margin_x",
1283 "margin_y",
1284 "margin_left",
1285 "margin_right",
1286 "margin_top",
1287 "margin_bottom",
1288 "on_toggle",
1289 ],
1290 "TextField" => &[
1291 "value",
1292 "placeholder",
1293 "font_size",
1294 "width",
1295 "height",
1296 "min_width",
1297 "max_width",
1298 "max_height",
1299 "flex",
1300 "margin",
1301 "margin_x",
1302 "margin_y",
1303 "margin_left",
1304 "margin_right",
1305 "margin_top",
1306 "margin_bottom",
1307 "on_submit",
1308 ],
1309 "TextArea" => &[
1310 "value",
1311 "placeholder",
1312 "max_length",
1313 "min_lines",
1314 "max_lines",
1315 "font_size",
1316 "width",
1317 "height",
1318 "min_width",
1319 "max_width",
1320 "min_height",
1321 "max_height",
1322 "flex",
1323 "margin",
1324 "margin_x",
1325 "margin_y",
1326 "margin_left",
1327 "margin_right",
1328 "margin_top",
1329 "margin_bottom",
1330 "on_edit_end",
1331 ],
1332 "Field" => &["value", "label", "placeholder"],
1333 "NumberField" => &["value", "label", "min", "max", "step", "precision"],
1334 "DateField" => &["value", "label"],
1335 "MultiSelect" => &["options", "label", "min_selected"],
1336 "Slider" => &[
1337 "value",
1338 "min",
1339 "max",
1340 "width",
1341 "height",
1342 "flex",
1343 "margin",
1344 "margin_x",
1345 "margin_y",
1346 "margin_left",
1347 "margin_right",
1348 "margin_top",
1349 "margin_bottom",
1350 "on_change",
1351 ],
1352 "DragValue" => &[
1353 "value",
1354 "min",
1355 "max",
1356 "step",
1357 "precision",
1358 "width",
1359 "height",
1360 "min_width",
1361 "max_width",
1362 "min_height",
1363 "max_height",
1364 "flex",
1365 "margin",
1366 "margin_x",
1367 "margin_y",
1368 "margin_left",
1369 "margin_right",
1370 "margin_top",
1371 "margin_bottom",
1372 "on_commit",
1373 ],
1374 "Dropdown" => &[
1375 "selected",
1376 "items",
1377 "placeholder",
1378 "font_size",
1379 "min_width",
1380 "max_width",
1381 "min_height",
1382 "max_height",
1383 "enabled",
1384 "flex",
1385 "margin",
1386 "margin_x",
1387 "margin_y",
1388 "margin_left",
1389 "margin_right",
1390 "margin_top",
1391 "margin_bottom",
1392 "on_select",
1393 ],
1394 "Progress" => &["value", "height", "track", "fill", "flex"],
1395 "Rect" => &[
1396 "x", "y", "w", "h", "fill", "color", "radius", "visible", "name",
1397 ],
1398 "Circle" => &["c", "r", "fill", "color", "visible", "name", "pick_r"],
1399 "Ellipse" => &["c", "rx", "ry", "fill", "color", "visible", "name"],
1400 "Ring" => &["c", "r", "width", "color", "visible", "name"],
1401 "Line" => &["a", "b", "width", "color", "visible", "name"],
1402 "Arrow" => &["a", "b", "width", "head", "color", "visible", "name"],
1403 "Capsule" => &["a", "b", "r", "fill", "color", "visible", "name"],
1404 "Polygon" => &["points", "fill", "color", "visible", "name"],
1405 "Polyline" => &["points", "width", "color", "visible", "name"],
1406 "Spline" => &["points", "width", "color", "visible", "name"],
1407 "Star" => &[
1408 "c", "outer", "inner", "n", "rotation", "color", "visible", "name",
1409 ],
1410 "Ngon" => &["c", "r", "sides", "rotation", "color", "visible", "name"],
1411 "Cross" => &["c", "half", "width", "color", "visible", "name"],
1412 "Text2d" => &["str", "t", "at", "size", "color", "visible"],
1413 "Svg" => &["d", "fill", "color", "visible"],
1414 "Dots" => &["step", "color", "radius", "visible"],
1415 "Axes" => &["color", "width", "at", "visible"],
1416 "Frame" => &["rect", "width", "color", "visible"],
1417 "Marquee" => &["rect", "color", "visible"],
1418 "Handle" => &["at", "color", "r", "visible"],
1419 "Image2d" => &["src", "rect", "x", "y", "w", "h", "uv", "visible", "name"],
1420 "Clip" => &["rect", "radius", "visible"],
1421 "Group" => &["translate", "rotate", "scale", "pivot"],
1422 _ => LAYOUT_PROPS,
1423 };
1424 if !allowed.contains(&name) {
1425 return Err(SeqError::new(
1426 file,
1427 arg.line,
1428 1,
1429 format!(
1430 "nieznana propka '{name}' w {} (dozwolone: {})",
1431 node.kind,
1432 allowed.join(", ")
1433 ),
1434 ));
1435 }
1436
1437 if name == "style" {
1438 let style_name = match &arg.value {
1439 crate::ir::SeqValue::Ident(n) => n.as_str(),
1440 _ => {
1441 return Err(SeqError::new(
1442 file,
1443 arg.line,
1444 1,
1445 "style: is a style name (or preset), not an expression".to_string(),
1446 ));
1447 }
1448 };
1449
1450 let want_target = match node.kind.as_str() {
1451 "Container" | "Panel" => "Box",
1452 "Text" => "Text",
1453 _ => "Button",
1454 };
1455 if want_target == "Button" && BUTTON_PRESETS.contains(&style_name) {
1456 return Ok(());
1457 }
1458 match find_style(styles, style_name) {
1459 Some(s) if s.target == want_target => {}
1460 Some(s) => {
1461 return Err(SeqError::new(
1462 file,
1463 arg.line,
1464 1,
1465 format!(
1466 "style '{style_name}' to {}, a {} wymaga {}",
1467 s.target, node.kind, want_target
1468 ),
1469 ));
1470 }
1471 None => {
1472 return Err(SeqError::new(
1473 file,
1474 arg.line,
1475 1,
1476 format!("nieznany styl '{style_name}' (brak bloku style)"),
1477 ));
1478 }
1479 }
1480 return Ok(());
1481 }
1482
1483 if name == "flex" && in_canvas {
1484 return Err(SeqError::new(
1485 file,
1486 arg.line,
1487 1,
1488 "flex not supported for Canvas shapes".to_string(),
1489 ));
1490 }
1491 Ok(())
1492}
1493
1494const BUILTIN_FNS: &[&str] = &[
1495 "starts_with",
1496 "strip_prefix",
1497 "parse_tag",
1498 "len",
1499 "clamp",
1500 "powf",
1501 "max",
1502 "min",
1503];
1504
1505#[derive(Clone, Debug, PartialEq)]
1506enum Ty {
1507 Int,
1508 Float,
1509 Str,
1510 Bool,
1511 Tuple(Vec<Ty>),
1512 Option(Box<Ty>),
1513 Row,
1514 Any,
1515 Null,
1516}
1517
1518impl Ty {
1519 fn from_sig(ty: &str) -> Option<Self> {
1520 let t = ty.trim();
1521 if let Some(inner) = t.strip_prefix("Option<").and_then(|s| s.strip_suffix('>')) {
1522 return Some(Self::Option(Box::new(Self::parse_sig(inner.trim())?)));
1523 }
1524 match t {
1525 "Int" => Some(Self::Int),
1526 "Float" => Some(Self::Float),
1527 "String" => Some(Self::Str),
1528 "Bool" => Some(Self::Bool),
1529 _ => None,
1530 }
1531 }
1532
1533 fn parse_sig(ty: &str) -> Option<Self> {
1534 let t = ty.trim();
1535 if let Some(inner) = t.strip_prefix("Option<").and_then(|s| s.strip_suffix('>')) {
1536 return Some(Self::Option(Box::new(Self::parse_sig(inner.trim())?)));
1537 }
1538 if let Some(inner) = t.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
1539 if inner.trim().is_empty() {
1540 return None;
1541 }
1542 let mut items = Vec::new();
1543 for part in inner.split(',') {
1544 items.push(Self::from_sig(part.trim())?);
1545 }
1546 return Some(Self::Tuple(items));
1547 }
1548 Self::from_sig(t)
1549 }
1550
1551 fn name(&self) -> String {
1552 match self {
1553 Self::Int => "Int".to_string(),
1554 Self::Float => "Float".to_string(),
1555 Self::Str => "String".to_string(),
1556 Self::Bool => "Bool".to_string(),
1557 Self::Tuple(items) => format!(
1558 "({})",
1559 items.iter().map(Self::name).collect::<Vec<_>>().join(", ")
1560 ),
1561 Self::Option(inner) => format!("Option<{}>", inner.name()),
1562 Self::Row => "wiersz".to_string(),
1563 Self::Any => "value".to_string(),
1564 Self::Null => "no value".to_string(),
1565 }
1566 }
1567
1568 fn is_number(&self) -> bool {
1569 matches!(self, Self::Int | Self::Float | Self::Any)
1570 }
1571
1572 fn fits(&self, target: &Self) -> bool {
1573 match (self, target) {
1574 (_, Self::Any) | (Self::Any, _) => true,
1575 (Self::Int, Self::Int) | (Self::Int, Self::Float) | (Self::Float, Self::Float) => true,
1576 (Self::Str, Self::Str) | (Self::Bool, Self::Bool) | (Self::Row, Self::Row) => true,
1577 (Self::Tuple(a), Self::Tuple(b)) => {
1578 a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.fits(y))
1579 }
1580 (Self::Option(a), Self::Option(b)) => a.fits(b),
1581 _ => false,
1582 }
1583 }
1584}
1585
1586#[derive(Clone, Debug)]
1587struct FnSig {
1588 params: Vec<Ty>,
1589 ret: Option<Ty>,
1590}
1591
1592type FnTable = std::collections::HashMap<String, FnSig>;
1593
1594struct LocalTy {
1595 ty: Ty,
1596 mutable: bool,
1597}
1598
1599struct FnCtx<'a> {
1600 file: &'a str,
1601 fns: &'a FnTable,
1602 props: &'a std::collections::HashMap<&'a str, Ty>,
1603 states: &'a std::collections::HashMap<String, Ty>,
1604}
1605
1606fn check_fns(file: &str, app: &SeqApp) -> Result<FnTable, SeqError> {
1607 let mut prop_tys: std::collections::HashMap<&str, Ty> = std::collections::HashMap::new();
1608 for d in &app.deferred {
1609 if d.kind == crate::ir::DeferredKind::Prop {
1610 let ty = d
1611 .sig
1612 .first()
1613 .and_then(|s| Ty::from_sig(s))
1614 .unwrap_or(Ty::Any);
1615 prop_tys.entry(d.name.as_str()).or_insert(ty);
1616 }
1617 }
1618 let mut states: std::collections::HashMap<String, Ty> = std::collections::HashMap::new();
1619 for s in &app.states {
1620 if states.contains_key(&s.name) {
1621 return Err(SeqError::new(
1622 file,
1623 s.line,
1624 1,
1625 format!("state '{}' zdefiniowany dwa razy", s.name),
1626 ));
1627 }
1628 let Some(decl) = Ty::from_sig(&s.ty) else {
1629 return Err(SeqError::new(
1630 file,
1631 s.line,
1632 1,
1633 format!("state '{}': typ Int/Float/String/Bool", s.name),
1634 ));
1635 };
1636 if prop_tys.contains_key(s.name.as_str()) {
1637 return Err(SeqError::new(
1638 file,
1639 s.line,
1640 1,
1641 format!("state '{}' koliduje z `prop {}`", s.name, s.name),
1642 ));
1643 }
1644 let init = match &s.init.kind {
1645 SeqExprKind::Int(_) => Ty::Int,
1646 SeqExprKind::Float(_) => Ty::Float,
1647 SeqExprKind::Str(_) => Ty::Str,
1648 SeqExprKind::Bool(_) => Ty::Bool,
1649 SeqExprKind::Var(name) if name == "None" => Ty::Option(Box::new(Ty::Any)),
1650 _ => {
1651 return Err(SeqError::new(
1652 file,
1653 s.line,
1654 1,
1655 format!("state '{}': initial value is literal", s.name),
1656 ));
1657 }
1658 };
1659 if !init.fits(&decl) {
1660 return Err(SeqError::new(
1661 file,
1662 s.line,
1663 1,
1664 format!(
1665 "state '{}': initial value is {} and type {}",
1666 s.name,
1667 init.name(),
1668 decl.name()
1669 ),
1670 ));
1671 }
1672 states.insert(s.name.clone(), decl);
1673 }
1674 let mut table: FnTable = std::collections::HashMap::new();
1675 for f in &app.fns {
1676 if table.contains_key(&f.name) {
1677 return Err(SeqError::new(
1678 file,
1679 f.line,
1680 1,
1681 format!("funkcja '{}' zdefiniowana dwa razy", f.name),
1682 ));
1683 }
1684 if !f
1685 .name
1686 .starts_with(|ch: char| ch.is_ascii_lowercase() || ch == '_')
1687 {
1688 return Err(SeqError::new(
1689 file,
1690 f.line,
1691 1,
1692 format!("function name '{}' in lower case (like connect)", f.name),
1693 ));
1694 }
1695 if BUILTIN_FNS.contains(&f.name.as_str()) {
1696 return Err(SeqError::new(
1697 file,
1698 f.line,
1699 1,
1700 format!("fn '{}' overrides built-in function", f.name),
1701 ));
1702 }
1703 if states.contains_key(&f.name) {
1704 return Err(SeqError::new(
1705 file,
1706 f.line,
1707 1,
1708 format!("fn '{}' koliduje ze `state {}`", f.name, f.name),
1709 ));
1710 }
1711 if prop_tys.contains_key(f.name.as_str()) {
1712 return Err(SeqError::new(
1713 file,
1714 f.line,
1715 1,
1716 format!("fn '{}' koliduje z `prop {}`", f.name, f.name),
1717 ));
1718 }
1719 let mut seen: Vec<&str> = Vec::new();
1720 let mut params = Vec::new();
1721 for p in &f.params {
1722 if seen.contains(&p.name.as_str()) {
1723 return Err(SeqError::new(
1724 file,
1725 p.line,
1726 1,
1727 format!("parametr '{}' dwa razy w fn {}", p.name, f.name),
1728 ));
1729 }
1730 seen.push(p.name.as_str());
1731 let Some(ty) = Ty::from_sig(&p.ty) else {
1732 return Err(SeqError::new(
1733 file,
1734 p.line,
1735 1,
1736 format!(
1737 "fn {}: nieznany typ parametru '{}' (Int/Float/String/Bool)",
1738 f.name, p.ty
1739 ),
1740 ));
1741 };
1742 params.push(ty);
1743 }
1744 let ret = match &f.ret {
1745 None => None,
1746 Some(r) => {
1747 let Some(ty) = Ty::parse_sig(r) else {
1748 return Err(SeqError::new(
1749 file,
1750 f.line,
1751 1,
1752 format!(
1753 "fn {}: nieznany typ zwracany '{r}' (Int/Float/String/Bool/(A, B))",
1754 f.name
1755 ),
1756 ));
1757 };
1758 Some(ty)
1759 }
1760 };
1761 table.insert(f.name.clone(), FnSig { params, ret });
1762 }
1763 for f in &app.fns {
1764 check_fn_body(file, f, &table, &prop_tys, &states)?;
1765 }
1766 Ok(table)
1767}
1768
1769fn check_fn_body(
1770 file: &str,
1771 f: &SeqFn,
1772 fns: &FnTable,
1773 props: &std::collections::HashMap<&str, Ty>,
1774 states: &std::collections::HashMap<String, Ty>,
1775) -> Result<(), SeqError> {
1776 let ctx = FnCtx {
1777 file,
1778 fns,
1779 props,
1780 states,
1781 };
1782 let mut frame = std::collections::HashMap::new();
1783 for p in &f.params {
1784 let ty = Ty::from_sig(&p.ty).expect("parameter types checked above");
1785 frame.insert(
1786 p.name.clone(),
1787 LocalTy {
1788 ty,
1789 mutable: p.mutable,
1790 },
1791 );
1792 }
1793 let mut locals = vec![frame];
1794 let ret = f
1795 .ret
1796 .as_ref()
1797 .map(|r| Ty::parse_sig(r).expect("return types checked above"));
1798 let returns = check_stmts(&ctx, &mut locals, &f.body, ret.as_ref())?;
1799 if ret.is_some() && !returns {
1800 return Err(SeqError::new(
1801 file,
1802 f.line,
1803 1,
1804 format!("fn {}: each path must end with `return`", f.name),
1805 ));
1806 }
1807 Ok(())
1808}
1809
1810fn check_stmts(
1811 ctx: &FnCtx<'_>,
1812 locals: &mut Vec<std::collections::HashMap<String, LocalTy>>,
1813 stmts: &[SeqStmt],
1814 ret: Option<&Ty>,
1815) -> Result<bool, SeqError> {
1816 let mut always = false;
1817 for stmt in stmts {
1818 if check_stmt(ctx, locals, stmt, ret)? {
1819 always = true;
1820 }
1821 }
1822 Ok(always)
1823}
1824
1825fn check_scoped(
1826 ctx: &FnCtx<'_>,
1827 locals: &mut Vec<std::collections::HashMap<String, LocalTy>>,
1828 stmts: &[SeqStmt],
1829 ret: Option<&Ty>,
1830) -> Result<bool, SeqError> {
1831 locals.push(std::collections::HashMap::new());
1832 let out = check_stmts(ctx, locals, stmts, ret);
1833 locals.pop();
1834 out
1835}
1836
1837#[allow(clippy::too_many_lines)]
1838fn check_stmt(
1839 ctx: &FnCtx<'_>,
1840 locals: &mut Vec<std::collections::HashMap<String, LocalTy>>,
1841 stmt: &SeqStmt,
1842 ret: Option<&Ty>,
1843) -> Result<bool, SeqError> {
1844 match stmt {
1845 SeqStmt::Let {
1846 name,
1847 mutable,
1848 value,
1849 line,
1850 } => {
1851 let ty = expr_ty(ctx, locals, value)?;
1852 if ty == Ty::Null {
1853 return Err(SeqError::new(
1854 ctx.file,
1855 *line,
1856 1,
1857 format!("'{name}': expression returns nothing (function without `->Type`?)"),
1858 ));
1859 }
1860 let frame = locals.last_mut().expect("reception");
1861 if frame.contains_key(name) {
1862 return Err(SeqError::new(
1863 ctx.file,
1864 *line,
1865 1,
1866 format!("lokalna '{name}' zdefiniowana dwa razy"),
1867 ));
1868 }
1869 frame.insert(
1870 name.clone(),
1871 LocalTy {
1872 ty,
1873 mutable: *mutable,
1874 },
1875 );
1876 Ok(false)
1877 }
1878 SeqStmt::Assign { name, value, line } => {
1879 let ty = expr_ty(ctx, locals, value)?;
1880 if ty == Ty::Null {
1881 return Err(SeqError::new(
1882 ctx.file,
1883 *line,
1884 1,
1885 format!("writing to '{name}': expression returns nothing"),
1886 ));
1887 }
1888 for frame in locals.iter_mut().rev() {
1889 if let Some(local) = frame.get_mut(name) {
1890 if !local.mutable {
1891 return Err(SeqError::new(
1892 ctx.file,
1893 *line,
1894 1,
1895 format!("'{name}' niemutowalna — `mut {name} = ...`"),
1896 ));
1897 }
1898 if !ty.fits(&local.ty) {
1899 return Err(SeqError::new(
1900 ctx.file,
1901 *line,
1902 1,
1903 format!(
1904 "wrong write type to '{name}' (is {}, given {})",
1905 local.ty.name(),
1906 ty.name()
1907 ),
1908 ));
1909 }
1910 return Ok(false);
1911 }
1912 }
1913 if let Some(st) = ctx.states.get(name) {
1914 if !ty.fits(st) {
1915 return Err(SeqError::new(
1916 ctx.file,
1917 *line,
1918 1,
1919 format!(
1920 "wrong type writing to state '{name}' (is {}, given {})",
1921 st.name(),
1922 ty.name()
1923 ),
1924 ));
1925 }
1926 return Ok(false);
1927 }
1928 if let Some(pt) = ctx.props.get(name.as_str()) {
1929 if !ty.fits(pt) {
1930 return Err(SeqError::new(
1931 ctx.file,
1932 *line,
1933 1,
1934 format!(
1935 "wrong write type to '{name}' (prop {}, given {})",
1936 pt.name(),
1937 ty.name()
1938 ),
1939 ));
1940 }
1941 return Ok(false);
1942 }
1943 Err(SeqError::new(
1944 ctx.file,
1945 *line,
1946 1,
1947 format!("nieznana zmienna '{name}' (lokalna/state/prop)"),
1948 ))
1949 }
1950 SeqStmt::If {
1951 cond,
1952 then,
1953 elifs,
1954 els,
1955 line,
1956 } => {
1957 check_cond(ctx, locals, cond, "if")?;
1958 let mut all = check_scoped(ctx, locals, then, ret)?;
1959 for (c, body) in elifs {
1960 check_cond(ctx, locals, c, "else if")?;
1961 all = check_scoped(ctx, locals, body, ret)? && all;
1962 }
1963 match els {
1964 Some(body) => {
1965 all = check_scoped(ctx, locals, body, ret)? && all;
1966 }
1967 None => all = false,
1968 }
1969 let _ = line;
1970 Ok(all)
1971 }
1972 SeqStmt::For {
1973 var, source, body, ..
1974 } => {
1975 let _ = source;
1976 locals.push(std::collections::HashMap::new());
1977 locals.last_mut().expect("reception").insert(
1978 var.clone(),
1979 LocalTy {
1980 ty: Ty::Row,
1981 mutable: false,
1982 },
1983 );
1984 let out = check_stmts(ctx, locals, body, ret);
1985 locals.pop();
1986 out?;
1987 Ok(false)
1988 }
1989 SeqStmt::Match { value, arms, line } => {
1990 let vty = expr_ty(ctx, locals, value)?;
1991 if !arms.iter().any(|a| matches!(a.pattern, MatchPat::Wildcard)) {
1992 return Err(SeqError::new(
1993 ctx.file,
1994 *line,
1995 1,
1996 "match requires branch `_`",
1997 ));
1998 }
1999 let mut all = true;
2000 for arm in arms {
2001 let ok = matches!(
2002 (&arm.pattern, &vty),
2003 (MatchPat::Str(_), Ty::Str | Ty::Any)
2004 | (MatchPat::Int(_), Ty::Int | Ty::Any)
2005 | (MatchPat::Bool(_), Ty::Bool | Ty::Any)
2006 | (MatchPat::Wildcard, _)
2007 );
2008 if !ok {
2009 let what = match &arm.pattern {
2010 MatchPat::Str(_) => "tekst",
2011 MatchPat::Int(_) => "liczba",
2012 MatchPat::Bool(_) => "true/false",
2013 MatchPat::Wildcard => "_",
2014 };
2015 return Err(SeqError::new(
2016 ctx.file,
2017 arm.line,
2018 1,
2019 format!("wzorzec '{what}' nie pasuje do {}", vty.name()),
2020 ));
2021 }
2022 all = check_scoped(ctx, locals, &arm.body, ret)? && all;
2023 }
2024 Ok(all)
2025 }
2026 SeqStmt::Return { value, line } => {
2027 match (ret, value) {
2028 (None, None) => {}
2029 (None, Some(_)) => {
2030 return Err(SeqError::new(
2031 ctx.file,
2032 *line,
2033 1,
2034 "funkcja bez `-> Typ` — tylko `return;`",
2035 ));
2036 }
2037 (Some(rt), None) => {
2038 return Err(SeqError::new(
2039 ctx.file,
2040 *line,
2041 1,
2042 format!("funkcja zwraca {} — `return expr;`", rt.name()),
2043 ));
2044 }
2045 (Some(rt), Some(e)) => {
2046 let ty = expr_ty(ctx, locals, e)?;
2047 if ty == Ty::Null {
2048 return Err(SeqError::new(
2049 ctx.file,
2050 e.line,
2051 1,
2052 "return: expression returns nothing",
2053 ));
2054 }
2055 if !ty.fits(rt) {
2056 return Err(SeqError::new(
2057 ctx.file,
2058 *line,
2059 1,
2060 format!("return: oczekiwano {}, jest {}", rt.name(), ty.name()),
2061 ));
2062 }
2063 }
2064 }
2065 Ok(true)
2066 }
2067 SeqStmt::Expr(e) => {
2068 expr_ty(ctx, locals, e)?;
2069 Ok(false)
2070 }
2071 }
2072}
2073
2074fn check_cond(
2075 ctx: &FnCtx<'_>,
2076 locals: &mut Vec<std::collections::HashMap<String, LocalTy>>,
2077 cond: &SeqExpr,
2078 what: &str,
2079) -> Result<(), SeqError> {
2080 let ty = expr_ty(ctx, locals, cond)?;
2081 if matches!(ty, Ty::Bool | Ty::Any) {
2082 Ok(())
2083 } else {
2084 Err(SeqError::new(
2085 ctx.file,
2086 cond.line,
2087 1,
2088 format!("condition `{what}` must be Bool (is {})", ty.name()),
2089 ))
2090 }
2091}
2092
2093fn expr_ty(
2094 ctx: &FnCtx<'_>,
2095 locals: &mut Vec<std::collections::HashMap<String, LocalTy>>,
2096 e: &SeqExpr,
2097) -> Result<Ty, SeqError> {
2098 match &e.kind {
2099 SeqExprKind::Int(_) => Ok(Ty::Int),
2100 SeqExprKind::Float(_) => Ok(Ty::Float),
2101 SeqExprKind::Str(_) => Ok(Ty::Str),
2102 SeqExprKind::Bool(_) => Ok(Ty::Bool),
2103 SeqExprKind::Var(name) => {
2104 if name == "None" {
2105 return Ok(Ty::Option(Box::new(Ty::Any)));
2106 }
2107 for frame in locals.iter().rev() {
2108 if let Some(local) = frame.get(name) {
2109 return Ok(local.ty.clone());
2110 }
2111 }
2112 if let Some(st) = ctx.states.get(name) {
2113 return Ok(st.clone());
2114 }
2115 if let Some(pt) = ctx.props.get(name.as_str()) {
2116 return Ok(pt.clone());
2117 }
2118 Err(SeqError::new(
2119 ctx.file,
2120 e.line,
2121 1,
2122 format!("nieznana zmienna '{name}' (parametr/lokalna/state/prop)"),
2123 ))
2124 }
2125 SeqExprKind::Field { base, field } => {
2126 for frame in locals.iter().rev() {
2127 if let Some(local) = frame.get(base) {
2128 match &local.ty {
2129 Ty::Row => {
2130 let _ = field;
2131 return Ok(Ty::Any);
2132 }
2133 Ty::Tuple(items) => {
2134 let Ok(i) = field.parse::<usize>() else {
2135 return Err(SeqError::new(
2136 ctx.file,
2137 e.line,
2138 1,
2139 format!("pole krotki '{base}' to `.0`/`.1` (jest '.{field}')"),
2140 ));
2141 };
2142 return items.get(i).cloned().ok_or_else(|| {
2143 SeqError::new(
2144 ctx.file,
2145 e.line,
2146 1,
2147 format!(
2148 "tuple '{base}' has {} elements (index {i})",
2149 items.len()
2150 ),
2151 )
2152 });
2153 }
2154 other => {
2155 return Err(SeqError::new(
2156 ctx.file,
2157 e.line,
2158 1,
2159 format!(
2160 "'{base}' to {}, nie wiersz `for` ani krotka",
2161 other.name()
2162 ),
2163 ));
2164 }
2165 }
2166 }
2167 }
2168 Err(SeqError::new(
2169 ctx.file,
2170 e.line,
2171 1,
2172 format!("field '{base}.{field}' requires a `for` loop variable"),
2173 ))
2174 }
2175 SeqExprKind::Tuple(items) => {
2176 let mut out = Vec::with_capacity(items.len());
2177 for item in items {
2178 let ty = expr_ty(ctx, locals, item)?;
2179 if ty == Ty::Null {
2180 return Err(SeqError::new(
2181 ctx.file,
2182 item.line,
2183 1,
2184 "element krotki nic nie zwraca",
2185 ));
2186 }
2187 out.push(ty);
2188 }
2189 Ok(Ty::Tuple(out))
2190 }
2191 SeqExprKind::Unary { op, expr } => {
2192 let ty = expr_ty(ctx, locals, expr)?;
2193 match op {
2194 UnOp::Neg => match ty {
2195 Ty::Int => Ok(Ty::Int),
2196 Ty::Float => Ok(Ty::Float),
2197 Ty::Any => Ok(Ty::Any),
2198 _ => Err(SeqError::new(
2199 ctx.file,
2200 e.line,
2201 1,
2202 format!("`-` wymaga liczby (jest {})", ty.name()),
2203 )),
2204 },
2205 UnOp::Not => match ty {
2206 Ty::Bool | Ty::Any => Ok(Ty::Bool),
2207 _ => Err(SeqError::new(
2208 ctx.file,
2209 e.line,
2210 1,
2211 format!("`!` wymaga Bool (jest {})", ty.name()),
2212 )),
2213 },
2214 }
2215 }
2216 SeqExprKind::Binary { op, lhs, rhs } => binary_ty(ctx, locals, *op, lhs, rhs, e.line),
2217 SeqExprKind::Call { name, args } => {
2218 let mut arg_tys = Vec::with_capacity(args.len());
2219 for a in args {
2220 let ty = expr_ty(ctx, locals, a)?;
2221 if ty == Ty::Null {
2222 return Err(SeqError::new(
2223 ctx.file,
2224 a.line,
2225 1,
2226 "argument nic nie zwraca (funkcja bez `-> Typ`?)",
2227 ));
2228 }
2229 arg_tys.push(ty);
2230 }
2231 if let Some(ty) = builtin_ty(ctx, name, &arg_tys, args, e.line)? {
2232 return Ok(ty);
2233 }
2234 let Some(sig) = ctx.fns.get(name) else {
2235 return Err(SeqError::new(
2236 ctx.file,
2237 e.line,
2238 1,
2239 format!("nieznana funkcja '{name}'"),
2240 ));
2241 };
2242 if arg_tys.len() != sig.params.len() {
2243 return Err(SeqError::new(
2244 ctx.file,
2245 e.line,
2246 1,
2247 format!(
2248 "fn {name}: expected {} arguments, given {}",
2249 sig.params.len(),
2250 arg_tys.len()
2251 ),
2252 ));
2253 }
2254 for (i, (ty, p)) in arg_tys.iter().zip(&sig.params).enumerate() {
2255 if !ty.fits(p) {
2256 return Err(SeqError::new(
2257 ctx.file,
2258 args[i].line,
2259 1,
2260 format!(
2261 "fn {name}: argument {} to {}, a parametr oczekuje {}",
2262 i + 1,
2263 ty.name(),
2264 p.name()
2265 ),
2266 ));
2267 }
2268 }
2269 Ok(sig.ret.clone().unwrap_or(Ty::Null))
2270 }
2271 }
2272}
2273
2274fn builtin_ty(
2275 ctx: &FnCtx<'_>,
2276 name: &str,
2277 tys: &[Ty],
2278 args: &[SeqExpr],
2279 line: usize,
2280) -> Result<Option<Ty>, SeqError> {
2281 if name == "Some" {
2282 if tys.len() != 1 {
2283 return Err(SeqError::new(
2284 ctx.file,
2285 line,
2286 1,
2287 format!("Some: oczekiwano 1 argumentu, dano {}", tys.len()),
2288 ));
2289 }
2290 return Ok(Some(Ty::Option(Box::new(tys[0].clone()))));
2291 }
2292 let arity = |n: usize| -> Result<(), SeqError> {
2293 if tys.len() == n {
2294 Ok(())
2295 } else {
2296 Err(SeqError::new(
2297 ctx.file,
2298 line,
2299 1,
2300 format!("{name}: expected {n} arguments, given {}", tys.len()),
2301 ))
2302 }
2303 };
2304 let want_str = |i: usize| -> Result<(), SeqError> {
2305 if matches!(tys[i], Ty::Str | Ty::Any) {
2306 Ok(())
2307 } else {
2308 Err(SeqError::new(
2309 ctx.file,
2310 args[i].line,
2311 1,
2312 format!(
2313 "{name}: argument {} must be String (is {})",
2314 i + 1,
2315 tys[i].name()
2316 ),
2317 ))
2318 }
2319 };
2320 let want_num = |i: usize| -> Result<(), SeqError> {
2321 if tys[i].is_number() {
2322 Ok(())
2323 } else {
2324 Err(SeqError::new(
2325 ctx.file,
2326 args[i].line,
2327 1,
2328 format!(
2329 "{name}: argument {} must be a number (is {})",
2330 i + 1,
2331 tys[i].name()
2332 ),
2333 ))
2334 }
2335 };
2336 match name {
2337 "starts_with" => {
2338 arity(2)?;
2339 want_str(0)?;
2340 want_str(1)?;
2341 Ok(Some(Ty::Bool))
2342 }
2343 "strip_prefix" => {
2344 arity(2)?;
2345 want_str(0)?;
2346 want_str(1)?;
2347 Ok(Some(Ty::Str))
2348 }
2349 "parse_tag" => {
2350 arity(1)?;
2351 want_str(0)?;
2352 Ok(Some(Ty::Int))
2353 }
2354 "len" => {
2355 arity(1)?;
2356 want_str(0)?;
2357 Ok(Some(Ty::Int))
2358 }
2359 "clamp" | "powf" => {
2360 let n = if name == "clamp" { 3 } else { 2 };
2361 arity(n)?;
2362 for i in 0..n {
2363 want_num(i)?;
2364 }
2365 Ok(Some(Ty::Float))
2366 }
2367 "max" | "min" => {
2368 arity(2)?;
2369 want_num(0)?;
2370 want_num(1)?;
2371 if tys.iter().all(|t| *t == Ty::Int) {
2372 Ok(Some(Ty::Int))
2373 } else {
2374 Ok(Some(Ty::Float))
2375 }
2376 }
2377 _ => Ok(None),
2378 }
2379}
2380
2381fn binary_ty(
2382 ctx: &FnCtx<'_>,
2383 locals: &mut Vec<std::collections::HashMap<String, LocalTy>>,
2384 op: BinOp,
2385 lhs: &SeqExpr,
2386 rhs: &SeqExpr,
2387 line: usize,
2388) -> Result<Ty, SeqError> {
2389 let a = expr_ty(ctx, locals, lhs)?;
2390 let b = expr_ty(ctx, locals, rhs)?;
2391 let numeric = |a: &Ty, b: &Ty| -> Option<Ty> {
2392 if a.is_number() && b.is_number() {
2393 if *a == Ty::Any || *b == Ty::Any {
2394 Some(Ty::Any)
2395 } else if *a == Ty::Int && *b == Ty::Int {
2396 Some(Ty::Int)
2397 } else {
2398 Some(Ty::Float)
2399 }
2400 } else {
2401 None
2402 }
2403 };
2404 let bad = |what: &str| -> SeqError {
2405 SeqError::new(
2406 ctx.file,
2407 line,
2408 1,
2409 format!("{what} (jest {} i {})", a.name(), b.name()),
2410 )
2411 };
2412 match op {
2413 BinOp::Add => {
2414 if a == Ty::Str || b == Ty::Str {
2415 if matches!(a, Ty::Str | Ty::Any) && matches!(b, Ty::Str | Ty::Any) {
2416 return Ok(Ty::Str);
2417 }
2418 return Err(bad("`+` concatenates String + String"));
2419 }
2420 numeric(&a, &b).ok_or_else(|| bad("`+` wymaga liczb"))
2421 }
2422 BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Rem => {
2423 let op_name = match op {
2424 BinOp::Sub => "-",
2425 BinOp::Mul => "*",
2426 BinOp::Div => "/",
2427 _ => "%",
2428 };
2429 numeric(&a, &b).ok_or_else(|| bad(&format!("`{op_name}` wymaga liczb")))
2430 }
2431 BinOp::Eq | BinOp::Ne => {
2432 let ok = (a.is_number() && b.is_number())
2433 || (a == Ty::Str && b == Ty::Str)
2434 || (a == Ty::Bool && b == Ty::Bool)
2435 || (matches!(a, Ty::Option(_)) && matches!(b, Ty::Option(_)))
2436 || a == Ty::Any
2437 || b == Ty::Any;
2438 if ok {
2439 Ok(Ty::Bool)
2440 } else {
2441 Err(bad("`==`/`!=` requires compatible types"))
2442 }
2443 }
2444 BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
2445 let ok = (a.is_number() && b.is_number()) || (a == Ty::Str && b == Ty::Str);
2446 if ok {
2447 Ok(Ty::Bool)
2448 } else {
2449 Err(bad("comparison requires numbers (or String)"))
2450 }
2451 }
2452 BinOp::And | BinOp::Or => {
2453 let ok = matches!(a, Ty::Bool | Ty::Any) && matches!(b, Ty::Bool | Ty::Any);
2454 if ok {
2455 Ok(Ty::Bool)
2456 } else {
2457 Err(bad("`&&`/`||` wymaga Bool"))
2458 }
2459 }
2460 }
2461}
2462
2463fn live_callback_ty(attr: &str) -> Option<Ty> {
2464 match attr {
2465 "on_toggle" => Some(Ty::Bool),
2466 "on_submit" | "on_edit_end" => Some(Ty::Str),
2467 "on_commit" | "on_change" => Some(Ty::Float),
2468 "on_select" => Some(Ty::Int),
2469 _ => None,
2470 }
2471}
2472
2473const GESTURE_CALLBACKS: &[&str] = &["on_click_at", "on_press", "on_drag"];
2474
2475const PLAIN_CALLBACKS: &[&str] = &["on_click", "on_run"];
2476
2477fn check_seq_calls(file: &str, app: &SeqApp, fns: &FnTable) -> Result<(), SeqError> {
2478 for w in &app.windows {
2479 for n in &w.nodes {
2480 check_calls_node(file, n, fns)?;
2481 }
2482 }
2483 for c in &app.components {
2484 for n in &c.body {
2485 check_calls_node(&c.file, n, fns)?;
2486 }
2487 }
2488 for s in &app.styles {
2489 for arg in &s.args {
2490 if contains_callseq(&arg.value) {
2491 return Err(SeqError::new(
2492 file,
2493 arg.line,
2494 1,
2495 "calling `.seq` function only in widget callbacks",
2496 ));
2497 }
2498 }
2499 }
2500 Ok(())
2501}
2502
2503fn check_calls_node(file: &str, node: &SeqNode, fns: &FnTable) -> Result<(), SeqError> {
2504 for arg in &node.args {
2505 if let SeqValue::CallSeq { name, args } = &arg.value {
2506 check_callback_call(file, arg, name, args, fns)?;
2507 } else if contains_callseq(&arg.value) {
2508 return Err(SeqError::new(
2509 file,
2510 arg.line,
2511 1,
2512 "calling the `.seq` function only as a direct attribute value (e.g. `on_click: { fn() }`)",
2513 ));
2514 }
2515 }
2516 for c in &node.children {
2517 check_calls_node(file, c, fns)?;
2518 }
2519 Ok(())
2520}
2521
2522fn contains_callseq(value: &SeqValue) -> bool {
2523 match value {
2524 SeqValue::CallSeq { .. } => true,
2525 SeqValue::Callback { args, .. } => args.iter().any(contains_callseq),
2526 SeqValue::Tuple(items) | SeqValue::Array(items) => items.iter().any(contains_callseq),
2527 _ => false,
2528 }
2529}
2530
2531fn check_callback_call(
2532 file: &str,
2533 arg: &crate::ir::SeqArg,
2534 name: &str,
2535 args: &[SeqValue],
2536 fns: &FnTable,
2537) -> Result<(), SeqError> {
2538 let attr = arg.name.as_str();
2539 let Some(sig) = fns.get(name) else {
2540 return Err(SeqError::new(
2541 file,
2542 arg.line,
2543 1,
2544 format!("nieznana funkcja '{name}'"),
2545 ));
2546 };
2547 let all_idents = || args.iter().all(|a| matches!(a, SeqValue::Ident(_)));
2548 let expected: Vec<Ty> = if let Some(lt) = live_callback_ty(attr) {
2549 if args.is_empty() {
2550 Vec::new()
2551 } else if args.len() == 1 && matches!(&args[0], SeqValue::Ident(n) if n == "value") {
2552 vec![lt]
2553 } else {
2554 return Err(SeqError::new(
2555 file,
2556 arg.line,
2557 1,
2558 format!("{attr}: {{ {name}() }} albo {{ {name}(value) }}"),
2559 ));
2560 }
2561 } else if PLAIN_CALLBACKS.contains(&attr) {
2562 if args.is_empty() {
2563 Vec::new()
2564 } else if attr == "on_run" {
2565 return Err(SeqError::new(file, arg.line, 1, "on_run: no arguments"));
2566 } else if args.len() == 1 {
2567 vec![match &args[0] {
2568 SeqValue::Num(n) if n.fract() == 0.0 => Ty::Int,
2569 SeqValue::Num(_) => Ty::Float,
2570 SeqValue::Str(_) => Ty::Str,
2571 SeqValue::Bool(_) => Ty::Bool,
2572 SeqValue::ItemField(_) => Ty::Any,
2573 _ => {
2574 return Err(SeqError::new(
2575 file,
2576 arg.line,
2577 1,
2578 "on_click: argument to liczba/tekst/Bool/item.pole (albo bez argumentu)"
2579 .to_string(),
2580 ));
2581 }
2582 }]
2583 } else {
2584 return Err(SeqError::new(
2585 file,
2586 arg.line,
2587 1,
2588 format!("on_click: {{ {name}(arg) }} (0–1 argument)"),
2589 ));
2590 }
2591 } else if GESTURE_CALLBACKS.contains(&attr) {
2592 if args.is_empty() {
2593 Vec::new()
2594 } else if !all_idents() {
2595 return Err(SeqError::new(
2596 file,
2597 arg.line,
2598 1,
2599 format!("{attr}: {{ {name}(x, y[, name|d]) }}"),
2600 ));
2601 } else {
2602 match args.len() {
2603 1 => vec![Ty::Str],
2604 2 => vec![Ty::Float, Ty::Float],
2605 3 => {
2606 let third_is_delta = matches!(&args[2], SeqValue::Ident(n) if n == "d");
2607 if third_is_delta {
2608 vec![Ty::Float, Ty::Float, Ty::Float]
2609 } else {
2610 vec![Ty::Float, Ty::Float, Ty::Str]
2611 }
2612 }
2613 _ => {
2614 return Err(SeqError::new(
2615 file,
2616 arg.line,
2617 1,
2618 format!("{attr}: {{ {name}(name) }} albo {{ {name}(x, y[, name|d]) }}"),
2619 ));
2620 }
2621 }
2622 }
2623 } else if attr == "on_wheel" || attr == "on_pinch" {
2624 if args.is_empty() {
2625 Vec::new()
2626 } else if args.len() == 3 && all_idents() {
2627 vec![Ty::Float, Ty::Float, Ty::Float]
2628 } else {
2629 return Err(SeqError::new(
2630 file,
2631 arg.line,
2632 1,
2633 format!(
2634 "{attr}: {{ {name}(x, y, {}) }}",
2635 if attr == "on_wheel" { "d" } else { "s" }
2636 ),
2637 ));
2638 }
2639 } else {
2640 return Err(SeqError::new(
2641 file,
2642 arg.line,
2643 1,
2644 format!("calling '{name}' only as callback (on_click/on_toggle/on_*/gesty/on_run)"),
2645 ));
2646 };
2647 if expected.len() != sig.params.len() {
2648 return Err(SeqError::new(
2649 file,
2650 arg.line,
2651 1,
2652 format!(
2653 "fn {name}: expected {} parameters, and {attr} provides {} values",
2654 sig.params.len(),
2655 expected.len()
2656 ),
2657 ));
2658 }
2659 for (i, (e, p)) in expected.iter().zip(&sig.params).enumerate() {
2660 if !e.fits(p) {
2661 return Err(SeqError::new(
2662 file,
2663 arg.line,
2664 1,
2665 format!(
2666 "fn {name}: parametr {} to {}, a {attr} podaje {}",
2667 i + 1,
2668 p.name(),
2669 e.name()
2670 ),
2671 ));
2672 }
2673 }
2674 Ok(())
2675}
2676
2677fn check_anims(file: &str, app: &crate::ir::SeqApp) -> Result<(), SeqError> {
2678 use std::collections::HashSet;
2679 let mut names: HashSet<String> = HashSet::new();
2680 for a in &app.anims {
2681 if !names.insert(a.name.clone()) {
2682 return Err(SeqError::new(
2683 file,
2684 a.line,
2685 1,
2686 format!("anim '{}' zdefiniowany dwa razy", a.name),
2687 ));
2688 }
2689 if let Some(m) = &a.loop_mode {
2690 if !matches!(m.as_str(), "once" | "loop" | "pingpong") {
2691 return Err(SeqError::new(
2692 file,
2693 a.line,
2694 1,
2695 format!("nieznany tryb loop '{}'", m),
2696 ));
2697 }
2698 }
2699 for t in &a.tracks {
2700 if let crate::ir::SeqValue::Array(items) = &t.value {
2701 let mut prev: Option<f32> = None;
2702 for kf in items {
2703 if let crate::ir::SeqValue::Tuple(parts) = kf {
2704 if let Some(crate::ir::SeqValue::Num(time)) = parts.first() {
2705 if let Some(p) = prev {
2706 if *time < p {
2707 return Err(SeqError::new(
2708 file,
2709 t.line,
2710 1,
2711 format!(
2712 "{}: track {}: czas {} po {}",
2713 a.name, t.name, time, p
2714 ),
2715 ));
2716 }
2717 }
2718 prev = Some(*time);
2719 }
2720 }
2721 }
2722 }
2723 }
2724 }
2725 Ok(())
2726}
2727
2728fn check_machines(file: &str, app: &crate::ir::SeqApp) -> Result<(), SeqError> {
2729 use std::collections::HashSet;
2730 let mut names: HashSet<String> = HashSet::new();
2731 for m in &app.machines {
2732 if !names.insert(m.name.clone()) {
2733 return Err(SeqError::new(
2734 file,
2735 m.line,
2736 1,
2737 format!("machine '{}' zdefiniowany dwa razy", m.name),
2738 ));
2739 }
2740 for tr in &m.transitions {
2741 for c in &tr.conditions {
2742 if c.starts_with("num(") {
2743 let ops = ["<", "<=", "==", "!=", ">=", ">"];
2744 if !ops.iter().any(|o| c.contains(o)) {
2745 return Err(SeqError::new(
2746 file,
2747 tr.line,
2748 1,
2749 "nieznany operator".to_string(),
2750 ));
2751 }
2752 }
2753 }
2754 }
2755 }
2756 Ok(())
2757}
2758
2759fn check_springs(file: &str, app: &crate::ir::SeqApp) -> Result<(), SeqError> {
2760 use std::collections::HashSet;
2761 let mut names: HashSet<String> = HashSet::new();
2762 for s in &app.springs {
2763 if !names.insert(s.name.clone()) {
2764 return Err(SeqError::new(
2765 file,
2766 s.line,
2767 1,
2768 format!("spring '{}' zdefiniowany dwa razy", s.name),
2769 ));
2770 }
2771 }
2772 Ok(())
2773}