Skip to content

Commit 2c17bf7

Browse files
authored
feat: DSL CLI & Helpful Error Reporting (#35)
## Problem We need a CLI to be able to compile our DSL. More importantly, we need useful error messages when the programmer did a (syntax, semantic, or other) mistake. ## Example Best described with a picture. ``` fn (pred: Predicate) remap(map: {I64 : I64)}) = match predicate | ColumnRef(idx) => ColumnRef(map(idx)) \ _ => predicate -> apply_children(child => rewrite_column_refs(child, map)) [rule] fn (expr: Logical) join_commute = match expr \ Join(left, right, Inner, cond) -> let right_indices = 0.right.schema_len, left_indices = 0..left.schema_len, remapping = left_indices.map(i => (i, i + right_len)) ++ right_indices.map(i => (left_len + i, i)).to_map, in Project( Join(right, left, Inner, cond.remap(remapping)), right_indices.map(i => ColumnRef(i)).to_array ) ``` ![image](https://github.com/user-attachments/assets/47eb24e4-9c61-4164-aa28-fa3411c8c401) ## Summary of changes - Unified error management for the compilation process. - Integrate a CLI & playground in `optd-dsl` where people can experiment with some code, print the AST, etc.
1 parent fc0e77e commit 2c17bf7

File tree

27 files changed

+769
-226
lines changed

27 files changed

+769
-226
lines changed

Cargo.lock

Lines changed: 89 additions & 85 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

optd-dsl/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,13 @@ ordered-float = "5.0.0"
1111
futures = "0.3.31"
1212
tokio.workspace = true
1313
async-recursion.workspace = true
14+
enum_dispatch = "0.3.13"
15+
clap = { version = "4.5.31", features = ["derive"] }
16+
17+
[lib]
18+
name = "optd_dsl"
19+
path = "src/lib.rs"
20+
21+
[[bin]]
22+
name = "optd-cli"
23+
path = "src/cli/main.rs"

optd-dsl/src/analyzer/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
pub mod hir;
2+
pub mod semantic;
3+
pub mod r#type;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use ariadne::{Report, Source};
2+
3+
use crate::utils::{error::Diagnose, span::Span};
4+
5+
#[derive(Debug)]
6+
pub struct SemanticError {}
7+
8+
impl Diagnose for SemanticError {
9+
fn report(&self) -> Report<Span> {
10+
todo!()
11+
}
12+
13+
fn source(&self) -> (String, Source) {
14+
todo!()
15+
}
16+
}

optd-dsl/src/analyzer/semantic/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod error;

optd-dsl/src/analyzer/type/error.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use ariadne::{Report, Source};
2+
3+
use crate::utils::{error::Diagnose, span::Span};
4+
5+
#[derive(Debug)]
6+
pub struct TypeError {}
7+
8+
impl Diagnose for TypeError {
9+
fn report(&self) -> Report<Span> {
10+
todo!()
11+
}
12+
13+
fn source(&self) -> (String, Source) {
14+
todo!()
15+
}
16+
}

optd-dsl/src/analyzer/type/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod error;

optd-dsl/src/cli/basic.op

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
data LogicalProps(schema_len: I64)
2+
3+
data Scalar with
4+
| ColumnRef(idx: Int64)
5+
| Literal with
6+
| IntLiteral(value: Int64)
7+
| StringLiteral(value: String)
8+
| BoolLiteral(value: Bool)
9+
\ NullLiteral
10+
| Arithmetic with
11+
| Mult(left: Scalar, right: Scalar)
12+
| Add(left: Scalar, right: Scalar)
13+
| Sub(left: Scalar, right: Scalar)
14+
\ Div(left: Scalar, right: Scalar)
15+
| Predicate with
16+
| And(children: [Predicate])
17+
| Or(children: [Predicate])
18+
| Not(child: Predicate)
19+
| Equals(left: Scalar, right: Scalar)
20+
| NotEquals(left: Scalar, right: Scalar)
21+
| LessThan(left: Scalar, right: Scalar)
22+
| LessThanEqual(left: Scalar, right: Scalar)
23+
| GreaterThan(left: Scalar, right: Scalar)
24+
| GreaterThanEqual(left: Scalar, right: Scalar)
25+
| IsNull(expr: Scalar)
26+
\ IsNotNull(expr: Scalar)
27+
| Function with
28+
| Cast(expr: Scalar, target_type: String)
29+
| Substring(str: Scalar, start: Scalar, length: Scalar)
30+
\ Concat(args: [Scalar])
31+
\ AggregateExpr with
32+
| Sum(expr: Scalar)
33+
| Count(expr: Scalar)
34+
| Min(expr: Scalar)
35+
| Max(expr: Scalar)
36+
\ Avg(expr: Scalar)
37+
38+
data Logical with
39+
| Scan(table_name: String)
40+
| Filter(child: Logical, cond: Predicate)
41+
| Project(child: Logical, exprs: [Scalar])
42+
| Join(
43+
left: Logical,
44+
right: Logical,
45+
typ: JoinType,
46+
cond: Predicate
47+
)
48+
\ Aggregate(
49+
child: Logical,
50+
group_by: [Scalar],
51+
aggregates: [AggregateExpr]
52+
)
53+
54+
data Physical with
55+
| Scan(table_name: String)
56+
| Filter(child: Physical, cond: Predicate)
57+
| Project(child: Physical, exprs: [Scalar])
58+
| Join with
59+
| HashJoin(
60+
build_side: Physical,
61+
probe_side: Physical,
62+
typ: String,
63+
cond: Predicate
64+
)
65+
| MergeJoin(
66+
left: Physical,
67+
right: Physical,
68+
typ: String,
69+
cond: Predicate
70+
)
71+
\ NestedLoopJoin(
72+
outer: Physical,
73+
inner: Physical,
74+
typ: String,
75+
cond: Predicate
76+
)
77+
| Aggregate(
78+
child: Physical,
79+
group_by: [Scalar],
80+
aggregates: [AggregateExpr]
81+
)
82+
\ Sort(
83+
child: Physical,
84+
order_by: [(Scalar, SortOrder)]
85+
)
86+
87+
data JoinType with
88+
| Inner
89+
| Left
90+
| Right
91+
| Full
92+
\ Semi
93+
94+
[rust]
95+
fn (expr: Scalar) apply_children(f: Scalar => Scalar) = ()
96+
97+
fn (pred: Predicate) remap(map: {I64 : I64)}) =
98+
match predicate
99+
| ColumnRef(idx) => ColumnRef(map(idx))
100+
\ _ => predicate -> apply_children(child => rewrite_column_refs(child, map))
101+
102+
[rule]
103+
fn (expr: Logical) join_commute = match expr
104+
\ Join(left, right, Inner, cond) ->
105+
let
106+
right_indices = 0.right.schema_len,
107+
left_indices = 0..left.schema_len,
108+
remapping = left_indices.map(i => (i, i + right_len)) ++
109+
right_indices.map(i => (left_len + i, i)).to_map,
110+
in
111+
Project(
112+
Join(right, left, Inner, cond.remap(remapping)),
113+
right_indices.map(i => ColumnRef(i)).to_array
114+
)

