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