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
c0517ed
chore: lowering -> lower
indietyp Jun 11, 2026
a39229a
feat: first expander
indietyp Jun 11, 2026
c003673
feat: first expander
indietyp Jun 11, 2026
e1af33e
feat: move to symbols for modules
indietyp Jun 11, 2026
46b88be
fix: compile errors
indietyp Jun 11, 2026
40be45e
feat: diagnostic errors
indietyp Jun 12, 2026
205f283
feat: diagnostic
indietyp Jun 12, 2026
e186491
feat: port let bindings (WIP)
indietyp Jun 12, 2026
3a29fbb
feat: port let bindings
indietyp Jun 12, 2026
3ecffe7
feat: port let bindings
indietyp Jun 12, 2026
e8b494c
feat: port let bindings
indietyp Jun 12, 2026
f45fa22
feat: port let bindings
indietyp Jun 12, 2026
f0e3b2d
feat: port as
indietyp Jun 12, 2026
aa8dda2
feat: port as
indietyp Jun 12, 2026
14f5cb0
feat: port index,input,access
indietyp Jun 12, 2026
c068363
feat: port type
indietyp Jun 12, 2026
fad2742
feat: convert type and newtype
indietyp Jun 12, 2026
e846959
feat: convert type and newtype
indietyp Jun 12, 2026
ec09867
feat: convert fn
indietyp Jun 12, 2026
000c2ee
feat: convert fn
indietyp Jun 12, 2026
1a7f2cf
feat: convert fn
indietyp Jun 12, 2026
a69b48c
feat: convert use
indietyp Jun 12, 2026
8adf916
feat: convert use
indietyp Jun 12, 2026
943c252
feat: convert use
indietyp Jun 12, 2026
6203da8
chore: docs
indietyp Jun 12, 2026
296ad75
feat: intrinsic
indietyp Jun 15, 2026
790e457
chore: lints
indietyp Jun 15, 2026
fc83de4
chore: move lowering tests to lower
indietyp Jun 15, 2026
d796ea3
fix: compiletest
indietyp Jun 15, 2026
916a063
allow for recursive definition of types
indietyp Jun 15, 2026
aea9710
chore: lints
indietyp Jun 15, 2026
523bc3f
feat: checkpoint
indietyp Jun 15, 2026
2d8da99
feat: checkpoint
indietyp Jun 15, 2026
5ccfd9e
chore: update snapshots
indietyp Jun 15, 2026
e013e12
chore: update snapshots
indietyp Jun 15, 2026
b3bfb02
feat: checkpoint
indietyp Jun 15, 2026
acfbe1e
feat: checkpoint
indietyp Jun 15, 2026
a5a902c
fix: more tests
indietyp Jun 15, 2026
871bee7
feat: checkpoint
indietyp Jun 15, 2026
54ab0ad
feat: checkpoint
indietyp Jun 15, 2026
bb68550
feat: checkpoint
indietyp Jun 15, 2026
04f6f9d
feat: update snapshots
indietyp Jun 15, 2026
e42b503
fix: clippy
indietyp Jun 15, 2026
7d8660c
chore: remove old modules
indietyp Jun 15, 2026
c084951
feat: make errors more predictable
indietyp Jun 15, 2026
d51ec48
feat: expander
indietyp Jun 16, 2026
56d5d6b
chore: remove USE expression from the AST
indietyp Jun 16, 2026
f3a56a0
fix: remove derives that should not be derived
indietyp Jun 16, 2026
e7b5f09
fix: docs and dependencies
indietyp Jun 16, 2026
3c6f61a
fix: docs
indietyp Jun 16, 2026
a926074
chore: remove stray debugging
indietyp Jun 18, 2026
b95cd7e
fix: formatting
indietyp Jun 18, 2026
a399b22
fix: delay access dummy error
indietyp Jun 18, 2026
195cb57
fix: scoped API change
indietyp Jun 19, 2026
3e715ff
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