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