Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
21633e1
chore: lowering -> lower
indietyp Jun 11, 2026
9456556
feat: first expander
indietyp Jun 11, 2026
d0dacaf
feat: first expander
indietyp Jun 11, 2026
f88f280
feat: move to symbols for modules
indietyp Jun 11, 2026
0df387d
fix: compile errors
indietyp Jun 11, 2026
9612dfb
feat: diagnostic errors
indietyp Jun 12, 2026
6c03972
feat: diagnostic
indietyp Jun 12, 2026
ab3f078
feat: port let bindings (WIP)
indietyp Jun 12, 2026
323c725
feat: port let bindings
indietyp Jun 12, 2026
fd71ca3
feat: port let bindings
indietyp Jun 12, 2026
8a5ae79
feat: port let bindings
indietyp Jun 12, 2026
7fbd804
feat: port let bindings
indietyp Jun 12, 2026
95e33d9
feat: port as
indietyp Jun 12, 2026
a84448b
feat: port as
indietyp Jun 12, 2026
714ab03
feat: port index,input,access
indietyp Jun 12, 2026
5df4571
feat: port type
indietyp Jun 12, 2026
993d67f
feat: convert type and newtype
indietyp Jun 12, 2026
dce112b
feat: convert type and newtype
indietyp Jun 12, 2026
954ba06
feat: convert fn
indietyp Jun 12, 2026
4db0075
feat: convert fn
indietyp Jun 12, 2026
7872e6e
feat: convert fn
indietyp Jun 12, 2026
b99ae3c
feat: convert use
indietyp Jun 12, 2026
16640af
feat: convert use
indietyp Jun 12, 2026
538edbe
feat: convert use
indietyp Jun 12, 2026
8c40c8c
chore: docs
indietyp Jun 12, 2026
954b863
feat: intrinsic
indietyp Jun 15, 2026
723f1de
chore: lints
indietyp Jun 15, 2026
e1ed0e3
chore: move lowering tests to lower
indietyp Jun 15, 2026
445bd48
fix: compiletest
indietyp Jun 15, 2026
4f90350
allow for recursive definition of types
indietyp Jun 15, 2026
51fe14e
chore: lints
indietyp Jun 15, 2026
76c6eaa
feat: checkpoint
indietyp Jun 15, 2026
764c372
feat: checkpoint
indietyp Jun 15, 2026
d98f96e
chore: update snapshots
indietyp Jun 15, 2026
e06897f
chore: update snapshots
indietyp Jun 15, 2026
915e4eb
feat: checkpoint
indietyp Jun 15, 2026
d850096
feat: checkpoint
indietyp Jun 15, 2026
ad6d4eb
fix: more tests
indietyp Jun 15, 2026
fa2ff89
feat: checkpoint
indietyp Jun 15, 2026
44f36f9
feat: checkpoint
indietyp Jun 15, 2026
0fe4e6a
feat: checkpoint
indietyp Jun 15, 2026
0e0548b
feat: update snapshots
indietyp Jun 15, 2026
cb65b03
fix: clippy
indietyp Jun 15, 2026
5dee02e
chore: remove old modules
indietyp Jun 15, 2026
997e420
feat: make errors more predictable
indietyp Jun 15, 2026
345d926
feat: expander
indietyp Jun 16, 2026
b5f73ee
chore: remove USE expression from the AST
indietyp Jun 16, 2026
fef3b37
fix: remove derives that should not be derived
indietyp Jun 16, 2026
0116eff
fix: docs and dependencies
indietyp Jun 16, 2026
9c8a5e0
fix: docs
indietyp Jun 16, 2026
290a5b0
chore: remove stray debugging
indietyp Jun 18, 2026
d06c1fb
fix: formatting
indietyp Jun 18, 2026
ae3ca8b
fix: delay access dummy error
indietyp Jun 18, 2026
2c29786
fix: scoped API change
indietyp Jun 19, 2026
ea95719
fix: lower nested call expr correctly
indietyp Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ cargo run -p hashql-compiletest suites --json
### Suite Categories

