Skip to main content

sequoia_seq/
check.rs

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