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    "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}