- `parse/*` - Parsing tests (e.g., `parse/syntax-dump`)
- `ast/lowering/*` - AST lowering phases
- `ast/lower/*` - AST lowering phases
- `hir/lower/*` - HIR lowering phases
- `hir/reify` - HIR generation from AST
- `mir/*` - MIR passes and generation
Expand Down
5 changes: 0 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion libs/@local/graph/api/src/rest/hashql/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ impl<'heap> Compilation<'heap> {
let Success {
value: types,
advisories,
} = hashql_ast::lowering::lower(sym::path::main, &mut ast, &env, &modules)
} = hashql_ast::lower::lower(sym::path::main, &mut ast, &env, &modules, &mut *scratch)
.map_category(|category| {
HashQlDiagnosticCategory::Ast(AstDiagnosticCategory::Lowering(category))
})
Expand Down
5 changes: 0 additions & 5 deletions libs/@local/hashql/ast/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@ hashql-diagnostics = { workspace = true, public = true }
# Private workspace dependencies

# Private third-party dependencies
derive_more = { workspace = true, features = ["display"] }
enum-iterator = { workspace = true }
foldhash = { workspace = true }
hashbrown = { workspace = true }
simple-mermaid = { workspace = true }
tracing = { workspace = true }

[dev-dependencies]
hashql-compiletest = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion libs/@local/hashql/ast/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use alloc::borrow::Cow;
use hashql_core::span::SpanId;
use hashql_diagnostics::{Diagnostic, category::DiagnosticCategory};

use crate::lowering::error::LoweringDiagnosticCategory;
use crate::lower::error::LoweringDiagnosticCategory;

pub type AstDiagnostic = Diagnostic<AstDiagnosticCategory, SpanId>;

Expand Down
63 changes: 0 additions & 63 deletions libs/@local/hashql/ast/src/format/dump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,12 @@ use crate::node::{
expr::{
AsExpr, CallExpr, ClosureExpr, DictExpr, Expr, ExprKind, FieldExpr, IfExpr, IndexExpr,
InputExpr, LetExpr, ListExpr, LiteralExpr, NewTypeExpr, StructExpr, TupleExpr, TypeExpr,
UseExpr,
call::{Argument, LabeledArgument},
closure::{ClosureParam, ClosureSignature},
dict::DictEntry,
list::ListElement,
r#struct::StructEntry,
tuple::TupleElement,
r#use::{Glob, UseBinding, UseKind},
},
generic::{GenericArgument, GenericConstraint, GenericParam, Generics},
id::NodeId,
Expand Down Expand Up @@ -352,62 +350,6 @@ impl_syntax_dump!(struct TypeExpr(name); []constraints value body);

impl_syntax_dump!(struct NewTypeExpr(name); []constraints value body);

impl SyntaxDump for UseBinding<'_> {
fn syntax_dump(&self, fmt: &mut Formatter, depth: usize) -> fmt::Result {
let Self {
id,
span,
name,
alias,
} = self;

let mut properties = vec![format!("name: {name}")];
if let Some(alias) = alias {
properties.push(format!("alias: {alias}"));
}

write_header(
fmt,
depth,
"UseBinding",
Some(*id),
Some(*span),
Some(&properties.join(", ")),
)
}
}

impl SyntaxDump for Glob {
fn syntax_dump(&self, fmt: &mut Formatter, depth: usize) -> fmt::Result {
let Self { id, span } = self;

write_header(fmt, depth, "Glob", Some(*id), Some(*span), None)
}
}

impl SyntaxDump for UseKind<'_> {
fn syntax_dump(&self, fmt: &mut Formatter, depth: usize) -> fmt::Result {
match self {
UseKind::Named(bindings) => {
write_header(fmt, depth, "UseKind", None, None, Some("Named"))?;

for binding in bindings {
binding.syntax_dump(fmt, depth + 1)?;
}

Ok(())
}
UseKind::Glob(glob) => {
write_header(fmt, depth, "UseKind", None, None, Some("Glob"))?;

glob.syntax_dump(fmt, depth + 1)
}
}
}
}

impl_syntax_dump!(struct UseExpr(); path kind body);

