Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
1792408
chore: lowering -> lower
indietyp Jun 11, 2026
a58bf56
feat: first expander
indietyp Jun 11, 2026
84c3f0b
feat: first expander
indietyp Jun 11, 2026
4ee37e1
feat: move to symbols for modules
indietyp Jun 11, 2026
a97f0ec
fix: compile errors
indietyp Jun 11, 2026
5dc4e5a
feat: diagnostic errors
indietyp Jun 12, 2026
47e34e8
feat: diagnostic
indietyp Jun 12, 2026
d85888a
feat: port let bindings (WIP)
indietyp Jun 12, 2026
d22ab90
feat: port let bindings
indietyp Jun 12, 2026
fff5560
feat: port let bindings
indietyp Jun 12, 2026
df5b68f
feat: port let bindings
indietyp Jun 12, 2026
7108d2a
feat: port let bindings
indietyp Jun 12, 2026
fa323ed
feat: port as
indietyp Jun 12, 2026
8cb0eb0
feat: port as
indietyp Jun 12, 2026
503b751
feat: port index,input,access
indietyp Jun 12, 2026
639dd2b
feat: port type
indietyp Jun 12, 2026
abad95a
feat: convert type and newtype
indietyp Jun 12, 2026
f76daf3
feat: convert type and newtype
indietyp Jun 12, 2026
73a85bc
feat: convert fn
indietyp Jun 12, 2026
07de942
feat: convert fn
indietyp Jun 12, 2026
363a75d
feat: convert fn
indietyp Jun 12, 2026
395b179
feat: convert use
indietyp Jun 12, 2026
8da474c
feat: convert use
indietyp Jun 12, 2026
11bb32d
feat: convert use
indietyp Jun 12, 2026
87fdec5
chore: docs
indietyp Jun 12, 2026
5438fc4
feat: intrinsic
indietyp Jun 15, 2026
386f46e
chore: lints
indietyp Jun 15, 2026
6195b29
chore: move lowering tests to lower
indietyp Jun 15, 2026
aeb7755
fix: compiletest
indietyp Jun 15, 2026
90d5fa4
allow for recursive definition of types
indietyp Jun 15, 2026
30fcf2b
chore: lints
indietyp Jun 15, 2026
78a5bc5
feat: checkpoint
indietyp Jun 15, 2026
ee308d9
feat: checkpoint
indietyp Jun 15, 2026
6580798
chore: update snapshots
indietyp Jun 15, 2026
864f804
chore: update snapshots
indietyp Jun 15, 2026
7822183
feat: checkpoint
indietyp Jun 15, 2026
df0a029
feat: checkpoint
indietyp Jun 15, 2026
5db3830
fix: more tests
indietyp Jun 15, 2026
0332347
feat: checkpoint
indietyp Jun 15, 2026
88f389e
feat: checkpoint
indietyp Jun 15, 2026
1de4b90
feat: checkpoint
indietyp Jun 15, 2026
f7884cb
feat: update snapshots
indietyp Jun 15, 2026
4014292
fix: clippy
indietyp Jun 15, 2026
6954598
chore: remove old modules
indietyp Jun 15, 2026
34a2807
feat: make errors more predictable
indietyp Jun 15, 2026
b168f8a
feat: expander
indietyp Jun 16, 2026
027247d
chore: remove USE expression from the AST
indietyp Jun 16, 2026
b5319e2
fix: remove derives that should not be derived
indietyp Jun 16, 2026
c690986
fix: docs and dependencies
indietyp Jun 16, 2026
a4c369d
fix: docs
indietyp Jun 16, 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
4 changes: 0 additions & 4 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
4 changes: 0 additions & 4 deletions libs/@local/hashql/ast/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ 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 }

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
134 changes: 134 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,134 @@
use core::mem;

use hashql_core::{
heap::BumpAllocator,
span::SpanId,
symbol::{Ident, IdentKind},
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,
{
let Some(field) = argument_to_field(expander, field) else {
return Expr::dummy();
};

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

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