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