Last active
October 7, 2025 18:52
-
-
Save re-masashi/982c233be01a0232b1606016717832ac to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use std::ops::Range; | |
| #[derive(Debug, Clone)] | |
| pub struct ASTNode { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub node: ASTNodeKind, | |
| pub attributes: Vec<Attribute>, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum ASTNodeKind { | |
| Expr(Expr), | |
| Function(Box<Function>), | |
| Struct(Struct), | |
| Enum(Enum), | |
| TypeAlias(TypeAlias), | |
| Impl(Impl), | |
| Trait(TraitDef), | |
| MacroDef(MacroDef), | |
| EffectDef(EffectDef), | |
| Error, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct TypeAnnot { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub expr: TypeAnnotKind, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum TypeAnnotKind { | |
| // Primitives | |
| Bool, | |
| Int, | |
| Float, | |
| String, | |
| Unit, | |
| Never, | |
| // Named types | |
| Named(String), | |
| // Generic types: List<T>, Option<T> | |
| Generic { | |
| name: String, | |
| args: Vec<TypeAnnot>, | |
| kind: Option<KindAnnot>, | |
| }, | |
| // Function types with effects | |
| Function { | |
| params: Vec<TypeAnnot>, | |
| return_type: Box<TypeAnnot>, | |
| effects: EffectAnnot, | |
| }, | |
| // Tuple types | |
| Tuple(Vec<TypeAnnot>), | |
| // Union types | |
| Union(Vec<TypeAnnot>), | |
| // Trait bounds | |
| Trait(Vec<String>), | |
| // HKT: Type constructor with explicit kind | |
| Constructor { | |
| name: String, | |
| kind: KindAnnot, | |
| }, | |
| // Type variable (in forall/exists) | |
| Variable { | |
| name: String, | |
| kind: Option<KindAnnot>, | |
| }, | |
| // GADT: Universal quantification | |
| // forall T. T ~ Int => T -> T | |
| Forall { | |
| vars: Vec<(String, KindAnnot)>, | |
| constraints: Vec<ConstraintAnnot>, | |
| body: Box<TypeAnnot>, | |
| }, | |
| // GADT: Existential types | |
| // exists T. (T, T -> Int) | |
| Exists { | |
| vars: Vec<(String, KindAnnot)>, | |
| constraints: Vec<ConstraintAnnot>, | |
| body: Box<TypeAnnot>, | |
| }, | |
| Error, | |
| } | |
| // Kind annotations for HKTs | |
| #[derive(Debug, Clone, PartialEq, Eq)] | |
| pub enum KindAnnot { | |
| Star, // * | |
| Arrow(Box<KindAnnot>, Box<KindAnnot>), // * -> * | |
| } | |
| // Effect annotations | |
| #[derive(Debug, Clone)] | |
| pub struct EffectAnnot { | |
| pub effects: Vec<String>, // IO, State, Exn, etc. | |
| pub rest: Option<String>, // polymorphic effect variable | |
| } | |
| impl EffectAnnot { | |
| pub fn pure() -> Self { | |
| Self { | |
| effects: vec![], | |
| rest: None, | |
| } | |
| } | |
| pub fn closed(effects: Vec<String>) -> Self { | |
| Self { | |
| effects, | |
| rest: None, | |
| } | |
| } | |
| pub fn open(effects: Vec<String>, rest: String) -> Self { | |
| Self { | |
| effects, | |
| rest: Some(rest), | |
| } | |
| } | |
| } | |
| // Type constraints (for GADTs) | |
| #[derive(Debug, Clone)] | |
| pub enum ConstraintAnnot { | |
| // Type equality: T ~ Int | |
| Equal(TypeAnnot, TypeAnnot), | |
| // Trait bound: T: Show | |
| Trait(String, String), // (type_var, trait_name) | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct Expr { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub expr: ExprKind, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum ExprKind { | |
| Int(i64), | |
| Float(f64), | |
| Bool(bool), | |
| String(String), | |
| Variable(String), | |
| BinOp(Box<Expr>, BinOp, Box<Expr>), | |
| UnOp(UnOp, Box<Expr>), | |
| Assign { | |
| l_val: Box<Expr>, | |
| r_val: Box<Expr>, | |
| op: AssignOp, | |
| }, | |
| Let { | |
| var: String, | |
| type_annot: Option<TypeAnnot>, | |
| value: Box<Expr>, | |
| }, | |
| Array(Vec<Expr>), | |
| Tuple(Vec<Expr>), | |
| Map(Vec<(Expr, Expr)>), | |
| EnumConstruct { | |
| name: String, | |
| variant: String, | |
| args: Vec<Expr>, | |
| }, | |
| Perform { | |
| effect: String, | |
| args: Vec<Expr>, | |
| }, | |
| Handle { | |
| body: Box<Expr>, | |
| handlers: Vec<EffectHandler>, | |
| }, | |
| Lambda { | |
| args: Vec<FnArg>, | |
| expression: Box<Expr>, | |
| }, | |
| Cast { | |
| expr: Box<Expr>, | |
| target_type: TypeAnnot, | |
| }, | |
| IfElse { | |
| condition: Box<Expr>, | |
| then: Box<Expr>, | |
| else_: Option<Box<Expr>>, | |
| }, | |
| Block(Vec<Expr>), | |
| With { | |
| context: Box<Expr>, | |
| var: String, | |
| body: Box<Expr>, | |
| }, | |
| Loop { | |
| label: Option<String>, | |
| body: Box<Expr>, | |
| }, | |
| Match(Box<Expr>, Vec<MatchArm>), | |
| For { | |
| iterator: Box<Expr>, | |
| value: String, | |
| expression: Box<Expr> | |
| }, | |
| While(Box<Expr>, Box<Expr>), | |
| IfLet { | |
| pattern: Pattern, | |
| expr: Box<Expr>, | |
| then: Box<Expr>, | |
| else_: Option<Box<Expr>>, | |
| }, | |
| WhileLet { | |
| pattern: Pattern, | |
| expr: Box<Expr>, | |
| body: Box<Expr>, | |
| }, | |
| Return(Option<Box<Expr>>), | |
| Break(Option<Box<Expr>>), | |
| Continue, | |
| Call(Box<Expr>, Vec<Expr>), | |
| Index(Box<Expr>, Box<Expr>), | |
| FieldAccess(Box<Expr>, String), | |
| OptionalChain(Box<Expr>, String), | |
| MacroCall(String, Vec<Expr>, Delimiter), | |
| Import(Import), | |
| Error, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct EffectHandler { | |
| pub span: Range<usize>, | |
| pub effect: String, | |
| pub params: Vec<String>, | |
| pub resume_param: String, // The continuation parameter | |
| pub body: Expr, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct Import { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub path: Vec<String>, // ["abc", "ab"] for "abc/ab.ni" | |
| pub items: Vec<(String, Option<String>)>, // for selective imports + aliases | |
| // none implies all being imported | |
| pub alias: Option<String>, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct TraitDef { | |
| pub span: Range<usize>, | |
| pub name: String, | |
| pub type_params: Vec<TypeParam>, | |
| pub super_traits: Vec<TypeAnnot>, | |
| pub methods: Vec<Function>, | |
| pub associated_types: Vec<AssociatedType>, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct AssociatedType { | |
| pub name: String, | |
| pub bounds: Vec<TypeAnnot>, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum UnOp { | |
| Not, | |
| Plus, | |
| Minus, | |
| Unwrap, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum BinOp { | |
| Add, | |
| Sub, | |
| Mul, | |
| Div, | |
| Mod, | |
| Pow, | |
| Eq, | |
| Greater, | |
| Less, | |
| GreaterEq, | |
| LessEq, | |
| NotEq, | |
| Pipe, | |
| And, | |
| Or, | |
| Nor, | |
| Xor, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum AssignOp { | |
| Assign, | |
| AddAssign, | |
| SubAssign, | |
| MulAssign, | |
| DivAssign, | |
| ModAssign, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct MatchArm { | |
| pub pattern: Pattern, | |
| pub guard: Option<Expr>, | |
| pub body: Box<Expr>, | |
| pub span: Range<usize>, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct Pattern { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub pat: PatKind, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum PatKind { | |
| Wildcard, | |
| Bind(String), | |
| Literal(Literal), | |
| Array(Vec<Pattern>), | |
| Or(Vec<Pattern>), | |
| As { | |
| name: String, | |
| pattern: Box<Pattern>, | |
| }, | |
| Struct { | |
| name: String, | |
| fields: Vec<(String, Pattern)>, | |
| }, | |
| Enum { | |
| name: String, | |
| variant: String, | |
| params: Vec<Pattern>, | |
| }, | |
| Range(Box<Expr>, Box<Expr>), | |
| Rest(String), | |
| Error, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum Literal { | |
| Int(i64), | |
| Float(f64), | |
| Bool(bool), | |
| String(String), | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct Function { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub vis: Visibility, | |
| pub name: String, | |
| pub type_params: Vec<TypeParam>, | |
| pub args: Vec<FnArg>, | |
| pub return_type: Option<TypeAnnot>, | |
| pub where_constraints: Vec<TypeConstraint>, | |
| pub effects: EffectAnnot, | |
| pub body: Option<Expr>, // no body implies declaration | |
| } | |
| #[derive(Debug, Clone, Copy, PartialEq)] | |
| pub enum Visibility { | |
| Public, | |
| Private, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct TypeParam { | |
| pub name: String, | |
| pub kind: Option<KindAnnot>, | |
| pub bounds: Vec<TypeAnnot>, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct TypeConstraint { | |
| pub span: Range<usize>, | |
| pub left: TypeAnnot, | |
| pub right: TypeAnnot, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct FnArg { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub name: String, | |
| pub type_: Option<TypeAnnot>, | |
| // pub default: Option<Expr>, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct Struct { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub name: String, | |
| pub type_params: Vec<TypeParam>, | |
| pub fields: Vec<(FnArg, Visibility)>, | |
| pub methods: Vec<Function>, // (desugar to impl) | |
| pub vis: Visibility, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct Enum { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub name: String, | |
| pub type_params: Vec<TypeParam>, | |
| pub variants: Vec<EnumVariant>, | |
| pub methods: Vec<Function>, // (desugar to impl) | |
| pub vis: Visibility, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct EnumVariant { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub name: String, | |
| pub types: Vec<TypeAnnot>, | |
| pub constraints: Vec<TypeConstraint>, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct TypeAlias { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub name: String, | |
| pub type_params: Vec<TypeParam>, | |
| pub target_type: TypeAnnot, | |
| pub where_constraints: Vec<TypeConstraint>, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct Impl { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub type_name: String, | |
| pub type_params: Vec<TypeParam>, | |
| pub methods: Vec<Function>, | |
| pub trait_: Option<String>, | |
| pub where_constraints: Vec<TypeConstraint>, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct Attribute { | |
| pub span: Range<usize>, | |
| pub name: String, | |
| pub args: Vec<AttributeArg>, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum AttributeArg { | |
| Ident(String), | |
| Literal(Literal), | |
| KeyValue(String, Box<Expr>), | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct MacroDef { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub name: String, | |
| pub rules: Vec<MacroRule>, | |
| pub hygiene: MacroHygiene, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum MacroHygiene { | |
| Hygienic, | |
| Unhygienic, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct MacroRule { | |
| pub span: Range<usize>, | |
| pub pattern: Vec<MacroToken>, | |
| pub body: Vec<MacroToken>, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum MacroToken { | |
| Ident(String), | |
| Literal(Literal), | |
| Punct(String), | |
| Group { | |
| delimiter: Delimiter, | |
| tokens: Vec<MacroToken>, | |
| }, | |
| MetaVar { | |
| name: String, | |
| kind: String, | |
| }, // $x:expr, $t:ty | |
| Repeat { | |
| tokens: Vec<MacroToken>, | |
| separator: Option<String>, | |
| kind: RepeatKind, | |
| }, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum Delimiter { | |
| Paren, // () | |
| Bracket, // [] | |
| Brace, // {} | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum RepeatKind { | |
| ZeroOrMore, // * | |
| OneOrMore, // + | |
| ZeroOrOne, // ? | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct EffectDef { | |
| pub span: Range<usize>, | |
| pub file: String, | |
| pub vis: Visibility, | |
| pub name: String, | |
| pub type_params: Vec<TypeParam>, | |
| pub operations: Vec<EffectOperation>, | |
| pub where_constraints: Vec<TypeConstraint>, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub struct EffectOperation { | |
| pub span: Range<usize>, | |
| pub name: String, | |
| pub params: Vec<TypeAnnot>, | |
| pub return_type: TypeAnnot, | |
| } | |
| #[derive(Debug, PartialEq, Eq, Hash, Clone)] | |
| pub enum Kind { | |
| Star, | |
| Arrow(Box<Kind>, Box<Kind>), | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ? at the end implies "optional" | |
| // * implies 0 or more | |
| // + implies 1 or more | |
| // | implies "or" | |
| // kindof like regex | |
| ASTNodeKind := | |
| Expr | |
| | Function | |
| | Struct | |
| | Enum | |
| | Impl_block | |
| | Type_alias | |
| | Macro_def | |
| | Trait_def | |
| | Effect_def | |
| Expr := | |
| Int = Token::Int | |
| | Float = Token::Float | |
| | Bool = Token::Bool | |
| | String = Token::String | |
| | Variable = Token::Variable | |
| | BinOp = Expr BinOp Expr | |
| | UnOp = UnOp Expr // special case is unwrap. instead of going in FRONT it goes behind. so its Expr UnOp syntactically | |
| | Let = Token::KeywordLet name: Token::Variable type_annot: (Token::Colon TypeAnnot)? Token::Assign value: Expr | |
| | Array = Token::LBracket (Expr Token::Comma)* Token::LBracket | |
| | Tuple = Expr Token::Comma Expr* | |
| | Token::LParen Expr Token::RParen | |
| | Block = Token::LBrace (Expr Token::Semicolon?)+ Token::RBrace | |
| | Map = Token::LBrace (Expr Token::Colon Expr Token::Comma)+ Token::RBrace | |
| | EnumConstruct = Token::Variable Token::Access Token::Variable Token::LParen (Expr Token::Comma)* Token::RParen | |
| | Perform = Token::Perform effect: Token::Variable args: (Token::LParen (Expr Token::Comma)* Token::RParen) | |
| | Handle = Token::Handle body: Expr Token::With Token::LBrace handlers: EffectHandler+ Token::RBrace | |
| | Lambda = Token::Fn Token::LParen args: (Token::Variable (Token::Colon TypeAnnot)? Token::Comma)* Token::RParen expression: Expr | |
| | Cast = expr: Expr Token::KeywordAs target_type: TypeAnnot | |
| | IfElse = Token::KeywordIf condition: Expr then: Expr (Token::KeywordElse else_: Expr)? | |
| | With = Token::KeywordWith context: Expr Token::KeywordAs var: Token::Variable body: Expr | |
| // ill add `loop` later | |
| | Match = Token::KeywordMatch Expr Token::LBrace MatchArm* Token::RBrace | |
| | For = Token::KeywordFor value: Token::Variable Token::KeywordIn iterator: Expr expression: Expr | |
| | While = Token::KeywordWhile Expr Expr | |
| | IfLet = Token::KeywordIf Token::KeywordLet pattern: Pattern Token::Assign expr: Expr then: Expr (Token::KeywordElse else: Expr)? | |
| | WhileLet = Token::KeywordWhile Token::KeywordLet pattern: Pattern Token::Assign expr: Expr body: Expr | |
| | Return = Token::KeywordReturn Expr? | |
| | Break = Token::KeywordBreak Expr? | |
| | Continue = Token::KeywordContinue | |
| | Call = Expr Token::LParen (Expr Token::Comma)* Token::RParen | |
| | Index = Expr Token::LBracket Expr Token::RBracket | |
| | FieldAccess = Expr Token::Dot Token::Variable | |
| | OptionalChain = Expr Token::OptionalChain Token::Variable | |
| | MacroCall = Token::MacroInvocation MacroDelimiterStart (Expr Token::Comma)* MacroDelimiterEnd | |
| | Import = Token::Import path: Token::String Token::RBrace items: (Token::Variable (Token::KeywordAs Token::Variable)?)* Token::KeywordAs Token::Variable | |
| | Error // just placeholder if needed | |
| Function := | |
| | Token::KeywordDef vis: Token::KeywordPub? name: Token::Variable type_params: (Token::Less TypeParam+ Token::Greater)? args: Token::LParen (Token::Variable (Token::Colon TypeAnnot)? Token::Comma)* Token::RParen (Token::Arrow return_type: TypeAnnot)? (Token::Where where_constrains: TypeConstraint*)? (Token::Effects effects: EffectAnnot*)? Expr | |
| Struct := | |
| | Token::KeywordStruct vis: Token::KeywordPub? name: Token::Variable type_params: (Token::Less TypeParam+ Token::Greater)? Token::LBrace fields|methods: ((Token::Variable Token::KeywordPub? Token::Colon TypeAnnot)|Function)* Token::RBrace | |
| Enum := | |
| | Token::KeywordEnum vis: Token::KeywordPub? name: Token::Variable type_params: (Token::Less TypeParam+ Token::Greater)? fields|methods: (EnumVariant|Function)* Token::RBrace | |
| TypeAlias = | |
| | Token::KeywordType name: Token::Variable type_params: (Token::Less TypeParam+ Token::Greater)? Token::KeywordWhere where_constrains: TypeConstraint* Token::Assign target_type: TypeAnnot | |
| Impl = | |
| | Token::KeywordImpl name: Token::Variable (Token::Colon trait_: Token::Variable)? type_params: (Token::Less TypeParam+ Token::Greater)? Token::KeywordWhere where_constrains: TypeConstraint* Token::LBrace methods: Function* Token::RBrace | |
| Trait = | |
| | Token::KeywordTrait name: Token::Variable (Token::Colon trait_: Token::Variable)? associated_types|methods: (Token::KeywordType TypeAnnot|Function)* | |
| // todo: add supertraits support. Show: Debug + Clone | |
| // todo: MacroDef | |
| EffectDef = | |
| | Token::KeywordEffect vis: Token::KeywordPub? name: Token::Variable type_params: (Token::Less TypeParam+ Token::Greater)? where_constrains: TypeConstraint* Token::LBrace EffectOperation* Token::RBrace | |
| EffectAnnot = | |
| | Token::Variable | |
| EffectHandler = | |
| | Token::LParen args: Token::Variable* Token::RParen | |
| TypeAnnot = | |
| | Token::KeywordBool | |
| | Token::KeywordInt | |
| | Token::KeywordFloat | |
| | Token::KeywordString | |
| | Named = Token::Variable | |
| | Unit = Token::LParen Token::RParen | |
| | Never = Token::Bang | |
| | Generic = idk | |
| | Token::Fn Token::LParen args: (TypeAnnot Token::Comma)* Token::RParen Token::Effects EffectAnnot* | |
| | Tuple = Token::LParen TypeAnnot+ Token::RParen | |
| | Union = TypeAnnot | (Token::Union TypeAnnot) + | |
| | Trait = Token::KeywordTrait Token::Variable+ | |
| | Constructor = idk | |
| | Forall = Token::KeywordForall idk | |
| | idk the rest |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // only the relevant function | |
| fn expr<'src>() -> impl Parser<'src, &'src [Token], Expr, extra::Err<Rich<'src, Token, Span>>> + Clone { | |
| recursive(|expr| { | |
| let literal = choice(( | |
| select! { Token::Int(n) => ExprKind::Int(n) }, | |
| select! { Token::Float(f) => ExprKind::Float(f) }, | |
| select! { Token::String(s) => ExprKind::String(s) }, | |
| select! { Token::Bool(b) => ExprKind::Bool(b) }, | |
| )); | |
| let variable = ident().map(ExprKind::Variable); | |
| // Lambda | |
| let lambda = just(Token::KeywordFn) | |
| .ignore_then( | |
| ident() | |
| .then(just(Token::Colon).ignore_then(type_annot()).or_not()) | |
| .map_with(|(name, ty), e| FnArg { | |
| span: to_range(e.span()), | |
| file: String::new(), | |
| name, | |
| type_: ty, | |
| }) | |
| .separated_by(just(Token::Comma)) | |
| .allow_trailing() | |
| .collect::<Vec<_>>() | |
| .delimited_by(just(Token::LParen), just(Token::RParen)) | |
| ) | |
| .then(expr.clone()) | |
| .map(|(args, body)| ExprKind::Lambda { | |
| args, | |
| expression: Box::new(body), | |
| }); | |
| // Block | |
| let block = expr.clone() | |
| .then_ignore(just(Token::Semicolon).or_not()) | |
| .repeated() | |
| .at_least(1) | |
| .collect::<Vec<_>>() | |
| .delimited_by(just(Token::LBrace), just(Token::RBrace)) | |
| .recover_with(via_parser(nested_delimiters( | |
| Token::LBrace, Token::RBrace, | |
| [(Token::LParen, Token::RParen), (Token::LBracket, Token::RBracket)], | |
| |_| vec![], | |
| ))) | |
| .map(ExprKind::Block); | |
| // Let | |
| let let_expr = just(Token::KeywordLet) | |
| .ignore_then(ident()) | |
| .then(just(Token::Colon).ignore_then(type_annot()).or_not()) | |
| .then_ignore(just(Token::Assign)) | |
| .then(expr.clone()) | |
| .map(|((var, ty), value)| ExprKind::Let { | |
| var, | |
| type_annot: ty, | |
| value: Box::new(value), | |
| }); | |
| // If-let | |
| let if_let = just(Token::KeywordIf) | |
| .ignore_then(just(Token::KeywordLet)) | |
| .ignore_then(pattern()) | |
| .then_ignore(just(Token::Assign)) | |
| .then(expr.clone()) | |
| .then(expr.clone()) | |
| .then(just(Token::KeywordElse).ignore_then(expr.clone()).or_not()) | |
| .map(|(((pattern, expr_val), then), else_)| ExprKind::IfLet { | |
| pattern, | |
| expr: Box::new(expr_val), | |
| then: Box::new(then), | |
| else_: else_.map(Box::new), | |
| }); | |
| // If-else | |
| let if_expr = just(Token::KeywordIf) | |
| .ignore_then(expr.clone()) | |
| .then(expr.clone()) | |
| .then(just(Token::KeywordElse).ignore_then(expr.clone()).or_not()) | |
| .map(|((cond, then_block), else_block)| ExprKind::IfElse { | |
| condition: Box::new(cond), | |
| then: Box::new(then_block), | |
| else_: else_block.map(Box::new), | |
| }); | |
| // While-let | |
| let while_let = just(Token::KeywordWhile) | |
| .ignore_then(just(Token::KeywordLet)) | |
| .ignore_then(pattern()) | |
| .then_ignore(just(Token::Assign)) | |
| .then(expr.clone()) | |
| .then(expr.clone()) | |
| .map(|((pattern, expr_val), body)| ExprKind::WhileLet { | |
| pattern, | |
| expr: Box::new(expr_val), | |
| body: Box::new(body), | |
| }); | |
| // While | |
| let while_expr = just(Token::KeywordWhile) | |
| .ignore_then(expr.clone()) | |
| .then(expr.clone()) | |
| .map(|(cond, body)| ExprKind::While(Box::new(cond), Box::new(body))); | |
| // Match - CRITICAL: Don't inline match_arm | |
| let match_arm = pattern() | |
| .then(just(Token::KeywordIf).ignore_then(expr.clone()).or_not()) | |
| .then_ignore(just(Token::FatArrow)) | |
| .then(expr.clone()) | |
| .map_with(|((pattern, guard), body), e| MatchArm { | |
| pattern, | |
| guard, | |
| body: Box::new(body), | |
| span: to_range(e.span()), | |
| }); | |
| let match_expr = just(Token::KeywordMatch) | |
| .ignore_then(expr.clone()) | |
| .then( | |
| match_arm | |
| .separated_by(just(Token::Comma)) | |
| .allow_trailing() | |
| .collect::<Vec<_>>() | |
| .delimited_by(just(Token::LBrace), just(Token::RBrace)) | |
| ) | |
| .map(|(scrutinee, arms)| ExprKind::Match(Box::new(scrutinee), arms)); | |
| // For | |
| let for_expr = just(Token::KeywordFor) | |
| .ignore_then(ident()) | |
| .then_ignore(just(Token::KeywordIn)) | |
| .then(expr.clone()) | |
| .then(expr.clone()) | |
| .map(|((value, iterator), body)| ExprKind::For { | |
| value, | |
| iterator: Box::new(iterator), | |
| expression: Box::new(body), | |
| }); | |
| // Loop | |
| let loop_expr = just(Token::KeywordLoop) | |
| .ignore_then(expr.clone()) | |
| .map(|body| ExprKind::Loop { | |
| label: None, | |
| body: Box::new(body), | |
| }); | |
| // With | |
| let with_expr = just(Token::KeywordWith) | |
| .ignore_then(expr.clone()) | |
| .then_ignore(just(Token::KeywordAs)) | |
| .then(ident()) | |
| .then(expr.clone()) | |
| .map(|((context, var), body)| ExprKind::With { | |
| context: Box::new(context), | |
| var, | |
| body: Box::new(body), | |
| }); | |
| // Return, break, continue | |
| let return_expr = just(Token::KeywordReturn) | |
| .then(expr.clone().or_not()) | |
| .map(|(_, e)| ExprKind::Return(e.map(Box::new))); | |
| let break_expr = just(Token::KeywordBreak) | |
| .then(expr.clone().or_not()) | |
| .map(|(_, e)| ExprKind::Break(e.map(Box::new))); | |
| let continue_expr = just(Token::KeywordContinue).to(ExprKind::Continue); | |
| // Perform | |
| let perform_expr = just(Token::KeywordPerform) | |
| .ignore_then(ident()) | |
| .then( | |
| expr.clone() | |
| .separated_by(just(Token::Comma)) | |
| .allow_trailing() | |
| .collect::<Vec<_>>() | |
| .delimited_by(just(Token::LParen), just(Token::RParen)) | |
| ) | |
| .map(|(effect, args)| ExprKind::Perform { effect, args }); | |
| // Handle | |
| let effect_handler = ident() | |
| .then( | |
| ident() | |
| .separated_by(just(Token::Comma)) | |
| .allow_trailing() | |
| .collect::<Vec<_>>() | |
| .delimited_by(just(Token::LParen), just(Token::RParen)) | |
| ) | |
| .then_ignore(just(Token::FatArrow)) | |
| .then(expr.clone()) | |
| .map_with(|((effect, mut params), body), e| { | |
| let resume_param = params.pop().unwrap_or_else(|| "k".to_string()); | |
| EffectHandler { | |
| span: to_range(e.span()), | |
| effect, | |
| params, | |
| resume_param, | |
| body, | |
| } | |
| }); | |
| let handle_expr = just(Token::KeywordHandle) | |
| .ignore_then(expr.clone()) | |
| .then_ignore(just(Token::KeywordWith)) | |
| .then( | |
| effect_handler | |
| .separated_by(just(Token::Comma)) | |
| .allow_trailing() | |
| .collect::<Vec<_>>() | |
| .delimited_by(just(Token::LBrace), just(Token::RBrace)) | |
| ) | |
| .map(|(body, handlers)| ExprKind::Handle { | |
| body: Box::new(body), | |
| handlers, | |
| }); | |
| // Enum construction | |
| let enum_construct = ident() | |
| .then_ignore(just(Token::Access)) | |
| .then(ident()) | |
| .then( | |
| expr.clone() | |
| .separated_by(just(Token::Comma)) | |
| .allow_trailing() | |
| .collect::<Vec<_>>() | |
| .delimited_by(just(Token::LParen), just(Token::RParen)) | |
| ) | |
| .map(|((name, variant), args)| ExprKind::EnumConstruct { | |
| name, | |
| variant, | |
| args, | |
| }); | |
| // Import | |
| let import = just(Token::KeywordImport) | |
| .ignore_then(select! { Token::String(s) => s }) | |
| .then( | |
| choice(( | |
| ident() | |
| .then(just(Token::KeywordAs).ignore_then(ident()).or_not()) | |
| .separated_by(just(Token::Comma)) | |
| .allow_trailing() | |
| .collect::<Vec<_>>() | |
| .delimited_by(just(Token::LBrace), just(Token::RBrace)) | |
| .map(|items| (items, None)), | |
| just(Token::KeywordAs) | |
| .ignore_then(ident()) | |
| .map(|alias| (vec![], Some(alias))), | |
| )) | |
| .or_not() | |
| ) | |
| .map_with(|(path, items), e| { | |
| let (items, alias) = items.unwrap_or((vec![], None)); | |
| let path_parts: Vec<String> = path.split('/').map(|s| s.to_string()).collect(); | |
| ExprKind::Import(Import { | |
| span: to_range(e.span()), | |
| file: String::new(), | |
| path: path_parts, | |
| items, | |
| alias, | |
| }) | |
| }); | |
| // Macro call | |
| let macro_call = select! { Token::MacroInvocation(name) => name } | |
| .then( | |
| expr.clone() | |
| .separated_by(just(Token::Comma)) | |
| .allow_trailing() | |
| .collect::<Vec<_>>() | |
| .delimited_by(just(Token::LParen), just(Token::RParen)) | |
| ) | |
| .map(|(name, args)| ExprKind::MacroCall(name, args, Delimiter::Paren)); | |
| // Parenthesized expression | |
| let paren_expr = expr.clone() | |
| .delimited_by(just(Token::LParen), just(Token::RParen)) | |
| .map(|e| e.expr); | |
| // ATOM - NO COLLECTIONS HERE! | |
| // Collections will be parsed AFTER as part of the delimiter check | |
| let atom = choice(( | |
| literal, | |
| // All keywords first | |
| let_expr, | |
| if_let, | |
| if_expr, | |
| while_let, | |
| while_expr, | |
| match_expr, | |
| for_expr, | |
| loop_expr, | |
| with_expr, | |
| return_expr, | |
| break_expr, | |
| continue_expr, | |
| perform_expr, | |
| handle_expr, | |
| // Other constructs | |
| import, | |
| macro_call, | |
| enum_construct, | |
| lambda, | |
| // Block | |
| block, | |
| // Variable AFTER keywords | |
| variable, | |
| // Parenthesized last | |
| paren_expr, | |
| )) | |
| .map_with(|kind, e| Expr { | |
| span: to_range(e.span()), | |
| file: String::new(), | |
| expr: kind, | |
| }); | |
| // NOW handle collections by checking delimiters FIRST | |
| // This breaks the left recursion | |
| let array_or_expr = just(Token::LBracket) | |
| .ignore_then( | |
| expr.clone() | |
| .separated_by(just(Token::Comma)) | |
| .allow_trailing() | |
| .collect::<Vec<_>>() | |
| ) | |
| .then_ignore(just(Token::RBracket)) | |
| .map_with(|exprs, e| Expr { | |
| span: to_range(e.span()), | |
| file: String::new(), | |
| expr: ExprKind::Array(exprs), | |
| }); | |
| let map_or_block = just(Token::LBrace) | |
| .then( | |
| choice(( | |
| // Try map first: expr : expr | |
| expr.clone() | |
| .then_ignore(just(Token::Colon)) | |
| .then(expr.clone()) | |
| .separated_by(just(Token::Comma)) | |
| .at_least(1) | |
| .allow_trailing() | |
| .collect::<Vec<_>>() | |
| .map(ExprKind::Map), | |
| // Otherwise it's a block | |
| expr.clone() | |
| .then_ignore(just(Token::Semicolon).or_not()) | |
| .repeated() | |
| .at_least(1) | |
| .collect::<Vec<_>>() | |
| .map(ExprKind::Block), | |
| )) | |
| ) | |
| .then_ignore(just(Token::RBrace)) | |
| .map_with(|(_, kind), e| Expr { | |
| span: to_range(e.span()), | |
| file: String::new(), | |
| expr: kind, | |
| }) | |
| .recover_with(via_parser(nested_delimiters( | |
| Token::LBrace, Token::RBrace, | |
| [(Token::LParen, Token::RParen), (Token::LBracket, Token::RBracket)], | |
| |_| Expr { | |
| span: 0..0, | |
| file: String::new(), | |
| expr: ExprKind::Error, | |
| }, | |
| ))); | |
| let tuple_or_paren = just(Token::LParen) | |
| .ignore_then( | |
| expr.clone() | |
| .separated_by(just(Token::Comma)) | |
| .at_least(1) | |
| .allow_trailing() | |
| .collect::<Vec<_>>() | |
| ) | |
| .then_ignore(just(Token::RParen)) | |
| .map_with(|exprs, e| { | |
| if exprs.len() == 1 { | |
| exprs.into_iter().next().unwrap() | |
| } else { | |
| Expr { | |
| span: to_range(e.span()), | |
| file: String::new(), | |
| expr: ExprKind::Tuple(exprs), | |
| } | |
| } | |
| }); | |
| // Try delimiter-based parsers first, then atom | |
| let base = choice(( | |
| array_or_expr, | |
| map_or_block, | |
| tuple_or_paren, | |
| atom, | |
| )).boxed(); | |
| // Postfix operations | |
| let call_args = expr.clone() | |
| .separated_by(just(Token::Comma)) | |
| .allow_trailing() | |
| .collect::<Vec<_>>() | |
| .delimited_by(just(Token::LParen), just(Token::RParen)); | |
| let index = expr.clone() | |
| .delimited_by(just(Token::LBracket), just(Token::RBracket)); | |
| let field = just(Token::Dot).ignore_then(ident()); | |
| let optional_chain = just(Token::OptionalChain).ignore_then(ident()); | |
| let cast = just(Token::KeywordAs).ignore_then(type_annot()); | |
| let postfix_op = choice(( | |
| call_args.map(PostOp::Call), | |
| index.map(|idx| PostOp::Index(Box::new(idx))), | |
| optional_chain.map(PostOp::OptionalChain), | |
| field.map(PostOp::Field), | |
| cast.map(PostOp::Cast), | |
| )); | |
| let postfix = base.foldl(postfix_op.repeated(), |lhs, op| { | |
| let span = lhs.span.clone(); | |
| Expr { | |
| span, | |
| file: String::new(), | |
| expr: match op { | |
| PostOp::Call(args) => ExprKind::Call(Box::new(lhs), args), | |
| PostOp::Index(idx) => ExprKind::Index(Box::new(lhs), idx), | |
| PostOp::Field(f) => ExprKind::FieldAccess(Box::new(lhs), f), | |
| PostOp::OptionalChain(f) => ExprKind::OptionalChain(Box::new(lhs), f), | |
| PostOp::Cast(ty) => ExprKind::Cast { expr: Box::new(lhs), target_type: ty }, | |
| }, | |
| } | |
| }); | |
| // Binary/unary operators | |
| postfix.pratt(( | |
| infix(right(1), just(Token::Pipe), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::Pipe, Box::new(r)) } | |
| }), | |
| infix(right(2), just(Token::Assign), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::Assign { | |
| l_val: Box::new(l), | |
| r_val: Box::new(r), | |
| op: AssignOp::Assign, | |
| }} | |
| }), | |
| infix(right(2), just(Token::AddAssign), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::Assign { | |
| l_val: Box::new(l), | |
| r_val: Box::new(r), | |
| op: AssignOp::AddAssign, | |
| }} | |
| }), | |
| infix(right(2), just(Token::SubAssign), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::Assign { | |
| l_val: Box::new(l), | |
| r_val: Box::new(r), | |
| op: AssignOp::SubAssign, | |
| }} | |
| }), | |
| infix(right(2), just(Token::MulAssign), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::Assign { | |
| l_val: Box::new(l), | |
| r_val: Box::new(r), | |
| op: AssignOp::MulAssign, | |
| }} | |
| }), | |
| infix(right(2), just(Token::DivAssign), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::Assign { | |
| l_val: Box::new(l), | |
| r_val: Box::new(r), | |
| op: AssignOp::DivAssign, | |
| }} | |
| }), | |
| infix(right(2), just(Token::ModAssign), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::Assign { | |
| l_val: Box::new(l), | |
| r_val: Box::new(r), | |
| op: AssignOp::ModAssign, | |
| }} | |
| }), | |
| infix(left(3), just(Token::Or), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::Or, Box::new(r)) } | |
| }), | |
| infix(left(3), just(Token::Nor), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::Nor, Box::new(r)) } | |
| }), | |
| infix(left(5), just(Token::Xor), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::Xor, Box::new(r)) } | |
| }), | |
| infix(left(7), just(Token::And), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::And, Box::new(r)) } | |
| }), | |
| infix(left(9), just(Token::Eq), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::Eq, Box::new(r)) } | |
| }), | |
| infix(left(9), just(Token::NotEq), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::NotEq, Box::new(r)) } | |
| }), | |
| infix(left(9), just(Token::Less), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::Less, Box::new(r)) } | |
| }), | |
| infix(left(9), just(Token::Greater), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::Greater, Box::new(r)) } | |
| }), | |
| infix(left(9), just(Token::LessEq), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::LessEq, Box::new(r)) } | |
| }), | |
| infix(left(9), just(Token::GreaterEq), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::GreaterEq, Box::new(r)) } | |
| }), | |
| infix(left(11), just(Token::Plus), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::Add, Box::new(r)) } | |
| }), | |
| infix(left(11), just(Token::Minus), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::Sub, Box::new(r)) } | |
| }), | |
| infix(left(13), just(Token::Mul), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::Mul, Box::new(r)) } | |
| }), | |
| infix(left(13), just(Token::Div), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::Div, Box::new(r)) } | |
| }), | |
| infix(left(13), just(Token::Mod), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::Mod, Box::new(r)) } | |
| }), | |
| infix(right(15), just(Token::Power), |l: Expr, _op, r: Expr, _extra| { | |
| let span = l.span.start..r.span.end; | |
| Expr { span, file: String::new(), expr: ExprKind::BinOp(Box::new(l), BinOp::Pow, Box::new(r)) } | |
| }), | |
| prefix(17, just(Token::Minus), |_op, e: Expr, _extra| { | |
| Expr { span: e.span.clone(), file: String::new(), expr: ExprKind::UnOp(UnOp::Minus, Box::new(e)) } | |
| }), | |
| prefix(17, just(Token::Not), |_op, e: Expr, _extra| { | |
| Expr { span: e.span.clone(), file: String::new(), expr: ExprKind::UnOp(UnOp::Not, Box::new(e)) } | |
| }), | |
| prefix(17, just(Token::Plus), |_op, e: Expr, _extra| { | |
| Expr { span: e.span.clone(), file: String::new(), expr: ExprKind::UnOp(UnOp::Plus, Box::new(e)) } | |
| }), | |
| )) | |
| .boxed() | |
| }) | |
| } | |
| fn type_annot<'src>() -> impl Parser<'src, &'src [Token], TypeAnnot, extra::Err<Rich<'src, Token, Span>>> + Clone { | |
| recursive(|ty| { | |
| // Primitive types (lowercase) | |
| let primitive = choice(( | |
| just(Token::KeywordInt).to(TypeAnnotKind::Int), | |
| just(Token::KeywordFloat).to(TypeAnnotKind::Float), | |
| just(Token::KeywordBool).to(TypeAnnotKind::Bool), | |
| just(Token::KeywordString).to(TypeAnnotKind::String), | |
| )); | |
| // Unit type: () | |
| let unit = just(Token::LParen) | |
| .then(just(Token::RParen)) | |
| .to(TypeAnnotKind::Unit); | |
| // Never type: ! | |
| let never = just(Token::Bang).to(TypeAnnotKind::Never); | |
| // Named type | |
| let named = ident().map(TypeAnnotKind::Named); | |
| // Generic type: Name<T, U> | |
| let generic = ident() | |
| .then( | |
| ty.clone() | |
| .separated_by(just(Token::Comma)) | |
| .at_least(1) | |
| .collect::<Vec<_>>() | |
| .delimited_by(just(Token::Less), just(Token::Greater)) | |
| ) | |
| .map(|(name, args)| TypeAnnotKind::Generic { | |
| name, | |
| args, | |
| kind: None, | |
| }); | |
| // Tuple type: (T, U, V) | |
| let tuple = ty.clone() | |
| .separated_by(just(Token::Comma)) | |
| .at_least(2) | |
| .collect::<Vec<_>>() | |
| .delimited_by(just(Token::LParen), just(Token::RParen)) | |
| .map(TypeAnnotKind::Tuple); | |
| // Function type: fn(T, U) -> V effects E | |
| let function = just(Token::KeywordFn) | |
| .ignore_then( | |
| ty.clone() | |
| .separated_by(just(Token::Comma)) | |
| .allow_trailing() | |
| .collect::<Vec<_>>() | |
| .delimited_by(just(Token::LParen), just(Token::RParen)) | |
| ) | |
| .then_ignore(just(Token::Arrow)) | |
| .then(ty.clone()) | |
| .then( | |
| select! { Token::Variable(s) if s == "effects" => () } | |
| .ignore_then( | |
| ident() | |
| .separated_by(just(Token::Comma)) | |
| .collect::<Vec<_>>() | |
| ) | |
| .map(EffectAnnot::closed) | |
| .or_not() | |
| ) | |
| .map(|((params, ret), effects)| TypeAnnotKind::Function { | |
| params, | |
| return_type: Box::new(ret), | |
| effects: effects.unwrap_or_else(EffectAnnot::pure), | |
| }); | |
| // Union type: T | U | V | |
| let union = ty.clone() | |
| .separated_by(just(Token::Union)) | |
| .at_least(2) | |
| .collect::<Vec<_>>() | |
| .map(TypeAnnotKind::Union); | |
| choice((function, union, tuple, generic, primitive, unit, never, named)) | |
| .map_with(|kind, e| TypeAnnot { | |
| span: to_range(e.span()), | |
| file: String::new(), | |
| expr: kind, | |
| }) | |
| .labelled("type") | |
| }) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment