1use brink_ir::{
49 BlockStmt, Content, ContentPart, Diagnostic, DiagnosticCode, ElseBranch, Expr, FileId, HirFile,
50 HostManifest, IfStmt, Knot, Name, Param, ResolutionMap, Stmt, StringPart, SymbolIndex,
51 TypeExpr,
52};
53
54use crate::infer::{EffectRow, Ty};
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
59pub enum Protocol {
60 Display,
65 Compare,
68 Iterate,
73}
74
75impl Protocol {
76 pub const ALL: [Protocol; 3] = [Protocol::Display, Protocol::Compare, Protocol::Iterate];
78
79 #[must_use]
82 pub fn method_name(self) -> &'static str {
83 match self {
84 Protocol::Display => "display",
85 Protocol::Compare => "compare",
86 Protocol::Iterate => "next",
87 }
88 }
89
90 #[must_use]
92 pub fn protocol_name(self) -> &'static str {
93 match self {
94 Protocol::Display => "display",
95 Protocol::Compare => "compare",
96 Protocol::Iterate => "iterate",
97 }
98 }
99
100 #[must_use]
102 pub fn arity(self) -> usize {
103 match self {
104 Protocol::Display | Protocol::Iterate => 1,
105 Protocol::Compare => 2,
106 }
107 }
108
109 #[must_use]
114 pub fn receiver_is_ref(self) -> bool {
115 matches!(self, Protocol::Iterate)
116 }
117
118 #[must_use]
120 pub fn contract_phrase(self) -> &'static str {
121 match self {
122 Protocol::Display | Protocol::Compare => "pure\u{b7}silent\u{b7}total",
123 Protocol::Iterate => "writes-receiver\u{b7}silent\u{b7}total",
124 }
125 }
126}
127
128#[must_use]
131pub fn is_reserved_protocol_name(name: &str) -> bool {
132 Protocol::ALL.iter().any(|p| p.method_name() == name)
133}
134
135#[must_use]
143pub fn iterate_element_ty(iterable: &Ty) -> Option<Ty> {
144 match iterable {
145 Ty::Array(elem) => Some((**elem).clone()),
146 Ty::Map(key, _) => Some((**key).clone()),
147 Ty::Range { .. } => Some(Ty::Int),
151 _ => None,
152 }
153}
154
155#[must_use]
164pub fn iterate_val_ty(iterable: &Ty) -> Option<Ty> {
165 match iterable {
166 Ty::Map(_, val) => Some((**val).clone()),
167 _ => None,
168 }
169}
170
171#[must_use]
194pub fn check_reserved_names(files: &[(FileId, &HirFile)]) -> Vec<Diagnostic> {
195 let mut out = Vec::new();
196 for &(file, hir) in files {
197 let mut push = |name: &Name, what: &str| {
198 if is_reserved_protocol_name(&name.text) {
199 out.push(Diagnostic {
200 file,
201 range: name.range,
202 code: DiagnosticCode::E113,
203 message: format!(
204 "`{}` is a reserved protocol method name (stdlib-spec \u{a7}9.6) and cannot name a {what}",
205 name.text
206 ),
207 });
208 }
209 };
210 for var in &hir.variables {
211 push(&var.name, "VAR");
212 walk_expr_for_lambdas(&var.value, &mut push);
213 }
214 for cst in &hir.constants {
215 push(&cst.name, "CONST");
216 walk_expr_for_lambdas(&cst.value, &mut push);
217 }
218 for ext in &hir.externals {
219 push(&ext.name, "EXTERNAL");
220 }
221 for knot in &hir.knots {
222 push(&knot.name, "knot or function");
223 walk_params(&knot.params, &mut push);
224 walk_stmts(&knot.body.stmts, &mut push);
225 for stitch in &knot.stitches {
226 push(&stitch.name, "stitch");
227 walk_params(&stitch.params, &mut push);
228 walk_stmts(&stitch.body.stmts, &mut push);
229 }
230 }
231 walk_stmts(&hir.root_content.stmts, &mut push);
232 }
233 out
234}
235
236fn walk_params(params: &[Param], push: &mut impl FnMut(&Name, &str)) {
237 for p in params {
238 push(&p.name, "parameter");
239 }
240}
241
242fn walk_expr_for_lambdas(expr: &Expr, push: &mut impl FnMut(&Name, &str)) {
259 match expr {
260 Expr::Lambda(l) => {
261 walk_params(&l.params, push);
262 for e in l.body.all_exprs() {
263 walk_expr_for_lambdas(e, push);
264 }
265 }
266 Expr::Call(_path, args) => {
267 for arg in args {
268 walk_expr_for_lambdas(arg, push);
269 }
270 }
271 Expr::Prefix(_, inner) | Expr::Postfix(inner, _) => walk_expr_for_lambdas(inner, push),
272 Expr::Infix(ie) => {
273 walk_expr_for_lambdas(&ie.lhs, push);
274 walk_expr_for_lambdas(&ie.rhs, push);
275 }
276 Expr::String(s) => {
277 for part in &s.parts {
278 if let StringPart::Interpolation(e) = part {
279 walk_expr_for_lambdas(e, push);
280 }
281 }
282 }
283 Expr::ArrayLiteral(a) => {
284 for e in &a.elements {
285 walk_expr_for_lambdas(e, push);
286 }
287 }
288 Expr::MapLiteral(m) => {
289 for (k, v) in &m.entries {
290 walk_expr_for_lambdas(k, push);
291 walk_expr_for_lambdas(v, push);
292 }
293 }
294 Expr::Index(idx) => {
295 walk_expr_for_lambdas(&idx.base, push);
296 walk_expr_for_lambdas(&idx.index, push);
297 }
298 Expr::StructLiteral(sl) => {
299 for (_name, val) in &sl.fields {
300 walk_expr_for_lambdas(val, push);
301 }
302 }
303 Expr::FieldAccess(fa) => walk_expr_for_lambdas(&fa.base, push),
304 Expr::FnLiteral(fl) => {
308 for arg in &fl.args {
309 walk_expr_for_lambdas(arg, push);
310 }
311 }
312 Expr::RefArg(ra) => walk_expr_for_lambdas(&ra.operand, push),
313 Expr::Range(r) => {
314 walk_expr_for_lambdas(&r.start, push);
315 walk_expr_for_lambdas(&r.end, push);
316 }
317 Expr::Fragment(stmts) => walk_stmts(stmts, push),
322 Expr::Int(_)
323 | Expr::Float(_)
324 | Expr::Bool(_)
325 | Expr::Null
326 | Expr::Path(_)
327 | Expr::DivertTarget(_)
328 | Expr::ListLiteral(_) => {}
329 }
330}
331
332fn walk_stmts(stmts: &[Stmt], push: &mut impl FnMut(&Name, &str)) {
339 for stmt in stmts {
340 match stmt {
341 Stmt::TempDecl(t) => {
342 push(&t.name, "temp");
343 if let Some(v) = &t.value {
344 walk_expr_for_lambdas(v, push);
345 }
346 }
347 Stmt::Content(c) => walk_content(c, push),
348 Stmt::ChoiceSet(cs) => {
349 for choice in &cs.choices {
350 if let Some(binding) = &choice.binding {
355 push(binding, "binding");
356 }
357 if let Some(cond) = &choice.condition {
358 walk_expr_for_lambdas(cond, push);
359 }
360 for c in [
366 &choice.start_content,
367 &choice.bracket_content,
368 &choice.inner_content,
369 ]
370 .into_iter()
371 .flatten()
372 {
373 walk_content(c, push);
374 }
375 walk_stmts(&choice.body.stmts, push);
376 }
377 walk_stmts(&cs.continuation.stmts, push);
378 }
379 Stmt::LabeledBlock(b) => walk_stmts(&b.stmts, push),
380 Stmt::Conditional(c) => {
381 for branch in &c.branches {
382 if let Some(binding) = &branch.binding {
383 push(binding, "binding");
384 }
385 if let Some(cond) = &branch.condition {
386 walk_expr_for_lambdas(cond, push);
387 }
388 walk_stmts(&branch.body.stmts, push);
389 }
390 }
391 Stmt::Sequence(s) => {
392 for branch in &s.branches {
393 walk_stmts(&branch.body.stmts, push);
394 }
395 }
396 Stmt::LogicBlock(lb) => walk_block_stmts(&lb.stmts, push),
397 Stmt::Divert(d) => {
398 for arg in &d.target.args {
399 walk_expr_for_lambdas(arg, push);
400 }
401 }
402 Stmt::TunnelCall(tc) => {
403 for target in &tc.targets {
404 for arg in &target.args {
405 walk_expr_for_lambdas(arg, push);
406 }
407 }
408 }
409 Stmt::ThreadStart(ts) => {
410 for arg in &ts.target.args {
411 walk_expr_for_lambdas(arg, push);
412 }
413 }
414 Stmt::Assignment(a) => {
415 walk_expr_for_lambdas(&a.target, push);
416 walk_expr_for_lambdas(&a.value, push);
417 }
418 Stmt::Return(r) => {
419 if let Some(v) = &r.value {
420 walk_expr_for_lambdas(v, push);
421 }
422 for arg in &r.onwards_args {
423 walk_expr_for_lambdas(arg, push);
424 }
425 }
426 Stmt::ExprStmt(e) | Stmt::AttachElement(e) => walk_expr_for_lambdas(e, push),
427 Stmt::Await(a) => {
428 if let Some(cond) = &a.condition {
429 walk_expr_for_lambdas(cond, push);
430 }
431 }
432 Stmt::EndOfLine | Stmt::EndElementRun => {}
433 }
434 }
435}
436
437fn walk_content(content: &Content, push: &mut impl FnMut(&Name, &str)) {
438 for part in &content.parts {
439 walk_content_part(part, push);
440 }
441}
442
443fn walk_content_part(part: &ContentPart, push: &mut impl FnMut(&Name, &str)) {
444 match part {
445 ContentPart::InlineConditional(c) => {
446 for branch in &c.branches {
447 if let Some(cond) = &branch.condition {
448 walk_expr_for_lambdas(cond, push);
449 }
450 walk_stmts(&branch.body.stmts, push);
451 }
452 }
453 ContentPart::InlineSequence(s) => {
454 for branch in &s.branches {
455 walk_stmts(&branch.body.stmts, push);
456 }
457 }
458 ContentPart::Span(span) => {
461 for child in &span.children {
462 walk_content_part(child, push);
463 }
464 }
465 ContentPart::Interpolation(e) => walk_expr_for_lambdas(e, push),
466 ContentPart::Text(_) | ContentPart::Glue | ContentPart::Spring => {}
467 }
468}
469
470fn walk_block_stmts(stmts: &[BlockStmt], push: &mut impl FnMut(&Name, &str)) {
474 for stmt in stmts {
475 match stmt {
476 BlockStmt::TempDecl(t) => {
477 push(&t.name, "temp");
478 if let Some(v) = &t.value {
479 walk_expr_for_lambdas(v, push);
480 }
481 }
482 BlockStmt::If(i) => walk_if(i, push),
483 BlockStmt::While(w) => {
484 if let Some(binding) = &w.binding {
485 push(binding, "binding");
486 }
487 walk_expr_for_lambdas(&w.condition, push);
488 walk_block_stmts(&w.body, push);
489 }
490 BlockStmt::For(f) => {
491 push(&f.var_name, "for-loop variable");
492 if let Some(val_name) = &f.val_name {
493 push(val_name, "for-loop variable");
494 }
495 walk_expr_for_lambdas(&f.iterable, push);
496 walk_block_stmts(&f.body, push);
497 }
498 BlockStmt::Assignment(a) => {
499 walk_expr_for_lambdas(&a.target, push);
500 walk_expr_for_lambdas(&a.value, push);
501 }
502 BlockStmt::Return(r) => {
503 if let Some(v) = &r.value {
504 walk_expr_for_lambdas(v, push);
505 }
506 for arg in &r.onwards_args {
507 walk_expr_for_lambdas(arg, push);
508 }
509 }
510 BlockStmt::ExprStmt(e) => walk_expr_for_lambdas(e, push),
511 BlockStmt::Await(a) => {
512 if let Some(cond) = &a.condition {
513 walk_expr_for_lambdas(cond, push);
514 }
515 }
516 BlockStmt::Break(_) | BlockStmt::Continue(_) => {}
517 }
518 }
519}
520
521fn walk_if(i: &IfStmt, push: &mut impl FnMut(&Name, &str)) {
522 if let Some(binding) = &i.binding {
525 push(binding, "binding");
526 }
527 walk_expr_for_lambdas(&i.condition, push);
528 walk_block_stmts(&i.body, push);
529 match &i.else_branch {
530 Some(ElseBranch::ElseIf(inner)) => walk_if(inner, push),
531 Some(ElseBranch::Else(stmts)) => walk_block_stmts(stmts, push),
532 None => {}
533 }
534}
535
536#[derive(Debug, Clone, PartialEq, Eq)]
542pub struct ProtocolImplDecl {
543 pub protocol: Protocol,
544 pub type_name: String,
546 pub function: String,
549}
550
551#[must_use]
566pub fn check_protocol_impls(
567 files: &[(FileId, &HirFile)],
568 index: &SymbolIndex,
569 resolutions: &ResolutionMap,
570 host_manifest: Option<&HostManifest>,
571 impls: &[ProtocolImplDecl],
572) -> Vec<Diagnostic> {
573 let mut out = Vec::new();
574 if impls.is_empty() {
575 return out;
576 }
577
578 let struct_names: std::collections::BTreeSet<&str> = files
579 .iter()
580 .flat_map(|(_, hir)| hir.structs.iter())
581 .map(|s| s.name.text.as_str())
582 .collect();
583
584 let mut checked: Vec<(&ProtocolImplDecl, FileId, &Knot)> = Vec::new();
587 let mut seen: std::collections::BTreeSet<(Protocol, &str)> = std::collections::BTreeSet::new();
588
589 for decl in impls {
590 let Some((file, knot)) = find_function(files, &decl.function) else {
591 out.push(registration_error(
592 files,
593 format!(
594 "protocol impl `{}` for `{}`: `{}` is not a declared function",
595 decl.protocol.protocol_name(),
596 decl.type_name,
597 decl.function
598 ),
599 ));
600 continue;
601 };
602 let at = |message: String| Diagnostic {
603 file,
604 range: knot.name.range,
605 code: DiagnosticCode::E115,
606 message,
607 };
608
609 if crate::infer::TowerTy::from_name(&decl.type_name).is_some() {
617 out.push(Diagnostic {
618 file,
619 range: knot.name.range,
620 code: DiagnosticCode::E118,
621 message: format!(
622 "protocol impl `{}` for `{}`: numeric-tower kinds are compiler-known and cannot implement registry protocols{}",
623 decl.protocol.protocol_name(),
624 decl.type_name,
625 if decl.protocol == Protocol::Compare {
626 " (tower values are not orderable — tower-mini-spec T4)"
627 } else {
628 ""
629 }
630 ),
631 });
632 continue;
633 }
634
635 if !struct_names.contains(decl.type_name.as_str()) {
636 out.push(at(format!(
637 "protocol impl `{}` for `{}`: the type is not a declared STRUCT (only user struct types may implement registry protocols)",
638 decl.protocol.protocol_name(),
639 decl.type_name
640 )));
641 continue;
642 }
643 if !seen.insert((decl.protocol, decl.type_name.as_str())) {
644 out.push(at(format!(
645 "duplicate protocol impl: `{}` for `{}` is already registered",
646 decl.protocol.protocol_name(),
647 decl.type_name
648 )));
649 continue;
650 }
651 if let Some(message) = shape_error(decl, knot) {
652 out.push(at(message));
653 continue;
654 }
655 checked.push((decl, file, knot));
656 }
657
658 if checked.is_empty() {
659 return out;
660 }
661
662 let rows = crate::infer::effects_project(files, index, resolutions, host_manifest);
666 for (decl, file, knot) in checked {
667 let Some(def_id) = index.by_name.get(&decl.function).and_then(|ids| {
668 ids.iter()
669 .copied()
670 .find(|id| index.symbols.get(id).is_some_and(|info| info.file == file))
671 }) else {
672 continue;
673 };
674 let Some(row) = rows.get(&def_id) else {
675 continue;
676 };
677 if let Some(message) = contract_error(decl.protocol, &decl.type_name, row, index) {
678 out.push(Diagnostic {
679 file,
680 range: knot.name.range,
681 code: DiagnosticCode::E114,
682 message,
683 });
684 }
685 }
686 out
687}
688
689fn find_function<'a>(files: &[(FileId, &'a HirFile)], name: &str) -> Option<(FileId, &'a Knot)> {
691 files.iter().find_map(|&(file, hir)| {
692 hir.knots
693 .iter()
694 .find(|k| k.is_function && k.name.text == name)
695 .map(|k| (file, k))
696 })
697}
698
699fn shape_error(decl: &ProtocolImplDecl, knot: &Knot) -> Option<String> {
704 let proto = decl.protocol;
705 if knot.params.len() != proto.arity() {
706 return Some(format!(
707 "protocol impl `{}` for `{}`: `{}` takes {} parameter(s), but the protocol method `{}` declares {}",
708 proto.protocol_name(),
709 decl.type_name,
710 knot.name.text,
711 knot.params.len(),
712 proto.method_name(),
713 proto.arity()
714 ));
715 }
716 for (i, param) in knot.params.iter().enumerate() {
717 let want_ref = i == 0 && proto.receiver_is_ref();
718 if param.is_ref != want_ref {
719 return Some(format!(
720 "protocol impl `{}` for `{}`: parameter `{}` must {} `ref` (the protocol method is `{}`)",
721 proto.protocol_name(),
722 decl.type_name,
723 param.name.text,
724 if want_ref { "be" } else { "not be" },
725 signature_phrase(proto),
726 ));
727 }
728 if let Some(TypeExpr::Named { name, .. }) = ¶m.annotation
731 && name != &decl.type_name
732 {
733 return Some(format!(
734 "protocol impl `{}` for `{}`: parameter `{}` is annotated `{}`, but the receiver of a protocol impl must be the implementing type",
735 proto.protocol_name(),
736 decl.type_name,
737 param.name.text,
738 name
739 ));
740 }
741 }
742 let want_return = match proto {
743 Protocol::Display => Some("string"),
744 Protocol::Compare => Some("int"),
745 Protocol::Iterate => None,
748 };
749 if let (Some(want), Some(TypeExpr::Named { name, .. })) = (want_return, &knot.return_type)
750 && name != want
751 {
752 return Some(format!(
753 "protocol impl `{}` for `{}`: return type is annotated `{}`, but `{}` returns `{}`",
754 proto.protocol_name(),
755 decl.type_name,
756 name,
757 signature_phrase(proto),
758 want
759 ));
760 }
761 None
762}
763
764fn signature_phrase(proto: Protocol) -> &'static str {
765 match proto {
766 Protocol::Display => "display(self: T): string",
767 Protocol::Compare => "compare(a: T, b: T): int",
768 Protocol::Iterate => "next(ref self): Option[T]",
769 }
770}
771
772fn contract_error(
781 proto: Protocol,
782 type_name: &str,
783 row: &EffectRow,
784 index: &SymbolIndex,
785) -> Option<String> {
786 let faults_exceed = row.faults_refined && !matches!(proto, Protocol::Iterate);
806 if !row.is_pessimal()
807 && row.reads.is_empty()
808 && row.writes.is_empty()
809 && row.calls.is_empty()
810 && !row.emits
811 && !row.tags
812 && !faults_exceed
813 {
814 return None;
815 }
816 let mut parts = Vec::new();
817 if row.is_pessimal() {
818 parts.push(
819 "calls through a function value or unresolved callee (unbounded row)".to_string(),
820 );
821 }
822 let name_of = |id: &brink_format::DefinitionId| {
823 index
824 .symbols
825 .get(id)
826 .map_or_else(|| format!("{id:?}"), |info| info.name.clone())
827 };
828 if !row.reads.is_empty() {
829 let names: Vec<String> = row.reads.iter().map(name_of).collect();
830 parts.push(format!("reads {}", names.join(", ")));
831 }
832 if !row.writes.is_empty() {
833 let names: Vec<String> = row.writes.iter().map(name_of).collect();
834 parts.push(format!("writes {}", names.join(", ")));
835 }
836 if !row.calls.is_empty() {
837 let names: Vec<String> = row.calls.iter().cloned().collect();
838 parts.push(format!("calls {}", names.join(", ")));
839 }
840 if row.emits {
841 parts.push("emits content".to_string());
842 }
843 if row.tags {
844 parts.push("touches the tag channel".to_string());
845 }
846 if faults_exceed {
847 parts.push("can raise a turn-terminating fault".to_string());
848 }
849 Some(format!(
850 "protocol impl `{}` for `{type_name}` exceeds the {} contract: {}",
851 proto.protocol_name(),
852 proto.contract_phrase(),
853 parts.join("; ")
854 ))
855}
856
857fn registration_error(files: &[(FileId, &HirFile)], message: String) -> Diagnostic {
858 Diagnostic {
859 file: files.first().map_or(FileId(0), |&(f, _)| f),
860 range: rowan::TextRange::empty(0.into()),
861 code: DiagnosticCode::E115,
862 message,
863 }
864}
865
866#[cfg(test)]
867mod tests {
868 use brink_ir::SymbolManifest;
869 use brink_ir::hir::HirFile;
870
871 use super::*;
872
873 fn lower(src: &str) -> (HirFile, SymbolManifest) {
874 let parsed = brink_syntax::parse(src);
875 let tree = parsed.tree();
876 let (hir, manifest, diags) = brink_ir::hir::lower(FileId(0), &tree);
877 assert!(diags.is_empty(), "lowering diagnostics: {diags:?}");
878 (hir, manifest)
879 }
880
881 fn reserved_diags(src: &str) -> Vec<Diagnostic> {
882 let (hir, _manifest) = lower(src);
883 check_reserved_names(&[(FileId(0), &hir)])
884 }
885
886 fn reserved_diags_native(src: &str) -> Vec<Diagnostic> {
892 let parse = brink_syntax_native::parse(src);
893 assert!(
894 parse.errors().is_empty(),
895 "fixture must parse cleanly: {:?}",
896 parse.errors()
897 );
898 let tree = parse.tree();
899 let (hir, _manifest, diags) = brink_ir::hir::lower_native::lower(FileId(0), &tree);
900 assert!(diags.is_empty(), "lowering diagnostics: {diags:?}");
901 check_reserved_names(&[(FileId(0), &hir)])
902 }
903
904 fn impl_diags(src: &str, impls: &[ProtocolImplDecl]) -> Vec<Diagnostic> {
905 let (hir, manifest) = lower(src);
906 let result = crate::analyze(&[(FileId(0), &hir, &manifest)]);
907 check_protocol_impls(
908 &[(FileId(0), &hir)],
909 &result.index,
910 &result.resolutions,
911 None,
912 impls,
913 )
914 }
915
916 fn decl(protocol: Protocol, type_name: &str, function: &str) -> ProtocolImplDecl {
917 ProtocolImplDecl {
918 protocol,
919 type_name: type_name.to_string(),
920 function: function.to_string(),
921 }
922 }
923
924 const POINT: &str = "STRUCT Point = #{\n x: float,\n y: float,\n}\n";
925
926 #[test]
929 fn knot_named_display_is_reserved() {
930 let diags = reserved_diags("== display ==\nHello.\n-> DONE\n");
931 assert_eq!(diags.len(), 1, "{diags:?}");
932 assert_eq!(diags[0].code, DiagnosticCode::E113);
933 }
934
935 #[test]
936 fn function_named_compare_is_reserved() {
937 let diags = reserved_diags("=== function compare(a, b) ===\n~ return 0\n");
938 assert_eq!(diags.len(), 1, "{diags:?}");
939 assert_eq!(diags[0].code, DiagnosticCode::E113);
940 }
941
942 #[test]
943 fn stitch_named_next_is_reserved() {
944 let diags = reserved_diags("== knot ==\n= next\nHello.\n-> DONE\n");
945 assert_eq!(diags.len(), 1, "{diags:?}");
946 assert_eq!(diags[0].code, DiagnosticCode::E113);
947 }
948
949 #[test]
950 fn var_const_external_named_reserved() {
951 let diags = reserved_diags("VAR display = 1\nCONST compare = 2\nEXTERNAL next(x)\n");
952 assert_eq!(diags.len(), 3, "{diags:?}");
953 assert!(diags.iter().all(|d| d.code == DiagnosticCode::E113));
954 }
955
956 #[test]
957 fn param_named_display_is_reserved() {
958 let diags = reserved_diags("=== function f(display) ===\n~ return display\n");
959 assert_eq!(diags.len(), 1, "{diags:?}");
960 assert_eq!(diags[0].code, DiagnosticCode::E113);
961 }
962
963 #[test]
964 fn temp_and_for_var_in_logic_block_are_reserved() {
965 let src = "== k ==\n~ {\n temp next = 1\n for display in #[1, 2] {\n next = next + display\n }\n}\n-> DONE\n";
966 let diags = reserved_diags(src);
967 assert_eq!(diags.len(), 2, "{diags:?}");
968 assert!(diags.iter().all(|d| d.code == DiagnosticCode::E113));
969 }
970
971 #[test]
972 fn weave_level_temp_named_next_is_reserved() {
973 let diags = reserved_diags("== k ==\n~ temp next = 1\n{next}\n-> DONE\n");
974 assert_eq!(diags.len(), 1, "{diags:?}");
975 assert_eq!(diags[0].code, DiagnosticCode::E113);
976 }
977
978 #[test]
979 fn list_members_and_type_names_are_not_reserved() {
980 let diags = reserved_diags("LIST steps = intro, next, outro\n");
984 assert!(diags.is_empty(), "{diags:?}");
985 }
986
987 #[test]
988 fn lambda_param_named_display_is_reserved() {
989 let diags = reserved_diags_native("var f = |display| display\n");
994 assert_eq!(diags.len(), 1, "{diags:?}");
995 assert_eq!(diags[0].code, DiagnosticCode::E113);
996 }
997
998 #[test]
999 fn lambda_param_named_display_in_choice_label_is_reserved() {
1000 let diags =
1006 reserved_diags_native("flow f() {\n {?\n * Gold: {fmt(|display| 0)}\n }\n}\n");
1007 assert_eq!(diags.len(), 1, "{diags:?}");
1008 assert_eq!(diags[0].code, DiagnosticCode::E113);
1009 }
1010
1011 #[test]
1012 fn ordinary_names_stay_clean() {
1013 let diags = reserved_diags(
1014 "VAR score = 1\n== k ==\n~ temp shown = score\n{shown}\n-> DONE\n=== function render(p) ===\n~ return \"x\"\n",
1015 );
1016 assert!(diags.is_empty(), "{diags:?}");
1017 }
1018
1019 #[test]
1022 fn well_formed_display_impl_is_clean() {
1023 let src = format!("{POINT}=== function render(p: Point): string ===\n~ return \"P\"\n");
1024 let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
1025 assert!(diags.is_empty(), "{diags:?}");
1026 }
1027
1028 #[test]
1029 fn unknown_function_is_e115() {
1030 let diags = impl_diags(POINT, &[decl(Protocol::Display, "Point", "nope")]);
1031 assert_eq!(diags.len(), 1, "{diags:?}");
1032 assert_eq!(diags[0].code, DiagnosticCode::E115);
1033 assert!(diags[0].message.contains("not a declared function"));
1034 }
1035
1036 #[test]
1037 fn non_struct_type_is_e115() {
1038 let src = "=== function render(p) ===\n~ return \"x\"\n";
1039 let diags = impl_diags(src, &[decl(Protocol::Display, "Point", "render")]);
1040 assert_eq!(diags.len(), 1, "{diags:?}");
1041 assert_eq!(diags[0].code, DiagnosticCode::E115);
1042 assert!(diags[0].message.contains("not a declared STRUCT"));
1043 }
1044
1045 #[test]
1046 fn wrong_arity_is_e115() {
1047 let src = format!("{POINT}=== function render(p, extra) ===\n~ return \"x\"\n");
1048 let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
1049 assert_eq!(diags.len(), 1, "{diags:?}");
1050 assert_eq!(diags[0].code, DiagnosticCode::E115);
1051 assert!(diags[0].message.contains("parameter"));
1052 }
1053
1054 #[test]
1055 fn display_receiver_must_not_be_ref() {
1056 let src = format!("{POINT}=== function render(ref p) ===\n~ return \"x\"\n");
1057 let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
1058 assert_eq!(diags.len(), 1, "{diags:?}");
1059 assert_eq!(diags[0].code, DiagnosticCode::E115);
1060 }
1061
1062 #[test]
1063 fn next_receiver_must_be_ref() {
1064 let src = format!("{POINT}=== function step(p) ===\n~ return 0\n");
1065 let diags = impl_diags(&src, &[decl(Protocol::Iterate, "Point", "step")]);
1066 assert_eq!(diags.len(), 1, "{diags:?}");
1067 assert_eq!(diags[0].code, DiagnosticCode::E115);
1068 assert!(diags[0].message.contains("ref"));
1069 }
1070
1071 #[test]
1072 fn contradicting_param_annotation_is_e115() {
1073 let src = format!("{POINT}=== function render(p: int) ===\n~ return \"x\"\n");
1074 let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
1075 assert_eq!(diags.len(), 1, "{diags:?}");
1076 assert_eq!(diags[0].code, DiagnosticCode::E115);
1077 assert!(diags[0].message.contains("annotated"));
1078 }
1079
1080 #[test]
1081 fn contradicting_return_annotation_is_e115() {
1082 let src =
1083 format!("{POINT}=== function cmp(a: Point, b: Point): string ===\n~ return \"x\"\n");
1084 let diags = impl_diags(&src, &[decl(Protocol::Compare, "Point", "cmp")]);
1085 assert_eq!(diags.len(), 1, "{diags:?}");
1086 assert_eq!(diags[0].code, DiagnosticCode::E115);
1087 assert!(diags[0].message.contains("return"));
1088 }
1089
1090 #[test]
1091 fn duplicate_registration_is_e115() {
1092 let src = format!(
1093 "{POINT}=== function render(p) ===\n~ return \"x\"\n=== function render2(p) ===\n~ return \"y\"\n"
1094 );
1095 let diags = impl_diags(
1096 &src,
1097 &[
1098 decl(Protocol::Display, "Point", "render"),
1099 decl(Protocol::Display, "Point", "render2"),
1100 ],
1101 );
1102 assert_eq!(diags.len(), 1, "{diags:?}");
1103 assert_eq!(diags[0].code, DiagnosticCode::E115);
1104 assert!(diags[0].message.contains("duplicate"));
1105 }
1106
1107 #[test]
1110 fn compare_for_tower_kind_is_e118() {
1111 let src = "=== function cmp(a, b) ===\n~ return 0\n";
1114 for kind in ["vec2", "vec3", "vec4", "quat", "mat2", "mat3", "mat4"] {
1115 let diags = impl_diags(src, &[decl(Protocol::Compare, kind, "cmp")]);
1116 assert_eq!(diags.len(), 1, "{kind}: {diags:?}");
1117 assert_eq!(diags[0].code, DiagnosticCode::E118, "{kind}");
1118 assert!(diags[0].message.contains("not orderable"), "{kind}");
1119 }
1120 }
1121
1122 #[test]
1123 fn display_and_iterate_for_tower_kind_are_e118() {
1124 let src = "=== function render(p) ===\n~ return \"x\"\n";
1125 for proto in [Protocol::Display, Protocol::Iterate] {
1126 let diags = impl_diags(src, &[decl(proto, "vec3", "render")]);
1127 assert_eq!(diags.len(), 1, "{proto:?}: {diags:?}");
1128 assert_eq!(diags[0].code, DiagnosticCode::E118, "{proto:?}");
1129 }
1130 }
1131
1132 #[test]
1133 fn tower_rejection_wins_over_a_shadowing_struct() {
1134 let src = "STRUCT vec3 = #{\n v: float,\n}\n=== function cmp(a, b) ===\n~ return 0\n";
1137 let diags = impl_diags(src, &[decl(Protocol::Compare, "vec3", "cmp")]);
1138 assert_eq!(diags.len(), 1, "{diags:?}");
1139 assert_eq!(diags[0].code, DiagnosticCode::E118);
1140 }
1141
1142 #[test]
1145 fn global_write_exceeds_display_contract() {
1146 let src = format!(
1147 "{POINT}VAR seen = 0\n=== function render(p) ===\n~ seen = seen + 1\n~ return \"x\"\n"
1148 );
1149 let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
1150 assert_eq!(diags.len(), 1, "{diags:?}");
1151 assert_eq!(diags[0].code, DiagnosticCode::E114);
1152 assert!(
1153 diags[0].message.contains("writes seen"),
1154 "{}",
1155 diags[0].message
1156 );
1157 }
1158
1159 #[test]
1160 fn global_read_exceeds_display_contract() {
1161 let src = format!("{POINT}VAR mood = 1\n=== function render(p) ===\n~ return mood\n");
1165 let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
1166 assert_eq!(diags.len(), 1, "{diags:?}");
1167 assert_eq!(diags[0].code, DiagnosticCode::E114);
1168 assert!(
1169 diags[0].message.contains("reads mood"),
1170 "{}",
1171 diags[0].message
1172 );
1173 }
1174
1175 #[test]
1176 fn emitting_impl_exceeds_silent() {
1177 let src = format!("{POINT}=== function render(p) ===\nLoud line.\n~ return \"x\"\n");
1178 let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
1179 assert_eq!(diags.len(), 1, "{diags:?}");
1180 assert_eq!(diags[0].code, DiagnosticCode::E114);
1181 assert!(diags[0].message.contains("emits"), "{}", diags[0].message);
1182 }
1183
1184 #[test]
1185 fn faulting_impl_exceeds_total() {
1186 let src = format!(
1192 "{POINT}=== function cmp(a, b) ===\n~ temp lowest = min(#[1.0, 2.0])\n~ return 0\n"
1193 );
1194 let diags = impl_diags(&src, &[decl(Protocol::Compare, "Point", "cmp")]);
1195 assert_eq!(diags.len(), 1, "{diags:?}");
1196 assert_eq!(diags[0].code, DiagnosticCode::E114);
1197 assert!(diags[0].message.contains("fault"), "{}", diags[0].message);
1198 }
1199
1200 #[test]
1207 fn f29_provably_total_impl_is_not_rejected_for_conservative_faults() {
1208 let src = format!(
1214 "{POINT}=== function cmp(a, b) ===\n~ temp lowest = min(#[1, 2])\n~ temp n = len(#[1, 2])\n~ return 0\n"
1215 );
1216 let diags = impl_diags(&src, &[decl(Protocol::Compare, "Point", "cmp")]);
1217 assert!(diags.is_empty(), "{diags:?}");
1218 }
1219
1220 #[test]
1221 fn f29_opaque_impl_keeps_the_conservative_union() {
1222 let src = format!(
1227 "{POINT}=== function helper() ===\n~ return 1\n\n=== function shape(self) ===\n~ temp f = #fn(helper)\n~ temp n = call(f)\n~ return \"p\"\n"
1228 );
1229 let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "shape")]);
1230 assert_eq!(diags.len(), 1, "{diags:?}");
1231 assert_eq!(diags[0].code, DiagnosticCode::E114);
1232 }
1233
1234 #[test]
1235 fn f29_value_dependent_fault_still_rejects() {
1236 let src = format!(
1239 "{POINT}=== function cmp(a, b) ===\n~ temp arr = #[1, 2]\n~ temp x = arr[5]\n~ return 0\n"
1240 );
1241 let diags = impl_diags(&src, &[decl(Protocol::Compare, "Point", "cmp")]);
1242 assert_eq!(diags.len(), 1, "{diags:?}");
1243 assert_eq!(diags[0].code, DiagnosticCode::E114);
1244 assert!(diags[0].message.contains("fault"), "{}", diags[0].message);
1245 }
1246
1247 #[test]
1248 fn pure_compare_impl_is_clean() {
1249 let src = format!("{POINT}=== function cmp(a: Point, b: Point): int ===\n~ return 0\n");
1250 let diags = impl_diags(&src, &[decl(Protocol::Compare, "Point", "cmp")]);
1251 assert!(diags.is_empty(), "{diags:?}");
1252 }
1253
1254 #[test]
1255 fn pure_next_impl_with_ref_receiver_is_clean() {
1256 let src =
1261 format!("{POINT}=== function step(ref p) ===\n~ p.x = p.x + 1.0\n~ return some(p.x)\n");
1262 let diags = impl_diags(&src, &[decl(Protocol::Iterate, "Point", "step")]);
1263 assert!(diags.is_empty(), "{diags:?}");
1264 }
1265
1266 #[test]
1267 fn next_impl_writing_a_global_still_exceeds() {
1268 let src = format!(
1271 "{POINT}VAR steps = 0\n=== function step(ref p) ===\n~ steps = steps + 1\n~ return some(p.x)\n"
1272 );
1273 let diags = impl_diags(&src, &[decl(Protocol::Iterate, "Point", "step")]);
1274 assert_eq!(diags.len(), 1, "{diags:?}");
1275 assert_eq!(diags[0].code, DiagnosticCode::E114);
1276 assert!(
1277 diags[0].message.contains("writes steps"),
1278 "{}",
1279 diags[0].message
1280 );
1281 }
1282
1283 #[test]
1286 fn iterate_element_types_cover_the_closed_set() {
1287 assert_eq!(
1288 iterate_element_ty(&Ty::Array(Box::new(Ty::Int))),
1289 Some(Ty::Int)
1290 );
1291 assert_eq!(
1292 iterate_element_ty(&Ty::Map(Box::new(Ty::String), Box::new(Ty::Int))),
1293 Some(Ty::String),
1294 "maps iterate keys"
1295 );
1296 assert_eq!(iterate_element_ty(&Ty::Int), None);
1297 assert_eq!(iterate_element_ty(&Ty::String), None);
1298 assert_eq!(iterate_element_ty(&Ty::List("Mood".into())), None);
1299 }
1300
1301 #[test]
1332 fn hir_file_condition_bearing_fields_stay_in_sync_with_the_e113_walk() {
1333 let (hir, _manifest) = lower("=== main ===\nHi.\n-> DONE\n");
1334
1335 let HirFile {
1336 root_content: _,
1344 knots: _,
1345 variables: _,
1346 constants: _,
1347 externals: _,
1348 lists: _,
1351 structs: _,
1352 includes: _,
1353 module: _,
1354 imports: _,
1355 visibility: _,
1356 was_directives: _,
1357 allow_scopes: _,
1358 element_matches: _,
1359 cue_names: _,
1360 native: _,
1361 claim_handlers: _,
1362 dispatch_handlers: _,
1363 } = hir;
1364 }
1365}