optd-dsl/src/cli/main.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//! CLI tool for the Optimizer DSL
2+
//!
3+
//! This tool provides a command-line interface for the Optimizer DSL compiler.
4+
//!
5+
//! # Usage
6+
//!
7+
//! ```
8+
//! # Parse a DSL file (validate syntax):
9+
//! optd parse path/to/file.op
10+
//!
11+
//! # Parse a file and print the AST:
12+
//! optd parse path/to/file.op --print-ast
13+
//!
14+
//! # Get help:
15+
//! optd --help
16+
//! optd parse --help
17+
//! ```
18+
//!
19+
//! When developing, you can run through cargo:
20+
//!
21+
//! ```
22+
//! cargo run -- parse examples/example.dsl
23+
//! cargo run -- parse examples/example.dsl --print-ast
24+
//! ```
25+
26+
use clap::{Parser, Subcommand};
27+
use optd_dsl::compiler::compile::{parse, CompileOptions};
28+
use optd_dsl::utils::error::Diagnose;
29+
use std::fs;
30+
use std::path::PathBuf;
31+
32+
#[derive(Parser)]
33+
#[command(
34+
name = "optd",
35+
about = "Optimizer DSL compiler and toolchain",
36+
version,
37+
author
38+
)]
39+
struct Cli {
40+
#[command(subcommand)]
41+
command: Commands,
42+
}
43+
44+
#[derive(Subcommand)]
45+
enum Commands {
46+
/// Parse a DSL file and validate its syntax
47+
Parse {
48+
/// Input file to parse
49+
#[arg(value_name = "FILE")]
50+
input: PathBuf,
51+
52+
/// Print the AST in a readable format
53+
#[arg(long)]
54+
print_ast: bool,
55+
},
56+
}
57+
58+
fn main() -> Result<(), Box<dyn std::error::Error>> {
59+
let cli = Cli::parse();
60+
61+
match &cli.command {
62+
Commands::Parse { input, print_ast } => {
63+
println!("Parsing file: {}", input.display());
64+
65+
// Improve file reading error handling
66+
let source = match fs::read_to_string(input) {
67+
Ok(content) => content,
68+
Err(e) => {
69+
if e.kind() == std::io::ErrorKind::NotFound {
70+
eprintln!("❌ Error: File not found: {}", input.display());
71+
eprintln!(
72+
"Please check that the file exists and you have correct permissions."
73+
);
74+
} else {
75+
eprintln!("❌ Error reading file: {}", e);
76+
}
77+
std::process::exit(1);
78+
}
79+
};
80+
81+
let options = CompileOptions {
82+
source_path: input.to_string_lossy().to_string(),
83+
};
84+
85+
match parse(&source, &options) {
86+
Ok(ast) => {
87+
println!("✅ Parse successful!");
88+
if *print_ast {
89+
println!("\nAST Structure:");
90+
println!("{:#?}", ast);
91+
}
92+
}
93+
Err(errors) => {
94+
eprintln!("❌ Parse failed with {} errors:", errors.len());
95+
for error in errors {
96+
error.print(std::io::stderr())?;
97+
}
98+
std::process::exit(1);
99+
}
100+
}
101+
}
102+
}
103+
104+
Ok(())
105+
}