impl_syntax_dump!(struct InputExpr(name); r#type ?default);

#[rustfmt::skip]
Expand Down Expand Up @@ -478,11 +420,6 @@ impl SyntaxDump for ExprKind<'_> {

new_type_expr.syntax_dump(fmt, depth + 1)
}
Self::Use(use_expr) => {
write_header(fmt, depth, "ExprKind", None, None, Some("Use"))?;

use_expr.syntax_dump(fmt, depth + 1)
}
Self::Input(input_expr) => {
write_header(fmt, depth, "ExprKind", None, None, Some("Input"))?;

Expand Down
54 changes: 12 additions & 42 deletions libs/@local/hashql/ast/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,47 +1,16 @@
//! # HashQL Abstract Syntax Tree
//! HashQL abstract syntax tree and lowering pipeline.
//!
//! This crate defines the Abstract Syntax Tree (AST) for HashQL, a side-effect free,
//! purely functional query language designed for bi-temporal graph databases.
//! This crate defines the AST for HashQL and the [`lower`] pipeline that
//! transforms it into a form suitable for HIR construction. The AST is
//! frontend-agnostic: nodes are defined independently of any syntax, with
//! J-Expr as the current primary parser.
//!
//! ## Overview
//! # Modules
//!
//! The HashQL AST provides a structured representation of HashQL programs after parsing.
//! It captures the hierarchical structure of the code with nodes for expressions, types,
//! and declarations. This AST is the foundation for subsequent compilation phases including
//! type checking, optimization, and evaluation.
//!
//! ## Key Features
//!
//! - **Frontend Agnostic**: The AST is designed to be independent of any particular syntax. While
//! the current primary interface is through J-Expr (JSON-based expressions), the AST can support
//! multiple frontend syntaxes.
//!
//! - **Memory Efficient**: Uses arena allocation through a custom [`heap`] module to minimize
//! allocations and improve performance during parsing and transformation.
//!
//! - **Source Tracking**: Each node maintains its location in the source code via span identifiers.
//!
//! - **Comprehensive Language Model**: Supports the full range of language constructs in HashQL,
//! including expressions, types, path references, and special forms.
//!
//! ## Core Modules
//!
//! - [`node`]: Defines the AST node types that represent language constructs
//! - [`lowering`]: Defines the lowering process for AST nodes, converting them into a more
//! optimized form suitable for conversion into the HIR.
//!
//! ## Special Forms
//!
//! HashQL implements several language constructs as "special forms" that are initially parsed
//! as function calls and then transformed into specialized AST nodes. These include:
//!
//! - `let` for variable binding
//! - `if` for conditional expressions
//! - `fn` for closure definitions
//! - `use` for module imports
//! - Field and index access expressions
//!
//! [`heap`]: hashql_core::heap
//! - [`node`]: AST node types (expressions, types, paths, generics).
//! - [`lower`]: Lowering pipeline (expansion, sanitization, type extraction).
//! - [`visit`]: Visitor trait for AST traversal.
//! - [`format`](mod@format): Debug formatting for AST dumps.
//!
//! ## Workspace dependencies
#![cfg_attr(doc, doc = simple_mermaid::mermaid!("../docs/dependency-diagram.mmd"))]
Expand All @@ -50,6 +19,7 @@
// Language Features
coverage_attribute,
macro_metavar_expr_concat,
default_field_values,

// Library Features
allocator_api,
Expand All @@ -61,6 +31,6 @@ extern crate alloc;

pub mod error;
pub mod format;
pub mod lowering;
pub mod lower;
pub mod node;
pub mod visit;
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,17 @@ use hashql_core::span::SpanId;
use hashql_diagnostics::{Diagnostic, category::DiagnosticCategory};

use super::{
import_resolver::error::ImportResolverDiagnosticCategory,
sanitizer::SanitizerDiagnosticCategory,
special_form_expander::error::SpecialFormExpanderDiagnosticCategory,
expander::error::ExpanderDiagnosticCategory, sanitizer::SanitizerDiagnosticCategory,
type_extractor::error::TypeExtractorDiagnosticCategory,
};

pub type LoweringDiagnostic = Diagnostic<LoweringDiagnosticCategory, SpanId>;

#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum LoweringDiagnosticCategory {
Expander(SpecialFormExpanderDiagnosticCategory),
Expander(ExpanderDiagnosticCategory),
Sanitizer(SanitizerDiagnosticCategory),
Resolver(ImportResolverDiagnosticCategory),

Extractor(TypeExtractorDiagnosticCategory),
}

