Phase 2: parser and AST
Add the second pipeline stage: a hand-written recursive-descent parser with a
cascade expression layer, plus the AST it builds. Chosen over chumsky for precise
control of the specification's sync-point error recovery and plain-English errors;
the AST and later checkers are agnostic to this choice.
- legis-ast: full AST for declarations, statements, the precedence-encoded
expression grammar, match/when in statement and expression forms, attempt,
ranges, actions, tags, and conditional blocks. Spans on items, statements,
expressions, and types.
- legis-parser: parses the whole grammar (minus the web route DSL, deferred to
the web phase), with error recovery to synchronization points and a 20-error
cap. Generic-call vs comparison is disambiguated by a restoring trial parse of
`<Type,...>(`. Non-associative equality is rejected.
- legis-cli: new `parse` command dumping the syntax tree.
- Reference-program-over-grammar decisions, all exercised by the calculator:
match arms may test a direct value (`is "+"`); arm bodies may be `{ }` blocks;
a `when` expression arm may hold a bare `give back`; a `contract` may describe
a constructor.
- legis-lexer: add the `while` keyword; make `value` a plain identifier (the
reference program uses it as a name), with `has value` matched contextually.
27 tests pass (6 diag + 12 lexer + 9 parser); fmt and clippy clean. The reference
calculator parses to a correct AST with no problems.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -128,6 +128,13 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "legis-ast"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"legis-diag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "legis-cli"
|
||||
version = "0.1.0"
|
||||
@@ -135,6 +142,7 @@ dependencies = [
|
||||
"clap",
|
||||
"legis-diag",
|
||||
"legis-lexer",
|
||||
"legis-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -149,6 +157,15 @@ dependencies = [
|
||||
"logos",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "legis-parser"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"legis-ast",
|
||||
"legis-diag",
|
||||
"legis-lexer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logos"
|
||||
version = "0.14.4"
|
||||
|
||||
@@ -3,6 +3,8 @@ resolver = "2"
|
||||
members = [
|
||||
"crates/legis-diag",
|
||||
"crates/legis-lexer",
|
||||
"crates/legis-ast",
|
||||
"crates/legis-parser",
|
||||
"crates/legis-cli",
|
||||
]
|
||||
|
||||
@@ -19,3 +21,5 @@ clap = { version = "4", features = ["derive"] }
|
||||
|
||||
legis-diag = { path = "crates/legis-diag" }
|
||||
legis-lexer = { path = "crates/legis-lexer" }
|
||||
legis-ast = { path = "crates/legis-ast" }
|
||||
legis-parser = { path = "crates/legis-parser" }
|
||||
|
||||
7
PLAN.md
7
PLAN.md
@@ -37,7 +37,7 @@ legis/
|
||||
├── crates/
|
||||
│ ├── legis-lexer/ # logos-based lexer, token stream, | disambiguation
|
||||
│ ├── legis-ast/ # AST node types, spans
|
||||
│ ├── legis-parser/ # chumsky parser, error recovery (max 20/file)
|
||||
│ ├── legis-parser/ # hand-written recursive-descent + Pratt, recovery (max 20/file)
|
||||
│ ├── legis-resolve/ # name resolution, forbidden/shorthand names, imports
|
||||
│ ├── legis-types/ # type checker, generics, behaviors, implicit success
|
||||
│ ├── legis-check/ # purity checker, ownership checker, secret checker
|
||||
@@ -66,11 +66,16 @@ legis/
|
||||
- **Done when:** lexer round-trips the calculator + a token-soup fixture to golden.
|
||||
|
||||
### Phase 2 — Parser + AST
|
||||
- Hand-written recursive-descent parser with a Pratt/cascade expression layer
|
||||
(chosen over `chumsky` for precise control of the spec's sync-point recovery
|
||||
and plain-English errors; AST + later checkers are agnostic to this choice).
|
||||
- Full grammar: declarations (object, behavior, choices, schema, type, function,
|
||||
pure function, method, test, benchmark), statements, the precedence-encoded
|
||||
expression grammar, `match`/`when` statement *and* expression forms, `attempt`,
|
||||
ranges, actions/short-actions, tags, conditional blocks.
|
||||
- Error recovery to synchronization points, 20-error cap.
|
||||
- Deferred to the web phase: the route DSL (`application.get(...){}`, `group`,
|
||||
route patterns) and the `rate_limit(100 per minute)` protection argument form.
|
||||
- **Done when:** calculator parses to AST; malformed fixtures produce recovered,
|
||||
capped, plain-English parse errors.
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ running the reference calculator program (`examples/calculator.lgi`).
|
||||
|-------|------|-------|
|
||||
| 0 | Workspace + plain-English diagnostics guard | done |
|
||||
| 1 | Lexer | done |
|
||||
| 2 | Parser + AST | next |
|
||||
| 3 | Name resolution | planned |
|
||||
| 2 | Parser + AST | done |
|
||||
| 3 | Name resolution | next |
|
||||
| 4 | Type checker | planned |
|
||||
| 5 | Purity / ownership / secret checkers | planned |
|
||||
| 6 | LLVM codegen + minimal standard library (MVP) | planned |
|
||||
@@ -32,6 +32,8 @@ running the reference calculator program (`examples/calculator.lgi`).
|
||||
crates/
|
||||
legis-diag shared diagnostics + the banned-jargon guard
|
||||
legis-lexer logos-based lexer
|
||||
legis-ast abstract syntax tree types
|
||||
legis-parser hand-written recursive-descent parser
|
||||
legis-cli the `legis` binary
|
||||
examples/
|
||||
calculator.lgi the specification's reference program
|
||||
@@ -42,6 +44,7 @@ examples/
|
||||
```
|
||||
cargo test # run the suite
|
||||
cargo run -p legis-cli -- tokens FILE # dump a file's token stream
|
||||
cargo run -p legis-cli -- parse FILE # dump a file's syntax tree
|
||||
```
|
||||
|
||||
## Toolchain
|
||||
|
||||
9
crates/legis-ast/Cargo.toml
Normal file
9
crates/legis-ast/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "legis-ast"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
legis-diag.workspace = true
|
||||
674
crates/legis-ast/src/lib.rs
Normal file
674
crates/legis-ast/src/lib.rs
Normal file
@@ -0,0 +1,674 @@
|
||||
//! The Legis abstract syntax tree.
|
||||
//!
|
||||
//! These types mirror the specification's grammar. Spans are carried on the nodes
|
||||
//! a later pass needs to point at — items, statements, expressions, and types —
|
||||
//! so diagnostics can highlight the exact source. Smaller helper nodes borrow the
|
||||
//! span of whatever contains them.
|
||||
//!
|
||||
//! A few shapes follow the *reference program* rather than the formal grammar
|
||||
//! where the two disagree (the grammar is known to be incomplete in places):
|
||||
//! `match` arms may match a direct value such as `is "+"`, and a `contract` may
|
||||
//! describe a constructor with `contract build name(...)`.
|
||||
|
||||
use legis_diag::Span;
|
||||
|
||||
/// A whole source file.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Program {
|
||||
pub items: Vec<Item>,
|
||||
}
|
||||
|
||||
/// A lowercase name (variable, field, function, parameter, ...).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Ident {
|
||||
pub name: String,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
/// An uppercase name (a type, behavior, choices, or variant).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TypeName {
|
||||
pub name: String,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A compiler directive written with `#` before a declaration.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Tag {
|
||||
Stub,
|
||||
RepeatsSafely,
|
||||
NotSecret,
|
||||
External(Ident),
|
||||
Unsafe,
|
||||
AllowWrap,
|
||||
AllowSeed,
|
||||
Deprecated(String),
|
||||
Experimental,
|
||||
ExpandInline,
|
||||
NoExpand,
|
||||
Cold,
|
||||
KeepName,
|
||||
Allow(Ident),
|
||||
FileAllow(Ident),
|
||||
NoLicense,
|
||||
Test,
|
||||
TestSetup,
|
||||
TestTeardown,
|
||||
Benchmark,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A type written in the source.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TypeExpr {
|
||||
pub kind: TypeKind,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum TypeKind {
|
||||
/// A primitive or plain named type, for example `Text` or `Vehicle`.
|
||||
Named(String),
|
||||
/// An angle-bracket type, for example `List<Whole>` or `Result<Whole, Text>`.
|
||||
Apply { name: String, args: Vec<TypeExpr> },
|
||||
/// A `BaseType of ItemType` type, for example `Stack of Whole`.
|
||||
GenericOf { base: String, item: String },
|
||||
/// A named tuple type, for example `(x: Decimal, y: Decimal)`.
|
||||
Tuple(Vec<(Ident, TypeExpr)>),
|
||||
/// A function type, for example `function(Whole) returns Bool`.
|
||||
Function {
|
||||
params: Vec<TypeExpr>,
|
||||
returns: Box<TypeExpr>,
|
||||
},
|
||||
/// The `Self` type, valid only inside a behavior.
|
||||
SelfType,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Top-level items
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One top-level declaration, with any doc comment and tags attached.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Item {
|
||||
pub doc: Option<String>,
|
||||
pub tags: Vec<Tag>,
|
||||
pub kind: ItemKind,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ItemKind {
|
||||
Import(Import),
|
||||
TypeAlias(TypeAlias),
|
||||
Object(ObjectDecl),
|
||||
Behavior(BehaviorDecl),
|
||||
Choices(ChoicesDecl),
|
||||
Schema(SchemaDecl),
|
||||
Function(FunctionDecl),
|
||||
Test(TestDecl),
|
||||
Benchmark(BenchmarkDecl),
|
||||
Conditional(ConditionalBlock),
|
||||
}
|
||||
|
||||
/// `use library ...`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Import {
|
||||
pub kind: ImportKind,
|
||||
pub alias: Option<Ident>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ImportKind {
|
||||
/// A standard or project library path such as `std.console`.
|
||||
Standard(Vec<Ident>),
|
||||
/// External code from outside Legis.
|
||||
External {
|
||||
language: Option<ExternalLang>,
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ExternalLang {
|
||||
Rust,
|
||||
C,
|
||||
Csharp,
|
||||
}
|
||||
|
||||
/// `type Name = ...;`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TypeAlias {
|
||||
pub name: TypeName,
|
||||
pub target: TypeExpr,
|
||||
}
|
||||
|
||||
/// `object Name follows ... { ... }`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ObjectDecl {
|
||||
pub name: TypeName,
|
||||
/// The item type for a generic object, as in `object Stack of ItemType`.
|
||||
pub generic_item: Option<TypeName>,
|
||||
pub follows: Vec<TypeName>,
|
||||
pub members: Vec<ObjectMember>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ObjectMember {
|
||||
Field(FieldDecl),
|
||||
Constructor(ConstructorDecl),
|
||||
Method(MethodDecl),
|
||||
}
|
||||
|
||||
/// A field on an object.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FieldDecl {
|
||||
pub doc: Option<String>,
|
||||
pub name: Ident,
|
||||
pub ty: TypeExpr,
|
||||
pub default: Option<Expr>,
|
||||
}
|
||||
|
||||
/// A `build` constructor.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ConstructorDecl {
|
||||
pub doc: Option<String>,
|
||||
pub tags: Vec<Tag>,
|
||||
/// The descriptive name of a named constructor, absent for the default one.
|
||||
pub name: Option<Ident>,
|
||||
pub params: Vec<Param>,
|
||||
pub returns: TypeExpr,
|
||||
pub body: Block,
|
||||
}
|
||||
|
||||
/// How a method or constructor takes `self`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SelfParam {
|
||||
/// `self` — takes the value.
|
||||
Value,
|
||||
/// `peek self` — reads without taking.
|
||||
Peek,
|
||||
/// `peek vary self` — reads and changes without taking.
|
||||
PeekVary,
|
||||
}
|
||||
|
||||
/// A method on an object or choices type.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MethodDecl {
|
||||
pub doc: Option<String>,
|
||||
pub tags: Vec<Tag>,
|
||||
pub name: Ident,
|
||||
pub self_param: Option<SelfParam>,
|
||||
pub params: Vec<Param>,
|
||||
pub returns: Option<TypeExpr>,
|
||||
pub runs_in_background: bool,
|
||||
pub protections: Vec<Protection>,
|
||||
pub body: Block,
|
||||
}
|
||||
|
||||
/// `behavior Name { ... }`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BehaviorDecl {
|
||||
pub name: TypeName,
|
||||
pub members: Vec<BehaviorMember>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum BehaviorMember {
|
||||
Contract(ContractDecl),
|
||||
Default(DefaultMethod),
|
||||
}
|
||||
|
||||
/// A method an object must provide.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ContractDecl {
|
||||
pub doc: Option<String>,
|
||||
/// True for a `contract build name(...)` constructor contract.
|
||||
pub is_constructor: bool,
|
||||
pub name: Ident,
|
||||
pub self_param: Option<SelfParam>,
|
||||
pub params: Vec<Param>,
|
||||
pub returns: Option<TypeExpr>,
|
||||
}
|
||||
|
||||
/// A method provided automatically by a behavior.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct DefaultMethod {
|
||||
pub doc: Option<String>,
|
||||
pub name: Ident,
|
||||
pub self_param: Option<SelfParam>,
|
||||
pub params: Vec<Param>,
|
||||
pub returns: Option<TypeExpr>,
|
||||
pub body: Block,
|
||||
}
|
||||
|
||||
/// `choices Name follows ... { ... }`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ChoicesDecl {
|
||||
pub name: TypeName,
|
||||
pub follows: Vec<TypeName>,
|
||||
pub members: Vec<ChoicesMember>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ChoicesMember {
|
||||
Variant(VariantDecl),
|
||||
Method(MethodDecl),
|
||||
}
|
||||
|
||||
/// One choice within a `choices` type.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct VariantDecl {
|
||||
pub doc: Option<String>,
|
||||
pub name: TypeName,
|
||||
pub fields: Vec<VariantField>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct VariantField {
|
||||
pub name: Ident,
|
||||
pub ty: TypeExpr,
|
||||
}
|
||||
|
||||
/// `schema Name { ... }`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SchemaDecl {
|
||||
pub name: TypeName,
|
||||
pub fields: Vec<SchemaField>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SchemaField {
|
||||
pub doc: Option<String>,
|
||||
pub name: Ident,
|
||||
pub ty: TypeExpr,
|
||||
pub annotations: Vec<SchemaAnnotation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SchemaAnnotation {
|
||||
PrimaryKey,
|
||||
AutoIncrement,
|
||||
Required,
|
||||
Unique,
|
||||
Default(Expr),
|
||||
ForeignKey { type_name: TypeName, field: Ident },
|
||||
}
|
||||
|
||||
/// A standalone function or pure function.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FunctionDecl {
|
||||
pub is_pure: bool,
|
||||
pub name: Ident,
|
||||
pub params: Vec<Param>,
|
||||
pub returns: Option<TypeExpr>,
|
||||
pub runs_in_background: bool,
|
||||
pub protections: Vec<Protection>,
|
||||
pub body: Block,
|
||||
}
|
||||
|
||||
/// A function parameter.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Param {
|
||||
pub kind: ParamKind,
|
||||
pub name: Ident,
|
||||
pub ty: TypeExpr,
|
||||
pub default: Option<Expr>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ParamKind {
|
||||
/// Takes the value.
|
||||
Value,
|
||||
/// `peek` — reads without taking.
|
||||
Peek,
|
||||
/// `peek vary` — reads and changes without taking.
|
||||
PeekVary,
|
||||
}
|
||||
|
||||
/// A protection applied with `protect with`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Protection {
|
||||
pub name: Ident,
|
||||
pub args: Vec<Arg>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TestDecl {
|
||||
pub name: Ident,
|
||||
pub body: Block,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BenchmarkDecl {
|
||||
pub name: Ident,
|
||||
pub body: Block,
|
||||
}
|
||||
|
||||
/// `#when condition { ... }` blocks.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ConditionalBlock {
|
||||
pub branches: Vec<(Condition, Vec<Item>)>,
|
||||
pub otherwise: Option<Vec<Item>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Condition {
|
||||
Platform(String),
|
||||
Build(String),
|
||||
Arch(String),
|
||||
And(Box<Condition>, Box<Condition>),
|
||||
Or(Box<Condition>, Box<Condition>),
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Statements
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A `{ ... }` block of statements.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Block {
|
||||
pub statements: Vec<Stmt>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Stmt {
|
||||
pub kind: StmtKind,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum StmtKind {
|
||||
/// `let name: Type = value;` (`is_vary` true for `vary`).
|
||||
Let {
|
||||
is_vary: bool,
|
||||
name: Ident,
|
||||
ty: Option<TypeExpr>,
|
||||
value: Expr,
|
||||
},
|
||||
/// `place = value;`.
|
||||
Assign { target: Place, value: Expr },
|
||||
/// An expression used for its effect, ending in `;`.
|
||||
Expr(Expr),
|
||||
/// `give back value;` or `give back;`.
|
||||
GiveBack(Option<Expr>),
|
||||
/// `if ... { } else if ... { } else { }`.
|
||||
If {
|
||||
branches: Vec<(Expr, Block)>,
|
||||
otherwise: Option<Block>,
|
||||
},
|
||||
/// `repeat for name in iterable { }`.
|
||||
RepeatFor {
|
||||
pattern: ForPattern,
|
||||
iterable: Expr,
|
||||
body: Block,
|
||||
},
|
||||
/// `repeat while condition { }`.
|
||||
RepeatWhile { condition: Expr, body: Block },
|
||||
/// `match` used as a statement.
|
||||
Match(MatchExpr),
|
||||
/// `when` used as a statement.
|
||||
When(WhenExpr),
|
||||
/// `attempt { } if fails with ... { } always { }`.
|
||||
Attempt(AttemptStmt),
|
||||
}
|
||||
|
||||
/// The left-hand side of an assignment: a name and a chain of fields.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Place {
|
||||
pub root: Ident,
|
||||
pub fields: Vec<Ident>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ForPattern {
|
||||
Single(Ident),
|
||||
Tuple(Vec<Ident>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AttemptStmt {
|
||||
pub body: Block,
|
||||
/// `if fails with ErrorType with name { }` clauses.
|
||||
pub typed_catches: Vec<TypedCatch>,
|
||||
/// `if fails with name { }` catch-all clause.
|
||||
pub catch_all: Option<(Ident, Block)>,
|
||||
pub always: Option<Block>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TypedCatch {
|
||||
pub error_type: TypeExpr,
|
||||
pub binding: Ident,
|
||||
pub body: Block,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// match / when
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MatchExpr {
|
||||
pub subject: Box<Expr>,
|
||||
pub arms: Vec<MatchArm>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MatchArm {
|
||||
pub pattern: Pattern,
|
||||
pub body: ArmBody,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Pattern {
|
||||
/// `is Type.Variant with binding`.
|
||||
Variant {
|
||||
path: Vec<String>,
|
||||
binding: Option<Ident>,
|
||||
span: Span,
|
||||
},
|
||||
/// `is "some value"` — a direct value pattern.
|
||||
Value(Expr),
|
||||
/// `otherwise`.
|
||||
Otherwise,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct WhenExpr {
|
||||
pub subject: Box<Expr>,
|
||||
pub arms: Vec<WhenArm>,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct WhenArm {
|
||||
pub kind: WhenArmKind,
|
||||
pub body: ArmBody,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum WhenArmKind {
|
||||
/// `succeeds with binding`.
|
||||
Succeeds { binding: Option<Ident> },
|
||||
/// `fails with ErrorType with binding`.
|
||||
Fails {
|
||||
error_type: Option<TypeExpr>,
|
||||
binding: Option<Ident>,
|
||||
},
|
||||
/// `has value binding`.
|
||||
HasValue { binding: Ident },
|
||||
/// `is empty`.
|
||||
IsEmpty,
|
||||
}
|
||||
|
||||
/// The body of a `match` or `when` arm.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ArmBody {
|
||||
/// A list of statements (statement form, or an expression-form block).
|
||||
Block(Vec<Stmt>),
|
||||
/// A single value (expression form).
|
||||
Value(Box<Expr>),
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Expressions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Expr {
|
||||
pub kind: ExprKind,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ExprKind {
|
||||
/// A whole-number value, kept as written.
|
||||
Whole(String),
|
||||
/// A decimal value, kept as written.
|
||||
Decimal(String),
|
||||
/// A text value, kept raw (interpolation resolved later).
|
||||
Text {
|
||||
raw: String,
|
||||
multiline: bool,
|
||||
},
|
||||
Bool(bool),
|
||||
Empty,
|
||||
/// A reference to a name.
|
||||
Name(String),
|
||||
/// A reference to a type name, for example `Operation` in `Operation.Add`.
|
||||
TypeRef(String),
|
||||
/// A list value `[ ... ]`.
|
||||
List(Vec<Expr>),
|
||||
/// A map value `{ "key": value }`.
|
||||
Map(Vec<(Expr, Expr)>),
|
||||
/// A named tuple value `(x: 1, y: 2)`.
|
||||
Tuple(Vec<(Ident, Expr)>),
|
||||
/// An object value `Type { field: value }`.
|
||||
ObjectInit {
|
||||
type_name: TypeName,
|
||||
fields: Vec<(Ident, Expr)>,
|
||||
},
|
||||
/// A constructor call `Type.build name(...)`.
|
||||
Construct {
|
||||
type_name: TypeName,
|
||||
constructor: Option<Ident>,
|
||||
args: Vec<Arg>,
|
||||
},
|
||||
/// Field access `base.field`.
|
||||
Field {
|
||||
base: Box<Expr>,
|
||||
field: Ident,
|
||||
},
|
||||
/// Safe field access `base?.field`.
|
||||
SafeField {
|
||||
base: Box<Expr>,
|
||||
field: Ident,
|
||||
},
|
||||
/// A call `callee<TypeArgs>(args)`.
|
||||
Call {
|
||||
callee: Box<Expr>,
|
||||
type_args: Vec<TypeExpr>,
|
||||
args: Vec<Arg>,
|
||||
},
|
||||
/// `!value` or `-value`.
|
||||
Unary {
|
||||
op: UnaryOp,
|
||||
operand: Box<Expr>,
|
||||
},
|
||||
/// A binary operation.
|
||||
Binary {
|
||||
op: BinaryOp,
|
||||
left: Box<Expr>,
|
||||
right: Box<Expr>,
|
||||
},
|
||||
/// `left |> right`.
|
||||
Pipe {
|
||||
value: Box<Expr>,
|
||||
into: Box<Expr>,
|
||||
},
|
||||
/// `left then right`.
|
||||
Compose {
|
||||
first: Box<Expr>,
|
||||
second: Box<Expr>,
|
||||
},
|
||||
/// `start..end` or `start..=end`.
|
||||
Range {
|
||||
start: Box<Expr>,
|
||||
end: Box<Expr>,
|
||||
inclusive: bool,
|
||||
},
|
||||
/// `pass up value`.
|
||||
PassUp(Box<Expr>),
|
||||
/// `wait value`.
|
||||
Wait(Box<Expr>),
|
||||
/// `success(value)`.
|
||||
Success(Option<Box<Expr>>),
|
||||
/// `fail(value)`.
|
||||
Fail(Box<Expr>),
|
||||
/// `action(params) returns Type { body }`.
|
||||
Action {
|
||||
params: Vec<Param>,
|
||||
returns: Option<TypeExpr>,
|
||||
body: Block,
|
||||
runs_in_background: bool,
|
||||
},
|
||||
/// `|params| expression`.
|
||||
ShortAction {
|
||||
params: Vec<Ident>,
|
||||
body: Box<Expr>,
|
||||
},
|
||||
/// A partial-application placeholder `...`.
|
||||
Placeholder,
|
||||
/// `match` used as an expression.
|
||||
Match(MatchExpr),
|
||||
/// `when` used as an expression.
|
||||
When(WhenExpr),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UnaryOp {
|
||||
Not,
|
||||
Negate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BinaryOp {
|
||||
Add,
|
||||
Subtract,
|
||||
Multiply,
|
||||
Divide,
|
||||
Remainder,
|
||||
Equals,
|
||||
NotEquals,
|
||||
Less,
|
||||
Greater,
|
||||
LessEquals,
|
||||
GreaterEquals,
|
||||
And,
|
||||
Or,
|
||||
}
|
||||
|
||||
/// An argument in a call or constructor.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Arg {
|
||||
/// A plain positional argument.
|
||||
Positional(Expr),
|
||||
/// A named argument `name: value`.
|
||||
Named { name: Ident, value: Expr },
|
||||
/// `peek value`.
|
||||
Peek(Expr),
|
||||
/// `peek vary value`.
|
||||
PeekVary(Expr),
|
||||
/// `...` — a partial-application placeholder.
|
||||
Placeholder,
|
||||
}
|
||||
@@ -12,4 +12,5 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
legis-lexer.workspace = true
|
||||
legis-parser.workspace = true
|
||||
legis-diag.workspace = true
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! The `legis` command-line tool.
|
||||
//!
|
||||
//! Phase 1 ships one inspection command, `tokens`, which dumps the token stream
|
||||
//! for a source file. Build, run, test, and format commands arrive in later
|
||||
//! phases as the compiler grows.
|
||||
//! So far it ships two inspection commands: `tokens` (a look inside the lexer)
|
||||
//! and `parse` (the abstract syntax tree). Build, run, test, and format commands
|
||||
//! arrive in later phases as the compiler grows.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
@@ -29,12 +29,45 @@ enum Command {
|
||||
/// The .lgi file to read.
|
||||
file: PathBuf,
|
||||
},
|
||||
/// Show the abstract syntax tree for a source file (a look inside the parser).
|
||||
Parse {
|
||||
/// The .lgi file to read.
|
||||
file: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Command::Tokens { file } => run_tokens(&file),
|
||||
Command::Parse { file } => run_parse(&file),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_parse(file: &PathBuf) -> ExitCode {
|
||||
let source = match std::fs::read_to_string(file) {
|
||||
Ok(text) => text,
|
||||
Err(error) => {
|
||||
eprintln!("error: could not read {}: {error}", file.display());
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
let (program, diagnostics) = legis_parser::parse(&source);
|
||||
|
||||
if diagnostics.is_empty() {
|
||||
println!("{program:#?}");
|
||||
println!(
|
||||
"\nparsed {} top-level item(s) with no problems.",
|
||||
program.items.len()
|
||||
);
|
||||
ExitCode::SUCCESS
|
||||
} else {
|
||||
for diagnostic in &diagnostics {
|
||||
print_diagnostic(diagnostic);
|
||||
}
|
||||
eprintln!("\nfound {} problem(s).", diagnostics.len());
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -166,6 +166,8 @@ pub enum Token {
|
||||
Otherwise,
|
||||
#[token("repeat")]
|
||||
Repeat,
|
||||
#[token("while")]
|
||||
While,
|
||||
#[token("for")]
|
||||
For,
|
||||
#[token("in")]
|
||||
@@ -182,8 +184,10 @@ pub enum Token {
|
||||
Always,
|
||||
#[token("has")]
|
||||
Has,
|
||||
#[token("value")]
|
||||
Value,
|
||||
// Note: `value` is deliberately not a keyword. The word appears in the
|
||||
// `has value` outcome, but the reference program also uses `value` as an
|
||||
// ordinary name, so it is a plain identifier and `has value` is recognized
|
||||
// by the parser checking for the name "value".
|
||||
|
||||
// --- Functional / misc keywords ---
|
||||
#[token("then")]
|
||||
|
||||
11
crates/legis-parser/Cargo.toml
Normal file
11
crates/legis-parser/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "legis-parser"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
legis-diag.workspace = true
|
||||
legis-lexer.workspace = true
|
||||
legis-ast.workspace = true
|
||||
2361
crates/legis-parser/src/lib.rs
Normal file
2361
crates/legis-parser/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user