optd-dsl/src/compiler/compile.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use crate::lexer::lex::lex;
2+
use crate::parser::ast::Module;
3+
use crate::parser::module::parse_module;
4+
use crate::utils::error::CompileError;
5+
6+
/// Compilation options for the DSL
7+
pub struct CompileOptions {
8+
/// Path to the main module source file
9+
pub source_path: String,
10+
}
11+
12+
/// Parse DSL source code to AST
13+
///
14+
/// This function performs lexing and parsing stages of compilation,
15+
/// returning either the parsed AST Module or collected errors.
16+
///
17+
/// # Arguments
18+
/// * `source` - The source code to parse
19+
/// * `options` - Compilation options including source path
20+
///
21+
/// # Returns
22+
/// * `Result<Module, Vec<CompileError>>` - The parsed AST or errors
23+
pub fn parse(source: &str, options: &CompileOptions) -> Result<Module, Vec<CompileError>> {
24+
let mut errors = Vec::new();
25+
26+
// Step 1: Lexing
27+
let (tokens_opt, lex_errors) = lex(source, &options.source_path);
28+
errors.extend(lex_errors);
29+
30+
match tokens_opt {
31+
Some(tokens) => {
32+
// Step 2: Parsing
33+
let (ast_opt, parse_errors) = parse_module(tokens, source, &options.source_path);
34+
errors.extend(parse_errors);
35+
36+
match ast_opt {
37+
Some(ast) if errors.is_empty() => Ok(ast),
38+
_ => Err(errors),
39+
}
40+
}
41+
None => Err(errors),
42+
}
43+
}
44+
45+
/// Compile DSL source code to HIR
46+
///
47+
/// This function performs the full compilation pipeline including lexing,
48+
/// parsing, and semantic analysis to produce HIR.
49+
///
50+
/// # Arguments
51+
/// * `source` - The source code to compile
52+
/// * `options` - Compilation options including source path
53+
///
54+
/// # Returns
55+
/// * `Result<HIR, Vec<CompileError>>` - The compiled HIR or errors
56+
pub fn compile(
57+
source: &str,
58+
options: &CompileOptions,
59+
) -> Result<crate::analyzer::hir::HIR, Vec<CompileError>> {
60+
// Step 1 & 2: Parse to AST
61+
let _ast = parse(source, options)?;
62+
63+
// Step 3: Semantic analysis to HIR
64+
todo!("Implement semantic analysis to convert AST to HIR")
65+
}

0 commit comments

Comments
 (0)