Expand All @@ -31,9 +29,8 @@ impl DiagnosticCategory for LoweringDiagnosticCategory {

fn subcategory(&self) -> Option<&dyn DiagnosticCategory> {
match self {
Self::Expander(special_form) => Some(special_form),
Self::Expander(expander) => Some(expander),
Self::Sanitizer(sanitizer) => Some(sanitizer),
Self::Resolver(resolver) => Some(resolver),
Self::Extractor(extractor) => Some(extractor),
}
}
Expand Down
138 changes: 138 additions & 0 deletions libs/@local/hashql/ast/src/lower/expander/access.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use core::mem;

use hashql_core::{
heap::BumpAllocator,
span::SpanId,
symbol::{Ident, IdentKind, sym},
value::Primitive,
};

use super::Expander;
use crate::{
lower::expander::error,
node::{
expr::{CallExpr, Expr, ExprKind, FieldExpr, call::Argument},
id::NodeId,
},
};

/// Extract a field identifier from an argument.
///
/// Handles both named field access (identifiers like `name`) and indexed field
/// access (integer literals like `0` for tuple fields). Integer literals are
/// validated for bounds.
fn argument_to_field<'heap, S>(
expander: &mut Expander<'_, 'heap, S>,
argument: &Argument<'heap>,
) -> Option<Ident<'heap>> {
// Integer literal for tuple field access
if let ExprKind::Literal(literal) = &argument.value.kind {
if let Some(annotation) = &literal.r#type {
expander
.diagnostics
.push(error::field_literal_type_annotation(annotation.span));
}

let Primitive::Integer(integer) = literal.kind else {
expander
.diagnostics
.push(error::invalid_field_literal_type(literal.span));
return None;
};

if integer.as_usize().is_none() {
expander
.diagnostics
.push(error::field_index_out_of_bounds(literal.span));
return None;
}

return Some(Ident {
span: literal.span,
value: integer.as_symbol(),
kind: IdentKind::Lexical,
});
}

// Named field access
if let ExprKind::Path(path) = &argument.value.kind
&& let Some(&ident) = path.as_ident()
{
return Some(ident);
}

expander
.diagnostics
.push(error::invalid_access_field(argument));

None
}

fn lower_access_impl<'heap, S>(
span: SpanId,
expander: &mut Expander<'_, 'heap, S>,

value: &mut Argument<'heap>,
field: &Argument<'heap>,
) -> Expr<'heap>
where
S: BumpAllocator,
{
// `argument_to_field` will add a diagnostic directly, therefore unlike the other expanders we
// just create the synthetic dummy
let field = argument_to_field(expander, field).unwrap_or_else(|| Ident::synthetic(sym::dummy));

let mut value = mem::replace(&mut value.value, Expr::dummy());
expander.visit(&mut value);

if field.value == sym::dummy {
return Expr::dummy();
}

Expr {
id: NodeId::PLACEHOLDER,
span,
kind: ExprKind::Field(FieldExpr {
id: NodeId::PLACEHOLDER,
span,
value: Box::new_in(value, expander.heap),
field,
}),
}
}

/// Lowers a `.` call into a [`FieldExpr`].
///
/// Form: `(. value field)`. The field must be either a named identifier
/// or a non-negative integer literal (for positional tuple access).
///
/// [`FieldExpr`]: crate::node::expr::FieldExpr
pub(super) fn lower_access<'heap, S>(
expander: &mut Expander<'_, 'heap, S>,
CallExpr {
id: _,
span,
function: _,
arguments,
labeled_arguments,
}: &mut CallExpr<'heap>,
) -> Expr<'heap>
where
S: BumpAllocator,
{
if !labeled_arguments.is_empty() {
expander
.diagnostics
.push(error::labeled_arguments_in_access(labeled_arguments));
}

if let [value, field] = &mut **arguments {
lower_access_impl(*span, expander, value, field)
} else {
expander
.diagnostics
.push(error::invalid_access_argument_count(*span, arguments));

Expr::dummy()
}
}
Loading
Loading