Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
b3563f9
chore: lowering -> lower
indietyp Jun 11, 2026
6b90a60
feat: first expander
indietyp Jun 11, 2026
2654fb2
feat: first expander
indietyp Jun 11, 2026
e62ced5
feat: move to symbols for modules
indietyp Jun 11, 2026
8530974
fix: compile errors
indietyp Jun 11, 2026
118d54c
feat: diagnostic errors
indietyp Jun 12, 2026
e93900a
feat: diagnostic
indietyp Jun 12, 2026
2545806
feat: port let bindings (WIP)
indietyp Jun 12, 2026
cff94b3
feat: port let bindings
indietyp Jun 12, 2026
7b188e5
feat: port let bindings
indietyp Jun 12, 2026
12f522a
feat: port let bindings
indietyp Jun 12, 2026
78c2b8f
feat: port let bindings
indietyp Jun 12, 2026
32ab769
feat: port as
indietyp Jun 12, 2026
cbcf84d
feat: port as
indietyp Jun 12, 2026
549c081
feat: port index,input,access
indietyp Jun 12, 2026
9c845e5
feat: port type
indietyp Jun 12, 2026
11dd7f9
feat: convert type and newtype
indietyp Jun 12, 2026
ac30e72
feat: convert type and newtype
indietyp Jun 12, 2026
e3f9f70
feat: convert fn
indietyp Jun 12, 2026
29e7034
feat: convert fn
indietyp Jun 12, 2026
bb90ebd
feat: convert fn
indietyp Jun 12, 2026
060031a
feat: convert use
indietyp Jun 12, 2026
e05098e
feat: convert use
indietyp Jun 12, 2026
ed90bc5
feat: convert use
indietyp Jun 12, 2026
3b737bf
chore: docs
indietyp Jun 12, 2026
bd6d234
feat: intrinsic
indietyp Jun 15, 2026
5a3a0a9
chore: lints
indietyp Jun 15, 2026
1150909
chore: move lowering tests to lower
indietyp Jun 15, 2026
e8f6570
fix: compiletest
indietyp Jun 15, 2026
dd66849
allow for recursive definition of types
indietyp Jun 15, 2026
eb94d69
chore: lints
indietyp Jun 15, 2026
0dc907d
feat: checkpoint
indietyp Jun 15, 2026
7942ec7
feat: checkpoint
indietyp Jun 15, 2026
3174b85
chore: update snapshots
indietyp Jun 15, 2026
87d04f6
chore: update snapshots
indietyp Jun 15, 2026
2a60ae3
feat: checkpoint
indietyp Jun 15, 2026
651092d
feat: checkpoint
indietyp Jun 15, 2026
1f65fa2
fix: more tests
indietyp Jun 15, 2026
e063292
feat: checkpoint
indietyp Jun 15, 2026
68e3507
feat: checkpoint
indietyp Jun 15, 2026
0c3fb1a
feat: checkpoint
indietyp Jun 15, 2026
8d380ea
feat: update snapshots
indietyp Jun 15, 2026
31c37c6
fix: clippy
indietyp Jun 15, 2026
babde72
chore: remove old modules
indietyp Jun 15, 2026
70c121b
feat: make errors more predictable
indietyp Jun 15, 2026
27e26f0
feat: expander
indietyp Jun 16, 2026
672df62
chore: remove USE expression from the AST
indietyp Jun 16, 2026
edac3b4
fix: remove derives that should not be derived
indietyp Jun 16, 2026
05c8169
fix: docs and dependencies
indietyp Jun 16, 2026
b49834a
fix: docs
indietyp Jun 16, 2026
6506c91
chore: remove stray debugging
indietyp Jun 18, 2026
8cc2ad4
fix: formatting
indietyp Jun 18, 2026
9914a9f
fix: delay access dummy error
indietyp Jun 18, 2026
886e597
fix: scoped API change
indietyp Jun 19, 2026
d6be0b8
fix: lower nested call expr correctly
indietyp Jun 19, 2026
13877fc
chore: docs
indietyp Jul 6, 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