Skip to content

Commit

Permalink
feat: DSL CLI & Helpful Error Reporting (#35)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
AlSchlo authored Mar 1, 2025
1 parent fc0e77e commit 2c17bf7
Show file tree
Hide file tree
Showing 27 changed files with 769 additions and 226 deletions.
174 changes: 89 additions & 85 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions optd-dsl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@ ordered-float = "5.0.0"
futures = "0.3.31"
tokio.workspace = true
async-recursion.workspace = true
enum_dispatch = "0.3.13"
clap = { version = "4.5.31", features = ["derive"] }

[lib]
name = "optd_dsl"
path = "src/lib.rs"

[[bin]]
name = "optd-cli"
path = "src/cli/main.rs"
2 changes: 2 additions & 0 deletions optd-dsl/src/analyzer/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pub mod hir;
pub mod semantic;
pub mod r#type;
16 changes: 16 additions & 0 deletions optd-dsl/src/analyzer/semantic/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use ariadne::{Report, Source};

use crate::utils::{error::Diagnose, span::Span};

#[derive(Debug)]
pub struct SemanticError {}

impl Diagnose for SemanticError {
fn report(&self) -> Report<Span> {
todo!()
}

fn source(&self) -> (String, Source) {
todo!()
}
}
1 change: 1 addition & 0 deletions optd-dsl/src/analyzer/semantic/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod error;
16 changes: 16 additions & 0 deletions optd-dsl/src/analyzer/type/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use ariadne::{Report, Source};

use crate::utils::{error::Diagnose, span::Span};

#[derive(Debug)]
pub struct TypeError {}

impl Diagnose for TypeError {
fn report(&self) -> Report<Span> {
todo!()
}

fn source(&self) -> (String, Source) {
todo!()
}
}
1 change: 1 addition & 0 deletions optd-dsl/src/analyzer/type/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod error;
114 changes: 114 additions & 0 deletions optd-dsl/src/cli/basic.op
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
data LogicalProps(schema_len: I64)

data Scalar with
| ColumnRef(idx: Int64)
| Literal with
| IntLiteral(value: Int64)
| StringLiteral(value: String)
| BoolLiteral(value: Bool)
\ NullLiteral
| Arithmetic with
| Mult(left: Scalar, right: Scalar)
| Add(left: Scalar, right: Scalar)
| Sub(left: Scalar, right: Scalar)
\ Div(left: Scalar, right: Scalar)
| Predicate with
| And(children: [Predicate])
| Or(children: [Predicate])
| Not(child: Predicate)
| Equals(left: Scalar, right: Scalar)
| NotEquals(left: Scalar, right: Scalar)
| LessThan(left: Scalar, right: Scalar)
| LessThanEqual(left: Scalar, right: Scalar)
| GreaterThan(left: Scalar, right: Scalar)
| GreaterThanEqual(left: Scalar, right: Scalar)
| IsNull(expr: Scalar)
\ IsNotNull(expr: Scalar)
| Function with
| Cast(expr: Scalar, target_type: String)
| Substring(str: Scalar, start: Scalar, length: Scalar)
\ Concat(args: [Scalar])
\ AggregateExpr with
| Sum(expr: Scalar)
| Count(expr: Scalar)
| Min(expr: Scalar)
| Max(expr: Scalar)
\ Avg(expr: Scalar)

data Logical with
| Scan(table_name: String)
| Filter(child: Logical, cond: Predicate)
| Project(child: Logical, exprs: [Scalar])
| Join(
left: Logical,
right: Logical,
typ: JoinType,
cond: Predicate
)
\ Aggregate(
child: Logical,
group_by: [Scalar],
aggregates: [AggregateExpr]
)

data Physical with
| Scan(table_name: String)
| Filter(child: Physical, cond: Predicate)
| Project(child: Physical, exprs: [Scalar])
| Join with
| HashJoin(
build_side: Physical,
probe_side: Physical,
typ: String,
cond: Predicate
)
| MergeJoin(
left: Physical,
right: Physical,
typ: String,
cond: Predicate
)
\ NestedLoopJoin(
outer: Physical,
inner: Physical,
typ: String,
cond: Predicate
)
| Aggregate(
child: Physical,
group_by: [Scalar],
aggregates: [AggregateExpr]
)
\ Sort(
child: Physical,
order_by: [(Scalar, SortOrder)]
)

data JoinType with
| Inner
| Left
| Right
| Full
\ Semi

[rust]
fn (expr: Scalar) apply_children(f: Scalar => Scalar) = ()

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
)
105 changes: 105 additions & 0 deletions optd-dsl/src/cli/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! CLI tool for the Optimizer DSL
//!
//! This tool provides a command-line interface for the Optimizer DSL compiler.
//!
//! # Usage
//!
//! ```
//! # Parse a DSL file (validate syntax):
//! optd parse path/to/file.op
//!
//! # Parse a file and print the AST:
//! optd parse path/to/file.op --print-ast
//!
//! # Get help:
//! optd --help
//! optd parse --help
//! ```
//!
//! When developing, you can run through cargo:
//!
//! ```
//! cargo run -- parse examples/example.dsl
//! cargo run -- parse examples/example.dsl --print-ast
//! ```
use clap::{Parser, Subcommand};
use optd_dsl::compiler::compile::{parse, CompileOptions};
use optd_dsl::utils::error::Diagnose;
use std::fs;
use std::path::PathBuf;

