Files
Doughlang/src/ast.rs
John e4c008bd4b doughlang: Expressions in patterns
ast:
- Document operators
- Parameterize Pat with Annotation
- Consolidate Path and Lit into "Value" (Expr)
- Consolidate NamedRecord/Namedtuple into PatOp::TypePrefixed
- Allow Pat<Pat,*> patterns
- Additional helper functions on Expr and Pat

lexer:
- Support inner-doc comment syntax `//!`
  - Cleans up `///` or `//!` prefix

parser:
- Make Parse::Prec `Default`
- Allow expression elision after `..`/`..=`
- Fix Parser::consume not updating elide_do state
- Add token splitting, to make `&&Expr` and `&&Pat` easier
- error: Embed Pat precedence in ParseError
2026-01-05 15:17:22 -05:00

422 lines
10 KiB
Rust

//! The Abstract Syntax Tree defines an interface between the parser and type checker
use crate::span::Span;
pub mod macro_matcher;
pub mod visit;
pub mod fold;
mod display;
/// An annotation: extra data added on to important AST nodes.
pub trait Annotation: Clone + std::fmt::Display + std::fmt::Debug + PartialEq + Eq {}
impl<T: Clone + std::fmt::Debug + std::fmt::Display + PartialEq + Eq> Annotation for T {}
/// A value with an annotation.
#[derive(Clone, PartialEq, Eq)]
pub struct Anno<T: Annotation, A: Annotation = Span>(pub T, pub A);
/// Expressions: The beating heart of Dough.
///
/// A program in Doughlang is a single expression which, at compile time,
/// sets up the state in which a program will run. This expression binds types,
/// functions, and values to names which are exposed at runtime.
///
/// Whereas in the body of a function, `do` sequences are ordered, in the global
/// scope (or subsequent module scopes, which are children of the global module,)
/// `do` sequences are considered unordered, and subexpressions may be reordered
/// in whichever way the compiler sees fit. This is especially important when
/// performing import resolution, as imports typically depend on the order
/// in which names are bound.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Expr<A: Annotation = Span> {
/// Omitted by semicolon insertion-elision rules
Omitted,
/// An identifier
Id(Path),
/// An escaped token for macro binding
MetId(String),
/// A literal bool, string, char, or int
Lit(Literal),
/// use Use
Use(Use),
/// `let Pat::NoTopAlt (= expr (else expr)?)?` |
/// `(fn | mod | impl) Pat::Fn Expr`
Bind(Box<Bind<A>>),
/// Expr { (Ident (: Expr)?),* }
Make(Box<Make<A>>),
/// Op Expr | Expr Op | Expr (Op Expr)+ | Op Expr Expr else Expr
Op(Op, Vec<Anno<Self, A>>),
}
/// Doughlang's AST is partitioned by data representation, so it
/// considers any expression which is composed solely of keywords,
/// symbols, and other expressions as operator expressions.
///
/// This includes:
/// - Do-sequence expressions: `Expr ; Expr `
/// - Type-cast expressions `Expr as Expr`
/// - Binding-modifier expressions: `pub Expr`, `#[Expr] Expr`
/// - Block and Group expressions: `{Expr?}`, `(Expr?)`
/// - Control flow: `if`, `while`, `loop`, `match`, `break`, `return`
/// - Function calls `Expr (Expr,*)`
/// - Traditional binary and unary operators (add, sub, neg, assign)
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Op {
/// `Expr (; Expr)*`
Do,
/// `Expr as Expr`
As,
/// `macro { (Pat => Expr)* }`
Macro,
/// `{ Expr }`
Block,
/// `[ Expr,* ]`
Array,
/// `[ Expr ; Expr ]`
ArRep,
/// `( Expr )`
Group,
/// `Expr (, Expr)*`
Tuple,
/// `#[ Expr ]`
Meta,
/// `Expr '?'`
Try,
/// `Expr [ Expr,* ]`
Index,
/// `Expr ( Expr,* )`
Call,
/// `pub Expr`
Pub,
/// `const Expr`
Const,
/// `static Expr`
Static,
/// `loop Expr`
Loop,
/// `match Expr { <Bind(Match, ..)>,* }`
Match,
/// `if Expr Expr (else Expr)?`
If,
/// `while Expr Expr (else Expr)?`
While,
/// `break Expr`
Break,
/// `return Expr`
Return,
/// `continue`
Continue,
/// `Expr . Expr`
Dot,
/// `Expr? ..Expr`
RangeEx,
/// `Expr? ..=Expr`
RangeIn,
/// `-Expr`
Neg,
/// `!Expr`
Not,
/// `!!Expr`
Identity,
/// `&Expr`
Refer,
/// `*Expr`
Deref,
/// `Expr * Expr`
Mul,
/// `Expr / Expr`
Div,
/// `Expr % Expr`
Rem,
/// `Expr + Expr`
Add,
/// `Expr - Expr`
Sub,
/// `Expr << Expr`
Shl,
/// `Expr >> Expr`
Shr,
/// `Expr & Expr`
And,
/// `Expr ^ Expr`
Xor,
/// `Expr | Expr`
Or,
/// `Expr < Expr`
Lt,
/// `Expr <= Expr`
Leq,
/// `Expr == Expr`
Eq,
/// `Expr != Expr`
Neq,
/// `Expr >= Expr`
Geq,
/// `Expr > Expr`
Gt,
/// `Expr && Expr`
LogAnd,
/// `Expr ^^ Expr`
LogXor,
/// `Expr || Expr`
LogOr,
/// `Expr = Expr`
Set,
/// `Expr *= Expr`
MulSet,
/// `Expr /= Expr`
DivSet,
/// `Expr %= Expr`
RemSet,
/// `Expr += Expr`
AddSet,
/// `Expr -= Expr`
SubSet,
/// `Expr <<= Expr`
ShlSet,
/// `Expr >>= Expr`
ShrSet,
/// `Expr &= Expr`
AndSet,
/// `Expr ^= Expr`
XorSet,
/// `Expr |= Expr`
OrSet,
}
/// A qualified identifier
///
/// TODO: qualify identifier
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Path {
// TODO: Identifier interning
pub parts: Vec<String>,
// TODO: generic parameters
}
/// A literal value (boolean, character, integer, string)
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Literal {
/// A boolean literal: true | false
Bool(bool),
/// A character literal: 'a', '\u{1f988}'
Char(char),
/// An integer literal: 0, 123, 0x10
Int(u128, u32),
/// A string literal:
Str(String),
}
/// A compound import declaration
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Use {
/// "*"
Glob,
/// Identifier
Name(String),
/// Identifier as Identifier
Alias(String, String),
/// Identifier :: Use
Path(String, Box<Use>),
/// { Use, * }
Tree(Vec<Use>),
}
/// A pattern binding
/// ```ignore
/// let Pat (= Expr (else Expr)?)?
/// type Pat (= Expr)?
/// fn Pat Expr
/// mod Pat Expr
/// impl Pat Expr
/// struct Pat
/// enum Pat
/// for Pat in Expr Expr (else Expr)?
/// Pat => Expr // in match
/// ```
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Bind<A: Annotation = Span>(
pub BindOp,
pub Vec<Path>,
pub Pat<A>,
pub Vec<Anno<Expr<A>, A>>,
);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BindOp {
/// A `let Pat (= Expr (else Expr)?)?` binding
Let,
/// A type-alias binding
Type,
/// A `fn Pat Expr` binding
Fn,
/// A `mod Pat Expr` binding
Mod,
/// An `impl Pat Expr` binding
Impl,
/// A struct definition
Struct,
/// An enum definition
Enum,
/// A `for Pat in Expr Expr (else Expr)?` binding
For,
/// A `Pat => Expr` binding
Match,
}
/// A make (constructor) expression
/// ```ignore
/// Expr { (Ident (: Expr)?),* }
/// ```
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Make<A: Annotation = Span>(pub Anno<Expr<A>, A>, pub Vec<MakeArm<A>>);
/// A single "arm" of a make expression
/// ```text
/// Identifier (':' Expr)?
/// ```
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MakeArm<A: Annotation = Span>(pub String, pub Option<Anno<Expr<A>, A>>);
/// Binding patterns for each kind of matchable value.
///
/// This covers both patterns in Match expressions, and type annotations.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Pat<A: Annotation = Span> {
/// Matches anything without binding
Ignore,
/// Matches nothing, ever
Never,
/// Matches nothing; used for macro substitution
MetId(String),
/// Matches anything, and binds it to a name
Name(String),
/// Matches a value by equality comparison
Value(Box<Anno<Expr<A>, A>>),
/// Matches a compound pattern
Op(PatOp, Vec<Pat<A>>),
}
/// Operators on lists of patterns
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PatOp {
/// Changes the visibility mode to "public"
Pub,
/// Changes the binding mode to "mutable"
Mut,
/// Matches the dereference of a pointer (`&pat`)
Ref,
/// Matches the dereference of a raw pointer (`*pat`)
Ptr,
/// Matches a partial decomposition (`..rest`) or upper-bounded range (`..100`)
Rest,
/// Matches an exclusive bounded range (`0..100`)
RangeEx,
/// Matches an inclusive bounded range (`0..=100`)
RangeIn,
/// Matches the elements of a record or struct { a, b, c }
Record,
/// Matches the elements of a tuple ( a, b, c )
Tuple,
/// Matches the elements of a slice or array [ a, b, c ]
Slice,
/// Matches a constant-size slice with repeating elements
ArRep,
/// Matches a type annotation or struct member
Typed,
/// Matches a prefix-type-annotated structure
TypePrefixed,
/// Matches a generic specialization annotation
Generic,
/// Changes the binding mode to "function-body"
Fn,
/// Matches one of a list of alternatives
Alt,
}
impl<T: Annotation, A: Annotation> std::fmt::Debug for Anno<T, A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<A as std::fmt::Debug>::fmt(&self.1, f)?;
f.write_str(": ")?;
<T as std::fmt::Debug>::fmt(&self.0, f)
}
}
impl<A: Annotation> Default for Expr<A> {
fn default() -> Self {
Self::Op(Op::Tuple, vec![])
}
}
impl<A: Annotation> Expr<A> {
pub const fn anno(self, annotation: A) -> Anno<Expr<A>, A> {
Anno(self, annotation)
}
pub fn and_do(self, annotation: A, other: Anno<Expr<A>, A>) -> Self {
let Self::Op(Op::Do, mut exprs) = self else {
return Self::Op(Op::Do, vec![self.anno(annotation), other]);
};
let Anno(Self::Op(Op::Do, mut other), _) = other else {
exprs.push(other);
return Self::Op(Op::Do, exprs);
};
exprs.append(&mut other);
Self::Op(Op::Do, exprs)
}
pub fn to_tuple(self, annotation: A) -> Self {
match self {
Self::Op(Op::Tuple, _) => self,
_ => Self::Op(Op::Tuple, vec![self.anno(annotation)]),
}
}
pub const fn is_place(&self) -> bool {
matches!(
self,
Self::Id(_) | Self::Op(Op::Index | Op::Dot | Op::Deref, _)
)
}
pub const fn is_value(&self) -> bool {
!self.is_place()
}
#[allow(clippy::type_complexity)]
pub const fn as_slice(&self) -> Option<(Op, &[Anno<Expr<A>, A>])> {
match self {
Expr::Op(op, args) => Some((*op, args.as_slice())),
_ => None,
}
}
}
impl<A: Annotation> Pat<A> {
pub fn to_tuple(self) -> Self {
match self {
Self::Op(PatOp::Tuple, _) => self,
_ => Self::Op(PatOp::Tuple, vec![self]),
}
}
}
impl From<&str> for Path {
fn from(value: &str) -> Self {
Self { parts: vec![value.to_owned()] }
}
}