refactor: split project into multiple crates

This commit is contained in:
Romain Paquet 2025-11-05 20:23:17 +01:00
parent 486af67fc2
commit 857f747524
27 changed files with 308 additions and 222 deletions

View file

@ -1,34 +1,13 @@
[package] [workspace]
name = "lilac" resolver = "3"
members = [
"lila",
"lila-ast",
"lila-checking",
"lila-cli",
"lila-jit",
"lila-parsing",
]
[workspace.package]
version = "0.0.1" version = "0.0.1"
edition = "2021"
[features]
default = ["pest"]
pest = ["dep:pest", "dep:pest_derive"]
tree-sitter = ["dep:tree-sitter", "dep:tree-sitter-lila"]
[dependencies]
clap = { version = "4.5.7", features = ["derive"] }
cranelift = "0.109.0"
cranelift-jit = "0.109.0"
cranelift-module = "0.109.0"
cranelift-native = "0.109.0"
lazy_static = "1.4.0"
pest = { version = "2.7.4", optional = true }
pest_derive = { version = "2.7.4", optional = true }
tree-sitter = { version = "0.22", optional = true }
ariadne = "0.4.1"
anyhow = "1.0.86"
[dependencies.tree-sitter-lila]
version = "0.0.1"
optional = true
git = "https://git.sr.ht/~rpqt/tree-sitter-lila"
branch = "main"
[dev-dependencies]
pretty_assertions = "1.4.0"
[build-dependencies]
cc = "*"

View file

