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 customizable via [`PrimitiveType`] impl. In the simplest case,
18//! there can be 2 primitive types: Booleans (`Bool`) and numbers (`Num`),
19//! as encapsulated 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`]: arith::Constraint
33//! [`Num`]: arith::Num
34//! [`Linearity`]: 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`]: arith::Substitutions
52//! [`TypeArithmetic`]: 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//! ## Method calls
62//!
63//! In agreement with the [`arithmetic-eval`] crate, methods are treated as syntactic sugar for functions.
64//! For example, `xs.map(sin)` is equivalent to `map(xs, sin)`.
65//!
66//! ## Type casts
67//!
68//! [A type cast](arithmetic_parser::Expr::TypeCast) is equivalent to introducing a new var
69//! with the specified annotation, assigning to it and returning the new var. That is,
70//! `x as Bool` is equivalent to `{ _x: Bool = x; _x }`. As such, casts are safe (cannot be used
71//! to transmute the type arbitrarily), unless `any` type is involved.
72//!
73//! # Crate features
74//!
75//! ## `std`
76//!
77//! *(On by default)*
78//!
79//! Enables support of types from `std`, such as the `Error` trait, and propagates to dependencies.
80//!
81//! ## `hashbrown`
82//!
83//! *(Off by default)*
84//!
85//! Imports hash maps and sets from the [eponymous crate][`hashbrown`] instead of using ones
86//! from the Rust std library. This feature is necessary if the `std` feature is disabled.
87//!
88//! # Examples
89//!
90//! ```
91//! use arithmetic_parser::grammars::{F32Grammar, Parse};
92//! use arithmetic_typing::{defs::Prelude, Annotated, TypeEnvironment, Type};
93//!
94//! # fn main() -> anyhow::Result<()> {
95//! let code = "sum = |xs| xs.fold(0, |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//! // Evaluate `code` to get the inferred `sum` function signature.
102//! let output_type = env.process_statements(&ast)?;
103//! assert!(output_type.is_void());
104//! assert_eq!(env["sum"].to_string(), "([Num; N]) -> Num");
105//! # Ok(())
106//! # }
107//! ```
108//!
109//! Defining and using generic functions:
110//!
111//! ```
112//! # use arithmetic_parser::grammars::{F32Grammar, Parse};
113//! # use arithmetic_typing::{defs::Prelude, Annotated, TypeEnvironment, Type};
114//! # fn main() -> anyhow::Result<()> {
115//! let code = "sum_with = |xs, init| xs.fold(init, |acc, x| acc + x);";
116//! let ast = Annotated::<F32Grammar>::parse_statements(code)?;
117//!
118//! let mut env = TypeEnvironment::new();
119//! env.insert("fold", Prelude::Fold);
120//!
121//! let output_type = env.process_statements(&ast)?;
122//! assert!(output_type.is_void());
123//! assert_eq!(
124//! env["sum_with"].to_string(),
125//! "for<'T: Ops> (['T; N], 'T) -> 'T"
126//! );
127//! // Note that `sum_with` is parametric by the element of the slice
128//! // (for which the linearity constraint is applied based on the arg usage)
129//! // *and* by its length.
130//!
131//! let usage_code = "
132//! num_sum: Num = (1, 2, 3).sum_with(0);
133//! tuple_sum: (Num, Num) = ((1, 2), (3, 4)).sum_with((0, 0));
134//! ";
135//! let ast = Annotated::<F32Grammar>::parse_statements(usage_code)?;
136//! // Both lengths and element types differ in these invocations,
137//! // but it works fine since they are treated independently.
138//! env.process_statements(&ast)?;
139//! # Ok(())
140//! # }
141//! ```
142//!
143//! [`arithmetic-parser`]: https://crates.io/crates/arithmetic-parser
144//! [`arithmetic-eval`]: https://crates.io/crates/arithmetic-eval
145
146#![cfg_attr(not(feature = "std"), no_std)]
147#![doc(html_root_url = "https://docs.rs/arithmetic-typing/0.4.0-beta.1")]
148#![warn(missing_docs, missing_debug_implementations)]
149#![warn(clippy::all, clippy::pedantic)]
150#![allow(
151 clippy::missing_errors_doc,
152 clippy::must_use_candidate,
153 clippy::module_name_repetitions,
154 clippy::similar_names, // too many false positives because of lhs / rhs
155 clippy::option_if_let_else // too many false positives
156)]
157
158use core::{fmt, marker::PhantomData, str::FromStr};
159
160use arithmetic_parser::{
161 grammars::{Features, Grammar, Parse, ParseLiteral},
162 InputSpan, NomResult,
163};
164
165use self::{arith::ConstraintSet, ast::TypeAst};
166pub use self::{
167 env::TypeEnvironment,
168 types::{
169 DynConstraints, FnWithConstraints, Function, FunctionBuilder, LengthVar, Object, Slice,
170 Tuple, TupleIndex, TupleLen, Type, TypeVar, UnknownLen,
171 },
172};
173
174pub mod arith;
175pub mod ast;
176pub mod defs;
177mod env;
178pub mod error;
179mod types;
180pub mod visit;
181
182// Polyfill for `alloc` types.
183mod alloc {
184 #[cfg(not(feature = "std"))]
185 extern crate alloc as std;
186
187 pub(crate) use std::{
188 borrow::{Cow, ToOwned},
189 boxed::Box,
190 format,
191 string::{String, ToString},
192 sync::Arc,
193 vec,
194 vec::Vec,
195 };
196
197 #[cfg(all(not(feature = "std"), not(feature = "hashbrown")))]
198 compile_error!(
199 "One of `std` or `hashbrown` features must be enabled in order \
200 to get a hash map implementation"
201 );
202
203 #[cfg(not(feature = "hashbrown"))]
204 pub(crate) use std::collections::{hash_map, HashMap, HashSet};
205
206 #[cfg(feature = "hashbrown")]
207 pub(crate) use hashbrown::{hash_map, HashMap, HashSet};
208}
209
210/// Primitive types in a certain type system.
211///
212/// More complex types, like [`Type`] and [`Function`], are defined with a type param
213/// which determines the primitive type(s). This type param must implement [`PrimitiveType`].
214///
215/// [`TypeArithmetic`] has a `PrimitiveType` impl as an associated type, and one of the required
216/// operations of this trait is to be able to infer type for literal values from a [`Grammar`].
217///
218/// # Implementation Requirements
219///
220/// - [`Display`](fmt::Display) and [`FromStr`] implementations must be consistent; i.e.,
221/// `Display` should produce output parseable by `FromStr`. `Display` will be used in
222/// `Display` impls for `Type` etc. `FromStr` will be used to read type annotations.
223/// - `Display` presentations must be identifiers, such as `Num`.
224/// - While not required, a `PrimitiveType` should usually contain a Boolean type and
225/// implement [`WithBoolean`]. This allows to reuse [`BoolArithmetic`] and/or [`NumArithmetic`]
226/// as building blocks for your [`TypeArithmetic`].
227///
228/// [`Grammar`]: arithmetic_parser::grammars::Grammar
229/// [`TypeArithmetic`]: crate::arith::TypeArithmetic
230/// [`WithBoolean`]: crate::arith::WithBoolean
231/// [`BoolArithmetic`]: crate::arith::BoolArithmetic
232/// [`NumArithmetic`]: crate::arith::NumArithmetic
233///
234/// # Examples
235///
236/// ```
237/// # use std::{fmt, str::FromStr};
238/// use arithmetic_typing::PrimitiveType;
239///
240/// #[derive(Debug, Clone, Copy, PartialEq)]
241/// enum NumOrBytes {
242/// /// Numeric value, such as 1.
243/// Num,
244/// /// Bytes value, such as 0x1234 or "hello".
245/// Bytes,
246/// }
247///
248/// // `NumOrBytes` should correspond to a "value" type in the `Grammar`,
249/// // for example:
250/// enum NumOrBytesValue {
251/// Num(f64),
252/// Bytes(Vec<u8>),
253/// }
254///
255/// impl fmt::Display for NumOrBytes {
256/// fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
257/// match self {
258/// Self::Num => formatter.write_str("Num"),
259/// Self::Bytes => formatter.write_str("Bytes"),
260/// }
261/// }
262/// }
263///
264/// impl FromStr for NumOrBytes {
265/// type Err = anyhow::Error;
266///
267/// fn from_str(s: &str) -> Result<Self, Self::Err> {
268/// match s {
269/// "Num" => Ok(Self::Num),
270/// "Bytes" => Ok(Self::Bytes),
271/// _ => Err(anyhow::anyhow!("expected `Num` or `Bytes`")),
272/// }
273/// }
274/// }
275///
276/// impl PrimitiveType for NumOrBytes {}
277/// ```
278pub trait PrimitiveType:
279 Clone + PartialEq + fmt::Debug + fmt::Display + FromStr + Send + Sync + 'static
280{
281 /// Returns well-known constraints for this type. These constraints are used
282 /// in standalone parsing of type signatures.
283 ///
284 /// The default implementation returns an empty set.
285 fn well_known_constraints() -> ConstraintSet<Self> {
286 ConstraintSet::default()
287 }
288}
289
290/// Grammar with support of type annotations. Works as a decorator.
291///
292/// # Examples
293///
294/// ```
295/// use arithmetic_parser::grammars::{F32Grammar, Parse};
296/// use arithmetic_typing::Annotated;
297///
298/// # fn main() -> anyhow::Result<()> {
299/// let code = "x: [Num] = (1, 2, 3);";
300/// let ast = Annotated::<F32Grammar>::parse_statements(code)?;
301/// # assert_eq!(ast.statements.len(), 1);
302/// # Ok(())
303/// # }
304/// ```
305#[derive(Debug)]
306pub struct Annotated<T>(PhantomData<T>);
307
308impl<T: ParseLiteral> ParseLiteral for Annotated<T> {
309 type Lit = T::Lit;
310
311 fn parse_literal(input: InputSpan<'_>) -> NomResult<'_, Self::Lit> {
312 <T as ParseLiteral>::parse_literal(input)
313 }
314}
315
316impl<T: ParseLiteral> Grammar for Annotated<T> {
317 type Type<'a> = TypeAst<'a>;
318
319 fn parse_type(input: InputSpan<'_>) -> NomResult<'_, Self::Type<'_>> {
320 use nom::{combinator::map, Parser as _};
321 map(TypeAst::parse, |ast| ast.extra).parse(input)
322 }
323}
324
325/// Supports all syntax features.
326impl<T: ParseLiteral> Parse for Annotated<T> {
327 type Base = Self;
328
329 const FEATURES: Features = Features::all();
330}