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