A reasonable configuration language
rcl-lang.org
configuration-language
json
1// RCL -- A reasonable configuration language.
2// Copyright 2024 Ruud van Asseldonk
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// A copy of the License has been included in the root of the repository.
7
8//! A type diff is the result of a subtype check.
9//!
10//! This module contains the definitions, and machinery for printing type diffs.
11use crate::error::{IntoError, Result};
12use crate::pprint::{concat, indent, Doc};
13use crate::source::Span;
14use crate::types::{AsTypeName, FunctionArg, Side, SourcedType, Type};
15
16/// A component in a subtype check `T ≤ U` where `U` is expected and `T` encountered.
17#[derive(Debug)]
18pub enum Mismatch {
19 /// The type error cannot be broken down further. Here are `T` and `U`.
20 Atom {
21 expected: SourcedType,
22 actual: SourcedType,
23 },
24
25 /// Both sides are a list, but the element type has an issue.
26 List(Box<TypeDiff<SourcedType>>),
27
28 /// Both sides are a set, but the element type has an issue.
29 Set(Box<TypeDiff<SourcedType>>),
30
31 /// Both sides are a dict, but the key or value (or both) have issues.
32 Dict(Box<TypeDiff<SourcedType>>, Box<TypeDiff<SourcedType>>),
33
34 /// Both sides are functions of the same arity, but args or result have issues.
35 Function(Vec<TypeDiff<FunctionArg>>, Box<TypeDiff<SourcedType>>),
36}
37
38/// The result of a subtype check `T ≤ U` where `U` is expected and `T` encountered.
39///
40/// The result is a tree, to be able to pinpoint where the check fails, enabling
41/// more helpful errors.
42///
43/// See also [`SourcedType::is_subtype_of`].
44#[derive(Debug)]
45pub enum TypeDiff<T> {
46 /// Yes, `T ≤ U`, and here is `T`.
47 Ok(T),
48
49 /// For `t: T`, we *might* have `t: U`. Here is `V` such that `T ≤ V ≤ U`.
50 Defer(T),
51
52 /// For all `t: T`, we have that `t` is not a value of `U`.
53 ///
54 /// Or, in some cases this is not strictly true, but we want to rule out
55 /// that case because it makes more sense. For example, we say that
56 /// `List[Number]` and `List[String]` are incompatible, even though `[]`
57 /// inhabits both.
58 Error(Mismatch),
59}
60
61/// The result of a static typecheck.
62pub enum Typed<T> {
63 /// The type is known statically, and this is the most specific type we infer.
64 Type(T),
65
66 /// We can't check this statically, a runtime check is needed.
67 ///
68 /// If the runtime check passes, then the value fits the returned type.
69 Defer(T),
70}
71
72impl<T> TypeDiff<T> {
73 pub fn check(self, at: Span) -> Result<Typed<T>> {
74 self.check_with_context(at, "")
75 }
76
77 pub fn check_unpack_scalar(self, at: Span) -> Result<Typed<T>> {
78 self.check_with_context(at, " in unpacked element")
79 }
80
81 /// Report the diff as a type error, or extract its result.
82 fn check_with_context(self, at: Span, location_context: &'static str) -> Result<Typed<T>> {
83 match self {
84 TypeDiff::Ok(t) => Ok(Typed::Type(t)),
85 TypeDiff::Defer(t) => Ok(Typed::Defer(t)),
86 TypeDiff::Error(Mismatch::Atom { actual, expected }) => {
87 let mut error = if let Type::Void = expected.type_ {
88 at.error(concat! {
89 "Expected a value of type "
90 "Void".format_type()
91 location_context
92 ", but no such values exist."
93 })
94 } else {
95 // Any can never be the top level-cause of a type error.
96 // As a supertype, any value is fine, and as the actual type,
97 // it should result in a runtime check rather than an error.
98 debug_assert_ne!(actual.type_, Type::Any, "Any should not cause errors.");
99 debug_assert_ne!(expected.type_, Type::Any, "Any should not cause errors.");
100
101 // A top-level type error, we can report with a simple message.
102 at.error(concat! {
103 "Type mismatch"
104 location_context
105 "."
106 })
107 .with_body(report_type_mismatch(&expected, &actual))
108 };
109
110 // If we have it, explain why the expected type is expected.
111 expected.explain_error(Side::Expected, &mut error);
112
113 // If the actual type doesn't come from the span that we are
114 // attributing the error to, then also include a note about that.
115 let should_report_source = match actual.source.span() {
116 Some(src_span) => at != src_span,
117 None => true,
118 };
119 if should_report_source {
120 actual.explain_error(Side::Actual, &mut error);
121 }
122 error.err()
123 }
124 TypeDiff::Error(diff) => {
125 // If the error is nested somewhere inside a type, then we
126 // resort to a more complex format where we first print the
127 // type itself, with the error part replaced with a placeholder,
128 // and then we add a secondary error to explain the placeholder.
129 crate::fmt_type::DiffFormatter::report(at, location_context, &diff).err()
130 }
131 }
132 }
133}
134
135/// Format a static type error body.
136///
137/// This does not include the "Type mismatch." message, so that the body can be
138/// used in various places.
139pub fn report_type_mismatch<T1: AsTypeName, T2: AsTypeName>(
140 expected: &T1,
141 actual: &T2,
142) -> Doc<'static> {
143 // If types are atoms, they are short to format, so we can put the message
144 // on one line. If they are composite, we put them in an indented block.
145 match (expected.is_atom(), actual.is_atom()) {
146 (true, true) => concat! {
147 "Expected " expected.format_type()
148 " but found " actual.format_type() "."
149 },
150 (true, false) => concat! {
151 "Expected " expected.format_type() " but found this type:"
152 Doc::HardBreak Doc::HardBreak
153 indent! { actual.format_type() }
154 },
155 (false, true) => concat! {
156 "Expected this type:"
157 Doc::HardBreak Doc::HardBreak
158 indent! { expected.format_type() }
159 Doc::HardBreak Doc::HardBreak
160 "But found " actual.format_type() "."
161 },
162 (false, false) => concat! {
163 "Expected this type:"
164 Doc::HardBreak Doc::HardBreak
165 indent! { expected.format_type() }
166 Doc::HardBreak Doc::HardBreak
167 "But found this type: "
168 Doc::HardBreak Doc::HardBreak
169 indent! { actual.format_type() }
170 },
171 }
172}