@ -26,10 +26,15 @@
let let
craneLib = crane.mkLib pkgs; craneLib = crane.mkLib pkgs;
pestFilter = path: _type: (builtins.match ".*\.pest$" path) != null; sourceFilter =
sourceFilter = path: type: (craneLib.filterCargoSources path type) || (pestFilter path type); path: type:
builtins.any (suffix: lib.hasSuffix suffix path) [
".pest"
]
|| (craneLib.filterCargoSources path type);
lilac-crate = craneLib.buildPackage ({ lilac-crate = craneLib.buildPackage ({
pname = "lilac";
src = lib.cleanSourceWith { src = lib.cleanSourceWith {
src = ./.; src = ./.;
filter = sourceFilter; filter = sourceFilter;

10
lila-ast/Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "lila-ast"
version = "0.0.1"
edition = "2021"
[features]
ariadne = [ "dep:ariadne" ]
[dependencies]
ariadne = { version = "0.4.1", optional = true }

View file

@ -1,5 +1,5 @@
use crate::ast::*;
use crate::typing::Type; use crate::typing::Type;
use crate::*;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct SExpr { pub struct SExpr {

View file

@ -1,10 +1,10 @@
pub mod expr; pub mod expr;
pub mod typing;
pub use expr::{BinaryExpression, Expr, SExpr}; pub use expr::{BinaryExpression, Expr, SExpr};
use crate::typing::Type; use crate::typing::Type;
use ariadne;
use std::{fmt::Display, path::Path}; use std::{fmt::Display, path::Path};
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
@ -54,6 +54,7 @@ pub struct Span {
pub end: usize, pub end: usize,
} }
#[cfg(feature = "ariadne")]
impl ariadne::Span for Span { impl ariadne::Span for Span {
type SourceId = SourceId; type SourceId = SourceId;

View file

@ -0,0 +1,56 @@
use std::fmt::Debug;
use crate::{BinaryOperator, Identifier, ModulePath, Span, Type, UnaryOperator};
#[derive(Debug, PartialEq)]
pub struct TypeError {
pub file: Option<std::path::PathBuf>,
pub module: ModulePath,
pub function: Option<String>,
pub kind: TypeErrorKind,
}
#[derive(PartialEq, Debug)]
pub struct TypeAndSpan {
pub ty: Type,
pub span: Span,
}
#[derive(PartialEq, Debug)]
pub struct BinOpAndSpan {
pub op: BinaryOperator,
pub span: Span,
}
#[derive(Debug, PartialEq)]
pub enum TypeErrorKind {
InvalidBinaryOperator {
operator: BinOpAndSpan,
lhs: TypeAndSpan,
rhs: TypeAndSpan,
},
BlockTypeDoesNotMatchFunctionType {
block_type: Type,
},
ReturnTypeDoesNotMatchFunctionType {
return_expr: Option<TypeAndSpan>,
return_stmt: TypeAndSpan,
},
UnknownIdentifier {
identifier: String,
},
AssignmentMismatch {
lht: Type,
rht: Type,
},
AssignUndeclared,
VariableRedeclaration,
UnknownFunctionCalled(Identifier),
WrongFunctionArguments,
ConditionIsNotBool,
IfElseMismatch,
InvalidUnaryOperator {
operator: UnaryOperator,
inner: Type,
},
}

View file

@ -0,0 +1,73 @@
pub mod error;
use crate::{FunctionDefinition, Identifier};
use std::fmt::Display;
#[derive(Debug, PartialEq, Clone)]
pub enum Type {
/// Not a real type, used for parsing pass
Undefined,
Bool,
Int,
Float,
Unit,
Str,
Function {
params: Vec<Type>,
returns: Box<Type>,
},
Custom(Identifier),
}
impl Display for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Type::Undefined => f.write_str("UNDEFINED"),
Type::Bool => f.write_str("Bool"),
Type::Int => f.write_str("Int"),
Type::Float => f.write_str("Float"),
Type::Unit => f.write_str("Unit"),
Type::Str => f.write_str("Str"),
Type::Custom(identifier) => f.write_str(identifier),
Type::Function { params, returns } => {
f.write_str("Fn(")?;
for param in params {
f.write_fmt(format_args!("{param}, "))?;
}
f.write_str(") -> ")?;
f.write_fmt(format_args!("{returns}"))
}
}
}
}
impl From<&str> for Type {
fn from(value: &str) -> Self {
match value {
"int" => Type::Int,
"float" => Type::Float,
"bool" => Type::Bool,
_ => Type::Custom(Identifier::from(value)),
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct Signature(pub Vec<Type>, pub Type);
impl From<Signature> for Type {
fn from(val: Signature) -> Self {
Type::Function {
params: val.0,
returns: Box::new(val.1),
}
}
}
impl FunctionDefinition {
pub fn signature(&self) -> Signature {
let return_type = self.return_type.clone().unwrap_or(Type::Unit);
let params_types = self.parameters.iter().map(|p| p.typ.clone()).collect();
Signature(params_types, return_type)
}
}

11
lila-checking/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "lila-checking"
version = "0.0.1"
edition = "2021"
[dependencies]
lila-ast = { path = "../lila-ast" }
[dev-dependencies]
lila-parsing = { path = "../lila-parsing" }
pretty_assertions = "1.4.0"

View file

@ -1,87 +1,21 @@
mod error;
use crate::ast::*;
use crate::typing::error::{TypeAndSpan, TypeError, TypeErrorKind};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display;
use lila_ast::typing::error::{BinOpAndSpan, TypeAndSpan, TypeError, TypeErrorKind};
use lila_ast::typing::{Signature, Type};
use lila_ast::*;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
#[derive(Debug, PartialEq, Clone)]
pub enum Type {
/// Not a real type, used for parsing pass
Undefined,
Bool,
Int,
Float,
Unit,
Str,
Function {
params: Vec<Type>,
returns: Box<Type>,
},
Custom(Identifier),
}
impl Display for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Type::Undefined => f.write_str("UNDEFINED"),
Type::Bool => f.write_str("Bool"),
Type::Int => f.write_str("Int"),
Type::Float => f.write_str("Float"),
Type::Unit => f.write_str("Unit"),
Type::Str => f.write_str("Str"),
Type::Custom(identifier) => f.write_str(identifier),
Type::Function { params, returns } => {
f.write_str("Fn(")?;
for param in params {
f.write_fmt(format_args!("{}, ", param))?;
}
f.write_str(") -> ")?;
f.write_fmt(format_args!("{}", returns))
}
}
}
}
impl From<&str> for Type {
fn from(value: &str) -> Self {
match value {
"int" => Type::Int,
"float" => Type::Float,
"bool" => Type::Bool,
_ => Type::Custom(Identifier::from(value)),
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct Signature(Vec<Type>, Type);
impl From<Signature> for Type {
fn from(val: Signature) -> Self {
Type::Function {
params: val.0,
returns: Box::new(val.1),
}
}
}
impl FunctionDefinition {
fn signature(&self) -> Signature {
let return_type = self.return_type.clone().unwrap_or(Type::Unit);
let params_types = self.parameters.iter().map(|p| p.typ.clone()).collect();
Signature(params_types, return_type)
}
}
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct CheckedModule(pub Module); pub struct CheckedModule(pub Module);
impl Module { pub trait TypeCheckModule {
pub fn type_check(&mut self) -> Result<(), Vec<TypeError>> { fn type_check(&mut self) -> Result<(), Vec<TypeError>>;
}
impl TypeCheckModule for Module {
fn type_check(&mut self) -> Result<(), Vec<TypeError>> {
let mut ctx = TypingContext::new(self.path.clone()); let mut ctx = TypingContext::new(self.path.clone());
ctx.file.clone_from(&self.file); ctx.file.clone_from(&self.file);
@ -325,7 +259,7 @@ impl TypeCheck for Expr {
typ, typ,
op_span, op_span,
}) => { }) => {
let operator = error::BinOpAndSpan { let operator = BinOpAndSpan {
op: op.clone(), op: op.clone(),
span: *op_span, span: *op_span,
}; };

View file

@ -1,11 +1,7 @@
use crate::{ use crate::TypeCheckModule;
ast::ModulePath, use lila_ast::typing::{error::*, Type};
parsing::{DefaultParser, Parser}, use lila_ast::ModulePath;
typing::error::*, use lila_parsing::{DefaultParser, Parser};
typing::*,
};
#[cfg(test)]
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]

22
lila-cli/Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "lila-cli"
version = "0.0.1"
edition = "2021"
[dependencies]
anyhow = "1.0.86"
ariadne = "0.4.1"
clap = { version = "4.5.7", features = ["derive"] }
lila = { path = "../lila" }
lila-ast = { path = "../lila-ast", features = ["ariadne"] }
lila-checking = { path = "../lila-checking" }
lila-jit = { path = "../lila-jit" }
lila-parsing = { path = "../lila-parsing" }
[dev-dependencies]
pretty_assertions = "1.4.0"
[[bin]]
name = "lilac"
path = "src/main.rs"

View file

@ -1,18 +1,15 @@
pub mod ast;
pub mod jit;
pub mod parsing;
pub mod source;
pub mod typing;
use std::default::Default; use std::default::Default;
use std::io::Write as _; use std::io::Write as _;
use std::path::PathBuf; use std::path::PathBuf;
use clap::{Parser as ClapParser, Subcommand}; use clap::{Parser as ClapParser, Subcommand};
use crate::ast::Module; use lila::report::ToReport;
use crate::parsing::Parser; use lila::source::SourceCache;
use crate::source::SourceCache; use lila_ast::Module;
use lila_checking::TypeCheckModule;
use lila_jit as jit;
use lila_parsing::{DefaultParser as LilaParser, Parser as _};
/// Experimental compiler for lila /// Experimental compiler for lila
#[derive(ClapParser, Debug)] #[derive(ClapParser, Debug)]
@ -59,13 +56,13 @@ enum Commands {
} }
fn parse(files: &[String]) -> Vec<Module> { fn parse(files: &[String]) -> Vec<Module> {
let mut parser = parsing::DefaultParser::default(); let mut parser = LilaParser::default();
let paths = files.iter().map(std::path::Path::new); let paths = files.iter().map(std::path::Path::new);
paths paths
.enumerate() .enumerate()
.map(|(i, path)| match parser.parse_file(path, i as u32) { .map(|(i, path)| match parser.parse_file(path, i as u32) {
Ok(module) => module, Ok(module) => module,
Err(e) => panic!("Parsing error: {:#?}", e), Err(e) => panic!("Parsing error: {e:#?}"),
}) })
.collect() .collect()
} }

15
lila-jit/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "lila-jit"
version = "0.0.1"
edition = "2021"
[dependencies]
ariadne = "0.4.1" # TODO: use ariadne only in CLI
cranelift = "0.109.0"
cranelift-jit = "0.109.0"
cranelift-module = "0.109.0"
cranelift-native = "0.109.0"
lila = { path = "../lila" }
lila-ast = { path = "../lila-ast", features = ["ariadne"] } # TODO: don't include ariadne feature
lila-checking = { path = "../lila-checking" }
lila-parsing = { path = "../lila-parsing" }

View file

@ -1,16 +1,17 @@
use crate::{
ast::{
self, expr::BinaryExpression, BinaryOperator, Expr, FunctionDefinition, ModulePath,
ReturnStatement, SourceId, Statement, UnaryOperator,
},
parsing::{DefaultParser, Parser},
typing::Type,
SourceCache,
};
use ariadne::Cache as _; use ariadne::Cache as _;
use cranelift::{codegen::ir::UserFuncName, prelude::*}; use cranelift::{codegen::ir::UserFuncName, prelude::*};
use cranelift_jit::{JITBuilder, JITModule}; use cranelift_jit::{JITBuilder, JITModule};
use cranelift_module::{DataDescription, FuncId, FuncOrDataId, Linkage, Module}; use cranelift_module::{DataDescription, FuncId, FuncOrDataId, Linkage, Module};
use lila::report::ToReport as _;
use lila::source::SourceCache;
use lila_ast as ast;
use lila_ast::typing::{self, Type};
use lila_ast::{
expr::BinaryExpression, BinaryOperator, Expr, FunctionDefinition, ModulePath, ReturnStatement,
SourceId, Statement, UnaryOperator,
};
use lila_checking::TypeCheckModule as _;
use lila_parsing::{DefaultParser, Parser};
use std::collections::HashMap; use std::collections::HashMap;
/// The basic JIT class. /// The basic JIT class.
@ -44,7 +45,7 @@ impl Default for JIT {
flag_builder.set("is_pic", "false").unwrap(); flag_builder.set("is_pic", "false").unwrap();
let isa_builder = cranelift_native::builder().unwrap_or_else(|msg| { let isa_builder = cranelift_native::builder().unwrap_or_else(|msg| {
panic!("host machine is not supported: {}", msg); panic!("host machine is not supported: {msg}");
}); });
let isa = isa_builder let isa = isa_builder
@ -137,12 +138,12 @@ impl JIT {
for param in &func.parameters { for param in &func.parameters {
assert_ne!(param.typ, Type::Unit); assert_ne!(param.typ, Type::Unit);
sig.params.append(&mut Vec::from(&param.typ)); sig.params.append(&mut to_abi_params(&param.typ));
} }
if let Some(return_type) = &func.return_type { if let Some(return_type) = &func.return_type {
if *return_type != Type::Unit { if *return_type != Type::Unit {
sig.returns = return_type.into(); sig.returns = to_abi_params(return_type);
} }
}; };
@ -235,8 +236,7 @@ impl JIT {
} }
} }
impl From<&Type> for Vec<AbiParam> { fn to_abi_params(value: &Type) -> Vec<AbiParam> {
fn from(value: &Type) -> Self {
match value { match value {
Type::Bool => vec![AbiParam::new(types::I8)], Type::Bool => vec![AbiParam::new(types::I8)],
Type::Int => vec![AbiParam::new(types::I32)], Type::Int => vec![AbiParam::new(types::I32)],
@ -244,7 +244,6 @@ impl From<&Type> for Vec<AbiParam> {
_ => unimplemented!(), _ => unimplemented!(),
} }
} }
}
/// A collection of state used for translating from AST nodes /// A collection of state used for translating from AST nodes
/// into Cranelift IR. /// into Cranelift IR.

29
lila-parsing/Cargo.toml Normal file
View file

@ -0,0 +1,29 @@
[package]
name = "lila-parsing"
version = "0.0.1"
edition = "2021"
[features]
default = ["pest"]
pest = ["dep:pest", "dep:pest_derive", "dep:lazy_static"]
tree-sitter = ["dep:tree-sitter", "dep:tree-sitter-lila"]
[dependencies]
anyhow = "1.0.86"
lazy_static = { version = "1.4.0", optional = true }
lila-ast = { path = "../lila-ast" }
pest = { version = "2.7.4", optional = true }
pest_derive = { version = "2.7.4", optional = true }
tree-sitter = { version = "0.22", optional = true }
[dependencies.tree-sitter-lila]
version = "0.0.1"
optional = true
git = "https://git.sr.ht/~rpqt/tree-sitter-lila"
branch = "main"
[build-dependencies]
cc = "*"
[dev-dependencies]
pretty_assertions = "1.4.0"

View file

@ -1,14 +1,12 @@
use expr::BinaryExpression; use lila_ast::typing::Type;
use pest::iterators::Pair; use pest::iterators::Pair;
use pest::pratt_parser::PrattParser; use pest::pratt_parser::PrattParser;
use pest::Parser as PestParser; use pest::Parser as PestParser;
use ReturnStatement;
use crate::ast::*; use lila_ast::*;
use crate::typing::Type;
#[derive(pest_derive::Parser)] #[derive(pest_derive::Parser)]
#[grammar = "parsing/backend/pest/grammar.pest"] #[grammar = "src/backend/pest/grammar.pest"]
struct LilaParser; struct LilaParser;
use lazy_static; use lazy_static;
@ -34,7 +32,7 @@ pub struct Parser {
source: SourceId, source: SourceId,
} }
impl crate::parsing::Parser for Parser { impl crate::Parser for Parser {
fn parse_as_module( fn parse_as_module(
&mut self, &mut self,
source: &str, source: &str,

View file

@ -1,7 +1,7 @@
mod backend; mod backend;
mod tests; mod tests;
use crate::ast::{Module, ModulePath, SourceId}; use lila_ast::{Module, ModulePath, SourceId};
pub trait Parser: Default { pub trait Parser: Default {
fn parse_file(&mut self, path: &std::path::Path, id: SourceId) -> anyhow::Result<Module> { fn parse_file(&mut self, path: &std::path::Path, id: SourceId) -> anyhow::Result<Module> {

View file

@ -3,9 +3,9 @@ use pretty_assertions::assert_eq;
#[test] #[test]
fn test_addition_function() { fn test_addition_function() {
use crate::ast::*; use crate::*;
use crate::parsing::*; use lila_ast::typing::*;
use crate::typing::*; use lila_ast::*;
let source = "fn add(a: int, b: int) int { a + b }"; let source = "fn add(a: int, b: int) int { a + b }";
let path = ModulePath::from("test"); let path = ModulePath::from("test");

8
lila/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "lila"
version = "0.0.1"
edition = "2021"
[dependencies]
ariadne = "0.4.1" # TODO: only CLI should use ariadne
lila-ast = { path = "../lila-ast" }

2
lila/src/lib.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod report;
pub mod source;

View file

@ -1,64 +1,14 @@
use ariadne::{ColorGenerator, Fmt, Label, Report, ReportKind, Span as _}; use ariadne::{ColorGenerator, Fmt, Label, Report, ReportKind, Span as _};
use std::fmt::Debug; use lila_ast::typing::error::{TypeError, TypeErrorKind};
use lila_ast::typing::Type;
use lila_ast::{Module, Span};
use super::{Span, UnaryOperator}; pub trait ToReport {
use crate::typing::{BinaryOperator, Identifier, ModulePath, Type}; fn to_report(&self, ast: &Module) -> Report<Span>;
#[derive(Debug, PartialEq)]
pub struct TypeError {
pub file: Option<std::path::PathBuf>,
pub module: ModulePath,
pub function: Option<String>,
pub kind: TypeErrorKind,
} }
#[derive(PartialEq, Debug)] impl ToReport for TypeError {
pub struct TypeAndSpan { fn to_report(&self, ast: &Module) -> Report<Span> {
pub ty: Type,
pub span: Span,
}
#[derive(PartialEq, Debug)]
pub struct BinOpAndSpan {
pub op: BinaryOperator,
pub span: Span,
}
#[derive(Debug, PartialEq)]
pub enum TypeErrorKind {
InvalidBinaryOperator {
operator: BinOpAndSpan,
lhs: TypeAndSpan,
rhs: TypeAndSpan,
},
BlockTypeDoesNotMatchFunctionType {
block_type: Type,
},
ReturnTypeDoesNotMatchFunctionType {
return_expr: Option<TypeAndSpan>,
return_stmt: TypeAndSpan,
},
UnknownIdentifier {
identifier: String,
},
AssignmentMismatch {
lht: Type,
rht: Type,
},
AssignUndeclared,
VariableRedeclaration,
UnknownFunctionCalled(Identifier),
WrongFunctionArguments,
ConditionIsNotBool,
IfElseMismatch,
InvalidUnaryOperator {
operator: UnaryOperator,
inner: Type,
},
}
impl TypeError {
pub fn to_report(&self, ast: &crate::ast::Module) -> Report<Span> {
let mut colors = ColorGenerator::new(); let mut colors = ColorGenerator::new();
let c0 = colors.next(); let c0 = colors.next();
let c1 = colors.next(); let c1 = colors.next();
@ -85,6 +35,7 @@ impl TypeError {
.with_color(c2) .with_color(c2)
.with_order(1), .with_order(1),
]) ])
// TODO: add hint for conversion
.finish() .finish()
} }
@ -129,7 +80,7 @@ impl TypeError {
if let Some(span) = function.return_type_span { if let Some(span) = function.return_type_span {
report.add_label( report.add_label(
Label::new(span) Label::new(span)
.with_message(format!("The signature shows {}", func_return_type_text)) .with_message(format!("The signature shows {func_return_type_text}"))
.with_color(signature_color), .with_color(signature_color),
); );
} }

View file

@ -1,5 +1,5 @@
use crate::ast::SourceId;
use ariadne::FileCache; use ariadne::FileCache;
use lila_ast::SourceId;
pub struct SourceCache { pub struct SourceCache {
pub paths: Vec<std::path::PathBuf>, pub paths: Vec<std::path::PathBuf>,