#[derive(Parser)]
#[command(
name = "optd",
about = "Optimizer DSL compiler and toolchain",
version,
author
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}

#[derive(Subcommand)]
enum Commands {
/// Parse a DSL file and validate its syntax
Parse {
/// Input file to parse
#[arg(value_name = "FILE")]
input: PathBuf,

/// Print the AST in a readable format
#[arg(long)]
print_ast: bool,
},
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();

match &cli.command {
Commands::Parse { input, print_ast } => {
println!("Parsing file: {}", input.display());

// Improve file reading error handling
let source = match fs::read_to_string(input) {
Ok(content) => content,
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
eprintln!("❌ Error: File not found: {}", input.display());
eprintln!(
"Please check that the file exists and you have correct permissions."
);
} else {
eprintln!("❌ Error reading file: {}", e);
}
std::process::exit(1);
}
};

let options = CompileOptions {
source_path: input.to_string_lossy().to_string(),
};

match parse(&source, &options) {
Ok(ast) => {
println!("✅ Parse successful!");
if *print_ast {
println!("\nAST Structure:");
println!("{:#?}", ast);
}
}
Err(errors) => {
eprintln!("❌ Parse failed with {} errors:", errors.len());
for error in errors {
error.print(std::io::stderr())?;
}
std::process::exit(1);
}
}
}
}

Ok(())
}
65 changes: 65 additions & 0 deletions optd-dsl/src/compiler/compile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use crate::lexer::lex::lex;
use crate::parser::ast::Module;
use crate::parser::module::parse_module;
use crate::utils::error::CompileError;

/// Compilation options for the DSL
pub struct CompileOptions {
/// Path to the main module source file
pub source_path: String,
}

/// Parse DSL source code to AST
///
/// This function performs lexing and parsing stages of compilation,
/// returning either the parsed AST Module or collected errors.
///
/// # Arguments
/// * `source` - The source code to parse
/// * `options` - Compilation options including source path
///
/// # Returns
/// * `Result<Module, Vec<CompileError>>` - The parsed AST or errors
pub fn parse(source: &str, options: &CompileOptions) -> Result<Module, Vec<CompileError>> {
let mut errors = Vec::new();

// Step 1: Lexing
let (tokens_opt, lex_errors) = lex(source, &options.source_path);
errors.extend(lex_errors);

match tokens_opt {
Some(tokens) => {
// Step 2: Parsing
let (ast_opt, parse_errors) = parse_module(tokens, source, &options.source_path);
errors.extend(parse_errors);

match ast_opt {
Some(ast) if errors.is_empty() => Ok(ast),
_ => Err(errors),
}
}
None => Err(errors),
}
}

/// Compile DSL source code to HIR
///
/// This function performs the full compilation pipeline including lexing,
/// parsing, and semantic analysis to produce HIR.
///
/// # Arguments
/// * `source` - The source code to compile
/// * `options` - Compilation options including source path
///
/// # Returns
/// * `Result<HIR, Vec<CompileError>>` - The compiled HIR or errors
pub fn compile(
source: &str,
options: &CompileOptions,
) -> Result<crate::analyzer::hir::HIR, Vec<CompileError>> {
// Step 1 & 2: Parse to AST
let _ast = parse(source, options)?;

// Step 3: Semantic analysis to HIR
todo!("Implement semantic analysis to convert AST to HIR")
}
1 change: 1 addition & 0 deletions optd-dsl/src/compiler/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod compile;
2 changes: 1 addition & 1 deletion optd-dsl/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use bridge::{from_optd::partial_logical_to_value, into_optd::value_to_partial_lo
use futures::StreamExt;
use optd_core::cascades::ir::PartialLogicalPlan;
use std::sync::Arc;
use utils::{errors::Error, streams::PartialLogicalPlanStream};
use utils::{error::Error, streams::PartialLogicalPlanStream};

use CoreData::*;
use Expr::*;
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion optd-dsl/src/engine/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
pub(super) mod errors;
pub(super) mod error;
pub(super) mod macros;
pub(super) mod streams;
2 changes: 1 addition & 1 deletion optd-dsl/src/engine/utils/streams.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use crate::analyzer::hir::{Expr, Value};
use crate::capture;
use crate::engine::utils::errors::Error;
use crate::engine::utils::error::Error;
use crate::utils::context::Context;
use futures::{stream, Stream, StreamExt};
use optd_core::cascades::ir::PartialLogicalPlan;
Expand Down
Loading

0 comments on commit 2c17bf7

Please sign in to comment.