use brink_ir::{
BlockStmt, Content, ContentPart, Diagnostic, DiagnosticCode, ElseBranch, Expr, FileId, HirFile,
HostManifest, IfStmt, Knot, Name, Param, ResolutionMap, Stmt, StringPart, SymbolIndex,
TypeExpr,
};
use crate::infer::{EffectRow, Ty};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Protocol {
Display,
Compare,
Iterate,
}
impl Protocol {
pub const ALL: [Protocol; 3] = [Protocol::Display, Protocol::Compare, Protocol::Iterate];
#[must_use]
pub fn method_name(self) -> &'static str {
match self {
Protocol::Display => "display",
Protocol::Compare => "compare",
Protocol::Iterate => "next",
}
}
#[must_use]
pub fn protocol_name(self) -> &'static str {
match self {
Protocol::Display => "display",
Protocol::Compare => "compare",
Protocol::Iterate => "iterate",
}
}
#[must_use]
pub fn arity(self) -> usize {
match self {
Protocol::Display | Protocol::Iterate => 1,
Protocol::Compare => 2,
}
}
#[must_use]
pub fn receiver_is_ref(self) -> bool {
matches!(self, Protocol::Iterate)
}
#[must_use]
pub fn contract_phrase(self) -> &'static str {
match self {
Protocol::Display | Protocol::Compare => "pure\u{b7}silent\u{b7}total",
Protocol::Iterate => "writes-receiver\u{b7}silent\u{b7}total",
}
}
}
#[must_use]
pub fn is_reserved_protocol_name(name: &str) -> bool {
Protocol::ALL.iter().any(|p| p.method_name() == name)
}
#[must_use]
pub fn iterate_element_ty(iterable: &Ty) -> Option<Ty> {
match iterable {
Ty::Array(elem) => Some((**elem).clone()),
Ty::Map(key, _) => Some((**key).clone()),
Ty::Range { .. } => Some(Ty::Int),
_ => None,
}
}
#[must_use]
pub fn iterate_val_ty(iterable: &Ty) -> Option<Ty> {
match iterable {
Ty::Map(_, val) => Some((**val).clone()),
_ => None,
}
}
#[must_use]
pub fn check_reserved_names(files: &[(FileId, &HirFile)]) -> Vec<Diagnostic> {
let mut out = Vec::new();
for &(file, hir) in files {
let mut push = |name: &Name, what: &str| {
if is_reserved_protocol_name(&name.text) {
out.push(Diagnostic {
file,
range: name.range,
code: DiagnosticCode::E113,
message: format!(
"`{}` is a reserved protocol method name (stdlib-spec \u{a7}9.6) and cannot name a {what}",
name.text
),
});
}
};
for var in &hir.variables {
push(&var.name, "VAR");
walk_expr_for_lambdas(&var.value, &mut push);
}
for cst in &hir.constants {
push(&cst.name, "CONST");
walk_expr_for_lambdas(&cst.value, &mut push);
}
for ext in &hir.externals {
push(&ext.name, "EXTERNAL");
}
for knot in &hir.knots {
push(&knot.name, "knot or function");
walk_params(&knot.params, &mut push);
walk_stmts(&knot.body.stmts, &mut push);
for stitch in &knot.stitches {
push(&stitch.name, "stitch");
walk_params(&stitch.params, &mut push);
walk_stmts(&stitch.body.stmts, &mut push);
}
}
walk_stmts(&hir.root_content.stmts, &mut push);
}
out
}
fn walk_params(params: &[Param], push: &mut impl FnMut(&Name, &str)) {
for p in params {
push(&p.name, "parameter");
}
}
fn walk_expr_for_lambdas(expr: &Expr, push: &mut impl FnMut(&Name, &str)) {
match expr {
Expr::Lambda(l) => {
walk_params(&l.params, push);
for e in l.body.all_exprs() {
walk_expr_for_lambdas(e, push);
}
}
Expr::Call(_path, args) => {
for arg in args {
walk_expr_for_lambdas(arg, push);
}
}
Expr::Prefix(_, inner) | Expr::Postfix(inner, _) => walk_expr_for_lambdas(inner, push),
Expr::Infix(ie) => {
walk_expr_for_lambdas(&ie.lhs, push);
walk_expr_for_lambdas(&ie.rhs, push);
}
Expr::String(s) => {
for part in &s.parts {
if let StringPart::Interpolation(e) = part {
walk_expr_for_lambdas(e, push);
}
}
}
Expr::ArrayLiteral(a) => {
for e in &a.elements {
walk_expr_for_lambdas(e, push);
}
}
Expr::MapLiteral(m) => {
for (k, v) in &m.entries {
walk_expr_for_lambdas(k, push);
walk_expr_for_lambdas(v, push);
}
}
Expr::Index(idx) => {
walk_expr_for_lambdas(&idx.base, push);
walk_expr_for_lambdas(&idx.index, push);
}
Expr::StructLiteral(sl) => {
for (_name, val) in &sl.fields {
walk_expr_for_lambdas(val, push);
}
}
Expr::FieldAccess(fa) => walk_expr_for_lambdas(&fa.base, push),
Expr::FnLiteral(fl) => {
for arg in &fl.args {
walk_expr_for_lambdas(arg, push);
}
}
Expr::RefArg(ra) => walk_expr_for_lambdas(&ra.operand, push),
Expr::Range(r) => {
walk_expr_for_lambdas(&r.start, push);
walk_expr_for_lambdas(&r.end, push);
}
Expr::Fragment(stmts) => walk_stmts(stmts, push),
Expr::Int(_)
| Expr::Float(_)
| Expr::Bool(_)
| Expr::Null
| Expr::Path(_)
| Expr::DivertTarget(_)
| Expr::ListLiteral(_) => {}
}
}
fn walk_stmts(stmts: &[Stmt], push: &mut impl FnMut(&Name, &str)) {
for stmt in stmts {
match stmt {
Stmt::TempDecl(t) => {
push(&t.name, "temp");
if let Some(v) = &t.value {
walk_expr_for_lambdas(v, push);
}
}
Stmt::Content(c) => walk_content(c, push),
Stmt::ChoiceSet(cs) => {
for choice in &cs.choices {
if let Some(binding) = &choice.binding {
push(binding, "binding");
}
if let Some(cond) = &choice.condition {
walk_expr_for_lambdas(cond, push);
}
for c in [
&choice.start_content,
&choice.bracket_content,
&choice.inner_content,
]
.into_iter()
.flatten()
{
walk_content(c, push);
}
walk_stmts(&choice.body.stmts, push);
}
walk_stmts(&cs.continuation.stmts, push);
}
Stmt::LabeledBlock(b) => walk_stmts(&b.stmts, push),
Stmt::Conditional(c) => {
for branch in &c.branches {
if let Some(binding) = &branch.binding {
push(binding, "binding");
}
if let Some(cond) = &branch.condition {
walk_expr_for_lambdas(cond, push);
}
walk_stmts(&branch.body.stmts, push);
}
}
Stmt::Sequence(s) => {
for branch in &s.branches {
walk_stmts(&branch.body.stmts, push);
}
}
Stmt::LogicBlock(lb) => walk_block_stmts(&lb.stmts, push),
Stmt::Divert(d) => {
for arg in &d.target.args {
walk_expr_for_lambdas(arg, push);
}
}
Stmt::TunnelCall(tc) => {
for target in &tc.targets {
for arg in &target.args {
walk_expr_for_lambdas(arg, push);
}
}
}
Stmt::ThreadStart(ts) => {
for arg in &ts.target.args {
walk_expr_for_lambdas(arg, push);
}
}
Stmt::Assignment(a) => {
walk_expr_for_lambdas(&a.target, push);
walk_expr_for_lambdas(&a.value, push);
}
Stmt::Return(r) => {
if let Some(v) = &r.value {
walk_expr_for_lambdas(v, push);
}
for arg in &r.onwards_args {
walk_expr_for_lambdas(arg, push);
}
}
Stmt::ExprStmt(e) | Stmt::AttachElement(e) => walk_expr_for_lambdas(e, push),
Stmt::Await(a) => {
if let Some(cond) = &a.condition {
walk_expr_for_lambdas(cond, push);
}
}
Stmt::EndOfLine | Stmt::EndElementRun => {}
}
}
}
fn walk_content(content: &Content, push: &mut impl FnMut(&Name, &str)) {
for part in &content.parts {
walk_content_part(part, push);
}
}
fn walk_content_part(part: &ContentPart, push: &mut impl FnMut(&Name, &str)) {
match part {
ContentPart::InlineConditional(c) => {
for branch in &c.branches {
if let Some(cond) = &branch.condition {
walk_expr_for_lambdas(cond, push);
}
walk_stmts(&branch.body.stmts, push);
}
}
ContentPart::InlineSequence(s) => {
for branch in &s.branches {
walk_stmts(&branch.body.stmts, push);
}
}
ContentPart::Span(span) => {
for child in &span.children {
walk_content_part(child, push);
}
}
ContentPart::Interpolation(e) => walk_expr_for_lambdas(e, push),
ContentPart::Text(_) | ContentPart::Glue | ContentPart::Spring => {}
}
}
fn walk_block_stmts(stmts: &[BlockStmt], push: &mut impl FnMut(&Name, &str)) {
for stmt in stmts {
match stmt {
BlockStmt::TempDecl(t) => {
push(&t.name, "temp");
if let Some(v) = &t.value {
walk_expr_for_lambdas(v, push);
}
}
BlockStmt::If(i) => walk_if(i, push),
BlockStmt::While(w) => {
if let Some(binding) = &w.binding {
push(binding, "binding");
}
walk_expr_for_lambdas(&w.condition, push);
walk_block_stmts(&w.body, push);
}
BlockStmt::For(f) => {
push(&f.var_name, "for-loop variable");
if let Some(val_name) = &f.val_name {
push(val_name, "for-loop variable");
}
walk_expr_for_lambdas(&f.iterable, push);
walk_block_stmts(&f.body, push);
}
BlockStmt::Assignment(a) => {
walk_expr_for_lambdas(&a.target, push);
walk_expr_for_lambdas(&a.value, push);
}
BlockStmt::Return(r) => {
if let Some(v) = &r.value {
walk_expr_for_lambdas(v, push);
}
for arg in &r.onwards_args {
walk_expr_for_lambdas(arg, push);
}
}
BlockStmt::ExprStmt(e) => walk_expr_for_lambdas(e, push),
BlockStmt::Await(a) => {
if let Some(cond) = &a.condition {
walk_expr_for_lambdas(cond, push);
}
}
BlockStmt::Break(_) | BlockStmt::Continue(_) => {}
}
}
}
fn walk_if(i: &IfStmt, push: &mut impl FnMut(&Name, &str)) {
if let Some(binding) = &i.binding {
push(binding, "binding");
}
walk_expr_for_lambdas(&i.condition, push);
walk_block_stmts(&i.body, push);
match &i.else_branch {
Some(ElseBranch::ElseIf(inner)) => walk_if(inner, push),
Some(ElseBranch::Else(stmts)) => walk_block_stmts(stmts, push),
None => {}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProtocolImplDecl {
pub protocol: Protocol,
pub type_name: String,
pub function: String,
}
#[must_use]
pub fn check_protocol_impls(
files: &[(FileId, &HirFile)],
index: &SymbolIndex,
resolutions: &ResolutionMap,
host_manifest: Option<&HostManifest>,
impls: &[ProtocolImplDecl],
) -> Vec<Diagnostic> {
let mut out = Vec::new();
if impls.is_empty() {
return out;
}
let struct_names: std::collections::BTreeSet<&str> = files
.iter()
.flat_map(|(_, hir)| hir.structs.iter())
.map(|s| s.name.text.as_str())
.collect();
let mut checked: Vec<(&ProtocolImplDecl, FileId, &Knot)> = Vec::new();
let mut seen: std::collections::BTreeSet<(Protocol, &str)> = std::collections::BTreeSet::new();
for decl in impls {
let Some((file, knot)) = find_function(files, &decl.function) else {
out.push(registration_error(
files,
format!(
"protocol impl `{}` for `{}`: `{}` is not a declared function",
decl.protocol.protocol_name(),
decl.type_name,
decl.function
),
));
continue;
};
let at = |message: String| Diagnostic {
file,
range: knot.name.range,
code: DiagnosticCode::E115,
message,
};
if crate::infer::TowerTy::from_name(&decl.type_name).is_some() {
out.push(Diagnostic {
file,
range: knot.name.range,
code: DiagnosticCode::E118,
message: format!(
"protocol impl `{}` for `{}`: numeric-tower kinds are compiler-known and cannot implement registry protocols{}",
decl.protocol.protocol_name(),
decl.type_name,
if decl.protocol == Protocol::Compare {
" (tower values are not orderable — tower-mini-spec T4)"
} else {
""
}
),
});
continue;
}
if !struct_names.contains(decl.type_name.as_str()) {
out.push(at(format!(
"protocol impl `{}` for `{}`: the type is not a declared STRUCT (only user struct types may implement registry protocols)",
decl.protocol.protocol_name(),
decl.type_name
)));
continue;
}
if !seen.insert((decl.protocol, decl.type_name.as_str())) {
out.push(at(format!(
"duplicate protocol impl: `{}` for `{}` is already registered",
decl.protocol.protocol_name(),
decl.type_name
)));
continue;
}
if let Some(message) = shape_error(decl, knot) {
out.push(at(message));
continue;
}
checked.push((decl, file, knot));
}
if checked.is_empty() {
return out;
}
let rows = crate::infer::effects_project(files, index, resolutions, host_manifest);
for (decl, file, knot) in checked {
let Some(def_id) = index.by_name.get(&decl.function).and_then(|ids| {
ids.iter()
.copied()
.find(|id| index.symbols.get(id).is_some_and(|info| info.file == file))
}) else {
continue;
};
let Some(row) = rows.get(&def_id) else {
continue;
};
if let Some(message) = contract_error(decl.protocol, &decl.type_name, row, index) {
out.push(Diagnostic {
file,
range: knot.name.range,
code: DiagnosticCode::E114,
message,
});
}
}
out
}
fn find_function<'a>(files: &[(FileId, &'a HirFile)], name: &str) -> Option<(FileId, &'a Knot)> {
files.iter().find_map(|&(file, hir)| {
hir.knots
.iter()
.find(|k| k.is_function && k.name.text == name)
.map(|k| (file, k))
})
}
fn shape_error(decl: &ProtocolImplDecl, knot: &Knot) -> Option<String> {
let proto = decl.protocol;
if knot.params.len() != proto.arity() {
return Some(format!(
"protocol impl `{}` for `{}`: `{}` takes {} parameter(s), but the protocol method `{}` declares {}",
proto.protocol_name(),
decl.type_name,
knot.name.text,
knot.params.len(),
proto.method_name(),
proto.arity()
));
}
for (i, param) in knot.params.iter().enumerate() {
let want_ref = i == 0 && proto.receiver_is_ref();
if param.is_ref != want_ref {
return Some(format!(
"protocol impl `{}` for `{}`: parameter `{}` must {} `ref` (the protocol method is `{}`)",
proto.protocol_name(),
decl.type_name,
param.name.text,
if want_ref { "be" } else { "not be" },
signature_phrase(proto),
));
}
if let Some(TypeExpr::Named { name, .. }) = ¶m.annotation
&& name != &decl.type_name
{
return Some(format!(
"protocol impl `{}` for `{}`: parameter `{}` is annotated `{}`, but the receiver of a protocol impl must be the implementing type",
proto.protocol_name(),
decl.type_name,
param.name.text,
name
));
}
}
let want_return = match proto {
Protocol::Display => Some("string"),
Protocol::Compare => Some("int"),
Protocol::Iterate => None,
};
if let (Some(want), Some(TypeExpr::Named { name, .. })) = (want_return, &knot.return_type)
&& name != want
{
return Some(format!(
"protocol impl `{}` for `{}`: return type is annotated `{}`, but `{}` returns `{}`",
proto.protocol_name(),
decl.type_name,
name,
signature_phrase(proto),
want
));
}
None
}
fn signature_phrase(proto: Protocol) -> &'static str {
match proto {
Protocol::Display => "display(self: T): string",
Protocol::Compare => "compare(a: T, b: T): int",
Protocol::Iterate => "next(ref self): Option[T]",
}
}
fn contract_error(
proto: Protocol,
type_name: &str,
row: &EffectRow,
index: &SymbolIndex,
) -> Option<String> {
let faults_exceed = row.faults_refined && !matches!(proto, Protocol::Iterate);
if !row.is_pessimal()
&& row.reads.is_empty()
&& row.writes.is_empty()
&& row.calls.is_empty()
&& !row.emits
&& !row.tags
&& !faults_exceed
{
return None;
}
let mut parts = Vec::new();
if row.is_pessimal() {
parts.push(
"calls through a function value or unresolved callee (unbounded row)".to_string(),
);
}
let name_of = |id: &brink_format::DefinitionId| {
index
.symbols
.get(id)
.map_or_else(|| format!("{id:?}"), |info| info.name.clone())
};
if !row.reads.is_empty() {
let names: Vec<String> = row.reads.iter().map(name_of).collect();
parts.push(format!("reads {}", names.join(", ")));
}
if !row.writes.is_empty() {
let names: Vec<String> = row.writes.iter().map(name_of).collect();
parts.push(format!("writes {}", names.join(", ")));
}
if !row.calls.is_empty() {
let names: Vec<String> = row.calls.iter().cloned().collect();
parts.push(format!("calls {}", names.join(", ")));
}
if row.emits {
parts.push("emits content".to_string());
}
if row.tags {
parts.push("touches the tag channel".to_string());
}
if faults_exceed {
parts.push("can raise a turn-terminating fault".to_string());
}
Some(format!(
"protocol impl `{}` for `{type_name}` exceeds the {} contract: {}",
proto.protocol_name(),
proto.contract_phrase(),
parts.join("; ")
))
}
fn registration_error(files: &[(FileId, &HirFile)], message: String) -> Diagnostic {
Diagnostic {
file: files.first().map_or(FileId(0), |&(f, _)| f),
range: rowan::TextRange::empty(0.into()),
code: DiagnosticCode::E115,
message,
}
}
#[cfg(test)]
mod tests {
use brink_ir::SymbolManifest;
use brink_ir::hir::HirFile;
use super::*;
fn lower(src: &str) -> (HirFile, SymbolManifest) {
let parsed = brink_syntax::parse(src);
let tree = parsed.tree();
let (hir, manifest, diags) = brink_ir::hir::lower(FileId(0), &tree);
assert!(diags.is_empty(), "lowering diagnostics: {diags:?}");
(hir, manifest)
}
fn reserved_diags(src: &str) -> Vec<Diagnostic> {
let (hir, _manifest) = lower(src);
check_reserved_names(&[(FileId(0), &hir)])
}
fn reserved_diags_native(src: &str) -> Vec<Diagnostic> {
let parse = brink_syntax_native::parse(src);
assert!(
parse.errors().is_empty(),
"fixture must parse cleanly: {:?}",
parse.errors()
);
let tree = parse.tree();
let (hir, _manifest, diags) = brink_ir::hir::lower_native::lower(FileId(0), &tree);
assert!(diags.is_empty(), "lowering diagnostics: {diags:?}");
check_reserved_names(&[(FileId(0), &hir)])
}
fn impl_diags(src: &str, impls: &[ProtocolImplDecl]) -> Vec<Diagnostic> {
let (hir, manifest) = lower(src);
let result = crate::analyze(&[(FileId(0), &hir, &manifest)]);
check_protocol_impls(
&[(FileId(0), &hir)],
&result.index,
&result.resolutions,
None,
impls,
)
}
fn decl(protocol: Protocol, type_name: &str, function: &str) -> ProtocolImplDecl {
ProtocolImplDecl {
protocol,
type_name: type_name.to_string(),
function: function.to_string(),
}
}
const POINT: &str = "STRUCT Point = #{\n x: float,\n y: float,\n}\n";
#[test]
fn knot_named_display_is_reserved() {
let diags = reserved_diags("== display ==\nHello.\n-> DONE\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E113);
}
#[test]
fn function_named_compare_is_reserved() {
let diags = reserved_diags("=== function compare(a, b) ===\n~ return 0\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E113);
}
#[test]
fn stitch_named_next_is_reserved() {
let diags = reserved_diags("== knot ==\n= next\nHello.\n-> DONE\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E113);
}
#[test]
fn var_const_external_named_reserved() {
let diags = reserved_diags("VAR display = 1\nCONST compare = 2\nEXTERNAL next(x)\n");
assert_eq!(diags.len(), 3, "{diags:?}");
assert!(diags.iter().all(|d| d.code == DiagnosticCode::E113));
}
#[test]
fn param_named_display_is_reserved() {
let diags = reserved_diags("=== function f(display) ===\n~ return display\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E113);
}
#[test]
fn temp_and_for_var_in_logic_block_are_reserved() {
let src = "== k ==\n~ {\n temp next = 1\n for display in #[1, 2] {\n next = next + display\n }\n}\n-> DONE\n";
let diags = reserved_diags(src);
assert_eq!(diags.len(), 2, "{diags:?}");
assert!(diags.iter().all(|d| d.code == DiagnosticCode::E113));
}
#[test]
fn weave_level_temp_named_next_is_reserved() {
let diags = reserved_diags("== k ==\n~ temp next = 1\n{next}\n-> DONE\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E113);
}
#[test]
fn list_members_and_type_names_are_not_reserved() {
let diags = reserved_diags("LIST steps = intro, next, outro\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn lambda_param_named_display_is_reserved() {
let diags = reserved_diags_native("var f = |display| display\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E113);
}
#[test]
fn lambda_param_named_display_in_choice_label_is_reserved() {
let diags =
reserved_diags_native("flow f() {\n {?\n * Gold: {fmt(|display| 0)}\n }\n}\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E113);
}
#[test]
fn ordinary_names_stay_clean() {
let diags = reserved_diags(
"VAR score = 1\n== k ==\n~ temp shown = score\n{shown}\n-> DONE\n=== function render(p) ===\n~ return \"x\"\n",
);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn well_formed_display_impl_is_clean() {
let src = format!("{POINT}=== function render(p: Point): string ===\n~ return \"P\"\n");
let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn unknown_function_is_e115() {
let diags = impl_diags(POINT, &[decl(Protocol::Display, "Point", "nope")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E115);
assert!(diags[0].message.contains("not a declared function"));
}
#[test]
fn non_struct_type_is_e115() {
let src = "=== function render(p) ===\n~ return \"x\"\n";
let diags = impl_diags(src, &[decl(Protocol::Display, "Point", "render")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E115);
assert!(diags[0].message.contains("not a declared STRUCT"));
}
#[test]
fn wrong_arity_is_e115() {
let src = format!("{POINT}=== function render(p, extra) ===\n~ return \"x\"\n");
let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E115);
assert!(diags[0].message.contains("parameter"));
}
#[test]
fn display_receiver_must_not_be_ref() {
let src = format!("{POINT}=== function render(ref p) ===\n~ return \"x\"\n");
let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E115);
}
#[test]
fn next_receiver_must_be_ref() {
let src = format!("{POINT}=== function step(p) ===\n~ return 0\n");
let diags = impl_diags(&src, &[decl(Protocol::Iterate, "Point", "step")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E115);
assert!(diags[0].message.contains("ref"));
}
#[test]
fn contradicting_param_annotation_is_e115() {
let src = format!("{POINT}=== function render(p: int) ===\n~ return \"x\"\n");
let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E115);
assert!(diags[0].message.contains("annotated"));
}
#[test]
fn contradicting_return_annotation_is_e115() {
let src =
format!("{POINT}=== function cmp(a: Point, b: Point): string ===\n~ return \"x\"\n");
let diags = impl_diags(&src, &[decl(Protocol::Compare, "Point", "cmp")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E115);
assert!(diags[0].message.contains("return"));
}
#[test]
fn duplicate_registration_is_e115() {
let src = format!(
"{POINT}=== function render(p) ===\n~ return \"x\"\n=== function render2(p) ===\n~ return \"y\"\n"
);
let diags = impl_diags(
&src,
&[
decl(Protocol::Display, "Point", "render"),
decl(Protocol::Display, "Point", "render2"),
],
);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E115);
assert!(diags[0].message.contains("duplicate"));
}
#[test]
fn compare_for_tower_kind_is_e118() {
let src = "=== function cmp(a, b) ===\n~ return 0\n";
for kind in ["vec2", "vec3", "vec4", "quat", "mat2", "mat3", "mat4"] {
let diags = impl_diags(src, &[decl(Protocol::Compare, kind, "cmp")]);
assert_eq!(diags.len(), 1, "{kind}: {diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E118, "{kind}");
assert!(diags[0].message.contains("not orderable"), "{kind}");
}
}
#[test]
fn display_and_iterate_for_tower_kind_are_e118() {
let src = "=== function render(p) ===\n~ return \"x\"\n";
for proto in [Protocol::Display, Protocol::Iterate] {
let diags = impl_diags(src, &[decl(proto, "vec3", "render")]);
assert_eq!(diags.len(), 1, "{proto:?}: {diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E118, "{proto:?}");
}
}
#[test]
fn tower_rejection_wins_over_a_shadowing_struct() {
let src = "STRUCT vec3 = #{\n v: float,\n}\n=== function cmp(a, b) ===\n~ return 0\n";
let diags = impl_diags(src, &[decl(Protocol::Compare, "vec3", "cmp")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E118);
}
#[test]
fn global_write_exceeds_display_contract() {
let src = format!(
"{POINT}VAR seen = 0\n=== function render(p) ===\n~ seen = seen + 1\n~ return \"x\"\n"
);
let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E114);
assert!(
diags[0].message.contains("writes seen"),
"{}",
diags[0].message
);
}
#[test]
fn global_read_exceeds_display_contract() {
let src = format!("{POINT}VAR mood = 1\n=== function render(p) ===\n~ return mood\n");
let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E114);
assert!(
diags[0].message.contains("reads mood"),
"{}",
diags[0].message
);
}
#[test]
fn emitting_impl_exceeds_silent() {
let src = format!("{POINT}=== function render(p) ===\nLoud line.\n~ return \"x\"\n");
let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "render")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E114);
assert!(diags[0].message.contains("emits"), "{}", diags[0].message);
}
#[test]
fn faulting_impl_exceeds_total() {
let src = format!(
"{POINT}=== function cmp(a, b) ===\n~ temp lowest = min(#[1.0, 2.0])\n~ return 0\n"
);
let diags = impl_diags(&src, &[decl(Protocol::Compare, "Point", "cmp")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E114);
assert!(diags[0].message.contains("fault"), "{}", diags[0].message);
}
#[test]
fn f29_provably_total_impl_is_not_rejected_for_conservative_faults() {
let src = format!(
"{POINT}=== function cmp(a, b) ===\n~ temp lowest = min(#[1, 2])\n~ temp n = len(#[1, 2])\n~ return 0\n"
);
let diags = impl_diags(&src, &[decl(Protocol::Compare, "Point", "cmp")]);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn f29_opaque_impl_keeps_the_conservative_union() {
let src = format!(
"{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"
);
let diags = impl_diags(&src, &[decl(Protocol::Display, "Point", "shape")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E114);
}
#[test]
fn f29_value_dependent_fault_still_rejects() {
let src = format!(
"{POINT}=== function cmp(a, b) ===\n~ temp arr = #[1, 2]\n~ temp x = arr[5]\n~ return 0\n"
);
let diags = impl_diags(&src, &[decl(Protocol::Compare, "Point", "cmp")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E114);
assert!(diags[0].message.contains("fault"), "{}", diags[0].message);
}
#[test]
fn pure_compare_impl_is_clean() {
let src = format!("{POINT}=== function cmp(a: Point, b: Point): int ===\n~ return 0\n");
let diags = impl_diags(&src, &[decl(Protocol::Compare, "Point", "cmp")]);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn pure_next_impl_with_ref_receiver_is_clean() {
let src =
format!("{POINT}=== function step(ref p) ===\n~ p.x = p.x + 1.0\n~ return some(p.x)\n");
let diags = impl_diags(&src, &[decl(Protocol::Iterate, "Point", "step")]);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn next_impl_writing_a_global_still_exceeds() {
let src = format!(
"{POINT}VAR steps = 0\n=== function step(ref p) ===\n~ steps = steps + 1\n~ return some(p.x)\n"
);
let diags = impl_diags(&src, &[decl(Protocol::Iterate, "Point", "step")]);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E114);
assert!(
diags[0].message.contains("writes steps"),
"{}",
diags[0].message
);
}
#[test]
fn iterate_element_types_cover_the_closed_set() {
assert_eq!(
iterate_element_ty(&Ty::Array(Box::new(Ty::Int))),
Some(Ty::Int)
);
assert_eq!(
iterate_element_ty(&Ty::Map(Box::new(Ty::String), Box::new(Ty::Int))),
Some(Ty::String),
"maps iterate keys"
);
assert_eq!(iterate_element_ty(&Ty::Int), None);
assert_eq!(iterate_element_ty(&Ty::String), None);
assert_eq!(iterate_element_ty(&Ty::List("Mood".into())), None);
}
#[test]
fn hir_file_condition_bearing_fields_stay_in_sync_with_the_e113_walk() {
let (hir, _manifest) = lower("=== main ===\nHi.\n-> DONE\n");
let HirFile {
root_content: _,
knots: _,
variables: _,
constants: _,
externals: _,
lists: _,
structs: _,
includes: _,
module: _,
imports: _,
visibility: _,
was_directives: _,
allow_scopes: _,
element_matches: _,
cue_names: _,
native: _,
claim_handlers: _,
dispatch_handlers: _,
} = hir;
}
}