arithmetic_typing/lib.rs
1//! Hindley–Milner type inference for arithmetic expressions parsed
2//! by the [`arithmetic-parser`] crate.
3//!
4//! This crate allows parsing type annotations as a part of a [`Grammar`], and to infer
5//! and check types for expressions / statements produced by `arithmetic-parser`.
6//! Type inference is *partially* compatible with the interpreter from [`arithmetic-eval`];
7//! if the inference algorithm succeeds on a certain expression / statement / block,
8//! it will execute successfully, but not all successfully executing items pass type inference.
9//! (An exception here is [`Type::Any`], which is specifically designed to circumvent
10//! the type system limitations. If `Any` is used too liberally, it can result in code passing
11//! type checks, but failing during execution.)
12//!
13//! # Type system
14//!
15//! The type system corresponds to types of `Value`s in `arithmetic-eval`:
16//!
17//! - Primitive types are customizeable via [`PrimitiveType`] impl. In the simplest case,
18//! there can be 2 primitive types: Booleans (`Bool`) and numbers (`Num`),
19//! as ecapsulated in [`Num`].
20//! - There are two container types - [tuples](Tuple) and [objects](Object).
21//! - Tuple types can be represented either
22//! in the tuple form, such as `(Num, Bool)`, or as a slice, such as `[Num; 3]`.
23//! As in Rust, all slice elements must have the same type. Unlike Rust, tuple and slice
24//! forms are equivalent; e.g., `[Num; 3]` and `(Num, Num, Num)` are the same type.
25//! - Object types are represented in a brace form, such as `{ x: Num }`. Objects can act as
26//! either specific types or type constraints.
27//! - Functions are first-class types. Functions can have type and/or const params.
28//! Const params always specify tuple length.
29//! - Type params can be constrained. Constraints are expressed via [`Constraint`]s.
30//! As an example, [`Num`] has a few known constraints, such as type [`Linearity`].
31//!
32//! [`Constraint`]: crate::arith::Constraint
33//! [`Num`]: crate::arith::Num
34//! [`Linearity`]: crate::arith::Linearity
35//!
36//! # Inference rules
37//!
38//! Inference mostly corresponds to [Hindley–Milner typing rules]. It does not require
39//! type annotations, but utilizes them if present. Type unification (encapsulated in
40//! [`Substitutions`]) is performed at each variable use or assignment. Variable uses include
41//! function calls and unary and binary ops; the op behavior is customizable
42//! via [`TypeArithmetic`].
43//!
44//! Whenever possible, the most generic type satisfying the constraints is used. In particular,
45//! this means that all type / length variables not resolved at the function definition site become
46//! parameters of the function. Likewise, each function call instantiates a separate instance
47//! of a generic function; type / length params for each call are assigned independently.
48//! See the example below for more details.
49//!
50//! [Hindley–Milner typing rules]: https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system#Typing_rules
51//! [`Substitutions`]: crate::arith::Substitutions
52//! [`TypeArithmetic`]: crate::arith::TypeArithmetic
53//!
54//! # Operations
55//!
56//! ## Field access
57//!
58//! See [`Tuple` docs](Tuple#indexing) for discussion of indexing expressions, such as `xs.0`,
59//! and [`Object` docs](Object) for discussion of field access, such as `point.x`.
60//!
61//! ## Type casts
62//!
63//! [A type cast](arithmetic_parser::Expr::TypeCast) is equivalent to introducing a new var
64//! with the specified annotation, assigning to it and returning the new var. That is,
65//! `x as Bool` is equivalent to `{ _x: Bool = x; _x }`. As such, casts are safe (cannot be used
66//! to transmute the type arbitrarily), unless `any` type is involved.
67//!
68//! # Examples
69//!
70//! ```
71//! use arithmetic_parser::grammars::{F32Grammar, Parse};
72//! use arithmetic_typing::{defs::Prelude, Annotated, TypeEnvironment, Type};
73//!
74//! # fn main() -> anyhow::Result<()> {
75//! let code = "sum = |xs| xs.fold(0, |acc, x| acc + x);";
76//! let ast = Annotated::<F32Grammar>::parse_statements(code)?;
77//!
78//! let mut env = TypeEnvironment::new();
79//! env.insert("fold", Prelude::Fold);
80//!
81//! // Evaluate `code` to get the inferred `sum` function signature.
82//! let output_type = env.process_statements(&ast)?;
83//! assert!(output_type.is_void());
84//! assert_eq!(env["sum"].to_string(), "([Num; N]) -> Num");
85//! # Ok(())
86//! # }
87//! ```
88//!
89//! Defining and using generic functions:
90//!
91//! ```
92//! # use arithmetic_parser::grammars::{F32Grammar, Parse};
93//! # use arithmetic_typing::{defs::Prelude, Annotated, TypeEnvironment, Type};
94//! # fn main() -> anyhow::Result<()> {
95//! let code = "sum_with = |xs, init| xs.fold(init, |acc, x| acc + x);";
96//! let ast = Annotated::<F32Grammar>::parse_statements(code)?;
97//!
98//! let mut env = TypeEnvironment::new();
99//! env.insert("fold", Prelude::Fold);
100//!
101//! let output_type = env.process_statements(&ast)?;
102//! assert!(output_type.is_void());
103//! assert_eq!(
104//! env["sum_with"].to_string(),
105//! "for<'T: Ops> (['T; N], 'T) -> 'T"
106//! );
107//! // Note that `sum_with` is parametric by the element of the slice
108//! // (for which the linearity constraint is applied based on the arg usage)
109//! // *and* by its length.
110//!
111//! let usage_code = r#"
112//! num_sum: Num = (1, 2, 3).sum_with(0);
113//! tuple_sum: (Num, Num) = ((1, 2), (3, 4)).sum_with((0, 0));
114//! "#;
115//! let ast = Annotated::<F32Grammar>::parse_statements(usage_code)?;
116//! // Both lengths and element types differ in these invocations,
117//! // but it works fine since they are treated independently.
118//! env.process_statements(&ast)?;
119//! # Ok(())
120//! # }
121//! ```
122//!
123//! [`arithmetic-parser`]: https://crates.io/crates/arithmetic-parser
124//! [`Grammar`]: arithmetic_parser::grammars::Grammar
125//! [`arithmetic-eval`]: https://crates.io/crates/arithmetic-eval
126
127#![doc(html_root_url = "https://docs.rs/arithmetic-typing/0.3.0")]
128#![warn(missing_docs, missing_debug_implementations)]
129#![warn(clippy::all, clippy::pedantic)]
130#![allow(
131 clippy::missing_errors_doc,
132 clippy::must_use_candidate,
133 clippy::module_name_repetitions,
134 clippy::similar_names, // too many false positives because of lhs / rhs
135 clippy::option_if_let_else // too many false positives
136)]
137
138use std::{fmt, marker::PhantomData, str::FromStr};
139
140use arithmetic_parser::{
141 grammars::{Features, Grammar, Parse, ParseLiteral},
142 InputSpan, NomResult,
143};
144
145pub mod arith;
146pub mod ast;
147pub mod defs;
148mod env;
149pub mod error;
150mod types;
151pub mod visit;
152
153pub use self::{
154 env::TypeEnvironment,
155 types::{
156 DynConstraints, FnWithConstraints, Function, FunctionBuilder, LengthVar, Object, Slice,
157 Tuple, TupleIndex, TupleLen, Type, TypeVar, UnknownLen,
158 },
159};
160
161use self::{arith::ConstraintSet, ast::TypeAst};
162
163/// Primitive types in a certain type system.
164///
165/// More complex types, like [`Type`] and [`Function`], are defined with a type param
166/// which determines the primitive type(s). This type param must implement [`PrimitiveType`].
167///
168/// [`TypeArithmetic`] has a `PrimitiveType` impl as an associated type, and one of the required
169/// operations of this trait is to be able to infer type for literal values from a [`Grammar`].
170///
171/// # Implementation Requirements
172///
173/// - [`Display`](fmt::Display) and [`FromStr`] implementations must be consistent; i.e.,
174/// `Display` should produce output parseable by `FromStr`. `Display` will be used in
175/// `Display` impls for `Type` etc. `FromStr` will be used to read type annotations.
176/// - `Display` presentations must be identifiers, such as `Num`.
177/// - While not required, a `PrimitiveType` should usually contain a Boolean type and
178/// implement [`WithBoolean`]. This allows to reuse [`BoolArithmetic`] and/or [`NumArithmetic`]
179/// as building blocks for your [`TypeArithmetic`].
180///
181/// [`Grammar`]: arithmetic_parser::grammars::Grammar
182/// [`TypeArithmetic`]: crate::arith::TypeArithmetic
183/// [`WithBoolean`]: crate::arith::WithBoolean
184/// [`BoolArithmetic`]: crate::arith::BoolArithmetic
185/// [`NumArithmetic`]: crate::arith::NumArithmetic
186///
187/// # Examples
188///
189/// ```
190/// # use std::{fmt, str::FromStr};
191/// use arithmetic_typing::PrimitiveType;
192///
193/// #[derive(Debug, Clone, Copy, PartialEq)]
194/// enum NumOrBytes {
195/// /// Numeric value, such as 1.
196/// Num,
197/// /// Bytes value, such as 0x1234 or "hello".
198/// Bytes,
199/// }
200///
201/// // `NumOrBytes` should correspond to a "value" type in the `Grammar`,
202/// // for example:
203/// enum NumOrBytesValue {
204/// Num(f64),
205/// Bytes(Vec<u8>),
206/// }
207///
208/// impl fmt::Display for NumOrBytes {
209/// fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
210/// match self {
211/// Self::Num => formatter.write_str("Num"),
212/// Self::Bytes => formatter.write_str("Bytes"),
213/// }
214/// }
215/// }
216///
217/// impl FromStr for NumOrBytes {
218/// type Err = anyhow::Error;
219///
220/// fn from_str(s: &str) -> Result<Self, Self::Err> {
221/// match s {
222/// "Num" => Ok(Self::Num),
223/// "Bytes" => Ok(Self::Bytes),
224/// _ => Err(anyhow::anyhow!("expected `Num` or `Bytes`")),
225/// }
226/// }
227/// }
228///
229/// impl PrimitiveType for NumOrBytes {}
230/// ```
231pub trait PrimitiveType:
232 Clone + PartialEq + fmt::Debug + fmt::Display + FromStr + Send + Sync + 'static
233{
234 /// Returns well-known constraints for this type. These constraints are used
235 /// in standalone parsing of type signatures.
236 ///
237 /// The default implementation returns an empty set.
238 fn well_known_constraints() -> ConstraintSet<Self> {
239 ConstraintSet::default()
240 }
241}
242
243/// Grammar with support of type annotations. Works as a decorator.
244///
245/// # Examples
246///
247/// ```
248/// use arithmetic_parser::grammars::{F32Grammar, Parse};
249/// use arithmetic_typing::Annotated;
250///
251/// # fn main() -> anyhow::Result<()> {
252/// let code = "x: [Num] = (1, 2, 3);";
253/// let ast = Annotated::<F32Grammar>::parse_statements(code)?;
254/// # assert_eq!(ast.statements.len(), 1);
255/// # Ok(())
256/// # }
257/// ```
258#[derive(Debug)]
259pub struct Annotated<T>(PhantomData<T>);
260
261impl<T: ParseLiteral> ParseLiteral for Annotated<T> {
262 type Lit = T::Lit;
263
264 fn parse_literal(input: InputSpan<'_>) -> NomResult<'_, Self::Lit> {
265 <T as ParseLiteral>::parse_literal(input)
266 }
267}
268
269impl<'a, T: ParseLiteral> Grammar<'a> for Annotated<T> {
270 type Type = TypeAst<'a>;
271
272 fn parse_type(input: InputSpan<'a>) -> NomResult<'a, Self::Type> {
273 use nom::combinator::map;
274 map(TypeAst::parse, |ast| ast.extra)(input)
275 }
276}
277
278/// Supports all syntax features.
279impl<T: ParseLiteral> Parse<'_> for Annotated<T> {
280 type Base = Self;
281
282 const FEATURES: Features = Features::all();
283}