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(¶m.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}