From 4af0146a65e1eca3cc3f85dc0c4d382353dc5227 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:43:16 +0200 Subject: [PATCH 01/25] feat: remove read module --- Cargo.toml | 2 + .../src/suite/eval_graph_read_entity.rs | 127 -------------- .../hashql/compiletest/src/suite/mod.rs | 5 +- libs/@local/hashql/eval/Cargo.toml | 22 +-- libs/@local/hashql/eval/src/error.rs | 9 +- libs/@local/hashql/eval/src/lib.rs | 3 - .../tests/ui/graph/read/entity/.spec.toml | 1 - .../entity/arithmetic-comparisons-and.jsonc | 20 --- .../entity/arithmetic-comparisons-and.stdout | 155 ----------------- .../entity/arithmetic-comparisons-or.jsonc | 20 --- .../entity/arithmetic-comparisons-or.stdout | 159 ------------------ .../graph/read/entity/boolean-literal.jsonc | 11 -- .../graph/read/entity/boolean-literal.stdout | 35 ---- .../ui/graph/read/entity/call-identity.jsonc | 18 -- .../ui/graph/read/entity/call-identity.stderr | 13 -- .../read/entity/complex-object-error.jsonc | 16 -- .../read/entity/complex-object-error.stderr | 27 --- .../entity/computed-path-indexing-error.jsonc | 18 -- .../computed-path-indexing-error.stderr | 15 -- .../read/entity/constructor-call-none.jsonc | 14 -- .../read/entity/constructor-call-none.stdout | 55 ------ .../read/entity/equality-comparison.jsonc | 14 -- .../read/entity/equality-comparison.stdout | 63 ------- .../read/entity/field-access-struct.jsonc | 14 -- .../read/entity/field-access-struct.stdout | 57 ------- .../read/entity/field-access-tuple.jsonc | 14 -- .../read/entity/field-access-tuple.stdout | 57 ------- .../graph/read/entity/field-in-dict-key.jsonc | 18 -- .../read/entity/field-in-dict-key.stderr | 7 - .../read/entity/field-in-dict-value.jsonc | 15 -- .../read/entity/field-in-dict-value.stderr | 7 - .../ui/graph/read/entity/field-in-list.jsonc | 15 -- .../ui/graph/read/entity/field-in-list.stderr | 7 - .../read/entity/field-in-struct-entry.jsonc | 15 -- .../read/entity/field-in-struct-entry.stderr | 7 - .../graph/read/entity/field-in-struct.jsonc | 15 -- .../graph/read/entity/field-in-struct.stderr | 7 - .../ui/graph/read/entity/field-in-tuple.jsonc | 15 -- .../graph/read/entity/field-in-tuple.stderr | 7 - .../ui/graph/read/entity/if-filter-expr.jsonc | 18 -- .../graph/read/entity/if-filter-expr.stderr | 11 -- .../ui/graph/read/entity/if-filter.jsonc | 11 -- .../ui/graph/read/entity/if-filter.stdout | 56 ------ .../read/entity/input-field-access.jsonc | 17 -- .../read/entity/input-field-access.stdout | 38 ----- .../read/entity/input-index-access.jsonc | 17 -- .../read/entity/input-index-access.stdout | 40 ----- .../graph/read/entity/input-parameter.jsonc | 14 -- .../graph/read/entity/input-parameter.stdout | 38 ----- .../entity/invalid-field-access-error.jsonc | 20 --- .../entity/invalid-field-access-error.stderr | 17 -- .../entity/invalid-index-access-error.jsonc | 20 --- .../entity/invalid-index-access-error.stderr | 17 -- .../read/entity/invalid-vertex-query.jsonc | 16 -- .../read/entity/invalid-vertex-query.stderr | 27 --- .../ui/graph/read/entity/let-expression.jsonc | 16 -- .../graph/read/entity/let-expression.stdout | 52 ------ .../graph/read/entity/let-propagation.jsonc | 17 -- .../graph/read/entity/let-propagation.stdout | 52 ------ .../ui/graph/read/entity/logical-and-or.jsonc | 11 -- .../graph/read/entity/logical-and-or.stdout | 71 -------- .../ui/graph/read/entity/logical-or-and.jsonc | 11 -- .../graph/read/entity/logical-or-and.stdout | 75 --------- .../read/entity/nested-binary-operation.jsonc | 18 -- .../entity/nested-binary-operation.stderr | 12 -- .../read/entity/nested-let-bindings.jsonc | 16 -- .../read/entity/nested-let-bindings.stdout | 52 ------ .../read/entity/scalar-property-filter.jsonc | 16 -- .../read/entity/scalar-property-filter.stdout | 51 ------ .../read/entity/top-level-variable.jsonc | 13 -- .../read/entity/top-level-variable.stdout | 38 ----- .../read/entity/top-type-assertion.jsonc | 11 -- .../read/entity/top-type-assertion.stdout | 35 ---- .../entity/type-assertion-in-comparison.jsonc | 19 --- .../type-assertion-in-comparison.stdout | 51 ------ .../read/entity/unsupported-closure.jsonc | 17 -- .../read/entity/unsupported-closure.stderr | 27 --- .../unsupported-nested-graph-read.jsonc | 27 --- .../unsupported-nested-graph-read.stderr | 27 --- .../entity/unsupported-type-constructor.jsonc | 16 -- .../unsupported-type-constructor.stderr | 27 --- 81 files changed, 15 insertions(+), 2239 deletions(-) delete mode 100644 libs/@local/hashql/compiletest/src/suite/eval_graph_read_entity.rs delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/.spec.toml delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-and.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-and.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-or.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-or.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/boolean-literal.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/boolean-literal.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/call-identity.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/call-identity.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/complex-object-error.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/complex-object-error.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/computed-path-indexing-error.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/computed-path-indexing-error.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/constructor-call-none.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/constructor-call-none.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/equality-comparison.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/equality-comparison.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-struct.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-struct.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-tuple.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-tuple.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-key.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-key.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-value.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-value.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-list.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-list.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct-entry.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct-entry.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-tuple.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-tuple.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter-expr.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter-expr.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/input-field-access.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/input-field-access.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/input-index-access.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/input-index-access.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/input-parameter.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/input-parameter.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-field-access-error.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-field-access-error.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-index-access-error.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-index-access-error.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-vertex-query.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-vertex-query.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/let-expression.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/let-expression.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/let-propagation.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/let-propagation.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-and-or.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-and-or.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-or-and.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-or-and.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-binary-operation.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-binary-operation.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-let-bindings.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-let-bindings.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/scalar-property-filter.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/scalar-property-filter.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/top-level-variable.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/top-level-variable.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/top-type-assertion.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/top-type-assertion.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/type-assertion-in-comparison.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/type-assertion-in-comparison.stdout delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-closure.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-closure.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-nested-graph-read.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-nested-graph-read.stderr delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-type-constructor.jsonc delete mode 100644 libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-type-constructor.stderr diff --git a/Cargo.toml b/Cargo.toml index 264fbcf46ce..4090f18bdb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -241,6 +241,8 @@ sentry-types = { version = "0.47.0", default-features = fa serde = { version = "1.0.228", default-features = false } serde_core = { version = "1.0.228", default-features = false } serde_json = { version = "1.0.145" } +sqruff-lib = {version = "0.37.3"} +sqruff-lib-core = {version = "0.37.3"} serde_plain = { version = "1.0.2", default-features = false } sha2 = { version = "0.11.0", default-features = false } similar-asserts = { version = "2.0.0", default-features = false } diff --git a/libs/@local/hashql/compiletest/src/suite/eval_graph_read_entity.rs b/libs/@local/hashql/compiletest/src/suite/eval_graph_read_entity.rs deleted file mode 100644 index 98c6db52680..00000000000 --- a/libs/@local/hashql/compiletest/src/suite/eval_graph_read_entity.rs +++ /dev/null @@ -1,127 +0,0 @@ -use core::fmt::Write as _; - -use hashql_ast::node::expr::Expr; -use hashql_core::{ - collections::FastHashMap, - module::ModuleRegistry, - pretty::{Formatter, RenderOptions}, - r#type::{TypeFormatterOptions, environment::Environment}, - value::{self, List, Opaque, Primitive, Struct, Value}, -}; -use hashql_eval::graph::read::{FilterSlice, GraphReadCompiler}; -use hashql_hir::{ - context::HirContext, - intern::Interner, - node::NodeData, - pretty::{NodeFormatter, NodeFormatterOptions}, - visit::Visitor as _, -}; - -use super::{RunContext, Suite, SuiteDiagnostic}; -use crate::suite::common::{Header, process_status}; - -pub(crate) struct EvalGraphReadEntitySuite; - -impl Suite for EvalGraphReadEntitySuite { - fn name(&self) -> &'static str { - "eval/graph/read/entity" - } - - fn description(&self) -> &'static str { - "Entity read operations in graph evaluation" - } - - fn run<'heap>( - &self, - RunContext { - heap, diagnostics, .. - }: RunContext<'_, 'heap>, - mut expr: Expr<'heap>, - ) -> Result { - let mut environment = Environment::new(heap); - let registry = ModuleRegistry::new(&environment); - let interner = Interner::new(heap); - let mut context = HirContext::new(&interner, ®istry); - - let mut output = String::new(); - - let result = hashql_ast::lowering::lower( - heap.intern_symbol("::main"), - &mut expr, - &environment, - ®istry, - ); - let types = process_status(diagnostics, result)?; - - let node = process_status(diagnostics, NodeData::from_ast(expr, &mut context, &types))?; - let node = process_status( - diagnostics, - hashql_hir::lower::lower(node, &types, &mut environment, &mut context), - )?; - - let formatter = Formatter::new(heap); - let mut formatter = NodeFormatter::new( - &formatter, - &environment, - &context, - NodeFormatterOptions { - r#type: TypeFormatterOptions::terse(), - }, - ); - - let _ = writeln!( - output, - "{}\n\n{}", - Header::new("HIR"), - formatter.render(node, RenderOptions::default().with_plain()), - ); - - let user_id_value = Value::Opaque(Opaque::new( - heap.intern_symbol("::graph::types::knowledge::entity::EntityUuid"), - Value::Opaque(Opaque::new( - heap.intern_symbol("::core::uuid::Uuid"), - Value::Primitive(Primitive::String(value::String::new( - heap.intern_symbol("e2851dbb-7376-4959-9bca-f72cafc4448f"), - ))), - )), - )); - - let mut inputs = FastHashMap::default(); - inputs.insert( - heap.intern_symbol("example_integer"), - Value::Primitive(Primitive::Integer(value::Integer::new_unchecked( - heap.intern_symbol("42"), - ))), - ); - inputs.insert(heap.intern_symbol("user_id"), user_id_value.clone()); - inputs.insert( - heap.intern_symbol("user"), - Value::Struct(Struct::from_fields( - heap, - [(heap.intern_symbol("id"), user_id_value.clone())], - )), - ); - inputs.insert( - heap.intern_symbol("user_ids"), - Value::List(List::from_values([user_id_value])), - ); - - let mut compiler = GraphReadCompiler::new(heap, &inputs); - compiler.visit_node(node); - let residual = process_status(diagnostics, compiler.finish())?; - - let FilterSlice::Entity { range } = residual.output[&node.id].clone(); - - let filters = residual.filters.entity(range); - - #[expect(clippy::use_debug)] - let _ = writeln!( - output, - "\n{}\n\n{:#?}", - Header::new("Entity Filter"), - filters - ); - - Ok(output) - } -} diff --git a/libs/@local/hashql/compiletest/src/suite/mod.rs b/libs/@local/hashql/compiletest/src/suite/mod.rs index 6326eeeeebb..1507ea45f0b 100644 --- a/libs/@local/hashql/compiletest/src/suite/mod.rs +++ b/libs/@local/hashql/compiletest/src/suite/mod.rs @@ -9,7 +9,6 @@ mod ast_lowering_special_form_expander; mod ast_lowering_type_definition_extractor; mod ast_lowering_type_extractor; pub(crate) mod common; -mod eval_graph_read_entity; mod eval_postgres; mod hir_lower_alias_replacement; mod hir_lower_checking; @@ -49,8 +48,7 @@ use self::{ ast_lowering_sanitizer::AstLoweringSanitizerSuite, ast_lowering_special_form_expander::AstLoweringSpecialFormExpanderSuite, ast_lowering_type_definition_extractor::AstLoweringTypeDefinitionExtractorSuite, - ast_lowering_type_extractor::AstLoweringTypeExtractorSuite, - eval_graph_read_entity::EvalGraphReadEntitySuite, eval_postgres::EvalPostgres, + ast_lowering_type_extractor::AstLoweringTypeExtractorSuite, eval_postgres::EvalPostgres, hir_lower_alias_replacement::HirLowerAliasReplacementSuite, hir_lower_checking::HirLowerTypeCheckingSuite, hir_lower_ctor::HirLowerCtorSuite, hir_lower_graph_hoisting::HirLowerGraphHoistingSuite, @@ -151,7 +149,6 @@ const SUITES: &[&dyn Suite] = &[ &AstLoweringSpecialFormExpanderSuite, &AstLoweringTypeDefinitionExtractorSuite, &AstLoweringTypeExtractorSuite, - &EvalGraphReadEntitySuite, &EvalPostgres, &HirLowerAliasReplacementSuite, &HirLowerCtorSuite, diff --git a/libs/@local/hashql/eval/Cargo.toml b/libs/@local/hashql/eval/Cargo.toml index 8d865a81ac9..5721f726761 100644 --- a/libs/@local/hashql/eval/Cargo.toml +++ b/libs/@local/hashql/eval/Cargo.toml @@ -18,42 +18,38 @@ hashql-mir = { workspace = true, public = true } # Private workspace dependencies hashql-core = { workspace = true } -type-system = { workspace = true, optional = true } # Private third-party dependencies -bytes.workspace = true +bytes = {workspace = true} derive_more = { workspace = true, features = ["display"] } futures-lite = "2.6.1" -postgres-protocol.workspace = true +postgres-protocol = {workspace = true} postgres-types = { workspace = true, features = ["uuid-1"] } serde = { workspace = true } serde_json = { workspace = true, features = ["raw_value"] } simple-mermaid = { workspace = true } -tokio.workspace = true -tokio-postgres.workspace = true +tokio ={workspace = true} +tokio-postgres = {workspace = true} tokio-util = { workspace = true, features = ["rt"] } url.workspace = true uuid.workspace = true [dev-dependencies] -error-stack.workspace = true +error-stack = {workspace = true} hash-graph-authorization = { workspace = true } -hash-graph-store.workspace = true -hash-graph-test-data.workspace = true +hash-graph-store = {workspace = true} +hash-graph-test-data = {workspace = true} hashql-compiletest = { workspace = true } hashql-diagnostics = { workspace = true, features = ["render"] } insta = { workspace = true } libtest-mimic = { workspace = true } regex = { workspace = true } similar-asserts = { workspace = true } -sqruff-lib = "0.37.3" -sqruff-lib-core = "0.37.3" +sqruff-lib = {workspace = true} +sqruff-lib-core = {workspace = true} testcontainers = { workspace = true, features = ["reusable-containers"] } testcontainers-modules = { workspace = true, features = ["postgres"] } -[features] -graph = ["dep:hash-graph-store", "dep:type-system"] - [lints] workspace = true diff --git a/libs/@local/hashql/eval/src/error.rs b/libs/@local/hashql/eval/src/error.rs index bdfc6b5e556..03e40d3634d 100644 --- a/libs/@local/hashql/eval/src/error.rs +++ b/libs/@local/hashql/eval/src/error.rs @@ -2,9 +2,8 @@ use alloc::borrow::Cow; use hashql_core::span::SpanId; use hashql_diagnostics::{Diagnostic, DiagnosticIssues, Severity, category::DiagnosticCategory}; +use hashql_mir::interpret::error::InterpretDiagnosticCategory; -#[cfg(feature = "graph")] -use crate::graph::error::GraphCompilerDiagnosticCategory; use crate::postgres::error::PostgresDiagnosticCategory; pub type EvalDiagnostic = Diagnostic; @@ -12,9 +11,8 @@ pub type EvalDiagnosticIssues = DiagnosticIssues Option<&dyn DiagnosticCategory> { match self { - #[cfg(feature = "graph")] - Self::Graph(graph) => Some(graph), Self::Postgres(postgres) => Some(postgres), + Self::Interpret(interpret) => Some(interpret), } } } diff --git a/libs/@local/hashql/eval/src/lib.rs b/libs/@local/hashql/eval/src/lib.rs index 16a54ff5638..6c4dd4ddc2d 100644 --- a/libs/@local/hashql/eval/src/lib.rs +++ b/libs/@local/hashql/eval/src/lib.rs @@ -12,7 +12,6 @@ // Library Features allocator_api, iter_array_chunks, - iterator_try_collect, maybe_uninit_fill, impl_trait_in_assoc_type, try_blocks @@ -25,8 +24,6 @@ extern crate alloc; pub mod context; pub mod error; -#[cfg(feature = "graph")] -pub mod graph; pub mod orchestrator; pub mod postgres; diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/.spec.toml b/libs/@local/hashql/eval/tests/ui/graph/read/entity/.spec.toml deleted file mode 100644 index cb4dba79907..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/.spec.toml +++ /dev/null @@ -1 +0,0 @@ -suite = "eval/graph/read/entity" diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-and.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-and.jsonc deleted file mode 100644 index 3f92710595d..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-and.jsonc +++ /dev/null @@ -1,20 +0,0 @@ -//@ run: pass -//@ description: Test arithmetic comparison operations (>, >=, <, <=) with numeric literals -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["&&", - [">", {"#literal": 1}, {"#literal": 2}], - ["&&", - [">=", {"#literal": 1}, {"#literal": 2}], - ["&&", - ["<", {"#literal": 1}, {"#literal": 2}], - ["<=", {"#literal": 1}, {"#literal": 2}] - ] - ] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-and.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-and.stdout deleted file mode 100644 index cfa00703cf4..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-and.stdout +++ /dev/null @@ -1,155 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %10 = ::graph::tmp::decision_time_now(), - %11 = %10() - in - %11, - %2 = thunk -> let %12 = 1 > 2 in %12, - %8 = thunk -> - let %13 = %2(), - %14 = if %13 - then - let %3 = 1 >= 2, - %7 = if %3 - then - let %4 = 1 < 2, - %6 = if %4 - then let %5 = 1 <= 2 in %5 - else false - in - %6 - else false - in - %7 - else false - in - %14, - %9 = thunk -> - let %15 = %1(), - %17 = ::graph::head::entities(%15) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %16 = %8() in %16 - ) - |> ::graph::tail::collect - in - %17 -in -%9 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Greater( - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 1 (1 digits, 1 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 2 (1 digits, 2 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - ), - GreaterOrEqual( - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 1 (1 digits, 1 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 2 (1 digits, 2 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - ), - Less( - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 1 (1 digits, 1 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 2 (1 digits, 2 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - ), - LessOrEqual( - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 1 (1 digits, 1 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 2 (1 digits, 2 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-or.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-or.jsonc deleted file mode 100644 index 6dfc94df30f..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-or.jsonc +++ /dev/null @@ -1,20 +0,0 @@ -//@ run: pass -//@ description: Test arithmetic comparison operations (>, >=, <, <=) with numeric literals -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["||", - [">", {"#literal": 1}, {"#literal": 2}], - ["||", - [">=", {"#literal": 1}, {"#literal": 2}], - ["||", - ["<", {"#literal": 1}, {"#literal": 2}], - ["<=", {"#literal": 1}, {"#literal": 2}] - ] - ] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-or.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-or.stdout deleted file mode 100644 index 655b53fa3c6..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-or.stdout +++ /dev/null @@ -1,159 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %10 = ::graph::tmp::decision_time_now(), - %11 = %10() - in - %11, - %2 = thunk -> let %12 = 1 > 2 in %12, - %8 = thunk -> - let %13 = %2(), - %14 = if %13 - then true - else - let %3 = 1 >= 2, - %7 = if %3 - then true - else - let %4 = 1 < 2, - %6 = if %4 - then true - else let %5 = 1 <= 2 in %5 - in - %6 - in - %7 - in - %14, - %9 = thunk -> - let %15 = %1(), - %17 = ::graph::head::entities(%15) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %16 = %8() in %16 - ) - |> ::graph::tail::collect - in - %17 -in -%9 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Any( - [ - Greater( - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 1 (1 digits, 1 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 2 (1 digits, 2 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - ), - GreaterOrEqual( - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 1 (1 digits, 1 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 2 (1 digits, 2 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - ), - Less( - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 1 (1 digits, 1 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 2 (1 digits, 2 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - ), - LessOrEqual( - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 1 (1 digits, 1 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 2 (1 digits, 2 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - ), - ], - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/boolean-literal.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/boolean-literal.jsonc deleted file mode 100644 index 89a08aa3b0c..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/boolean-literal.jsonc +++ /dev/null @@ -1,11 +0,0 @@ -//@ run: pass -//@ description: Test boolean literal values in filter expressions -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - {"#literal": true} - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/boolean-literal.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/boolean-literal.stdout deleted file mode 100644 index d51096d9ac3..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/boolean-literal.stdout +++ /dev/null @@ -1,35 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %3 = ::graph::tmp::decision_time_now(), - %4 = %3() - in - %4, - %2 = thunk -> - let %5 = %1(), - %6 = ::graph::head::entities(%5) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> true) - |> ::graph::tail::collect - in - %6 -in -%2 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/call-identity.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/call-identity.jsonc deleted file mode 100644 index bbfb7bff1f7..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/call-identity.jsonc +++ /dev/null @@ -1,18 +0,0 @@ -//@ run: fail -//@ description: Function calls not supported in filter expressions -// prettier-ignore -["let", "identity", ["fn", {"#tuple": ["T"]}, {"#struct": {"value": "T"}}, "T", "value"], -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - ["identity", "vertex.metadata.record_id.entity_id.entity_uuid"], - //~^ ERROR Function call not supported here - ["::graph::types::knowledge::entity::EntityUuid", - ["::core::uuid::Uuid", { "#literal": "e2851dbb-7376-4959-9bca-f72cafc4448f" }] - ] - ] - ] - ] -]] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/call-identity.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/call-identity.stderr deleted file mode 100644 index 0a9deb20c50..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/call-identity.stderr +++ /dev/null @@ -1,13 +0,0 @@ -error[graph-read-compiler::call-unsupported]: Function calls not supported - ╭▸ - 9 │ ┌ ["==", -10 │ │ ["identity", "vertex.metadata.record_id.entity_id.entity_uuid"], - │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Function call not supported here -11 │ │ //~^ ERROR Function call not supported here -12 │ │ ["::graph::types::knowledge::entity::EntityUuid", - ‡ │ -15 │ │ ] - │ └───────┘ ... within this filter expression - │ - ├ help: Filter expressions do not currently support function calls. Move the function call outside the filter expression, assign the result to a variable, and use that variable in the filter instead. - ╰ note: Function calls in filter expressions are not yet implemented. This feature may be added in future versions for specific categories of pure functions. Progress is tracked in https://linear.app/hash/issue/H-4913/hashql-implement-vm. \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/complex-object-error.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/complex-object-error.jsonc deleted file mode 100644 index 6e6495ec3fa..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/complex-object-error.jsonc +++ /dev/null @@ -1,16 +0,0 @@ -//@ run: fail -//@ description: Complex objects cannot be queried directly in filter expressions -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - "vertex.metadata.record_id.entity_id", - //~^ ERROR Cannot query against this complex object - "vertex.metadata.record_id.entity_id" - //~^ ERROR Cannot query against this complex object - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/complex-object-error.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/complex-object-error.stderr deleted file mode 100644 index b687aef6b27..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/complex-object-error.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error[graph-read-compiler::path-conversion]: Cannot query against complex object - ╭▸ - 8 │ ┌ ["==", - 9 │ │ "vertex.metadata.record_id.entity_id", -10 │ │ //~^ ERROR Cannot query against this complex object -11 │ │ "vertex.metadata.record_id.entity_id" - │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Cannot query against this complex object -12 │ │ //~^ ERROR Cannot query against this complex object -13 │ │ ] - │ └───────┘ ... within this filter expression - │ - ├ help: Filter expressions can only query against simple scalar properties that map to database columns, not complex objects. Use individual properties of the object instead (e.g., `entity.metadata.record_id.entity_id.entity_uuid` instead of `entity.metadata.record_id`). - ╰ note: This is a temporary limitation of the current query compiler. Support for querying against complex objects in filter expressions is being tracked in https://linear.app/hash/issue/H-4911/hashql-allow-for-querying-against-complex-objects. - -error[graph-read-compiler::path-conversion]: Cannot query against complex object - ╭▸ - 8 │ ┌ ["==", - 9 │ │ "vertex.metadata.record_id.entity_id", - │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Cannot query against this complex object -10 │ │ //~^ ERROR Cannot query against this complex object -11 │ │ "vertex.metadata.record_id.entity_id" -12 │ │ //~^ ERROR Cannot query against this complex object -13 │ │ ] - │ └───────┘ ... within this filter expression - │ - ├ help: Filter expressions can only query against simple scalar properties that map to database columns, not complex objects. Use individual properties of the object instead (e.g., `entity.metadata.record_id.entity_id.entity_uuid` instead of `entity.metadata.record_id`). - ╰ note: This is a temporary limitation of the current query compiler. Support for querying against complex objects in filter expressions is being tracked in https://linear.app/hash/issue/H-4911/hashql-allow-for-querying-against-complex-objects. \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/computed-path-indexing-error.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/computed-path-indexing-error.jsonc deleted file mode 100644 index 1b731a38bb3..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/computed-path-indexing-error.jsonc +++ /dev/null @@ -1,18 +0,0 @@ -//@ run: fail -//@ description: Test error when using computed paths for indexing in filter expressions -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - {"#literal": 2}, - ["[]", - ["input", "user_ids", "Dict<::graph::types::knowledge::entity::EntityUuid, Integer>"], - "vertex.metadata.record_id.entity_id.entity_uuid" - //~^ ERROR Cannot use computed value as index - ] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/computed-path-indexing-error.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/computed-path-indexing-error.stderr deleted file mode 100644 index 015461c77a9..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/computed-path-indexing-error.stderr +++ /dev/null @@ -1,15 +0,0 @@ -error[graph-read-compiler::path-indexing-unsupported]: Indexing through traversal paths not supported - ╭▸ - 8 │ ┌ ["==", - 9 │ │ {"#literal": 2}, -10 │ │ ["[]", -11 │ │ ["input", "user_ids", "Dict<::graph::types::knowledge::entity::EntityUuid, Integer>"], - │ │ ───────────────────────────────────────────────────────────────────────────────────── ... when indexing this value -12 │ │ "vertex.metadata.record_id.entity_id.entity_uuid" - │ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Cannot use computed value as index - ‡ │ -15 │ │ ] - │ └───────┘ ... within this filter expression - │ - ├ help: Dynamic indexing using database values is not currently supported in filter expressions. Use a literal value like `["key"]` or `[0]` instead of computed values like `[entity.id]`. This limitation exists because such operations are complex to translate into database queries. - ╰ note: This is a temporary limitation of the database query compiler. Support for dynamic indexing using computed values in filter expressions is being tracked in https://linear.app/hash/issue/H-4914/hashql-support-indexing-into-collections-based-on-query-paths. \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/constructor-call-none.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/constructor-call-none.jsonc deleted file mode 100644 index 521b9496e92..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/constructor-call-none.jsonc +++ /dev/null @@ -1,14 +0,0 @@ -//@ run: pass -//@ description: Test constructor calls with None type in filter expressions -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - ["None"], - ["None"] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/constructor-call-none.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/constructor-call-none.stdout deleted file mode 100644 index 7c33f3c43dc..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/constructor-call-none.stdout +++ /dev/null @@ -1,55 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %8 = ::graph::tmp::decision_time_now(), - %9 = %8() - in - %9, - %2 = thunk -> let %10 = ::core::option::None in %10, - %3 = thunk -> let %11 = ::core::option::None in %11, - %4 = thunk -> - let %12 = %2(), - %13 = %12() - in - %13, - %5 = thunk -> - let %14 = %3(), - %15 = %14() - in - %15, - %6 = thunk -> - let %16 = %4(), - %17 = %5(), - %18 = %16 == %17 - in - %18, - %7 = thunk -> - let %19 = %1(), - %21 = ::graph::head::entities(%19) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %20 = %6() in %20 - ) - |> ::graph::tail::collect - in - %21 -in -%7 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Parameter { - parameter: Any( - Null, - ), - convert: None, - }, - Parameter { - parameter: Any( - Null, - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/equality-comparison.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/equality-comparison.jsonc deleted file mode 100644 index 03f50f7fbce..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/equality-comparison.jsonc +++ /dev/null @@ -1,14 +0,0 @@ -//@ run: pass -//@ description: Test equality (==) and inequality (!=) comparison operators -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["||", - ["==", {"#literal": true}, {"#literal": false}], - ["!=", {"#literal": true}, {"#literal": false}] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/equality-comparison.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/equality-comparison.stdout deleted file mode 100644 index 9e951a07522..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/equality-comparison.stdout +++ /dev/null @@ -1,63 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %6 = ::graph::tmp::decision_time_now(), - %7 = %6() - in - %7, - %2 = thunk -> let %8 = true == false in %8, - %4 = thunk -> - let %9 = %2(), - %10 = if %9 - then true - else let %3 = true != false in %3 - in - %10, - %5 = thunk -> - let %11 = %1(), - %13 = ::graph::head::entities(%11) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %12 = %4() in %12 - ) - |> ::graph::tail::collect - in - %13 -in -%5 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Any( - [ - Equal( - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - Parameter { - parameter: Boolean( - false, - ), - convert: None, - }, - ), - NotEqual( - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - Parameter { - parameter: Boolean( - false, - ), - convert: None, - }, - ), - ], - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-struct.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-struct.jsonc deleted file mode 100644 index 756ea98cb76..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-struct.jsonc +++ /dev/null @@ -1,14 +0,0 @@ -//@ run: pass -//@ description: Field access on values should succeed -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - [".", {"#struct": {"a": { "#literal": 1 }}}, "a"], - {"#literal": 1} - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-struct.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-struct.stdout deleted file mode 100644 index ee451861a44..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-struct.stdout +++ /dev/null @@ -1,57 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %5 = ::graph::tmp::decision_time_now(), - %6 = %5() - in - %6, - %2 = thunk -> let %7 = (a: 1) in %7, - %3 = thunk -> - let %8 = %2(), - %9 = %8.a == 1 - in - %9, - %4 = thunk -> - let %10 = %1(), - %12 = ::graph::head::entities(%10) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %11 = %3() in %11 - ) - |> ::graph::tail::collect - in - %12 -in -%4 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 1 (1 digits, 1 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 1 (1 digits, 1 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-tuple.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-tuple.jsonc deleted file mode 100644 index 33593ee82ec..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-tuple.jsonc +++ /dev/null @@ -1,14 +0,0 @@ -//@ run: pass -//@ description: Field access on values should succeed -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - [".", {"#tuple": [{ "#literal": 1 }]}, { "#literal": 0 }], - {"#literal": 1} - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-tuple.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-tuple.stdout deleted file mode 100644 index 0f9c4a324d9..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-tuple.stdout +++ /dev/null @@ -1,57 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %5 = ::graph::tmp::decision_time_now(), - %6 = %5() - in - %6, - %2 = thunk -> let %7 = (1,) in %7, - %3 = thunk -> - let %8 = %2(), - %9 = %8.0 == 1 - in - %9, - %4 = thunk -> - let %10 = %1(), - %12 = ::graph::head::entities(%10) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %11 = %3() in %11 - ) - |> ::graph::tail::collect - in - %12 -in -%4 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 1 (1 digits, 1 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - Parameter { - parameter: Decimal( - Real( - FBig { - significand: 1 (1 digits, 1 bits), - exponent: 10 ^ 0, - precision: 1, - rounding: HalfAway, - }, - ), - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-key.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-key.jsonc deleted file mode 100644 index 09a6bd2a85d..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-key.jsonc +++ /dev/null @@ -1,18 +0,0 @@ -//@ run: fail -//@ description: Paths inside data constructs are unsupported -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - ["[]", - {"#dict": [["vertex.metadata.record_id.entity_id.entity_uuid", {"#literal": "a"}]]}, - //~^ ERROR path expressions are not supported in data constructs - ["::graph::types::knowledge::entity::EntityUuid", ["::core::uuid::Uuid", {"#literal": "a"}]] - ], - "vertex.metadata.record_id.entity_id.entity_uuid" - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-key.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-key.stderr deleted file mode 100644 index 10261ec0ebd..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-key.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error[graph-read-compiler::path-in-data-construct-unsupported]: Path in data construct unsupported - ╭▸ -10 │ {"#dict": [["vertex.metadata.record_id.entity_id.entity_uuid", {"#literal": "a"}]]}, - │ ━━━━━━ path expressions are not supported in data constructs - │ - ├ help: rewrite the logic to avoid path expressions in data constructs - ╰ note: path expressions within complex data constructs (tuples, lists, dictionaries, structs) are not supported by the current query compiler. This is a fundamental limitation that will be addressed in the next-generation compiler \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-value.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-value.jsonc deleted file mode 100644 index ceb60dd5927..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-value.jsonc +++ /dev/null @@ -1,15 +0,0 @@ -//@ run: fail -//@ description: Paths inside data constructs are unsupported -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - ["[]", {"#dict": {"a": "vertex.metadata.record_id.entity_id.entity_uuid"}}, {"#literal": "a"}], - //~^ ERROR path expressions are not supported in data constructs - "vertex.metadata.record_id.entity_id.entity_uuid" - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-value.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-value.stderr deleted file mode 100644 index a8b654b103d..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-dict-value.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error[graph-read-compiler::path-in-data-construct-unsupported]: Path in data construct unsupported - ╭▸ -9 │ ["[]", {"#dict": {"a": "vertex.metadata.record_id.entity_id.entity_uuid"}}, {"#literal": "a"}], - │ ━━━━━━ path expressions are not supported in data constructs - │ - ├ help: rewrite the logic to avoid path expressions in data constructs - ╰ note: path expressions within complex data constructs (tuples, lists, dictionaries, structs) are not supported by the current query compiler. This is a fundamental limitation that will be addressed in the next-generation compiler \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-list.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-list.jsonc deleted file mode 100644 index 3d7a94d26e7..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-list.jsonc +++ /dev/null @@ -1,15 +0,0 @@ -//@ run: fail -//@ description: Paths inside data constructs are unsupported -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - ["[]", {"#list": ["vertex.metadata.record_id.entity_id.entity_uuid"]}, { "#literal": 0 }], - //~^ ERROR path expressions are not supported in data constructs - "vertex.metadata.record_id.entity_id.entity_uuid" - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-list.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-list.stderr deleted file mode 100644 index e731890cd1d..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-list.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error[graph-read-compiler::path-in-data-construct-unsupported]: Path in data construct unsupported - ╭▸ -9 │ ["[]", {"#list": ["vertex.metadata.record_id.entity_id.entity_uuid"]}, { "#literal": 0 }], - │ ━━━━━━ path expressions are not supported in data constructs - │ - ├ help: rewrite the logic to avoid path expressions in data constructs - ╰ note: path expressions within complex data constructs (tuples, lists, dictionaries, structs) are not supported by the current query compiler. This is a fundamental limitation that will be addressed in the next-generation compiler \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct-entry.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct-entry.jsonc deleted file mode 100644 index 14fc38dfe05..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct-entry.jsonc +++ /dev/null @@ -1,15 +0,0 @@ -//@ run: fail -//@ description: Mentions of the variable inside data constructs are unsupported -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - [".", [".", [".", [".", [".", {"#struct": {"a": "vertex"}}, "a"], "metadata"], "record_id"], "entity_id"], "entity_uuid"], - //~^ ERROR path expressions are not supported in data constructs - "vertex.metadata.record_id.entity_id.entity_uuid" - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct-entry.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct-entry.stderr deleted file mode 100644 index 88ba6294db9..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct-entry.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error[graph-read-compiler::path-in-data-construct-unsupported]: Path in data construct unsupported - ╭▸ -9 │ [".", [".", [".", [".", [".", {"#struct": {"a": "vertex"}}, "a"], "metadata"], "record_id"], "entity_id"], "entity_uuid"], - │ ━━━━━━ path expressions are not supported in data constructs - │ - ├ help: rewrite the logic to avoid path expressions in data constructs - ╰ note: path expressions within complex data constructs (tuples, lists, dictionaries, structs) are not supported by the current query compiler. This is a fundamental limitation that will be addressed in the next-generation compiler \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct.jsonc deleted file mode 100644 index 5e56de0632e..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct.jsonc +++ /dev/null @@ -1,15 +0,0 @@ -//@ run: fail -//@ description: Paths inside data constructs are unsupported -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - [".", {"#struct": {"a": "vertex.metadata.record_id.entity_id.entity_uuid"}}, "a"], - //~^ ERROR path expressions are not supported in data constructs - "vertex.metadata.record_id.entity_id.entity_uuid" - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct.stderr deleted file mode 100644 index 84a2454f9b8..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-struct.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error[graph-read-compiler::path-in-data-construct-unsupported]: Path in data construct unsupported - ╭▸ -9 │ [".", {"#struct": {"a": "vertex.metadata.record_id.entity_id.entity_uuid"}}, "a"], - │ ━━━━━━ path expressions are not supported in data constructs - │ - ├ help: rewrite the logic to avoid path expressions in data constructs - ╰ note: path expressions within complex data constructs (tuples, lists, dictionaries, structs) are not supported by the current query compiler. This is a fundamental limitation that will be addressed in the next-generation compiler \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-tuple.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-tuple.jsonc deleted file mode 100644 index 806f7e9a819..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-tuple.jsonc +++ /dev/null @@ -1,15 +0,0 @@ -//@ run: fail -//@ description: Paths inside data constructs are unsupported -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - [".", {"#tuple": ["vertex.metadata.record_id.entity_id.entity_uuid"]}, { "#literal": 0 }], - //~^ ERROR path expressions are not supported in data constructs - "vertex.metadata.record_id.entity_id.entity_uuid" - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-tuple.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-tuple.stderr deleted file mode 100644 index 9db41195e8a..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/field-in-tuple.stderr +++ /dev/null @@ -1,7 +0,0 @@ -error[graph-read-compiler::path-in-data-construct-unsupported]: Path in data construct unsupported - ╭▸ -9 │ [".", {"#tuple": ["vertex.metadata.record_id.entity_id.entity_uuid"]}, { "#literal": 0 }], - │ ━━━━━━ path expressions are not supported in data constructs - │ - ├ help: rewrite the logic to avoid path expressions in data constructs - ╰ note: path expressions within complex data constructs (tuples, lists, dictionaries, structs) are not supported by the current query compiler. This is a fundamental limitation that will be addressed in the next-generation compiler \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter-expr.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter-expr.jsonc deleted file mode 100644 index 19775b76b99..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter-expr.jsonc +++ /dev/null @@ -1,18 +0,0 @@ -//@ run: fail -//@ description: Test if filter with branch fails -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - "vertex.metadata.record_id.entity_id.entity_uuid", - ["if", {"#literal": true}, - "vertex.metadata.record_id.entity_id.entity_uuid", - "vertex.metadata.record_id.entity_id.entity_uuid" - ] - //~^ ERROR conditional expressions are not supported in filter contexts - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter-expr.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter-expr.stderr deleted file mode 100644 index 81a40f482bf..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter-expr.stderr +++ /dev/null @@ -1,11 +0,0 @@ -error[graph-read-compiler::branch-unsupported]: Branch construct unsupported - ╭▸ -10 │ ┏ ["if", {"#literal": true}, -11 │ ┃ "vertex.metadata.record_id.entity_id.entity_uuid", -12 │ ┃ "vertex.metadata.record_id.entity_id.entity_uuid" -13 │ ┃ ] - │ ┗━━━━━━━━━┛ conditional expressions are not supported in filter contexts - │ - ├ help: rewrite the logic to avoid conditional statements - ├ note: conditional statements (`if`/`else`) are not supported by the current query compiler. This is a fundamental limitation that will be addressed in the next-generation compiler - ╰ note: conditional logic must be handled outside the query or using other language constructs \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter.jsonc deleted file mode 100644 index 376b951cac4..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter.jsonc +++ /dev/null @@ -1,11 +0,0 @@ -//@ run: pass -//@ description: Test if filter with branch fails -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["if", {"#literal": true}, {"#literal": true}, {"#literal": false}] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter.stdout deleted file mode 100644 index 1f1682697fe..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/if-filter.stdout +++ /dev/null @@ -1,56 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %4 = ::graph::tmp::decision_time_now(), - %5 = %4() - in - %5, - %2 = thunk -> let %6 = if true then true else false in %6, - %3 = thunk -> - let %7 = %1(), - %9 = ::graph::head::entities(%7) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %8 = %2() in %8 - ) - |> ::graph::tail::collect - in - %9 -in -%3 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Any( - [ - Equal( - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - ), - Equal( - Parameter { - parameter: Boolean( - false, - ), - convert: None, - }, - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - ), - ], - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-field-access.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-field-access.jsonc deleted file mode 100644 index 8d90be06e07..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-field-access.jsonc +++ /dev/null @@ -1,17 +0,0 @@ -//@ run: pass -//@ description: Test field access on input parameter values -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - "vertex.metadata.record_id.entity_id.entity_uuid", - [".", - ["input", "user", {"#struct": {"id": "::graph::types::knowledge::entity::EntityUuid"}}], - "id" - ] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-field-access.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-field-access.stdout deleted file mode 100644 index 6178ff59e34..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-field-access.stdout +++ /dev/null @@ -1,38 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %5 = ::graph::tmp::decision_time_now(), - %6 = %5() - in - %6, - %2 = thunk -> let %7 = $user in %7, - %4 = thunk -> - let %8 = %1(), - %10 = ::graph::head::entities(%8) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %9 = %2(), - %3 = vertex:0.metadata.record_id.entity_id.entity_uuid == %9.id - in - %3 - ) - |> ::graph::tail::collect - in - %10 -in -%4 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Path { - path: Uuid, - }, - Parameter { - parameter: Text( - "e2851dbb-7376-4959-9bca-f72cafc4448f", - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-index-access.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-index-access.jsonc deleted file mode 100644 index b579e9bee9b..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-index-access.jsonc +++ /dev/null @@ -1,17 +0,0 @@ -//@ run: pass -//@ description: Test index access on input parameter values -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - ["as", "vertex.metadata.record_id.entity_id.entity_uuid", ["|", "::graph::types::knowledge::entity::EntityUuid", "Null"]], - ["[]", - ["input", "user_ids", "List<::graph::types::knowledge::entity::EntityUuid>"], - {"#literal": 0} - ] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-index-access.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-index-access.stdout deleted file mode 100644 index 0a130839985..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-index-access.stdout +++ /dev/null @@ -1,40 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %6 = ::graph::tmp::decision_time_now(), - %7 = %6() - in - %7, - %2 = thunk -> let %8 = $user_ids in %8, - %3 = thunk -> 0, - %5 = thunk -> - let %9 = %1(), - %12 = ::graph::head::entities(%9) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %10 = %2(), - %11 = %3(), - %4 = vertex:0.metadata.record_id.entity_id.entity_uuid == %10[%11] - in - %4 - ) - |> ::graph::tail::collect - in - %12 -in -%5 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Path { - path: Uuid, - }, - Parameter { - parameter: Text( - "e2851dbb-7376-4959-9bca-f72cafc4448f", - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-parameter.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-parameter.jsonc deleted file mode 100644 index fdeac99dfaf..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-parameter.jsonc +++ /dev/null @@ -1,14 +0,0 @@ -//@ run: pass -//@ description: Test input parameter usage in entity filter expressions -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - "vertex.metadata.record_id.entity_id.entity_uuid", - ["input", "user_id", "::graph::types::knowledge::entity::EntityUuid"] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-parameter.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-parameter.stdout deleted file mode 100644 index ac94cedaa27..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/input-parameter.stdout +++ /dev/null @@ -1,38 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %5 = ::graph::tmp::decision_time_now(), - %6 = %5() - in - %6, - %2 = thunk -> let %7 = $user_id in %7, - %4 = thunk -> - let %8 = %1(), - %10 = ::graph::head::entities(%8) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %9 = %2(), - %3 = vertex:0.metadata.record_id.entity_id.entity_uuid == %9 - in - %3 - ) - |> ::graph::tail::collect - in - %10 -in -%4 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Path { - path: Uuid, - }, - Parameter { - parameter: Text( - "e2851dbb-7376-4959-9bca-f72cafc4448f", - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-field-access-error.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-field-access-error.jsonc deleted file mode 100644 index 6def4aa9358..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-field-access-error.jsonc +++ /dev/null @@ -1,20 +0,0 @@ -//@ run: fail -//@ description: Test internal compiler error on invalid field access -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - "vertex.metadata.record_id.entity_id.entity_uuid", - // This is one of the few times where we can actually test if we hit a compiler error, - // because we don't typecheck the input in this test (yet) - [".", - ["input", "user", {"#struct": {"ID": "::graph::types::knowledge::entity::EntityUuid"}}], - "ID" - //~^ INTERNAL COMPILER ERROR Field access for `ID` failed unexpectedly - ] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-field-access-error.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-field-access-error.stderr deleted file mode 100644 index c4ea1fa086b..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-field-access-error.stderr +++ /dev/null @@ -1,17 +0,0 @@ -error[graph-read-compiler::field-access-internal-error]: Internal error during field access - ╭▸ -13 │ ["input", "user", {"#struct": {"ID": "::graph::types::knowledge::entity::EntityUuid"}}], - │ ─────────────────────────────────────────────────────────────────────────────────────── ... on this value -14 │ "ID" - │ ━━ Field access for `ID` failed unexpectedly - │ - ├ help: This is an internal compiler error. The field access should have been validated by the type checker, but the operation failed during compilation. Please report this as a bug with the code that triggered this error. - ├ note: This error indicates a bug in the type checker or compiler. The field access was expected to succeed based on type information, but failed during evaluation. - ├ note: Internal error that occurred: Field `ID` not found - ├ help: This is a bug in the compiler, not an issue with your code. - ├ help: Please report this issue along with a minimal code example that reproduces the error. - ╰ note: Internal compiler errors indicate a bug in the compiler itself that needs to be fixed. - - We would appreciate if you could file a GitHub or Linear issue and reference this error. - - When reporting this issue, please include your query, any relevant type definitions, and the complete error message shown above. \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-index-access-error.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-index-access-error.jsonc deleted file mode 100644 index 6c18f653947..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-index-access-error.jsonc +++ /dev/null @@ -1,20 +0,0 @@ -//@ run: fail -//@ description: Test internal compiler error on invalid index access -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - // This is one of the few times where we can actually test if we hit a compiler error, - // because we don't typecheck the input in this test (yet) - ["==", - {"#literal": null}, - ["[]", - ["input", "user_ids", "Dict"], - {"#literal": "abc"} - //~^ INTERNAL COMPILER ERROR Index access failed unexpectedly - ] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-index-access-error.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-index-access-error.stderr deleted file mode 100644 index 951c1e9818d..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-index-access-error.stderr +++ /dev/null @@ -1,17 +0,0 @@ -error[graph-read-compiler::index-access-internal-error]: Internal error during index access - ╭▸ -13 │ ["input", "user_ids", "Dict"], - │ ──────────────────────────────────────────────────────────────────────────────────── ... on this value -14 │ {"#literal": "abc"} - │ ━━━━━ Index access failed unexpectedly - │ - ├ help: This is an internal compiler error. The index access should have been validated by the type checker, but the operation failed during compilation. Please report this as a bug with the code that triggered this error. - ├ note: This error indicates a bug in the type checker or compiler. The index access was expected to succeed based on type information, but failed during evaluation. - ├ note: Internal error that occurred: Unable to access list with index type `string` - ├ help: This is a bug in the compiler, not an issue with your code. - ├ help: Please report this issue along with a minimal code example that reproduces the error. - ╰ note: Internal compiler errors indicate a bug in the compiler itself that needs to be fixed. - - We would appreciate if you could file a GitHub or Linear issue and reference this error. - - When reporting this issue, please include your query, any relevant type definitions, and the complete error message shown above. \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-vertex-query.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-vertex-query.jsonc deleted file mode 100644 index 977b7d3b5b4..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-vertex-query.jsonc +++ /dev/null @@ -1,16 +0,0 @@ -//@ run: fail -//@ description: Test error when querying vertex directly without specific path -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - "vertex", - //~^ ERROR Cannot query against this complex object - "vertex" - //~^ ERROR Cannot query against this complex object - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-vertex-query.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-vertex-query.stderr deleted file mode 100644 index eb695967950..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/invalid-vertex-query.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error[graph-read-compiler::path-conversion]: Cannot query against complex object - ╭▸ - 8 │ ┌ ["==", - 9 │ │ "vertex", -10 │ │ //~^ ERROR Cannot query against this complex object -11 │ │ "vertex" - │ │ ━━━━━━ Cannot query against this complex object -12 │ │ //~^ ERROR Cannot query against this complex object -13 │ │ ] - │ └───────┘ ... within this filter expression - │ - ├ help: Filter expressions can only query against simple scalar properties that map to database columns, not complex objects. Use individual properties of the object instead (e.g., `entity.metadata.record_id.entity_id.entity_uuid` instead of `entity.metadata.record_id`). - ╰ note: This is a temporary limitation of the current query compiler. Support for querying against complex objects in filter expressions is being tracked in https://linear.app/hash/issue/H-4911/hashql-allow-for-querying-against-complex-objects. - -error[graph-read-compiler::path-conversion]: Cannot query against complex object - ╭▸ - 8 │ ┌ ["==", - 9 │ │ "vertex", - │ │ ━━━━━━ Cannot query against this complex object -10 │ │ //~^ ERROR Cannot query against this complex object -11 │ │ "vertex" -12 │ │ //~^ ERROR Cannot query against this complex object -13 │ │ ] - │ └───────┘ ... within this filter expression - │ - ├ help: Filter expressions can only query against simple scalar properties that map to database columns, not complex objects. Use individual properties of the object instead (e.g., `entity.metadata.record_id.entity_id.entity_uuid` instead of `entity.metadata.record_id`). - ╰ note: This is a temporary limitation of the current query compiler. Support for querying against complex objects in filter expressions is being tracked in https://linear.app/hash/issue/H-4911/hashql-allow-for-querying-against-complex-objects. \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/let-expression.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/let-expression.jsonc deleted file mode 100644 index f8a770a832a..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/let-expression.jsonc +++ /dev/null @@ -1,16 +0,0 @@ -//@ run: pass -//@ description: Test let expressions within filter comparisons -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - ["let", "foo", "vertex.metadata.record_id.entity_id.entity_uuid", "foo"], - ["::graph::types::knowledge::entity::EntityUuid", - ["::core::uuid::Uuid", { "#literal": "e2851dbb-7376-4959-9bca-f72cafc4448f" }] - ] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/let-expression.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/let-expression.stdout deleted file mode 100644 index 87d6d875e58..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/let-expression.stdout +++ /dev/null @@ -1,52 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %2 = thunk -> - let %9 = ::graph::tmp::decision_time_now(), - %10 = %9() - in - %10, - %3 = thunk -> let %11 = ::core::uuid::Uuid in %11, - %4 = thunk -> - let %12 = %3(), - %13 = %12("e2851dbb-7376-4959-9bca-f72cafc4448f") - in - %13, - %5 = thunk -> - let %14 = ::graph::types::knowledge::entity::EntityUuid in %14, - %6 = thunk -> - let %15 = %4(), - %16 = %5(), - %17 = %16(%15) - in - %17, - %8 = thunk -> - let %18 = %2(), - %20 = ::graph::head::entities(%18) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let foo:0 = vertex:0.metadata.record_id.entity_id.entity_uuid, - %19 = %6(), - %7 = foo:0 == %19 - in - %7 - ) - |> ::graph::tail::collect - in - %20 -in -%8 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Path { - path: Uuid, - }, - Parameter { - parameter: Text( - "e2851dbb-7376-4959-9bca-f72cafc4448f", - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/let-propagation.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/let-propagation.jsonc deleted file mode 100644 index 47ee09e18a1..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/let-propagation.jsonc +++ /dev/null @@ -1,17 +0,0 @@ -//@ run: pass -//@ description: Let bindings propagate correctly in graph operations -// prettier-ignore -["let", "foo", {"#literal": 2}, -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - "vertex.metadata.record_id.entity_id.entity_uuid", - ["::graph::types::knowledge::entity::EntityUuid", - ["::core::uuid::Uuid", { "#literal": "e2851dbb-7376-4959-9bca-f72cafc4448f" }] - ] - ] - ] - ] -]] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/let-propagation.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/let-propagation.stdout deleted file mode 100644 index 84b4980b14f..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/let-propagation.stdout +++ /dev/null @@ -1,52 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let foo:0 = thunk -> 2, - %2 = thunk -> - let %9 = ::graph::tmp::decision_time_now(), - %10 = %9() - in - %10, - %3 = thunk -> let %11 = ::core::uuid::Uuid in %11, - %4 = thunk -> - let %12 = %3(), - %13 = %12("e2851dbb-7376-4959-9bca-f72cafc4448f") - in - %13, - %5 = thunk -> - let %14 = ::graph::types::knowledge::entity::EntityUuid in %14, - %6 = thunk -> - let %15 = %4(), - %16 = %5(), - %17 = %16(%15) - in - %17, - %8 = thunk -> - let %18 = %2(), - %20 = ::graph::head::entities(%18) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %19 = %6(), - %7 = vertex:0.metadata.record_id.entity_id.entity_uuid == %19 - in - %7 - ) - |> ::graph::tail::collect - in - %20 -in -%8 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Path { - path: Uuid, - }, - Parameter { - parameter: Text( - "e2851dbb-7376-4959-9bca-f72cafc4448f", - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-and-or.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-and-or.jsonc deleted file mode 100644 index ab6c0535913..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-and-or.jsonc +++ /dev/null @@ -1,11 +0,0 @@ -//@ run: pass -//@ description: Test logical AND and OR operations with boolean literals -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["&&", {"#literal": true}, ["||", {"#literal": true}, {"#literal": false}]] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-and-or.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-and-or.stdout deleted file mode 100644 index 1f0219908f2..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-and-or.stdout +++ /dev/null @@ -1,71 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %5 = ::graph::tmp::decision_time_now(), - %6 = %5() - in - %6, - %3 = thunk -> - let %7 = if true then let %2 = true || false in %2 else false in %7, - %4 = thunk -> - let %8 = %1(), - %10 = ::graph::head::entities(%8) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %9 = %3() in %9 - ) - |> ::graph::tail::collect - in - %10 -in -%4 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - ), - Any( - [ - Equal( - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - ), - Equal( - Parameter { - parameter: Boolean( - false, - ), - convert: None, - }, - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - ), - ], - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-or-and.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-or-and.jsonc deleted file mode 100644 index 39014ed6a57..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-or-and.jsonc +++ /dev/null @@ -1,11 +0,0 @@ -//@ run: pass -//@ description: Test logical AND and OR operations with boolean literals -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["||", {"#literal": true}, ["&&", {"#literal": true}, {"#literal": false}]] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-or-and.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-or-and.stdout deleted file mode 100644 index 188d9fd7c87..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/logical-or-and.stdout +++ /dev/null @@ -1,75 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %5 = ::graph::tmp::decision_time_now(), - %6 = %5() - in - %6, - %3 = thunk -> - let %7 = if true then true else let %2 = true && false in %2 in %7, - %4 = thunk -> - let %8 = %1(), - %10 = ::graph::head::entities(%8) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %9 = %3() in %9 - ) - |> ::graph::tail::collect - in - %10 -in -%4 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Any( - [ - Equal( - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - ), - All( - [ - Equal( - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - ), - Equal( - Parameter { - parameter: Boolean( - false, - ), - convert: None, - }, - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - ), - ], - ), - ], - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-binary-operation.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-binary-operation.jsonc deleted file mode 100644 index 3f58d179224..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-binary-operation.jsonc +++ /dev/null @@ -1,18 +0,0 @@ -//@ run: fail -//@ description: Nested binary operations not supported in filter expressions -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - {"#literal": true}, - ["&&", - //~^ ERROR Operation `&&` not supported here - {"#literal": true}, - {"#literal": true} - ] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-binary-operation.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-binary-operation.stderr deleted file mode 100644 index 4fda2d5c924..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-binary-operation.stderr +++ /dev/null @@ -1,12 +0,0 @@ -error[graph-read-compiler::binary-operation-unsupported]: Binary operations not supported in this context - ╭▸ - 8 │ ┌ ["==", - 9 │ │ {"#literal": true}, -10 │ │ ["&&", - │ │ ━━ Operation `&&` not supported here - ‡ │ -15 │ │ ] - │ └───────┘ ... within this filter expression - │ - ├ help: The `&&` operation can only be used at the top level of filter conditions, not as an operand in other operations. For example, `(a && b) == c` is not allowed, but `(a && b) && (c == d)` is valid. - ╰ note: This is an intentional current limitation to keep expressions simple, but there are plans to remove this restriction in the future to allow more complex expressions. Progress on this enhancement is tracked in https://linear.app/hash/issue/H-4911/hashql-allow-for-querying-against-complex-objects. \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-let-bindings.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-let-bindings.jsonc deleted file mode 100644 index 93cc67cc2f3..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-let-bindings.jsonc +++ /dev/null @@ -1,16 +0,0 @@ -//@ run: pass -//@ description: Test nested let bindings in filter expressions -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["let", "entity_uuid", "vertex.metadata.record_id.entity_id.entity_uuid", - ["let", "user_id", - ["::graph::types::knowledge::entity::EntityUuid", - ["::core::uuid::Uuid", { "#literal": "e2851dbb-7376-4959-9bca-f72cafc4448f" }] - ], - ["==", "entity_uuid", "user_id"]]] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-let-bindings.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-let-bindings.stdout deleted file mode 100644 index 34f7c162b8a..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/nested-let-bindings.stdout +++ /dev/null @@ -1,52 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %3 = thunk -> - let %9 = ::graph::tmp::decision_time_now(), - %10 = %9() - in - %10, - %4 = thunk -> let %11 = ::core::uuid::Uuid in %11, - %5 = thunk -> - let %12 = %4(), - %13 = %12("e2851dbb-7376-4959-9bca-f72cafc4448f") - in - %13, - %6 = thunk -> - let %14 = ::graph::types::knowledge::entity::EntityUuid in %14, - user_id:0 = thunk -> - let %15 = %5(), - %16 = %6(), - %17 = %16(%15) - in - %17, - %8 = thunk -> - let %18 = %3(), - %20 = ::graph::head::entities(%18) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let entity_uuid:0 = vertex:0.metadata.record_id.entity_id.entity_uuid, - %19 = user_id:0(), - %7 = entity_uuid:0 == %19 - in - %7 - ) - |> ::graph::tail::collect - in - %20 -in -%8 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Path { - path: Uuid, - }, - Parameter { - parameter: Text( - "e2851dbb-7376-4959-9bca-f72cafc4448f", - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/scalar-property-filter.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/scalar-property-filter.jsonc deleted file mode 100644 index 19b3da4129b..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/scalar-property-filter.jsonc +++ /dev/null @@ -1,16 +0,0 @@ -//@ run: pass -//@ description: Test basic scalar property filtering by entity UUID -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - "vertex.metadata.record_id.entity_id.entity_uuid", - ["::graph::types::knowledge::entity::EntityUuid", - ["::core::uuid::Uuid", { "#literal": "e2851dbb-7376-4959-9bca-f72cafc4448f" }] - ] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/scalar-property-filter.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/scalar-property-filter.stdout deleted file mode 100644 index c18b764d780..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/scalar-property-filter.stdout +++ /dev/null @@ -1,51 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %8 = ::graph::tmp::decision_time_now(), - %9 = %8() - in - %9, - %2 = thunk -> let %10 = ::core::uuid::Uuid in %10, - %3 = thunk -> - let %11 = %2(), - %12 = %11("e2851dbb-7376-4959-9bca-f72cafc4448f") - in - %12, - %4 = thunk -> - let %13 = ::graph::types::knowledge::entity::EntityUuid in %13, - %5 = thunk -> - let %14 = %3(), - %15 = %4(), - %16 = %15(%14) - in - %16, - %7 = thunk -> - let %17 = %1(), - %19 = ::graph::head::entities(%17) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %18 = %5(), - %6 = vertex:0.metadata.record_id.entity_id.entity_uuid == %18 - in - %6 - ) - |> ::graph::tail::collect - in - %19 -in -%7 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Path { - path: Uuid, - }, - Parameter { - parameter: Text( - "e2851dbb-7376-4959-9bca-f72cafc4448f", - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/top-level-variable.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/top-level-variable.jsonc deleted file mode 100644 index 537f19a78af..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/top-level-variable.jsonc +++ /dev/null @@ -1,13 +0,0 @@ -//@ run: pass -//@ description: Test variables at the top level of filter expressions -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["let", "foo", {"#literal": true}, - "foo" - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/top-level-variable.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/top-level-variable.stdout deleted file mode 100644 index 344f26f1ae2..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/top-level-variable.stdout +++ /dev/null @@ -1,38 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %2 = thunk -> - let %4 = ::graph::tmp::decision_time_now(), - %5 = %4() - in - %5, - foo:0 = thunk -> true, - %3 = thunk -> - let %6 = %2(), - %8 = ::graph::head::entities(%6) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %7 = foo:0() in %7 - ) - |> ::graph::tail::collect - in - %8 -in -%3 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/top-type-assertion.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/top-type-assertion.jsonc deleted file mode 100644 index d2011129473..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/top-type-assertion.jsonc +++ /dev/null @@ -1,11 +0,0 @@ -//@ run: pass -//@ description: Test type assertions (is operator) in filter expressions -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["as", {"#literal": true}, "Boolean"] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/top-type-assertion.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/top-type-assertion.stdout deleted file mode 100644 index d51096d9ac3..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/top-type-assertion.stdout +++ /dev/null @@ -1,35 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %3 = ::graph::tmp::decision_time_now(), - %4 = %3() - in - %4, - %2 = thunk -> - let %5 = %1(), - %6 = ::graph::head::entities(%5) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> true) - |> ::graph::tail::collect - in - %6 -in -%2 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - Parameter { - parameter: Boolean( - true, - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/type-assertion-in-comparison.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/type-assertion-in-comparison.jsonc deleted file mode 100644 index f70cb592e89..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/type-assertion-in-comparison.jsonc +++ /dev/null @@ -1,19 +0,0 @@ -//@ run: pass -//@ description: Test type assertions within comparison operations -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - "vertex.metadata.record_id.entity_id.entity_uuid", - ["as", - ["::graph::types::knowledge::entity::EntityUuid", - ["::core::uuid::Uuid", { "#literal": "e2851dbb-7376-4959-9bca-f72cafc4448f" }] - ], - "::graph::types::knowledge::entity::EntityUuid" - ] - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/type-assertion-in-comparison.stdout b/libs/@local/hashql/eval/tests/ui/graph/read/entity/type-assertion-in-comparison.stdout deleted file mode 100644 index c18b764d780..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/type-assertion-in-comparison.stdout +++ /dev/null @@ -1,51 +0,0 @@ -════ HIR ═══════════════════════════════════════════════════════════════════════ - -let %1 = thunk -> - let %8 = ::graph::tmp::decision_time_now(), - %9 = %8() - in - %9, - %2 = thunk -> let %10 = ::core::uuid::Uuid in %10, - %3 = thunk -> - let %11 = %2(), - %12 = %11("e2851dbb-7376-4959-9bca-f72cafc4448f") - in - %12, - %4 = thunk -> - let %13 = ::graph::types::knowledge::entity::EntityUuid in %13, - %5 = thunk -> - let %14 = %3(), - %15 = %4(), - %16 = %15(%14) - in - %16, - %7 = thunk -> - let %17 = %1(), - %19 = ::graph::head::entities(%17) - |> ::graph::body::filter((vertex:0: Entity): Boolean -> - let %18 = %5(), - %6 = vertex:0.metadata.record_id.entity_id.entity_uuid == %18 - in - %6 - ) - |> ::graph::tail::collect - in - %19 -in -%7 - -════ Entity Filter ═════════════════════════════════════════════════════════════ - -[ - Equal( - Path { - path: Uuid, - }, - Parameter { - parameter: Text( - "e2851dbb-7376-4959-9bca-f72cafc4448f", - ), - convert: None, - }, - ), -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-closure.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-closure.jsonc deleted file mode 100644 index 655f1a460bf..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-closure.jsonc +++ /dev/null @@ -1,17 +0,0 @@ -//@ run: fail -//@ description: Closures not supported in filter expressions -// prettier-ignore -["let", "identity", ["fn", {"#tuple": ["T"]}, {"#struct": {"value": "T"}}, "T", "value"], -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - "identity", // This is very silly and isn't something you should probably do - //~^ ERROR Closure definition not supported here - "identity" - //~^ ERROR Closure definition not supported here - ] - ] - ] -]] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-closure.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-closure.stderr deleted file mode 100644 index ed54e64a7a2..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-closure.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error[graph-read-compiler::closure-unsupported]: Closures not supported - ╭▸ - 9 │ ┌ ["==", -10 │ │ "identity", // This is very silly and isn't something you should probably do -11 │ │ //~^ ERROR Closure definition not supported here -12 │ │ "identity" - │ │ ━━━━━━━━ Closure definition not supported here -13 │ │ //~^ ERROR Closure definition not supported here -14 │ │ ] - │ └───────┘ ... within this filter expression - │ - ├ help: Filter expressions do not currently support closure definitions. Move the closure outside the filter expression, assign the result to a variable, and use that variable in the filter instead. - ╰ note: Closures in filter expressions are not yet implemented. This is a current limitation that is being tracked in https://linear.app/hash/issue/H-4913/hashql-implement-vm. - -error[graph-read-compiler::closure-unsupported]: Closures not supported - ╭▸ - 9 │ ┌ ["==", -10 │ │ "identity", // This is very silly and isn't something you should probably do - │ │ ━━━━━━━━ Closure definition not supported here -11 │ │ //~^ ERROR Closure definition not supported here -12 │ │ "identity" -13 │ │ //~^ ERROR Closure definition not supported here -14 │ │ ] - │ └───────┘ ... within this filter expression - │ - ├ help: Filter expressions do not currently support closure definitions. Move the closure outside the filter expression, assign the result to a variable, and use that variable in the filter instead. - ╰ note: Closures in filter expressions are not yet implemented. This is a current limitation that is being tracked in https://linear.app/hash/issue/H-4913/hashql-implement-vm. \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-nested-graph-read.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-nested-graph-read.jsonc deleted file mode 100644 index 78a54c3bf19..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-nested-graph-read.jsonc +++ /dev/null @@ -1,27 +0,0 @@ -//@ run: fail -//@ description: Nested graph read operations not supported in filter expressions -// prettier-ignore -["let", "read", - ["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - "vertex.metadata.record_id.entity_id.entity_uuid", - "vertex.metadata.record_id.entity_id.entity_uuid" - ] - ] - ]], -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - "read", - //~^ ERROR Nested graph read operations not supported here - "read" - //~^ ERROR Nested graph read operations not supported here - ] - ] - ] -]] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-nested-graph-read.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-nested-graph-read.stderr deleted file mode 100644 index 0ce53e5c7f8..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-nested-graph-read.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error[graph-read-compiler::nested-graph-read-unsupported]: Nested graph read operations not supported - ╭▸ -19 │ ┌ ["==", -20 │ │ "read", -21 │ │ //~^ ERROR Nested graph read operations not supported here -22 │ │ "read" - │ │ ━━━━ Nested graph read operations not supported here -23 │ │ //~^ ERROR Nested graph read operations not supported here -24 │ │ ] - │ └───────┘ ... within this filter expression - │ - ├ help: Filter expressions do not support nested graph operations. Use a separate query for the nested graph read and pass the result to this filter expression. - ╰ note: Nested graph reads in filter expressions are not yet implemented. This is a current limitation that is being tracked in https://linear.app/hash/issue/H-4913/hashql-implement-vm. - -error[graph-read-compiler::nested-graph-read-unsupported]: Nested graph read operations not supported - ╭▸ -19 │ ┌ ["==", -20 │ │ "read", - │ │ ━━━━ Nested graph read operations not supported here -21 │ │ //~^ ERROR Nested graph read operations not supported here -22 │ │ "read" -23 │ │ //~^ ERROR Nested graph read operations not supported here -24 │ │ ] - │ └───────┘ ... within this filter expression - │ - ├ help: Filter expressions do not support nested graph operations. Use a separate query for the nested graph read and pass the result to this filter expression. - ╰ note: Nested graph reads in filter expressions are not yet implemented. This is a current limitation that is being tracked in https://linear.app/hash/issue/H-4913/hashql-implement-vm. \ No newline at end of file diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-type-constructor.jsonc b/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-type-constructor.jsonc deleted file mode 100644 index e647ac27d2f..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-type-constructor.jsonc +++ /dev/null @@ -1,16 +0,0 @@ -//@ run: fail -//@ description: Type constructors not supported as values in filter expressions -// prettier-ignore -["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["::graph::tmp::decision_time_now"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - ["==", - "Some", - //~^ ERROR Cannot use constructor as value here - "Some" - //~^ ERROR Cannot use constructor as value here - ] - ] - ] -] diff --git a/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-type-constructor.stderr b/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-type-constructor.stderr deleted file mode 100644 index 3955ba00ab8..00000000000 --- a/libs/@local/hashql/eval/tests/ui/graph/read/entity/unsupported-type-constructor.stderr +++ /dev/null @@ -1,27 +0,0 @@ -error[graph-read-compiler::type-constructor-unsupported]: Type constructors not supported as values - ╭▸ - 8 │ ┌ ["==", - 9 │ │ "Some", -10 │ │ //~^ ERROR Cannot use constructor as value here -11 │ │ "Some" - │ │ ━━━━ Cannot use constructor as value here -12 │ │ //~^ ERROR Cannot use constructor as value here -13 │ │ ] - │ └───────┘ ... within this filter expression - │ - ├ help: Constructor functions cannot currently be used as first-class values in filter expressions. You can still call constructors to create values (e.g., `SomeType(x)`), but you cannot use the constructor itself in comparisons or pass it as an argument within filter contexts. - ╰ note: This is a current limitation of the filter expression compiler. Constructors work as first-class values elsewhere in the language, and support for this in filter expressions is being tracked in https://linear.app/hash/issue/H-4913/hashql-implement-vm. - -error[graph-read-compiler::type-constructor-unsupported]: Type constructors not supported as values - ╭▸ - 8 │ ┌ ["==", - 9 │ │ "Some", - │ │ ━━━━ Cannot use constructor as value here -10 │ │ //~^ ERROR Cannot use constructor as value here -11 │ │ "Some" -12 │ │ //~^ ERROR Cannot use constructor as value here -13 │ │ ] - │ └───────┘ ... within this filter expression - │ - ├ help: Constructor functions cannot currently be used as first-class values in filter expressions. You can still call constructors to create values (e.g., `SomeType(x)`), but you cannot use the constructor itself in comparisons or pass it as an argument within filter contexts. - ╰ note: This is a current limitation of the filter expression compiler. Constructors work as first-class values elsewhere in the language, and support for this in filter expressions is being tracked in https://linear.app/hash/issue/H-4913/hashql-implement-vm. \ No newline at end of file From a62a89ad440e6e29a7e931915d871b7c6d80b415 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:44:35 +0200 Subject: [PATCH 02/25] chore: add new dependency --- libs/@local/graph/api/Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/@local/graph/api/Cargo.toml b/libs/@local/graph/api/Cargo.toml index 8099f3985e1..b8bea66f12b 100644 --- a/libs/@local/graph/api/Cargo.toml +++ b/libs/@local/graph/api/Cargo.toml @@ -44,7 +44,8 @@ hash-graph-type-defs = { workspace = true } hash-graph-validation = { workspace = true } hashql-ast = { workspace = true } hashql-diagnostics = { workspace = true, features = ["serde", "render"] } -hashql-eval = { workspace = true, features = ["graph"] } +hashql-eval = { workspace = true } +hashql-mir = {workspace = true} hashql-hir = { workspace = true } hashql-syntax-jexpr = { workspace = true } type-system = { workspace = true, features = ["utoipa"] } From de060f1abf5dd375dd16314e7511dfc3a7d4a0c3 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:45:24 +0200 Subject: [PATCH 03/25] chore: format --- Cargo.toml | 4 +-- libs/@local/graph/api/Cargo.toml | 2 +- libs/@local/hashql/eval/Cargo.toml | 54 +++++++++++++++--------------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4090f18bdb4..07db1564b3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -241,8 +241,6 @@ sentry-types = { version = "0.47.0", default-features = fa serde = { version = "1.0.228", default-features = false } serde_core = { version = "1.0.228", default-features = false } serde_json = { version = "1.0.145" } -sqruff-lib = {version = "0.37.3"} -sqruff-lib-core = {version = "0.37.3"} serde_plain = { version = "1.0.2", default-features = false } sha2 = { version = "0.11.0", default-features = false } similar-asserts = { version = "2.0.0", default-features = false } @@ -251,6 +249,8 @@ smallvec = { version = "2.0.0-alpha.11", default-featu smol_str = { version = "0.3.4" } sort-package-json = { version = "0.0.12" } specta = { version = "2.0.0-rc.22", default-features = false } +sqruff-lib = { version = "0.37.3" } +sqruff-lib-core = { version = "0.37.3" } stacker = { version = "0.1.22", default-features = false } supports-color = { version = "3.0.2", default-features = false } supports-unicode = { version = "3.0.0", default-features = false } diff --git a/libs/@local/graph/api/Cargo.toml b/libs/@local/graph/api/Cargo.toml index b8bea66f12b..b9074104a3a 100644 --- a/libs/@local/graph/api/Cargo.toml +++ b/libs/@local/graph/api/Cargo.toml @@ -45,8 +45,8 @@ hash-graph-validation = { workspace = true } hashql-ast = { workspace = true } hashql-diagnostics = { workspace = true, features = ["serde", "render"] } hashql-eval = { workspace = true } -hashql-mir = {workspace = true} hashql-hir = { workspace = true } +hashql-mir = { workspace = true } hashql-syntax-jexpr = { workspace = true } type-system = { workspace = true, features = ["utoipa"] } diff --git a/libs/@local/hashql/eval/Cargo.toml b/libs/@local/hashql/eval/Cargo.toml index 5721f726761..5367b8da166 100644 --- a/libs/@local/hashql/eval/Cargo.toml +++ b/libs/@local/hashql/eval/Cargo.toml @@ -20,35 +20,35 @@ hashql-mir = { workspace = true, public = true } hashql-core = { workspace = true } # Private third-party dependencies -bytes = {workspace = true} -derive_more = { workspace = true, features = ["display"] } -futures-lite = "2.6.1" -postgres-protocol = {workspace = true} -postgres-types = { workspace = true, features = ["uuid-1"] } -serde = { workspace = true } -serde_json = { workspace = true, features = ["raw_value"] } -simple-mermaid = { workspace = true } -tokio ={workspace = true} -tokio-postgres = {workspace = true} -tokio-util = { workspace = true, features = ["rt"] } -url.workspace = true -uuid.workspace = true +bytes = { workspace = true } +derive_more = { workspace = true, features = ["display"] } +futures-lite = "2.6.1" +postgres-protocol = { workspace = true } +postgres-types = { workspace = true, features = ["uuid-1"] } +serde = { workspace = true } +serde_json = { workspace = true, features = ["raw_value"] } +simple-mermaid = { workspace = true } +tokio = { workspace = true } +tokio-postgres = { workspace = true } +tokio-util = { workspace = true, features = ["rt"] } +url.workspace = true +uuid.workspace = true [dev-dependencies] -error-stack = {workspace = true} -hash-graph-authorization = { workspace = true } -hash-graph-store = {workspace = true} -hash-graph-test-data = {workspace = true} -hashql-compiletest = { workspace = true } -hashql-diagnostics = { workspace = true, features = ["render"] } -insta = { workspace = true } -libtest-mimic = { workspace = true } -regex = { workspace = true } -similar-asserts = { workspace = true } -sqruff-lib = {workspace = true} -sqruff-lib-core = {workspace = true} -testcontainers = { workspace = true, features = ["reusable-containers"] } -testcontainers-modules = { workspace = true, features = ["postgres"] } +error-stack = { workspace = true } +hash-graph-authorization = { workspace = true } +hash-graph-store = { workspace = true } +hash-graph-test-data = { workspace = true } +hashql-compiletest = { workspace = true } +hashql-diagnostics = { workspace = true, features = ["render"] } +insta = { workspace = true } +libtest-mimic = { workspace = true } +regex = { workspace = true } +similar-asserts = { workspace = true } +sqruff-lib = { workspace = true } +sqruff-lib-core = { workspace = true } +testcontainers = { workspace = true, features = ["reusable-containers"] } +testcontainers-modules = { workspace = true, features = ["postgres"] } [lints] workspace = true From 57407681dfc211f9f2823602bf7054f1cf8f9862 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Tue, 2 Jun 2026 14:07:42 +0200 Subject: [PATCH 04/25] feat: error module --- Cargo.lock | 1 + libs/@local/graph/api/src/rest/entity.rs | 8 +- .../api/src/rest/entity_query_request.rs | 106 +++++++++------ libs/@local/hashql/compiletest/Cargo.toml | 2 +- .../@local/hashql/compiletest/src/pipeline.rs | 1 + .../mir_pass_analysis_data_dependency.rs | 3 +- .../suite/mir_pass_transform_cfg_simplify.rs | 4 +- .../suite/mir_pass_transform_pre_inline.rs | 2 +- .../hashql/compiletest/src/suite/mir_reify.rs | 10 +- libs/@local/hashql/eval/Cargo.toml | 1 + libs/@local/hashql/eval/src/error.rs | 9 +- .../hashql/eval/src/orchestrator/error.rs | 124 +++++++++++++---- .../hashql/eval/src/orchestrator/mod.rs | 32 +++-- libs/@local/hashql/mir/src/error.rs | 9 +- libs/@local/hashql/mir/src/interpret/error.rs | 127 +++++++++++------- libs/@local/hashql/mir/src/reify/atom.rs | 4 +- libs/@local/hashql/mir/src/reify/error.rs | 5 +- libs/@local/hashql/mir/src/reify/mod.rs | 43 +++--- libs/@local/hashql/mir/src/reify/rvalue.rs | 4 +- .../@local/hashql/mir/src/reify/terminator.rs | 4 +- libs/@local/hashql/mir/src/reify/transform.rs | 4 +- 21 files changed, 334 insertions(+), 169 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d1ff7516b3..5000af8f238 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3574,6 +3574,7 @@ dependencies = [ "hashql-diagnostics", "hashql-eval", "hashql-hir", + "hashql-mir", "hashql-syntax-jexpr", "http 1.4.0", "hyper", diff --git a/libs/@local/graph/api/src/rest/entity.rs b/libs/@local/graph/api/src/rest/entity.rs index 28b88f015db..f0196e2555d 100644 --- a/libs/@local/graph/api/src/rest/entity.rs +++ b/libs/@local/graph/api/src/rest/entity.rs @@ -40,7 +40,7 @@ use hash_graph_types::{ }, }; use hash_temporal_client::TemporalClient; -use hashql_core::heap::Heap; +use hashql_core::heap::{Heap, Scratch}; use serde::{Deserialize as _, Serialize}; use serde_json::value::RawValue as RawJsonvalue; use type_system::{ @@ -454,6 +454,7 @@ where // TODO: https://linear.app/hash/issue/H-5351/reuse-parts-between-compilation-units let mut heap = Heap::uninitialized(); + let mut scratch = Scratch::new(); if matches!(query, EntityQuery::Query { .. }) { // The heap is going to be used in the compilation of the query and therefore needs to be @@ -463,7 +464,7 @@ where heap.prime(); } - let filter = query.compile(&heap, CompilationOptions { interactive })?; + let filter = query.compile(&heap, &mut scratch, CompilationOptions { interactive })?; let params = options .into_params(filter, api_config) @@ -567,6 +568,7 @@ where // TODO: https://linear.app/hash/issue/H-5351/reuse-parts-between-compilation-units let mut heap = Heap::uninitialized(); + let mut scratch = Scratch::new(); if matches!(query, EntityQuery::Query { .. }) { // The heap is going to be used in the compilation of the query and therefore needs to be @@ -576,7 +578,7 @@ where heap.prime(); } - let filter = query.compile(&heap, CompilationOptions { interactive })?; + let filter = query.compile(&heap, &mut scratch, CompilationOptions { interactive })?; let params = options .into_traversal_params(filter, traversal, api_config) diff --git a/libs/@local/graph/api/src/rest/entity_query_request.rs b/libs/@local/graph/api/src/rest/entity_query_request.rs index 63b025672c7..c5a07712f56 100644 --- a/libs/@local/graph/api/src/rest/entity_query_request.rs +++ b/libs/@local/graph/api/src/rest/entity_query_request.rs @@ -41,8 +41,7 @@ use hash_graph_store::{ }; use hashql_ast::error::AstDiagnosticCategory; use hashql_core::{ - collections::fast_hash_map_with_capacity, - heap::Heap, + heap::{Heap, Scratch}, module::ModuleRegistry, span::{SpanId, SpanTable}, r#type::environment::Environment, @@ -54,11 +53,9 @@ use hashql_diagnostics::{ severity::Critical, source::{DiagnosticSpan, Source, SourceId, Sources}, }; -use hashql_eval::{ - error::EvalDiagnosticCategory, - graph::{error::GraphCompilerDiagnosticCategory, read::FilterSlice}, -}; -use hashql_hir::{error::HirDiagnosticCategory, visit::Visitor as _}; +use hashql_eval::error::EvalDiagnosticCategory; +use hashql_hir::error::HirDiagnosticCategory; +use hashql_mir::{def::DefIdVec, error::MirDiagnosticCategory}; use hashql_syntax_jexpr::{error::JExprDiagnosticCategory, span::Span}; use http::StatusCode; use serde::Deserialize; @@ -210,6 +207,7 @@ enum HashQLDiagnosticCategory { JExpr(JExprDiagnosticCategory), Ast(AstDiagnosticCategory), Hir(HirDiagnosticCategory), + Mir(MirDiagnosticCategory), Eval(EvalDiagnosticCategory), } @@ -236,6 +234,7 @@ impl DiagnosticCategory for HashQLDiagnosticCategory { Self::JExpr(jexpr) => Some(jexpr), Self::Ast(ast) => Some(ast), Self::Hir(hir) => Some(hir), + Self::Mir(mir) => Some(mir), Self::Eval(eval) => Some(eval), } } @@ -332,6 +331,7 @@ pub enum EntityQuery<'q> { impl<'q> EntityQuery<'q> { fn compile_query<'heap>( heap: &'heap Heap, + scratch: &mut Scratch, spans: &mut SpanTable, query: &RawJsonValue, ) -> Status, HashQLDiagnosticCategory, SpanId> { @@ -368,13 +368,13 @@ impl<'q> EntityQuery<'q> { })?; let interner = hashql_hir::intern::Interner::new(heap); - let mut context = hashql_hir::context::HirContext::new(&interner, &modules); + let mut hir_context = hashql_hir::context::HirContext::new(&interner, &modules); // Reify the HIR from the AST let Success { value: hir, advisories, - } = hashql_hir::node::NodeData::from_ast(ast, &mut context, &types) + } = hashql_hir::node::NodeData::from_ast(ast, &mut hir_context, &types) .map_category(|category| { HashQLDiagnosticCategory::Hir(HirDiagnosticCategory::Reification(category)) }) @@ -384,48 +384,75 @@ impl<'q> EntityQuery<'q> { let Success { value: hir, advisories, - } = hashql_hir::lower::lower(hir, &types, &mut env, &mut context) + } = hashql_hir::lower::lower(hir, &types, &mut env, &mut hir_context) .map_category(|category| { HashQLDiagnosticCategory::Hir(HirDiagnosticCategory::Lowering(category)) }) .with_diagnostics(advisories)?; - // Evaluate the HIR - // TODO: https://linear.app/hash/issue/BE-41/hashql-expose-input-in-graph-api - let inputs = fast_hash_map_with_capacity(0); - let mut compiler = hashql_eval::graph::read::GraphReadCompiler::new(heap, &inputs); - - compiler.visit_node(hir); + let interner = hashql_mir::intern::Interner::new(heap); + let mut bodies = DefIdVec::new_in(heap); + let mut mir_context = hashql_mir::context::MirContext { + heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }; + let mut reify_context = hashql_mir::reify::ReifyContext { + bodies: &mut bodies, + mir: &mut mir_context, + hir: &hir_context, + scratch: &*scratch, + }; + // Reify the MIR from the HIR let Success { - value: result, + value: mir, advisories, - } = compiler - .finish() + } = hashql_mir::reify::from_hir(hir, &mut reify_context) .map_category(|category| { - HashQLDiagnosticCategory::Eval(EvalDiagnosticCategory::Graph( - GraphCompilerDiagnosticCategory::Read(category), - )) + HashQLDiagnosticCategory::Mir(MirDiagnosticCategory::Reify(category)) }) .with_diagnostics(advisories)?; - let output = result.output.get(&hir.id).expect("TODO"); - - // Compile the Filter into one - let filters = match output { - FilterSlice::Entity { range } => result.filters.entity(range.clone()), - }; - - let filter = match filters { - [] => Filter::All(Vec::new()), - [filter] => filter.clone(), - _ => Filter::All(filters.to_vec()), - }; + todo!("lower the MIR"); - Ok(Success { - value: filter, - advisories, - }) + // Evaluate the HIR + // TODO: https://linear.app/hash/issue/BE-41/hashql-expose-input-in-graph-api + // let inputs = fast_hash_map_with_capacity(0); + // let mut compiler = hashql_eval::graph::read::GraphReadCompiler::new(heap, &inputs); + + // compiler.visit_node(hir); + + // let Success { + // value: result, + // advisories, + // } = compiler + // .finish() + // .map_category(|category| { + // HashQLDiagnosticCategory::Eval(EvalDiagnosticCategory::Graph( + // GraphCompilerDiagnosticCategory::Read(category), + // )) + // }) + // .with_diagnostics(advisories)?; + + // let output = result.output.get(&hir.id).expect("TODO"); + + // // Compile the Filter into one + // let filters = match output { + // FilterSlice::Entity { range } => result.filters.entity(range.clone()), + // }; + + // let filter = match filters { + // [] => Filter::All(Vec::new()), + // [filter] => filter.clone(), + // _ => Filter::All(filters.to_vec()), + // }; + + // Ok(Success { + // value: filter, + // advisories, + // }) } /// Compiles a query into an executable entity filter. @@ -441,6 +468,7 @@ impl<'q> EntityQuery<'q> { pub(crate) fn compile( self, heap: &'q Heap, + scratch: &mut Scratch, options: CompilationOptions, ) -> Result, BoxedResponse> { match self { @@ -451,7 +479,7 @@ impl<'q> EntityQuery<'q> { let Success { value: filter, advisories, - } = Self::compile_query(heap, &mut spans, query).map_err(|failure| { + } = Self::compile_query(heap, scratch, &mut spans, query).map_err(|failure| { failure_to_response(failure, query.get(), &spans, options) })?; if !advisories.is_empty() { diff --git a/libs/@local/hashql/compiletest/Cargo.toml b/libs/@local/hashql/compiletest/Cargo.toml index 65b99360e5e..1d4784fd3cc 100644 --- a/libs/@local/hashql/compiletest/Cargo.toml +++ b/libs/@local/hashql/compiletest/Cargo.toml @@ -11,7 +11,7 @@ version.workspace = true hashql-ast = { workspace = true, public = true } hashql-core = { workspace = true, public = true } hashql-diagnostics = { workspace = true, features = ["render"], public = true } -hashql-eval = { workspace = true, features = ["graph"], public = true } +hashql-eval = { workspace = true, public = true } hashql-hir = { workspace = true, public = true } hashql-mir = { workspace = true, public = true } hashql-syntax-jexpr = { workspace = true, public = true } diff --git a/libs/@local/hashql/compiletest/src/pipeline.rs b/libs/@local/hashql/compiletest/src/pipeline.rs index 49fe6b68dac..7ba98d9b5ad 100644 --- a/libs/@local/hashql/compiletest/src/pipeline.rs +++ b/libs/@local/hashql/compiletest/src/pipeline.rs @@ -188,6 +188,7 @@ impl<'heap> Pipeline<'heap> { bodies: &mut bodies, mir: &mut mir_context, hir: &hir_context, + scratch: &self.scratch, }; let entry = tri!(hashql_mir::reify::from_hir(node, &mut reify_context)); diff --git a/libs/@local/hashql/compiletest/src/suite/mir_pass_analysis_data_dependency.rs b/libs/@local/hashql/compiletest/src/suite/mir_pass_analysis_data_dependency.rs index d249c5cbc15..8d10d49f139 100644 --- a/libs/@local/hashql/compiletest/src/suite/mir_pass_analysis_data_dependency.rs +++ b/libs/@local/hashql/compiletest/src/suite/mir_pass_analysis_data_dependency.rs @@ -38,7 +38,8 @@ impl Suite for MirPassAnalysisDataDependency { let mut buffer = Vec::new(); - let (root, mut bodies) = mir_reify(heap, expr, &interner, &mut environment, diagnostics)?; + let (root, mut bodies, _) = + mir_reify(heap, expr, &interner, &mut environment, diagnostics)?; writeln!(buffer, "{}\n", Header::new("MIR")).expect("should be able to write to buffer"); mir_format_text(heap, &environment, &mut buffer, root, &bodies); diff --git a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_cfg_simplify.rs b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_cfg_simplify.rs index fa54cc8f4f7..a4e19a5d7cf 100644 --- a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_cfg_simplify.rs +++ b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_cfg_simplify.rs @@ -49,7 +49,8 @@ pub(crate) fn mir_pass_transform_cfg_simplify<'heap>( environment: &mut Environment<'heap>, diagnostics: &mut Vec, ) -> Result<(DefId, DefIdVec>, Scratch), SuiteDiagnostic> { - let (root, mut bodies) = mir_reify(heap, expr, interner, environment, diagnostics)?; + let (root, mut bodies, mut scratch) = + mir_reify(heap, expr, interner, environment, diagnostics)?; render(heap, environment, root, &bodies); @@ -59,7 +60,6 @@ pub(crate) fn mir_pass_transform_cfg_simplify<'heap>( interner, diagnostics: DiagnosticIssues::new(), }; - let mut scratch = Scratch::new(); let mut pass = CfgSimplify::new_in(&mut scratch); for body in bodies.as_mut_slice() { diff --git a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_pre_inline.rs b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_pre_inline.rs index 25c2c906b5e..1386621c949 100644 --- a/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_pre_inline.rs +++ b/libs/@local/hashql/compiletest/src/suite/mir_pass_transform_pre_inline.rs @@ -168,7 +168,7 @@ pub(crate) fn mir_pass_transform_pre_inline<'heap>( environment: &mut Environment<'heap>, diagnostics: &mut Vec, ) -> Result<(DefId, DefIdVec>, Scratch), SuiteDiagnostic> { - let (root, mut bodies) = mir_reify(heap, expr, interner, environment, diagnostics)?; + let (root, mut bodies, _) = mir_reify(heap, expr, interner, environment, diagnostics)?; render.render( &mut RenderContext { diff --git a/libs/@local/hashql/compiletest/src/suite/mir_reify.rs b/libs/@local/hashql/compiletest/src/suite/mir_reify.rs index ce16f3b4aa6..3483b2eff71 100644 --- a/libs/@local/hashql/compiletest/src/suite/mir_reify.rs +++ b/libs/@local/hashql/compiletest/src/suite/mir_reify.rs @@ -8,7 +8,7 @@ use std::{ use error_stack::ReportSink; use hashql_ast::node::expr::Expr; use hashql_core::{ - heap::Heap, + heap::{Heap, Scratch}, id::IdVec, module::ModuleRegistry, pretty::Formatter, @@ -32,7 +32,8 @@ pub(crate) fn mir_reify<'heap>( interner: &Interner<'heap>, environment: &mut Environment<'heap>, diagnostics: &mut Vec, -) -> Result<(DefId, DefIdVec>), SuiteDiagnostic> { +) -> Result<(DefId, DefIdVec>, Scratch), SuiteDiagnostic> { + let scratch = Scratch::new(); let registry = ModuleRegistry::new(environment); let hir_interner = hashql_hir::intern::Interner::new(heap); let mut hir_context = HirContext::new(&hir_interner, ®istry); @@ -66,11 +67,12 @@ pub(crate) fn mir_reify<'heap>( bodies: &mut bodies, mir: &mut mir_context, hir: &hir_context, + scratch: &scratch, }, ), )?; - Ok((root, bodies)) + Ok((root, bodies, scratch)) } pub(crate) fn mir_format_text<'heap>( @@ -208,7 +210,7 @@ impl Suite for MirReifySuite { let mut environment = Environment::new(heap); let interner = Interner::new(heap); - let (root, bodies) = mir_reify(heap, expr, &interner, &mut environment, diagnostics)?; + let (root, bodies, _) = mir_reify(heap, expr, &interner, &mut environment, diagnostics)?; let mut buffer = Vec::new(); mir_format_text(heap, &environment, &mut buffer, root, &bodies); diff --git a/libs/@local/hashql/eval/Cargo.toml b/libs/@local/hashql/eval/Cargo.toml index 5367b8da166..ea9aeddd1a1 100644 --- a/libs/@local/hashql/eval/Cargo.toml +++ b/libs/@local/hashql/eval/Cargo.toml @@ -49,6 +49,7 @@ sqruff-lib = { workspace = true } sqruff-lib-core = { workspace = true } testcontainers = { workspace = true, features = ["reusable-containers"] } testcontainers-modules = { workspace = true, features = ["postgres"] } +type-system = { workspace = true } [lints] workspace = true diff --git a/libs/@local/hashql/eval/src/error.rs b/libs/@local/hashql/eval/src/error.rs index 03e40d3634d..91388d645fc 100644 --- a/libs/@local/hashql/eval/src/error.rs +++ b/libs/@local/hashql/eval/src/error.rs @@ -2,9 +2,10 @@ use alloc::borrow::Cow; use hashql_core::span::SpanId; use hashql_diagnostics::{Diagnostic, DiagnosticIssues, Severity, category::DiagnosticCategory}; -use hashql_mir::interpret::error::InterpretDiagnosticCategory; -use crate::postgres::error::PostgresDiagnosticCategory; +use crate::{ + orchestrator::OrchestratorDiagnosticCategory, postgres::error::PostgresDiagnosticCategory, +}; pub type EvalDiagnostic = Diagnostic; pub type EvalDiagnosticIssues = DiagnosticIssues; @@ -12,7 +13,7 @@ pub type EvalDiagnosticIssues = DiagnosticIssues Option<&dyn DiagnosticCategory> { match self { Self::Postgres(postgres) => Some(postgres), - Self::Interpret(interpret) => Some(interpret), + Self::Orchestrator(orchestrator) => Some(orchestrator), } } } diff --git a/libs/@local/hashql/eval/src/orchestrator/error.rs b/libs/@local/hashql/eval/src/orchestrator/error.rs index b19b096192e..a3c5471b4b2 100644 --- a/libs/@local/hashql/eval/src/orchestrator/error.rs +++ b/libs/@local/hashql/eval/src/orchestrator/error.rs @@ -1,12 +1,22 @@ -//! Errors that occur while fulfilling [`GraphRead`] suspensions. +//! Error types for the orchestration layer. //! -//! These are internal runtime errors: failures in compiled query execution, -//! row decoding, or parameter encoding. The user wrote HashQL, not SQL; if -//! the bridge fails, it indicates a bug in the compiler or runtime. +//! The orchestrator sits between the MIR interpreter and external data sources +//! (PostgreSQL). Errors fall into two families: +//! +//! - **Interpreter errors**: failures in the MIR interpreter itself (type invariant violations, +//! control flow errors, etc.). These are produced by the interpreter and forwarded through the +//! orchestrator. +//! - **Bridge errors**: failures while fulfilling [`GraphRead`] suspensions (query execution, row +//! decoding, parameter encoding). The user wrote HashQL, not SQL; if the bridge fails, it +//! indicates a bug in the compiler or runtime. +//! +//! [`OrchestratorDiagnosticCategory`] unifies both families under a single +//! category hierarchy so that downstream consumers (the eval crate) see one +//! coherent diagnostic type from the orchestration layer. //! //! [`GraphRead`]: hashql_mir::body::terminator::GraphRead -use alloc::string::String; +use alloc::{borrow::Cow, string::String}; use hashql_core::{ pretty::{Formatter, RenderOptions}, @@ -15,15 +25,15 @@ use hashql_core::{ r#type::{TypeFormatter, TypeFormatterOptions, TypeId, environment::Environment}, }; use hashql_diagnostics::{ - Diagnostic, Label, category::TerminalDiagnosticCategory, diagnostic::Message, + Diagnostic, Label, + category::{DiagnosticCategory, TerminalDiagnosticCategory}, + diagnostic::Message, severity::Severity, }; use hashql_mir::{ body::{basic_block::BasicBlockId, local::Local}, def::DefId, - interpret::error::{ - InterpretDiagnostic, InterpretDiagnosticCategory, SuspensionDiagnosticCategory, - }, + interpret::error::InterpretDiagnosticCategory, }; use super::{Indexed, codec::JsonValueKind}; @@ -89,8 +99,64 @@ const VALUE_SERIALIZATION: TerminalDiagnosticCategory = TerminalDiagnosticCatego name: "Value Serialization", }; -const fn category(terminal: &'static TerminalDiagnosticCategory) -> InterpretDiagnosticCategory { - InterpretDiagnosticCategory::Suspension(SuspensionDiagnosticCategory(terminal)) +/// Type alias for orchestrator diagnostics. +/// +/// The default severity kind is [`Severity`], which allows any severity level. +pub type OrchestratorDiagnostic = + Diagnostic; + +/// Diagnostic subcategory for errors that occur while fulfilling a suspension. +/// +/// Wraps a [`TerminalDiagnosticCategory`] that identifies the specific bridge +/// failure (query execution, row hydration, parameter encoding, etc.). +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct BridgeDiagnosticCategory(pub &'static TerminalDiagnosticCategory); + +impl DiagnosticCategory for BridgeDiagnosticCategory { + fn id(&self) -> Cow<'_, str> { + Cow::Borrowed("bridge") + } + + fn name(&self) -> Cow<'_, str> { + Cow::Borrowed("Bridge") + } + + fn subcategory(&self) -> Option<&dyn DiagnosticCategory> { + Some(self.0) + } +} + +/// Top-level diagnostic category for the orchestration layer. +/// +/// Unifies interpreter errors (forwarded from the MIR interpreter) and bridge +/// errors (failures while fulfilling suspensions) under a single hierarchy. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum OrchestratorDiagnosticCategory { + /// An error produced by the MIR interpreter itself. + Interpret(InterpretDiagnosticCategory), + /// An error from the bridge while fulfilling a suspension. + Bridge(BridgeDiagnosticCategory), +} + +impl DiagnosticCategory for OrchestratorDiagnosticCategory { + fn id(&self) -> Cow<'_, str> { + Cow::Borrowed("orchestrator") + } + + fn name(&self) -> Cow<'_, str> { + Cow::Borrowed("Orchestrator") + } + + fn subcategory(&self) -> Option<&dyn DiagnosticCategory> { + match self { + Self::Interpret(cat) => Some(cat), + Self::Bridge(cat) => Some(cat), + } + } +} + +const fn category(terminal: &'static TerminalDiagnosticCategory) -> OrchestratorDiagnosticCategory { + OrchestratorDiagnosticCategory::Bridge(BridgeDiagnosticCategory(terminal)) } /// Errors that occur while decoding a JSON value into a typed [`Value`]. @@ -356,7 +422,7 @@ pub enum BridgeError<'heap> { } impl<'heap> BridgeError<'heap> { - pub fn into_diagnostic(self, span: SpanId, env: &Environment<'heap>) -> InterpretDiagnostic { + pub fn into_diagnostic(self, span: SpanId, env: &Environment<'heap>) -> OrchestratorDiagnostic { match self { Self::QueryExecution { sql, source } => query_execution(span, &sql, &source), Self::RowHydration { column, source } => row_hydration(span, column, &source), @@ -388,7 +454,11 @@ impl<'heap> BridgeError<'heap> { } } -fn query_execution(span: SpanId, sql: &str, error: &tokio_postgres::Error) -> InterpretDiagnostic { +fn query_execution( + span: SpanId, + sql: &str, + error: &tokio_postgres::Error, +) -> OrchestratorDiagnostic { let mut diagnostic = Diagnostic::new(category(&QUERY_EXECUTION), Severity::Bug).primary( Label::new(span, "compiled query was rejected by the database"), ); @@ -411,7 +481,7 @@ fn row_hydration( value: column, }: Indexed, source: &tokio_postgres::Error, -) -> InterpretDiagnostic { +) -> OrchestratorDiagnostic { let mut diagnostic = Diagnostic::new(category(&ROW_HYDRATION), Severity::Bug).primary(Label::new( span, @@ -429,7 +499,7 @@ fn row_hydration( /// Adds notes describing a [`DecodeError`] to a diagnostic. fn add_decode_error_notes( - diagnostic: &mut InterpretDiagnostic, + diagnostic: &mut OrchestratorDiagnostic, source: &DecodeError<'_>, env: &Environment<'_>, ) { @@ -543,7 +613,7 @@ fn value_deserialization( }: Indexed, source: &DecodeError<'_>, env: &Environment<'_>, -) -> InterpretDiagnostic { +) -> OrchestratorDiagnostic { let mut diagnostic = Diagnostic::new(category(&VALUE_DESERIALIZATION), Severity::Bug).primary(Label::new( span, @@ -565,7 +635,7 @@ fn continuation_deserialization( local: Local, source: &DecodeError<'_>, env: &Environment<'_>, -) -> InterpretDiagnostic { +) -> OrchestratorDiagnostic { let mut diagnostic = Diagnostic::new(category(&CONTINUATION_DESERIALIZATION), Severity::Bug) .primary(Label::new( span, @@ -582,7 +652,11 @@ fn continuation_deserialization( diagnostic } -fn invalid_continuation_block_id(span: SpanId, body: DefId, block_id: i32) -> InterpretDiagnostic { +fn invalid_continuation_block_id( + span: SpanId, + body: DefId, + block_id: i32, +) -> OrchestratorDiagnostic { let mut diagnostic = Diagnostic::new(category(&INVALID_CONTINUATION_BLOCK_ID), Severity::Bug).primary( Label::new(span, "continuation returned an invalid block ID"), @@ -599,7 +673,7 @@ fn invalid_continuation_block_id(span: SpanId, body: DefId, block_id: i32) -> In diagnostic } -fn invalid_continuation_local(span: SpanId, body: DefId, local: i32) -> InterpretDiagnostic { +fn invalid_continuation_local(span: SpanId, body: DefId, local: i32) -> OrchestratorDiagnostic { let mut diagnostic = Diagnostic::new(category(&INVALID_CONTINUATION_LOCAL), Severity::Bug) .primary(Label::new(span, "continuation returned an invalid local")); @@ -618,7 +692,7 @@ fn parameter_encoding( span: SpanId, parameter: usize, error: &(dyn core::error::Error + Send + Sync), -) -> InterpretDiagnostic { +) -> OrchestratorDiagnostic { let mut diagnostic = Diagnostic::new(category(&PARAMETER_ENCODING), Severity::Bug).primary(Label::new( span, @@ -637,7 +711,7 @@ fn parameter_encoding( diagnostic } -fn query_lookup(span: SpanId, body: DefId, block: BasicBlockId) -> InterpretDiagnostic { +fn query_lookup(span: SpanId, body: DefId, block: BasicBlockId) -> OrchestratorDiagnostic { let mut diagnostic = Diagnostic::new(category(&QUERY_LOOKUP), Severity::Bug).primary( Label::new(span, "no compiled query found for this data access"), ); @@ -653,7 +727,7 @@ fn query_lookup(span: SpanId, body: DefId, block: BasicBlockId) -> InterpretDiag diagnostic } -fn incomplete_continuation(span: SpanId, body: DefId, field: &str) -> InterpretDiagnostic { +fn incomplete_continuation(span: SpanId, body: DefId, field: &str) -> OrchestratorDiagnostic { let mut diagnostic = Diagnostic::new(category(&INCOMPLETE_CONTINUATION), Severity::Bug) .primary(Label::new( span, @@ -671,7 +745,7 @@ fn incomplete_continuation(span: SpanId, body: DefId, field: &str) -> InterpretD diagnostic } -fn missing_execution_residual(span: SpanId, body: DefId) -> InterpretDiagnostic { +fn missing_execution_residual(span: SpanId, body: DefId) -> OrchestratorDiagnostic { let mut diagnostic = Diagnostic::new(category(&MISSING_EXECUTION_RESIDUAL), Severity::Bug) .primary(Label::new( span, @@ -689,7 +763,7 @@ fn missing_execution_residual(span: SpanId, body: DefId) -> InterpretDiagnostic diagnostic } -fn invalid_filter_return(span: SpanId, body: DefId) -> InterpretDiagnostic { +fn invalid_filter_return(span: SpanId, body: DefId) -> OrchestratorDiagnostic { let mut diagnostic = Diagnostic::new(category(&INVALID_FILTER_RETURN), Severity::Bug) .primary(Label::new(span, "filter body returned a non-boolean value")); @@ -704,7 +778,7 @@ fn invalid_filter_return(span: SpanId, body: DefId) -> InterpretDiagnostic { diagnostic } -fn value_serialization(span: SpanId, error: &serde_json::Error) -> InterpretDiagnostic { +fn value_serialization(span: SpanId, error: &serde_json::Error) -> OrchestratorDiagnostic { let mut diagnostic = Diagnostic::new(category(&VALUE_SERIALIZATION), Severity::Bug) .primary(Label::new(span, "cannot serialize runtime value to JSON")); diff --git a/libs/@local/hashql/eval/src/orchestrator/mod.rs b/libs/@local/hashql/eval/src/orchestrator/mod.rs index 978193b83ba..56e7acb85e2 100644 --- a/libs/@local/hashql/eval/src/orchestrator/mod.rs +++ b/libs/@local/hashql/eval/src/orchestrator/mod.rs @@ -30,9 +30,9 @@ //! validates them, then flushes the decoded state into the interpreter's callstack. //! - `request`: per-suspension-type handlers (currently [`GraphRead`]). //! - `tail`: result accumulation strategies (currently collection into a list). -//! - `error`: error types for all failure modes in the bridge. All variants use `Severity::Bug` -//! because the user wrote HashQL, not SQL: if the bridge fails, the compiler or runtime produced -//! something invalid. +//! - `error`: diagnostic category hierarchy ([`OrchestratorDiagnosticCategory`]) and bridge error +//! types. Bridge errors use `Severity::Bug` because the user wrote HashQL, not SQL: if the bridge +//! fails, the compiler or runtime produced something invalid. //! //! [`GraphRead`]: hashql_mir::body::terminator::GraphRead //! [`Suspension`]: hashql_mir::interpret::suspension::Suspension @@ -48,19 +48,21 @@ use hashql_mir::{ def::DefId, interpret::{ CallStack, Inputs, Runtime, RuntimeConfig, RuntimeError, - error::InterpretDiagnostic, suspension::{Continuation, Suspension}, value::Value, }, }; use tokio_postgres::Client; -pub use self::events::{AppendEventLog, Event, EventLog}; use self::{error::BridgeError, request::GraphReadOrchestrator}; +pub use self::{ + error::{OrchestratorDiagnostic, OrchestratorDiagnosticCategory}, + events::{AppendEventLog, Event, EventLog}, +}; use crate::{context::EvalContext, postgres::PreparedQueries}; pub mod codec; -pub(crate) mod error; +pub mod error; mod events; mod partial; mod postgres; @@ -164,7 +166,7 @@ impl<'ctx, 'heap, C, E: EventLog, A: Allocator> Orchestrator<'_, 'ctx, 'heap, C, /// /// # Errors /// - /// Returns an [`InterpretDiagnostic`] if the interpreter fails or any + /// Returns an [`OrchestratorDiagnostic`] if the interpreter fails or any /// suspension cannot be fulfilled (database errors, decoding failures, /// filter evaluation failures). /// @@ -177,7 +179,7 @@ impl<'ctx, 'heap, C, E: EventLog, A: Allocator> Orchestrator<'_, 'ctx, 'heap, C, args: impl IntoIterator, IntoIter: ExactSizeIterator>, alloc: L, - ) -> Result, InterpretDiagnostic> + ) -> Result, OrchestratorDiagnostic> where C: AsRef, { @@ -209,16 +211,18 @@ impl<'ctx, 'heap, C, E: EventLog, A: Allocator> Orchestrator<'_, 'ctx, 'heap, C, } }; - Err( - error.into_diagnostic(callstack.unwind().map(|(_, span)| span), |suspension| { + Err(error.into_diagnostic_with( + callstack.unwind().map(|(_, span)| span), + |suspension| { let span = callstack .unwind() .next() .map_or(self.context.bodies[body].span, |(_, span)| span); suspension.into_diagnostic(span, self.context.env) - }), - ) + }, + OrchestratorDiagnosticCategory::Interpret, + )) } /// Convenience wrapper around [`run_in`](Self::run_in) that uses the @@ -226,14 +230,14 @@ impl<'ctx, 'heap, C, E: EventLog, A: Allocator> Orchestrator<'_, 'ctx, 'heap, C, /// /// # Errors /// - /// Returns an [`InterpretDiagnostic`] on failure. See + /// Returns an [`OrchestratorDiagnostic`] on failure. See /// [`run_in`](Self::run_in). pub async fn run( &self, inputs: &Inputs<'heap, Global>, body: DefId, args: impl IntoIterator, IntoIter: ExactSizeIterator>, - ) -> Result, InterpretDiagnostic> + ) -> Result, OrchestratorDiagnostic> where C: AsRef, { diff --git a/libs/@local/hashql/mir/src/error.rs b/libs/@local/hashql/mir/src/error.rs index a11414cb575..847414be7f5 100644 --- a/libs/@local/hashql/mir/src/error.rs +++ b/libs/@local/hashql/mir/src/error.rs @@ -3,8 +3,11 @@ use alloc::borrow::Cow; use hashql_core::span::SpanId; use hashql_diagnostics::{Diagnostic, DiagnosticCategory, DiagnosticIssues, Severity}; -use crate::pass::{ - execution::PlacementDiagnosticCategory, transform::error::TransformationDiagnosticCategory, +use crate::{ + pass::{ + execution::PlacementDiagnosticCategory, transform::error::TransformationDiagnosticCategory, + }, + reify::ReifyDiagnosticCategory, }; pub type MirDiagnostic = Diagnostic; @@ -12,6 +15,7 @@ pub type MirDiagnosticIssues = DiagnosticIssues Option<&dyn DiagnosticCategory> { match self { + Self::Reify(category) => Some(category), Self::Placement(category) => Some(category), Self::Transformation(category) => Some(category), } diff --git a/libs/@local/hashql/mir/src/interpret/error.rs b/libs/@local/hashql/mir/src/interpret/error.rs index 10fe9e2c79b..bc94c731995 100644 --- a/libs/@local/hashql/mir/src/interpret/error.rs +++ b/libs/@local/hashql/mir/src/interpret/error.rs @@ -29,24 +29,6 @@ use crate::body::{ /// The default severity kind is [`Severity`], which allows any severity level. pub type InterpretDiagnostic = Diagnostic; -/// Diagnostic subcategory for errors that occur while fulfilling a suspension. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub struct SuspensionDiagnosticCategory(pub &'static TerminalDiagnosticCategory); - -impl DiagnosticCategory for SuspensionDiagnosticCategory { - fn id(&self) -> Cow<'_, str> { - Cow::Borrowed("suspension") - } - - fn name(&self) -> Cow<'_, str> { - Cow::Borrowed("Suspension") - } - - fn subcategory(&self) -> Option<&dyn DiagnosticCategory> { - Some(self.0) - } -} - // Terminal categories for ICEs const LOCAL_ACCESS: TerminalDiagnosticCategory = TerminalDiagnosticCategory { id: "local-access", @@ -105,8 +87,6 @@ pub enum InterpretDiagnosticCategory { RuntimeLimit, /// Required input not provided. InputResolution, - /// Error from fulfilling a suspension (e.g. database query failure). - Suspension(SuspensionDiagnosticCategory), } impl DiagnosticCategory for InterpretDiagnosticCategory { @@ -127,7 +107,6 @@ impl DiagnosticCategory for InterpretDiagnosticCategory { Self::BoundsCheck => Some(&BOUNDS_CHECK), Self::RuntimeLimit => Some(&RUNTIME_LIMIT), Self::InputResolution => Some(&INPUT_RESOLUTION), - Self::Suspension(category) => Some(category), } } } @@ -383,20 +362,43 @@ pub enum RuntimeError<'heap, E, A: Allocator> { } impl RuntimeError<'_, E, A> { - /// Converts this runtime error into a diagnostic using the provided callstack. + /// Converts this runtime error into an [`InterpretDiagnostic`] using the + /// provided callstack. /// /// The callstack provides span information for error localization. The first /// frame's span is used as the primary label, and subsequent frames are added /// as secondary labels to show the call trace. + /// + /// `on_suspension` converts the suspension payload `E` into a diagnostic. + /// When `E = !` (no suspension possible), the closure is never invoked. + /// + /// For callers that need a different output category (e.g. an orchestrator + /// wrapping interpreter errors), use [`into_diagnostic_with`](Self::into_diagnostic_with). pub fn into_diagnostic( self, callstack: impl IntoIterator, on_suspension: impl FnOnce(E) -> InterpretDiagnostic, ) -> InterpretDiagnostic { + self.into_diagnostic_with(callstack, on_suspension, core::convert::identity) + } + + /// Converts this runtime error into a diagnostic, lifting the category + /// through `map_category`. + /// + /// Like [`into_diagnostic`](Self::into_diagnostic), but allows the caller + /// to embed [`InterpretDiagnosticCategory`] inside a broader category + /// hierarchy. The `on_suspension` closure produces a diagnostic with the + /// same output category `C`. + pub fn into_diagnostic_with( + self, + callstack: impl IntoIterator, + on_suspension: impl FnOnce(E) -> Diagnostic, + on_otherwise: impl FnOnce(InterpretDiagnosticCategory) -> C, + ) -> Diagnostic { let mut spans = callstack.into_iter(); let primary_span = spans.next().unwrap_or(SpanId::SYNTHETIC); - let mut diagnostic = self.make_diagnostic(primary_span, on_suspension); + let mut diagnostic = self.make_diagnostic(primary_span, on_suspension, on_otherwise); // Add callstack frames as secondary labels for span in spans { @@ -406,39 +408,70 @@ impl RuntimeError<'_, E, A> { diagnostic } - fn make_diagnostic( + fn make_diagnostic( self, span: SpanId, - on_suspension: impl FnOnce(E) -> InterpretDiagnostic, - ) -> InterpretDiagnostic { + on_suspension: impl FnOnce(E) -> Diagnostic, + on_otherwise: impl FnOnce(InterpretDiagnosticCategory) -> C, + ) -> Diagnostic { match self { - Self::UninitializedLocal { local, decl } => uninitialized_local(span, local, decl), - Self::InvalidIndexType { base, index } => invalid_index_type(span, &base, &index), - Self::InvalidSubscriptType { base } => invalid_subscript_type(span, &base), - Self::InvalidProjectionType { base } => invalid_projection_type(span, &base), + Self::UninitializedLocal { local, decl } => { + uninitialized_local(span, local, decl).map_category(on_otherwise) + } + Self::InvalidIndexType { base, index } => { + invalid_index_type(span, &base, &index).map_category(on_otherwise) + } + Self::InvalidSubscriptType { base } => { + invalid_subscript_type(span, &base).map_category(on_otherwise) + } + Self::InvalidProjectionType { base } => { + invalid_projection_type(span, &base).map_category(on_otherwise) + } Self::InvalidProjectionByNameType { base } => { - invalid_projection_by_name_type(span, &base) + invalid_projection_by_name_type(span, &base).map_category(on_otherwise) + } + Self::UnknownField { base, field } => { + unknown_field(span, &base, field).map_category(on_otherwise) + } + Self::UnknownFieldByName { base, field } => { + unknown_field_by_name(span, &base, field).map_category(on_otherwise) } - Self::UnknownField { base, field } => unknown_field(span, &base, field), - Self::UnknownFieldByName { base, field } => unknown_field_by_name(span, &base, field), Self::StructFieldLengthMismatch { values, fields } => { - struct_field_length_mismatch(span, values, fields) + struct_field_length_mismatch(span, values, fields).map_category(on_otherwise) + } + Self::InvalidDiscriminantType { r#type } => { + invalid_discriminant_type(span, &r#type).map_category(on_otherwise) + } + Self::InvalidDiscriminant { value } => { + invalid_discriminant(span, value).map_category(on_otherwise) + } + Self::UnreachableReached => unreachable_reached(span).map_category(on_otherwise), + Self::BinaryTypeMismatch(mismatch) => { + binary_type_mismatch(span, *mismatch).map_category(on_otherwise) + } + Self::UnaryTypeMismatch(mismatch) => { + unary_type_mismatch(span, *mismatch).map_category(on_otherwise) + } + Self::ApplyNonPointer { r#type } => { + apply_non_pointer(span, &r#type).map_category(on_otherwise) + } + Self::CallstackEmpty => callstack_empty(span).map_category(on_otherwise), + Self::OutOfRange { length, index } => { + out_of_range(span, length, index).map_category(on_otherwise) + } + Self::InputNotFound { name } => input_not_found(span, name).map_category(on_otherwise), + Self::RecursionLimitExceeded { limit } => { + recursion_limit_exceeded(span, limit).map_category(on_otherwise) + } + Self::IntegerOverflow { operation } => { + integer_overflow(span, operation).map_category(on_otherwise) } - Self::InvalidDiscriminantType { r#type } => invalid_discriminant_type(span, &r#type), - Self::InvalidDiscriminant { value } => invalid_discriminant(span, value), - Self::UnreachableReached => unreachable_reached(span), - Self::BinaryTypeMismatch(mismatch) => binary_type_mismatch(span, *mismatch), - Self::UnaryTypeMismatch(mismatch) => unary_type_mismatch(span, *mismatch), - Self::ApplyNonPointer { r#type } => apply_non_pointer(span, &r#type), - Self::CallstackEmpty => callstack_empty(span), - Self::OutOfRange { length, index } => out_of_range(span, length, index), - Self::InputNotFound { name } => input_not_found(span, name), - Self::RecursionLimitExceeded { limit } => recursion_limit_exceeded(span, limit), - Self::IntegerOverflow { operation } => integer_overflow(span, operation), Self::UnexpectedValueType { expected, actual } => { - unexpected_value_type(span, &expected, &actual) + unexpected_value_type(span, &expected, &actual).map_category(on_otherwise) + } + Self::InvalidConstructor { name } => { + invalid_constructor(span, name).map_category(on_otherwise) } - Self::InvalidConstructor { name } => invalid_constructor(span, name), Self::Suspension(suspension) => on_suspension(suspension), } } diff --git a/libs/@local/hashql/mir/src/reify/atom.rs b/libs/@local/hashql/mir/src/reify/atom.rs index 394b9b793b0..ca26410b975 100644 --- a/libs/@local/hashql/mir/src/reify/atom.rs +++ b/libs/@local/hashql/mir/src/reify/atom.rs @@ -1,3 +1,5 @@ +use core::alloc::Allocator; + use hashql_core::{id::Id as _, r#type::kind::TypeKind}; use hashql_hir::node::{ Node, @@ -25,7 +27,7 @@ use crate::{ }, }; -impl<'heap> Reifier<'_, '_, '_, '_, 'heap> { +impl<'heap, A: Allocator, S: Allocator> Reifier<'_, '_, '_, '_, 'heap, A, S> { fn local(&mut self, node: Node<'heap>) -> Local { let NodeKind::Variable(Variable::Local(local)) = node.kind else { self.state diff --git a/libs/@local/hashql/mir/src/reify/error.rs b/libs/@local/hashql/mir/src/reify/error.rs index e4ad649cccc..ffdbf04023e 100644 --- a/libs/@local/hashql/mir/src/reify/error.rs +++ b/libs/@local/hashql/mir/src/reify/error.rs @@ -8,9 +8,8 @@ use hashql_diagnostics::{ severity::{Critical, Severity}, }; -pub(crate) type ReifyDiagnostic = Diagnostic; -pub(crate) type ReifyDiagnosticIssues = - DiagnosticIssues; +pub type ReifyDiagnostic = Diagnostic; +pub type ReifyDiagnosticIssues = DiagnosticIssues; // Terminal categories for user-facing errors const UNSUPPORTED_FEATURE: TerminalDiagnosticCategory = TerminalDiagnosticCategory { diff --git a/libs/@local/hashql/mir/src/reify/mod.rs b/libs/@local/hashql/mir/src/reify/mod.rs index e72e49d3116..19180f265fb 100644 --- a/libs/@local/hashql/mir/src/reify/mod.rs +++ b/libs/@local/hashql/mir/src/reify/mod.rs @@ -6,7 +6,8 @@ mod terminator; mod transform; mod types; -use core::debug_assert_matches; +use alloc::alloc::Global; +use core::{alloc::Allocator, debug_assert_matches}; use hashql_core::{ collections::{ @@ -36,11 +37,11 @@ use hashql_hir::{ }, }; +pub use self::error::{ReifyDiagnostic, ReifyDiagnosticCategory, ReifyDiagnosticIssues}; use self::{ current::CurrentBlock, error::{ - ReifyDiagnosticCategory, ReifyDiagnosticIssues, expected_anf_thunk, expected_anf_variable, - external_modules_unsupported, local_not_thunk, + expected_anf_thunk, expected_anf_variable, external_modules_unsupported, local_not_thunk, }, types::unwrap_closure_type, }; @@ -65,13 +66,15 @@ use crate::{ /// /// This structure contains the essential components needed to transform HIR(ANF) into MIR, /// including symbol tables, type information, and memory management. -pub struct ReifyContext<'mir, 'hir, 'env, 'heap> { +pub struct ReifyContext<'mir, 'hir, 'env, 'heap, A: Allocator = Global, S: Allocator = Global> { /// Mutable reference to the collection of MIR bodies being generated. - pub bodies: &'mir mut DefIdVec>, + pub bodies: &'mir mut DefIdVec, A>, /// MIR context. pub mir: &'mir mut MirContext<'env, 'heap>, /// HIR context containing the source nodes and variable mappings. pub hir: &'hir HirContext<'hir, 'heap>, + /// Scratch allocator for temporary memory usage during reification. + pub scratch: S, } /// Tracks the mapping between variable IDs and their corresponding thunk definition IDs. @@ -83,12 +86,12 @@ pub struct ReifyContext<'mir, 'hir, 'env, 'heap> { /// /// Thunks are sparse and limited to the first few IDs since nested thunks are not allowed. /// Using a vector here is memory-efficient given this constraint. -pub struct Thunks { - defs: VarIdVec>, +pub struct Thunks { + defs: VarIdVec, S>, set: MixedBitSet, } -impl Thunks { +impl Thunks { fn insert(&mut self, var: VarId, def: DefId) { self.defs.insert(var, def); self.set.insert(var); @@ -99,9 +102,9 @@ impl Thunks { /// /// This structure maintains global state needed throughout reification, including /// thunk mappings, constructor definitions, and memory pools for efficient allocation. -struct CrossCompileState<'heap> { +struct CrossCompileState<'heap, S: Allocator> { /// Mapping of variable IDs to their thunk definitions. - thunks: Thunks, + thunks: Thunks, /// Collection of diagnostics encountered during reification. diagnostics: ReifyDiagnosticIssues, @@ -117,12 +120,12 @@ struct CrossCompileState<'heap> { /// Each `Reifier` instance is responsible for converting a single function/thunk/closure /// from HIR to MIR. It maintains its own local state for basic blocks, variable mappings, /// and local allocation while sharing global state through the context and cross-compile state. -struct Reifier<'ctx, 'mir, 'hir, 'env, 'heap> { +struct Reifier<'ctx, 'mir, 'hir, 'env, 'heap, A: Allocator, S: Allocator> { /// Reference to the global reification context. - context: &'ctx mut ReifyContext<'mir, 'hir, 'env, 'heap>, + context: &'ctx mut ReifyContext<'mir, 'hir, 'env, 'heap, A, S>, /// Reference to the shared cross-compilation state. - state: &'ctx mut CrossCompileState<'heap>, + state: &'ctx mut CrossCompileState<'heap, S>, /// Basic blocks being constructed for the current function body. blocks: BasicBlockVec, &'heap Heap>, @@ -133,10 +136,12 @@ struct Reifier<'ctx, 'mir, 'hir, 'env, 'heap> { local_decls: LocalVec, &'heap Heap>, } -impl<'ctx, 'mir, 'hir, 'env, 'heap> Reifier<'ctx, 'mir, 'hir, 'env, 'heap> { +impl<'ctx, 'mir, 'hir, 'env, 'heap, A: Allocator, S: Allocator> + Reifier<'ctx, 'mir, 'hir, 'env, 'heap, A, S> +{ const fn new( - context: &'ctx mut ReifyContext<'mir, 'hir, 'env, 'heap>, - state: &'ctx mut CrossCompileState<'heap>, + context: &'ctx mut ReifyContext<'mir, 'hir, 'env, 'heap, A, S>, + state: &'ctx mut CrossCompileState<'heap, S>, ) -> Self { let blocks = BasicBlockVec::new_in(context.mir.heap); let local_decls = LocalVec::new_in(context.mir.heap); @@ -473,9 +478,9 @@ impl<'ctx, 'mir, 'hir, 'env, 'heap> Reifier<'ctx, 'mir, 'hir, 'env, 'heap> { /// /// See [BE-67](https://linear.app/hash/issue/BE-67/hashql-implement-modules) for /// planned multi-module architecture. -pub fn from_hir<'heap>( +pub fn from_hir<'heap, A: Allocator, S: Allocator + Clone>( node: Node<'heap>, - context: &mut ReifyContext<'_, '_, '_, 'heap>, + context: &mut ReifyContext<'_, '_, '_, 'heap, A, S>, ) -> Status { // The node is already in HIR(ANF) - each node will be a thunk. let NodeKind::Let(Let { bindings, body }) = node.kind else { @@ -496,7 +501,7 @@ pub fn from_hir<'heap>( }; let thunks = Thunks { - defs: VarIdVec::new(), + defs: VarIdVec::new_in(context.scratch.clone()), set: MixedBitSet::new_empty(context.hir.counter.var.size()), }; let mut state = CrossCompileState { diff --git a/libs/@local/hashql/mir/src/reify/rvalue.rs b/libs/@local/hashql/mir/src/reify/rvalue.rs index b29a0442b80..4e2104e0a03 100644 --- a/libs/@local/hashql/mir/src/reify/rvalue.rs +++ b/libs/@local/hashql/mir/src/reify/rvalue.rs @@ -1,3 +1,5 @@ +use core::alloc::Allocator; + use hashql_core::{ id::{Id as _, IdVec}, symbol::sym, @@ -32,7 +34,7 @@ use crate::{ interpret::value::{Int, TryFromPrimitiveError}, }; -impl<'mir, 'heap> Reifier<'_, 'mir, '_, '_, 'heap> { +impl<'mir, 'heap, A: Allocator, S: Allocator> Reifier<'_, 'mir, '_, '_, 'heap, A, S> { fn rvalue_data(&mut self, data: Data<'heap>) -> RValue<'heap> { match data { Data::Primitive(primitive) => { diff --git a/libs/@local/hashql/mir/src/reify/terminator.rs b/libs/@local/hashql/mir/src/reify/terminator.rs index cfba67fb9d3..437bdbe68ed 100644 --- a/libs/@local/hashql/mir/src/reify/terminator.rs +++ b/libs/@local/hashql/mir/src/reify/terminator.rs @@ -1,3 +1,5 @@ +use core::alloc::Allocator; + use hashql_core::{heap, id::Id as _, span::SpanId}; use hashql_hir::node::{ branch, @@ -23,7 +25,7 @@ use crate::{ def::DefId, }; -impl<'mir, 'heap> Reifier<'_, 'mir, '_, '_, 'heap> { +impl<'mir, 'heap, A: Allocator, S: Allocator> Reifier<'_, 'mir, '_, '_, 'heap, A, S> { fn terminator_graph_read_head( &mut self, head: graph::GraphReadHead<'heap>, diff --git a/libs/@local/hashql/mir/src/reify/transform.rs b/libs/@local/hashql/mir/src/reify/transform.rs index a3249dca65a..5bbc4bfca06 100644 --- a/libs/@local/hashql/mir/src/reify/transform.rs +++ b/libs/@local/hashql/mir/src/reify/transform.rs @@ -1,3 +1,5 @@ +use core::alloc::Allocator; + use hashql_core::{ collections::TinyVec, id::{IdVec, bit_vec::BitRelations as _}, @@ -28,7 +30,7 @@ use crate::{ def::DefId, }; -impl<'mir, 'heap> Reifier<'_, 'mir, '_, '_, 'heap> { +impl<'mir, 'heap, A: Allocator, S: Allocator> Reifier<'_, 'mir, '_, '_, 'heap, A, S> { pub(super) fn transform_closure( &mut self, block: &mut CurrentBlock<'mir, 'heap>, From 5846cb4f101838546f181b2bc59c9d5cfcbc012b Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Tue, 2 Jun 2026 18:10:30 +0200 Subject: [PATCH 05/25] feat: introduce hashql_eval interner --- libs/@local/graph/api/src/rest/entity.rs | 2 + .../@local/graph/api/src/rest/entity/query.rs | 261 +++++++++++ .../@local/hashql/compiletest/src/pipeline.rs | 49 +- .../compiletest/src/suite/eval_postgres.rs | 7 +- libs/@local/hashql/core/src/heap/mod.rs | 2 +- libs/@local/hashql/core/src/heap/pool.rs | 434 ++++++++++++------ libs/@local/hashql/core/src/lib.rs | 6 +- libs/@local/hashql/eval/src/context.rs | 31 +- libs/@local/hashql/eval/src/intern.rs | 28 ++ libs/@local/hashql/eval/src/lib.rs | 1 + .../eval/src/orchestrator/codec/decode/mod.rs | 4 +- .../src/orchestrator/codec/decode/tests.rs | 58 ++- .../hashql/eval/src/orchestrator/mod.rs | 6 +- .../hashql/eval/src/orchestrator/partial.rs | 21 +- .../hashql/eval/src/postgres/filter/mod.rs | 6 +- .../hashql/eval/src/postgres/filter/tests.rs | 10 +- libs/@local/hashql/eval/src/postgres/mod.rs | 6 +- .../eval/tests/orchestrator/execution.rs | 43 +- .../hashql/eval/tests/orchestrator/inputs.rs | 249 ++++++---- .../hashql/eval/tests/orchestrator/main.rs | 12 +- .../hashql/eval/tests/orchestrator/output.rs | 6 +- .../hashql/mir/src/interpret/value/struct.rs | 27 +- libs/@local/hashql/mir/src/pass/mod.rs | 109 ++++- 23 files changed, 995 insertions(+), 383 deletions(-) create mode 100644 libs/@local/graph/api/src/rest/entity/query.rs create mode 100644 libs/@local/hashql/eval/src/intern.rs diff --git a/libs/@local/graph/api/src/rest/entity.rs b/libs/@local/graph/api/src/rest/entity.rs index f0196e2555d..2b332db1b7a 100644 --- a/libs/@local/graph/api/src/rest/entity.rs +++ b/libs/@local/graph/api/src/rest/entity.rs @@ -1,5 +1,7 @@ //! Web routes for CRU operations on entities. +mod query; + use alloc::sync::Arc; use std::collections::HashMap; diff --git a/libs/@local/graph/api/src/rest/entity/query.rs b/libs/@local/graph/api/src/rest/entity/query.rs new file mode 100644 index 00000000000..8f397158226 --- /dev/null +++ b/libs/@local/graph/api/src/rest/entity/query.rs @@ -0,0 +1,261 @@ +use alloc::borrow::Cow; +use core::mem; + +use hashql_ast::error::AstDiagnosticCategory; +use hashql_core::{ + heap::{Heap, HeapPool, ResetAllocator, Scratch, ScratchPool}, + module::ModuleRegistry, + span::{SpanId, SpanTable}, + r#type::environment::Environment, +}; +use hashql_diagnostics::{ + Diagnostic, DiagnosticCategory, DiagnosticIssues, Failure, Source, Sources, Status, + StatusExt as _, Success, category::canonical_category_id, severity::Critical, source::SourceId, +}; +use hashql_eval::{ + context::CodeGenerationContext, + error::EvalDiagnosticCategory, + postgres::{PostgresCompiler, PreparedQueries}, +}; +use hashql_hir::error::HirDiagnosticCategory; +use hashql_mir::{ + body::Body, + def::{DefId, DefIdVec}, + error::MirDiagnosticCategory, + pass::LowerConfig, +}; +use hashql_syntax_jexpr::{error::JExprDiagnosticCategory, span::Span}; +use serde_json::value::RawValue; +use tokio::task::LocalSet; + +struct QueryContext { + scratches: ScratchPool, + heaps: HeapPool, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +enum HashQlDiagnosticCategory { + JExpr(JExprDiagnosticCategory), + Ast(AstDiagnosticCategory), + Hir(HirDiagnosticCategory), + Mir(MirDiagnosticCategory), + Eval(EvalDiagnosticCategory), +} + +impl serde::Serialize for HashQlDiagnosticCategory { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_str(&canonical_category_id(self)) + } +} + +impl DiagnosticCategory for HashQlDiagnosticCategory { + fn id(&self) -> Cow<'_, str> { + Cow::Borrowed("hashql") + } + + fn name(&self) -> Cow<'_, str> { + Cow::Borrowed("HashQL") + } + + fn subcategory(&self) -> Option<&dyn DiagnosticCategory> { + match self { + Self::JExpr(jexpr) => Some(jexpr), + Self::Ast(ast) => Some(ast), + Self::Hir(hir) => Some(hir), + Self::Mir(mir) => Some(mir), + Self::Eval(eval) => Some(eval), + } + } +} + +fn process_status( + diagnostics: &mut DiagnosticIssues, + status: Status, +) -> Result>> +where + C: DiagnosticCategory + 'static, +{ + match status { + Ok(Success { value, advisories }) => { + diagnostics.append(&mut advisories.generalize()); + + Ok(value) + } + Err(Failure { primary, secondary }) => { + diagnostics.append(&mut secondary.generalize()); + + Err(primary) + } + } +} + +fn query(ctx: &QueryContext, query: &RawValue) { + let mut scratch = ctx.scratches.get(); + let heap = ctx.heaps.get(); + + let compilation = Compilation::compile(&heap, &mut scratch, query.get()); +} + +struct CodeCompilationArtifact<'heap> { + interpreter: DefIdVec, &'heap Heap>, + postgres: PreparedQueries<'heap, &'heap Heap>, +} + +struct Compilation<'heap, 'query> { + sources: Sources<'query>, + spans: SpanTable, + + interner: hashql_eval::intern::Interner<'heap>, + env: Environment<'heap>, + + entrypoint: DefId, + artifact: CodeCompilationArtifact<'heap>, +} + +impl<'heap, 'query> Compilation<'heap, 'query> { + pub fn compile( + heap: &'heap Heap, + scratch: &mut Scratch, + query: &'query str, + ) -> Status { + let mut sources = Sources::new(); + let source_id = sources.push(Source::new(query)); + + let mut spans = SpanTable::new(source_id); + + // Parse the query + let mut parser = hashql_syntax_jexpr::Parser::new(heap, &mut spans); + let mut ast = parser.parse_expr(query.as_bytes()).map_err(|diagnostic| { + Failure::new( + diagnostic + .map_category(HashQlDiagnosticCategory::JExpr) + .map_severity(|severity| { + Critical::try_new(severity).unwrap_or_else(|| { + tracing::error!( + ?severity, + "JExpr returned an error of non-critical severity" + ); + Critical::ERROR + }) + }), + ) + })?; + + let mut env = Environment::new(heap); + let modules = ModuleRegistry::new(&env); + + // Lower the AST + let Success { + value: types, + advisories, + } = hashql_ast::lowering::lower(heap.intern_symbol("main"), &mut ast, &env, &modules) + .map_category(|category| { + HashQlDiagnosticCategory::Ast(AstDiagnosticCategory::Lowering(category)) + })?; + + let interner = hashql_hir::intern::Interner::new(heap); + let mut hir_context = hashql_hir::context::HirContext::new(&interner, &modules); + + // Reify the HIR from the AST + let Success { + value: hir, + advisories, + } = hashql_hir::node::NodeData::from_ast(ast, &mut hir_context, &types) + .map_category(|category| { + HashQlDiagnosticCategory::Hir(HirDiagnosticCategory::Reification(category)) + }) + .with_diagnostics(advisories)?; + + // Lower the HIR + let Success { + value: hir, + advisories, + } = hashql_hir::lower::lower(hir, &types, &mut env, &mut hir_context) + .map_category(|category| { + HashQlDiagnosticCategory::Hir(HirDiagnosticCategory::Lowering(category)) + }) + .with_diagnostics(advisories)?; + + let interner = hashql_mir::intern::Interner::new(heap); + let mut bodies = DefIdVec::new_in(heap); + let mut mir_context = hashql_mir::context::MirContext { + heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }; + let mut reify_context = hashql_mir::reify::ReifyContext { + bodies: &mut bodies, + mir: &mut mir_context, + hir: &hir_context, + scratch: &*scratch, + }; + + // Reify the MIR from the HIR + let Success { + value: entrypoint, + advisories, + } = hashql_mir::reify::from_hir(hir, &mut reify_context) + .map_category(|category| { + HashQlDiagnosticCategory::Mir(MirDiagnosticCategory::Reify(category)) + }) + .with_diagnostics(advisories)?; + + // Lower the MIR + let Success { + value: (), + advisories, + } = hashql_mir::pass::lower( + &mut mir_context, + scratch, + &mut bodies, + &LowerConfig::default(), + ) + .map_category(HashQlDiagnosticCategory::Mir) + .with_diagnostics(advisories)?; + + // Plan the execution + let Success { + value: execution, + advisories, + } = hashql_mir::pass::place(&mut mir_context, scratch, &mut bodies) + .map_category(HashQlDiagnosticCategory::Mir) + .with_diagnostics(advisories)?; + + // Build the postgres fragments + let interner = interner.into(); + let mut context = CodeGenerationContext::new_in( + &env, + &interner, + &bodies, + &execution, + heap, + &mut *scratch, + ); + + let mut postgres = PostgresCompiler::new_in(&mut context, &mut *scratch); + let queries = postgres.compile(); + scratch.reset(); + + context + .diagnostics + .into_status(()) + .map_category(HashQlDiagnosticCategory::Eval) + .with_diagnostics(advisories)?; + + Status::success(Self { + sources, + spans, + env, + interner, + entrypoint, + artifact: CodeCompilationArtifact { + interpreter: bodies, + postgres: queries, + }, + }) + } +} diff --git a/libs/@local/hashql/compiletest/src/pipeline.rs b/libs/@local/hashql/compiletest/src/pipeline.rs index 7ba98d9b5ad..44465cb52fa 100644 --- a/libs/@local/hashql/compiletest/src/pipeline.rs +++ b/libs/@local/hashql/compiletest/src/pipeline.rs @@ -17,7 +17,7 @@ //! individual crate APIs. use hashql_core::{ - heap::{Heap, ResetAllocator as _, Scratch}, + heap::{Heap, Scratch}, module::ModuleRegistry, span::{SpanId, SpanTable}, r#type::environment::Environment, @@ -31,12 +31,7 @@ use hashql_mir::{ body::Body, context::MirContext, def::{DefId, DefIdSlice, DefIdVec}, - pass::{ - Changed, GlobalAnalysisPass as _, GlobalTransformPass as _, GlobalTransformState, - analysis::SizeEstimationAnalysis, - execution::{ExecutionAnalysis, ExecutionAnalysisResidual}, - transform::{Inline, InlineConfig, PostInline, PreInline}, - }, + pass::{self, LowerConfig, execution::ExecutionAnalysisResidual}, reify::ReifyContext, }; use hashql_syntax_jexpr::span::Span; @@ -218,24 +213,14 @@ impl<'heap> Pipeline<'heap> { bodies: &mut DefIdSlice>, ) -> Result<(), BoxedDiagnostic<'static, SpanId>> { let mut context = MirContext::new(&self.env, interner); - let mut state = GlobalTransformState::new_in(&*bodies, self.heap); - self.scratch.reset(); - - let mut pass = PreInline::new_in(&mut self.scratch); - let _: Changed = pass.run(&mut context, &mut state, bodies); - self.scratch.reset(); - - let mut pass = Inline::new_in(InlineConfig::default(), &mut self.scratch); - let _: Changed = pass.run(&mut context, &mut state, bodies); - self.scratch.reset(); - - let mut pass = PostInline::new_in(&mut self.scratch); - let _: Changed = pass.run(&mut context, &mut state, bodies); - self.scratch.reset(); - - let status = context.diagnostics.generalize().boxed().into_status(()); - process_status(&mut self.diagnostics, status)?; + let result = pass::lower( + &mut context, + &mut self.scratch, + bodies, + &LowerConfig::default(), + ); + process_status(&mut self.diagnostics, result)?; Ok(()) } @@ -262,20 +247,8 @@ impl<'heap> Pipeline<'heap> { > { let mut context = MirContext::new(&self.env, interner); - let mut pass = SizeEstimationAnalysis::new_in(&self.scratch); - pass.run(&mut context, bodies); - let footprints = pass.finish(); - self.scratch.reset(); - - let pass = ExecutionAnalysis { - footprints: &footprints, - scratch: &mut self.scratch, - }; - let analysis = pass.run_all_in(&mut context, bodies, self.heap); - self.scratch.reset(); - - let status = context.diagnostics.generalize().boxed().into_status(()); - process_status(&mut self.diagnostics, status)?; + let status = pass::place(&mut context, &mut self.scratch, bodies); + let analysis = process_status(&mut self.diagnostics, status)?; Ok(analysis) } diff --git a/libs/@local/hashql/compiletest/src/suite/eval_postgres.rs b/libs/@local/hashql/compiletest/src/suite/eval_postgres.rs index 721796be1e9..f38539e2592 100644 --- a/libs/@local/hashql/compiletest/src/suite/eval_postgres.rs +++ b/libs/@local/hashql/compiletest/src/suite/eval_postgres.rs @@ -7,7 +7,7 @@ use hashql_core::{ r#type::{TypeFormatter, TypeFormatterOptions, environment::Environment}, }; use hashql_diagnostics::DiagnosticIssues; -use hashql_eval::{context::EvalContext, postgres::PostgresCompiler}; +use hashql_eval::{context::CodeGenerationContext, postgres::PostgresCompiler}; use hashql_mir::{ body::{Body, basic_block::BasicBlockId, terminator::TerminatorKind}, context::MirContext, @@ -117,12 +117,13 @@ impl Suite for EvalPostgres { let mir_buf = format_mir_with_placement(heap, &environment, &bodies, &analysis); secondary_outputs.insert("mir", mir_buf); - let mut context = EvalContext::new_in( + let interner = interner.into(); + let mut context = CodeGenerationContext::new_in( &environment, &interner, &bodies, &analysis, - context.heap, + heap, &mut scratch, ); scratch.reset(); diff --git a/libs/@local/hashql/core/src/heap/mod.rs b/libs/@local/hashql/core/src/heap/mod.rs index 68360ad9803..f1e2cd1116f 100644 --- a/libs/@local/hashql/core/src/heap/mod.rs +++ b/libs/@local/hashql/core/src/heap/mod.rs @@ -111,7 +111,7 @@ pub use self::{ clone::{CloneIn, TryCloneIn}, convert::{FromIn, IntoIn}, iter::{CollectIn, FromIteratorIn}, - pool::{ScratchPool, ScratchPoolGuard}, + pool::{HeapPool, HeapPoolGuard, ScratchPool, ScratchPoolGuard}, scratch::Scratch, transfer::TransferInto, }; diff --git a/libs/@local/hashql/core/src/heap/pool.rs b/libs/@local/hashql/core/src/heap/pool.rs index 7a7b7c64a6f..358c1136629 100644 --- a/libs/@local/hashql/core/src/heap/pool.rs +++ b/libs/@local/hashql/core/src/heap/pool.rs @@ -1,75 +1,71 @@ -//! Pool of scratch allocators for parallel bump allocation. +//! Allocator pools for parallel bump allocation. //! -//! [`ScratchPool`] enables bump allocation across multiple threads. Each thread -//! borrows its own [`ScratchPoolGuard`] via [`get`](ScratchPool::get), which provides -//! an independent bump allocator. +//! [`ScratchPool`] provides thread-safe scratch allocation. Each thread borrows +//! its own [`ScratchPoolGuard`] via [`get`](ScratchPool::get), which derefs to +//! [`Scratch`](super::Scratch). //! -//! # Usage -//! -//! ``` -//! # #![feature(allocator_api)] -//! use hashql_core::heap::ScratchPool; -//! -//! let pool = ScratchPool::new(); -//! -//! let guard = pool.get(); -//! let mut vec: Vec = Vec::new_in(&guard); -//! vec.push(42); -//! ``` +//! [`HeapPool`] provides thread-safe heap allocation with symbol interning. +//! Each thread borrows its own [`HeapPoolGuard`] via [`get`](HeapPool::get), +//! which derefs to [`Heap`](super::Heap). -use core::{alloc, mem, ptr}; +use core::{ + mem::ManuallyDrop, + ops::{Deref, DerefMut}, +}; +use std::sync::nonpoison::Mutex; -use bump_scope::{BumpBox, BumpPool, BumpPoolGuard}; +use super::{Heap, ResetAllocator as _, Scratch}; -use super::{AllocatorScope, BumpAllocator, allocator::Checkpoint}; - -/// A pool of scratch allocators for parallel bump allocation. -/// -/// Unlike [`Scratch`](super::Scratch) which is `!Sync`, `ScratchPool` can be shared -/// across threads. Each thread obtains its own [`ScratchPoolGuard`] via [`get`](Self::get), -/// which provides an independent bump allocator. -/// -/// # Example +/// A pool of [`Scratch`] allocators for parallel bump allocation. /// -/// ``` -/// # #![feature(allocator_api)] -/// use hashql_core::heap::ScratchPool; +/// Each thread obtains its own [`Scratch`] via [`get`](Self::get). The +/// allocator is returned to the pool and reset when the guard is dropped. /// -/// let pool = ScratchPool::new(); -/// let guard = pool.get(); -/// -/// let mut vec: Vec = Vec::new_in(&guard); -/// vec.push(1); -/// vec.push(2); -/// ``` -pub struct ScratchPool(BumpPool); +/// By default the pool is unbounded. Use [`bounded`](Self::bounded) to cap +/// the number of retained allocators; excess allocators are dropped instead +/// of returned. +pub struct ScratchPool { + pool: Mutex>, + max_size: usize, +} impl ScratchPool { - /// Creates a new empty scratch pool. + /// Creates a new unbounded scratch pool. #[must_use] #[inline] - pub fn new() -> Self { - Self(BumpPool::new()) + pub const fn new() -> Self { + Self { + pool: Mutex::new(Vec::new()), + max_size: usize::MAX, + } } - /// Borrows an allocator from the pool. + /// Creates a new scratch pool that retains at most `max_size` allocators. /// - /// Each call may reuse a previously returned allocator or create a new one. + /// When a guard is dropped and the pool is already at capacity, the + /// allocator is dropped instead of returned to the pool. + #[must_use] #[inline] - pub fn get(&self) -> ScratchPoolGuard<'_> { - ScratchPoolGuard(self.0.get()) + pub const fn bounded(max_size: usize) -> Self { + Self { + pool: Mutex::new(Vec::new()), + max_size, + } } - /// Resets all allocators in the pool, freeing all allocations at once. - /// - /// The pool retains its current capacity. - /// - /// # Panics + /// Borrows a [`Scratch`] from the pool. /// - /// All [`ScratchPoolGuard`]s must have been dropped before calling this method. + /// Reuses a previously returned allocator if available, otherwise creates + /// a new one. The allocator is reset and returned to the pool when the + /// guard is dropped. #[inline] - pub fn reset(&mut self) { - self.0.reset(); + pub fn get(&self) -> ScratchPoolGuard<'_> { + let scratch = self.pool.lock().pop().unwrap_or_default(); + + ScratchPoolGuard { + pool: self, + scratch: ManuallyDrop::new(scratch), + } } } @@ -79,123 +75,283 @@ impl Default for ScratchPool { } } -/// A borrowed allocator from a [`ScratchPool`]. +/// A borrowed [`Scratch`] from a [`ScratchPool`]. /// -/// Implements [`BumpAllocator`] and [`Allocator`](alloc::Allocator), so it can be -/// used anywhere a bump allocator is expected. -pub struct ScratchPoolGuard<'pool>(BumpPoolGuard<'pool>); +/// Derefs to [`Scratch`], so it can be used anywhere a `&Scratch` is expected. +/// On drop, the allocator is reset and returned to the pool for reuse. +pub struct ScratchPoolGuard<'pool> { + pool: &'pool ScratchPool, + scratch: ManuallyDrop, +} -impl BumpAllocator for ScratchPoolGuard<'_> { - type Checkpoint = Checkpoint; - type Scoped<'scope> = AllocatorScope<'scope>; +impl Deref for ScratchPoolGuard<'_> { + type Target = Scratch; #[inline] - fn scoped(&mut self, func: impl FnOnce(Self::Scoped<'_>) -> T) -> T { - self.0.scoped(|scope| func(AllocatorScope(scope))) + fn deref(&self) -> &Scratch { + &self.scratch } +} +impl DerefMut for ScratchPoolGuard<'_> { #[inline] - fn checkpoint(&self) -> Self::Checkpoint { - Checkpoint(self.0.checkpoint()) + fn deref_mut(&mut self) -> &mut Scratch { + &mut self.scratch + } +} + +#[expect(unsafe_code, reason = "ManuallyDrop::take in Drop")] +impl Drop for ScratchPoolGuard<'_> { + fn drop(&mut self) { + // SAFETY: This is the `Drop` impl, so `drop` is called exactly once. + // After `take`, `self.scratch` is logically moved out and will not be + // read or dropped again. + let mut scratch = unsafe { ManuallyDrop::take(&mut self.scratch) }; + scratch.reset(); + + let mut scratches = self.pool.pool.lock(); + + if scratches.len() < self.pool.max_size { + scratches.push(scratch); + } } +} +/// A pool of [`Heap`] allocators for parallel allocation with symbol interning. +/// +/// Each thread obtains its own [`Heap`] via [`get`](Self::get). The heap is +/// returned to the pool and reset when the guard is dropped. +/// +/// By default the pool is unbounded. Use [`bounded`](Self::bounded) to cap +/// the number of retained heaps; excess heaps are dropped instead of returned. +pub struct HeapPool { + pool: Mutex>, + max_size: usize, +} + +impl HeapPool { + /// Creates a new unbounded heap pool. + #[must_use] #[inline] - unsafe fn rollback(&self, checkpoint: Self::Checkpoint) { - // SAFETY: The same safety preconditions apply. - unsafe { self.0.reset_to(checkpoint.0) } + pub const fn new() -> Self { + Self { + pool: Mutex::new(Vec::new()), + max_size: usize::MAX, + } } + /// Creates a new heap pool that retains at most `max_size` heaps. + /// + /// When a guard is dropped and the pool is already at capacity, the heap + /// is dropped instead of returned to the pool. + #[must_use] #[inline] - fn try_allocate_slice_copy(&self, slice: &[T]) -> Result<&mut [T], alloc::AllocError> { - self.0 - .try_alloc_slice_copy(slice) - .map(BumpBox::leak) - .map_err(|_err| alloc::AllocError) + pub const fn bounded(max_size: usize) -> Self { + Self { + pool: Mutex::new(Vec::new()), + max_size, + } } + /// Borrows a [`Heap`] from the pool. + /// + /// Reuses a previously returned heap if available, otherwise creates a new + /// one. The heap is reset and returned to the pool when the guard is + /// dropped. #[inline] - fn try_allocate_slice_uninit( - &self, - len: usize, - ) -> Result<&mut [mem::MaybeUninit], alloc::AllocError> { - const { - assert!( - !core::mem::needs_drop::(), - "Cannot allocate a type that needs drop" - ); - }; - - self.0 - .try_alloc_uninit_slice(len) - .map(BumpBox::leak) - .map_err(|_err| alloc::AllocError) + pub fn get(&self) -> HeapPoolGuard<'_> { + let heap = self.pool.lock().pop().unwrap_or_default(); + + HeapPoolGuard { + pool: self, + heap: ManuallyDrop::new(heap), + } } } -// SAFETY: Delegates to bump_scope via the internal BumpPoolGuard. -#[expect(unsafe_code, reason = "proxy to bump")] -unsafe impl alloc::Allocator for ScratchPoolGuard<'_> { - #[inline] - fn allocate(&self, layout: alloc::Layout) -> Result, alloc::AllocError> { - bump_scope::alloc::Allocator::allocate(&*self.0, layout).map_err(|_err| alloc::AllocError) +impl Default for HeapPool { + fn default() -> Self { + Self::new() } +} + +/// A borrowed [`Heap`] from a [`HeapPool`]. +/// +/// Derefs to [`Heap`], so it can be used anywhere a `&Heap` is expected. +/// On drop, the heap is reset and returned to the pool for reuse. +pub struct HeapPoolGuard<'pool> { + pool: &'pool HeapPool, + heap: ManuallyDrop, +} + +impl Deref for HeapPoolGuard<'_> { + type Target = Heap; #[inline] - fn allocate_zeroed( - &self, - layout: alloc::Layout, - ) -> Result, alloc::AllocError> { - bump_scope::alloc::Allocator::allocate_zeroed(&*self.0, layout) - .map_err(|_err| alloc::AllocError) + fn deref(&self) -> &Heap { + &self.heap } +} +impl DerefMut for HeapPoolGuard<'_> { #[inline] - unsafe fn deallocate(&self, ptr: ptr::NonNull, layout: alloc::Layout) { - // SAFETY: Caller upholds Allocator contract. - unsafe { - bump_scope::alloc::Allocator::deallocate(&*self.0, ptr, layout); - } + fn deref_mut(&mut self) -> &mut Heap { + &mut self.heap } +} - #[inline] - unsafe fn grow( - &self, - ptr: ptr::NonNull, - old_layout: alloc::Layout, - new_layout: alloc::Layout, - ) -> Result, alloc::AllocError> { - // SAFETY: Caller upholds Allocator contract. - unsafe { - bump_scope::alloc::Allocator::grow(&*self.0, ptr, old_layout, new_layout) - .map_err(|_err| alloc::AllocError) +#[expect(unsafe_code, reason = "ManuallyDrop::take in Drop")] +impl Drop for HeapPoolGuard<'_> { + fn drop(&mut self) { + // SAFETY: This is the `Drop` impl, so `drop` is called exactly once. + // After `take`, `self.heap` is logically moved out and will not be + // read or dropped again. + let mut heap = unsafe { ManuallyDrop::take(&mut self.heap) }; + heap.reset(); + + let mut heaps = self.pool.pool.lock(); + + if heaps.len() < self.pool.max_size { + heaps.push(heap); } } +} - #[inline] - unsafe fn grow_zeroed( - &self, - ptr: ptr::NonNull, - old_layout: alloc::Layout, - new_layout: alloc::Layout, - ) -> Result, alloc::AllocError> { - // SAFETY: Caller upholds Allocator contract. - unsafe { - bump_scope::alloc::Allocator::grow_zeroed(&*self.0, ptr, old_layout, new_layout) - .map_err(|_err| alloc::AllocError) - } +const _: () = { + const fn assert_send() {} + const fn assert_sync() {} + + assert_send::(); + assert_sync::(); + assert_send::>(); + + assert_send::(); + assert_sync::(); + assert_send::>(); +}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn scratch_pool_reuses_allocators() { + let pool = ScratchPool::new(); + + let guard = pool.get(); + let mut vec: Vec = Vec::new_in(&*guard); + vec.push(42); + assert_eq!(vec[0], 42); + drop(vec); + drop(guard); + + // Second get reuses the allocator. + let guard = pool.get(); + let mut vec: Vec = Vec::new_in(&*guard); + vec.push(99); + assert_eq!(vec[0], 99); + drop(vec); + drop(guard); + + assert_eq!(pool.pool.lock().len(), 1); } - #[inline] - unsafe fn shrink( - &self, - ptr: ptr::NonNull, - old_layout: alloc::Layout, - new_layout: alloc::Layout, - ) -> Result, alloc::AllocError> { - // SAFETY: Caller upholds Allocator contract. - unsafe { - bump_scope::alloc::Allocator::shrink(&*self.0, ptr, old_layout, new_layout) - .map_err(|_err| alloc::AllocError) - } + #[test] + fn scratch_pool_grows_under_concurrent_demand() { + let pool = ScratchPool::new(); + + let guard1 = pool.get(); + let guard2 = pool.get(); + + let mut vec1: Vec = Vec::new_in(&*guard1); + let mut vec2: Vec = Vec::new_in(&*guard2); + vec1.push(1); + vec2.push(2); + assert_eq!(vec1[0], 1); + assert_eq!(vec2[0], 2); + + drop(vec1); + drop(vec2); + drop(guard1); + drop(guard2); + + assert_eq!(pool.pool.lock().len(), 2); + } + + #[test] + fn scratch_pool_bounded_drops_excess() { + let pool = ScratchPool::bounded(1); + + let guard1 = pool.get(); + let guard2 = pool.get(); + drop(guard1); + drop(guard2); + + assert_eq!(pool.pool.lock().len(), 1); + } + + #[test] + fn heap_pool_reuses_heaps() { + let pool = HeapPool::new(); + + // First get: creates a new heap. + let guard = pool.get(); + let sym1 = guard.intern_symbol("hello"); + assert_eq!(sym1.as_str(), "hello"); + drop(guard); + + // Second get: should reuse the heap (pool has one to hand back). + // The returned heap was reset, so the runtime symbol is gone, + // but interning the same string still works (gets a fresh allocation). + let guard = pool.get(); + let sym2 = guard.intern_symbol("hello"); + assert_eq!(sym2.as_str(), "hello"); + drop(guard); + + // Pool should have exactly one heap in it now. + assert_eq!(pool.pool.lock().len(), 1); + } + + #[test] + fn heap_pool_bounded_drops_excess() { + let pool = HeapPool::bounded(1); + + // Borrow two heaps simultaneously. + let guard1 = pool.get(); + let guard2 = pool.get(); + guard1.intern_symbol("a"); + guard2.intern_symbol("b"); + + // Return both. Only one should be retained. + drop(guard1); + drop(guard2); + assert_eq!(pool.pool.lock().len(), 1); + + // A new get still works (reuses the one retained heap). + let guard = pool.get(); + assert_eq!(pool.pool.lock().len(), 0); + drop(guard); + assert_eq!(pool.pool.lock().len(), 1); + } + + #[test] + fn heap_pool_grows_under_concurrent_demand() { + let pool = HeapPool::new(); + + // Borrow two heaps simultaneously. + let guard1 = pool.get(); + let guard2 = pool.get(); + + // Both are independent: interning in one doesn't affect the other. + let sym1 = guard1.intern_symbol("aaa"); + let sym2 = guard2.intern_symbol("bbb"); + assert_eq!(sym1.as_str(), "aaa"); + assert_eq!(sym2.as_str(), "bbb"); + + drop(guard1); + drop(guard2); + + // Both heaps returned to pool. + assert_eq!(pool.pool.lock().len(), 2); } } diff --git a/libs/@local/hashql/core/src/lib.rs b/libs/@local/hashql/core/src/lib.rs index 30bcbe06294..11ea5e65acc 100644 --- a/libs/@local/hashql/core/src/lib.rs +++ b/libs/@local/hashql/core/src/lib.rs @@ -26,17 +26,19 @@ const_range, const_trait_impl, extend_one, + extern_types, get_disjoint_mut_helpers, + get_mut_unchecked, iter_intersperse, iter_map_windows, iter_next_chunk, + nonpoison_mutex, slice_partition_dedup, slice_swap_unchecked, step_trait, + sync_nonpoison, try_trait_v2, variant_count, - get_mut_unchecked, - extern_types )] extern crate alloc; diff --git a/libs/@local/hashql/eval/src/context.rs b/libs/@local/hashql/eval/src/context.rs index dd7c7bd15d4..c4b171cf2d5 100644 --- a/libs/@local/hashql/eval/src/context.rs +++ b/libs/@local/hashql/eval/src/context.rs @@ -11,7 +11,6 @@ use hashql_mir::{ local::Local, }, def::{DefId, DefIdSlice, DefIdVec}, - intern::Interner, pass::{ analysis::dataflow::{ TraversalLivenessAnalysis, @@ -21,7 +20,7 @@ use hashql_mir::{ }, }; -use crate::error::EvalDiagnosticIssues; +use crate::{error::EvalDiagnosticIssues, intern::Interner}; struct BasicBlockLiveOut( Box, TraversalPathBitSet)>, A>, @@ -49,7 +48,7 @@ impl Index<(DefId, BasicBlockId)> for LiveOut { } } -pub struct EvalContext<'ctx, 'heap, A: Allocator> { +pub struct CodeGenerationContext<'ctx, 'heap, A: Allocator> { pub env: &'ctx Environment<'heap>, pub interner: &'ctx Interner<'heap>, @@ -61,7 +60,7 @@ pub struct EvalContext<'ctx, 'heap, A: Allocator> { pub alloc: A, } -impl<'ctx, 'heap, A: Allocator> EvalContext<'ctx, 'heap, A> { +impl<'ctx, 'heap, A: Allocator> CodeGenerationContext<'ctx, 'heap, A> { pub fn new_in( env: &'ctx Environment<'heap>, interner: &'ctx Interner<'heap>, @@ -121,3 +120,27 @@ impl<'ctx, 'heap, A: Allocator> EvalContext<'ctx, 'heap, A> { } } } + +pub struct CodeExecutionContext<'ctx, 'heap, A: Allocator> { + pub env: &'ctx Environment<'heap>, + pub interner: &'ctx Interner<'heap>, + + pub bodies: &'ctx DefIdSlice>, + pub execution: &'ctx DefIdSlice>>, + + pub alloc: A, +} + +impl<'ctx, 'heap, A: Allocator> From> + for CodeExecutionContext<'ctx, 'heap, A> +{ + fn from(context: CodeGenerationContext<'ctx, 'heap, A>) -> Self { + Self { + env: context.env, + interner: context.interner, + bodies: context.bodies, + execution: context.execution, + alloc: context.alloc, + } + } +} diff --git a/libs/@local/hashql/eval/src/intern.rs b/libs/@local/hashql/eval/src/intern.rs new file mode 100644 index 00000000000..dee91f141df --- /dev/null +++ b/libs/@local/hashql/eval/src/intern.rs @@ -0,0 +1,28 @@ +use hashql_core::{intern::InternSet, symbol::Symbol}; + +/// Interner for the evaluation stage. +/// +/// Must be created from the MIR interner via [`From`] to preserve +/// [`Interned`](hashql_core::intern::Interned) pointer identity across +/// the MIR/eval boundary. +#[derive(Debug)] +pub struct Interner<'heap> { + pub symbols: InternSet<'heap, [Symbol<'heap>]>, +} + +#[cfg(test)] +impl<'heap> Interner<'heap> { + pub(crate) fn testing(heap: &'heap hashql_core::heap::Heap) -> Self { + Self { + symbols: InternSet::new(heap), + } + } +} + +impl<'heap> From> for Interner<'heap> { + fn from(interner: hashql_mir::intern::Interner<'heap>) -> Self { + Self { + symbols: interner.symbols, + } + } +} diff --git a/libs/@local/hashql/eval/src/lib.rs b/libs/@local/hashql/eval/src/lib.rs index 6c4dd4ddc2d..ec19faed689 100644 --- a/libs/@local/hashql/eval/src/lib.rs +++ b/libs/@local/hashql/eval/src/lib.rs @@ -24,6 +24,7 @@ extern crate alloc; pub mod context; pub mod error; +pub mod intern; pub mod orchestrator; pub mod postgres; diff --git a/libs/@local/hashql/eval/src/orchestrator/codec/decode/mod.rs b/libs/@local/hashql/eval/src/orchestrator/codec/decode/mod.rs index 0c8fd4dcfd0..85883ba9a9b 100644 --- a/libs/@local/hashql/eval/src/orchestrator/codec/decode/mod.rs +++ b/libs/@local/hashql/eval/src/orchestrator/codec/decode/mod.rs @@ -42,7 +42,7 @@ mod tests; /// [`Unknown`]: hashql_core::type::kind::TypeKind::Unknown pub struct Decoder<'env, 'heap, A> { env: &'env Environment<'heap>, - interner: &'env hashql_mir::intern::Interner<'heap>, + interner: &'env crate::intern::Interner<'heap>, alloc: A, } @@ -50,7 +50,7 @@ pub struct Decoder<'env, 'heap, A> { impl<'env, 'heap, A: Allocator> Decoder<'env, 'heap, A> { pub const fn new( env: &'env Environment<'heap>, - interner: &'env hashql_mir::intern::Interner<'heap>, + interner: &'env crate::intern::Interner<'heap>, alloc: A, ) -> Self { Self { diff --git a/libs/@local/hashql/eval/src/orchestrator/codec/decode/tests.rs b/libs/@local/hashql/eval/src/orchestrator/codec/decode/tests.rs index d632d0eec36..64ca25aa1f4 100644 --- a/libs/@local/hashql/eval/src/orchestrator/codec/decode/tests.rs +++ b/libs/@local/hashql/eval/src/orchestrator/codec/decode/tests.rs @@ -6,12 +6,10 @@ use hashql_core::{ symbol::sym, r#type::{TypeId, builder::TypeBuilder, environment::Environment}, }; -use hashql_mir::{ - intern::Interner, - interpret::value::{self, Value}, -}; +use hashql_mir::interpret::value::{self, Value}; use super::{DecodeError, Decoder, JsonValueRef}; +use crate::intern::Interner; fn str_value(content: &str) -> Value<'_, Global> { Value::String(value::Str::from(Rc::::from(content))) @@ -28,7 +26,7 @@ fn decoder<'env, 'heap>( fn primitive_string() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -42,7 +40,7 @@ fn primitive_string() { fn primitive_integer() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -57,7 +55,7 @@ fn primitive_integer() { fn primitive_number() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -72,7 +70,7 @@ fn primitive_number() { fn primitive_boolean_true() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -89,7 +87,7 @@ fn primitive_boolean_true() { fn primitive_boolean_false() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -106,7 +104,7 @@ fn primitive_boolean_false() { fn primitive_null() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -120,7 +118,7 @@ fn primitive_null() { fn primitive_type_mismatch() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -132,7 +130,7 @@ fn primitive_type_mismatch() { fn struct_matching_fields() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -157,7 +155,7 @@ fn struct_matching_fields() { fn struct_missing_field() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -174,7 +172,7 @@ fn struct_missing_field() { fn struct_extra_field() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -192,7 +190,7 @@ fn struct_extra_field() { fn tuple_correct_length() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -221,7 +219,7 @@ fn tuple_correct_length() { fn tuple_length_mismatch() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -236,7 +234,7 @@ fn tuple_length_mismatch() { fn union_first_variant_matches() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -253,7 +251,7 @@ fn union_first_variant_matches() { fn union_second_variant_matches() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -269,7 +267,7 @@ fn union_second_variant_matches() { fn union_no_variant_matches() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -283,7 +281,7 @@ fn union_no_variant_matches() { fn opaque_wraps_inner() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -303,7 +301,7 @@ fn opaque_wraps_inner() { fn list_intrinsic() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -329,7 +327,7 @@ fn list_intrinsic() { fn dict_intrinsic() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -360,7 +358,7 @@ fn dict_intrinsic() { fn intersection_type_error() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -374,7 +372,7 @@ fn intersection_type_error() { fn closure_type_error() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -388,7 +386,7 @@ fn closure_type_error() { fn never_type_error() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -400,7 +398,7 @@ fn never_type_error() { fn unknown_type_integer_fallback() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -415,7 +413,7 @@ fn unknown_type_integer_fallback() { fn unknown_type_float_fallback() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -430,7 +428,7 @@ fn unknown_type_float_fallback() { fn unknown_type_array_becomes_list() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -450,7 +448,7 @@ fn unknown_type_array_becomes_list() { fn unknown_type_non_url_object_becomes_dict() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); @@ -469,7 +467,7 @@ fn unknown_type_non_url_object_becomes_dict() { fn unknown_type_url_object_becomes_struct() { let heap = Heap::new(); let env = Environment::new(&heap); - let interner = Interner::new(&heap); + let interner = Interner::testing(&heap); let types = TypeBuilder::synthetic(&env); let decoder = decoder(&env, &interner); diff --git a/libs/@local/hashql/eval/src/orchestrator/mod.rs b/libs/@local/hashql/eval/src/orchestrator/mod.rs index 56e7acb85e2..7cb4198ecaa 100644 --- a/libs/@local/hashql/eval/src/orchestrator/mod.rs +++ b/libs/@local/hashql/eval/src/orchestrator/mod.rs @@ -59,7 +59,7 @@ pub use self::{ error::{OrchestratorDiagnostic, OrchestratorDiagnosticCategory}, events::{AppendEventLog, Event, EventLog}, }; -use crate::{context::EvalContext, postgres::PreparedQueries}; +use crate::{context::CodeExecutionContext, postgres::PreparedQueries}; pub mod codec; pub mod error; @@ -119,7 +119,7 @@ impl Deref for Indexed { pub struct Orchestrator<'env, 'ctx, 'heap, C, E, A: Allocator> { client: C, queries: &'env PreparedQueries<'heap, A>, - context: &'env EvalContext<'ctx, 'heap, A>, + context: &'env CodeExecutionContext<'ctx, 'heap, A>, /// Event sink for execution tracing. See [`EventLog`]. pub event_log: E, } @@ -128,7 +128,7 @@ impl<'env, 'ctx, 'heap, C, A: Allocator> Orchestrator<'env, 'ctx, 'heap, C, (), pub const fn new( client: C, queries: &'env PreparedQueries<'heap, A>, - context: &'env EvalContext<'ctx, 'heap, A>, + context: &'env CodeExecutionContext<'ctx, 'heap, A>, ) -> Self { Self { client, diff --git a/libs/@local/hashql/eval/src/orchestrator/partial.rs b/libs/@local/hashql/eval/src/orchestrator/partial.rs index ef81274e199..0231885b92c 100644 --- a/libs/@local/hashql/eval/src/orchestrator/partial.rs +++ b/libs/@local/hashql/eval/src/orchestrator/partial.rs @@ -31,7 +31,6 @@ use hashql_core::{ r#type::{TypeId, environment::Environment}, }; use hashql_mir::{ - intern::Interner, interpret::value::{Int, Num, Opaque, StructBuilder, Value}, pass::execution::{ VertexType, @@ -46,7 +45,7 @@ use super::{ codec::{JsonValueRef, decode::Decoder}, error::BridgeError, }; -use crate::postgres::ColumnDescriptor; +use crate::{intern::Interner, postgres::ColumnDescriptor}; macro_rules! hydrate { ($this:ident -> $entry:ident $(-> $field:ident)+ = $value:expr) => { @@ -243,7 +242,7 @@ impl<'heap, A: Allocator> PartialEncodings<'heap, A> { let mut builder: StructBuilder<'heap, A, 1> = StructBuilder::new(); self.vectors.finish_in(&mut builder, sym::vectors); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new( sym::path::EntityEncodings, Rc::new_in(value, alloc), @@ -286,7 +285,7 @@ impl<'heap, A: Allocator> PartialLinkEntityId<'heap, A> { )), ); - let inner = Value::Struct(builder.finish(interner, alloc.clone())); + let inner = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new(sym::path::EntityId, Rc::new_in(inner, alloc))) } } @@ -315,7 +314,7 @@ impl<'heap, A: Allocator> PartialProvenance<'heap, A> { self.inferred.finish_in(&mut builder, sym::inferred); self.edition.finish_in(&mut builder, sym::edition); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new( sym::path::EntityProvenance, @@ -350,7 +349,7 @@ impl<'heap, A: Allocator> PartialTemporalVersioning<'heap, A> { self.transaction_time .finish_in(&mut builder, sym::transaction_time); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new( sym::path::TemporalMetadata, Rc::new_in(value, alloc), @@ -390,7 +389,7 @@ impl<'heap, A: Allocator> PartialEntityId<'heap, A> { self.draft_id .finish_in(&mut builder, sym::draft_id, alloc.clone()); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new(sym::path::EntityId, Rc::new_in(value, alloc))) } } @@ -425,7 +424,7 @@ impl<'heap, A: Allocator> PartialRecordId<'heap, A> { .finish_in(&mut builder, sym::entity_id); self.edition_id.finish_in(&mut builder, sym::edition_id); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new(sym::path::RecordId, Rc::new_in(value, alloc))) } } @@ -483,7 +482,7 @@ impl<'heap, A: Allocator> PartialLinkData<'heap, A> { self.right_entity_provenance .finish_in(&mut builder, sym::right_entity_provenance); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new(sym::path::LinkData, Rc::new_in(value, alloc))) } @@ -561,7 +560,7 @@ impl<'heap, A: Allocator> PartialMetadata<'heap, A> { self.property_metadata .finish_in(&mut builder, sym::property_metadata); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new( sym::path::EntityMetadata, Rc::new_in(value, alloc), @@ -618,7 +617,7 @@ impl<'heap, A: Allocator> PartialEntity<'heap, A> { .map(|partial| partial.finish_in(interner, alloc.clone())) .finish_in(&mut builder, sym::encodings); - let value = Value::Struct(builder.finish(interner, alloc.clone())); + let value = Value::Struct(builder.finish(&interner.symbols, alloc.clone())); Value::Opaque(Opaque::new(sym::path::Entity, Rc::new_in(value, alloc))) } diff --git a/libs/@local/hashql/eval/src/postgres/filter/mod.rs b/libs/@local/hashql/eval/src/postgres/filter/mod.rs index 588d832dd71..b020e109294 100644 --- a/libs/@local/hashql/eval/src/postgres/filter/mod.rs +++ b/libs/@local/hashql/eval/src/postgres/filter/mod.rs @@ -62,7 +62,7 @@ use super::{ traverse::eval_entity_path, types::{IntegerType, integer_type}, }; -use crate::{context::EvalContext, error::EvalDiagnosticIssues}; +use crate::{context::CodeGenerationContext, error::EvalDiagnosticIssues}; /// Internal representation of a continuation result before casting to the SQL composite type. /// @@ -233,7 +233,7 @@ fn finish_switch_int( /// internal buffer retrievable via [`Self::into_diagnostics`]. pub(crate) struct GraphReadFilterCompiler<'ctx, 'heap, A: Allocator = Global, S: Allocator = Global> { - context: &'ctx EvalContext<'ctx, 'heap, A>, + context: &'ctx CodeGenerationContext<'ctx, 'heap, A>, body: &'ctx Body<'heap>, env: Local, @@ -247,7 +247,7 @@ pub(crate) struct GraphReadFilterCompiler<'ctx, 'heap, A: Allocator = Global, S: impl<'ctx, 'heap, A: Allocator, S: Allocator> GraphReadFilterCompiler<'ctx, 'heap, A, S> { pub(crate) fn new( - context: &'ctx EvalContext<'ctx, 'heap, A>, + context: &'ctx CodeGenerationContext<'ctx, 'heap, A>, body: &'ctx Body<'heap>, env: Local, scratch: S, diff --git a/libs/@local/hashql/eval/src/postgres/filter/tests.rs b/libs/@local/hashql/eval/src/postgres/filter/tests.rs index cc7ef967ac3..e00a2b9e809 100644 --- a/libs/@local/hashql/eval/src/postgres/filter/tests.rs +++ b/libs/@local/hashql/eval/src/postgres/filter/tests.rs @@ -40,7 +40,7 @@ use sqruff_lib::core::{config::FluffConfig, linter::core::Linter}; use sqruff_lib_core::dialects::init::DialectKind; use crate::{ - context::EvalContext, + context::CodeGenerationContext, postgres::{DatabaseContext, PostgresCompiler, filter::GraphReadFilterCompiler}, }; @@ -151,9 +151,9 @@ fn format_body<'heap>(fixture: &Fixture<'heap>, heap: &'heap Heap) -> String { fn compile_filter_islands<'heap>(fixture: &Fixture<'heap>, heap: &'heap Heap) -> FilterReport { let mut scratch = Scratch::new(); let def = fixture.def(); - let interner = Interner::new(heap); + let interner = crate::intern::Interner::testing(heap); - let context = EvalContext::new_in( + let context = CodeGenerationContext::new_in( &fixture.env, &interner, &fixture.bodies, @@ -278,9 +278,9 @@ fn compile_full_query_with_mask<'heap>( ) -> QueryReport { let mut scratch = Scratch::new(); let def = fixture.def(); - let interner = Interner::new(heap); + let interner = crate::intern::Interner::testing(heap); - let mut context = EvalContext::new_in( + let mut context = CodeGenerationContext::new_in( &fixture.env, &interner, &fixture.bodies, diff --git a/libs/@local/hashql/eval/src/postgres/mod.rs b/libs/@local/hashql/eval/src/postgres/mod.rs index 43744c10948..ded7f37ca79 100644 --- a/libs/@local/hashql/eval/src/postgres/mod.rs +++ b/libs/@local/hashql/eval/src/postgres/mod.rs @@ -64,7 +64,7 @@ pub use self::{ continuation::ContinuationField, parameters::{Parameter, ParameterIndex, ParameterValue, Parameters, TemporalAxis}, }; -use crate::context::EvalContext; +use crate::context::CodeGenerationContext; mod continuation; pub(crate) mod error; @@ -240,7 +240,7 @@ impl<'heap, A: Allocator> PreparedQueries<'heap, A> { /// /// [`GraphRead`]: hashql_mir::body::terminator::GraphRead pub struct PostgresCompiler<'eval, 'ctx, 'heap, A: Allocator, S: Allocator> { - context: &'eval mut EvalContext<'ctx, 'heap, A>, + context: &'eval mut CodeGenerationContext<'ctx, 'heap, A>, alloc: A, scratch: S, @@ -257,7 +257,7 @@ pub struct PostgresCompiler<'eval, 'ctx, 'heap, A: Allocator, S: Allocator> { impl<'eval, 'ctx, 'heap, A: Allocator, S: BumpAllocator> PostgresCompiler<'eval, 'ctx, 'heap, A, S> { - pub fn new_in(context: &'eval mut EvalContext<'ctx, 'heap, A>, scratch: S) -> Self + pub fn new_in(context: &'eval mut CodeGenerationContext<'ctx, 'heap, A>, scratch: S) -> Self where A: Clone, { diff --git a/libs/@local/hashql/eval/tests/orchestrator/execution.rs b/libs/@local/hashql/eval/tests/orchestrator/execution.rs index c26eff6549a..ced37221692 100644 --- a/libs/@local/hashql/eval/tests/orchestrator/execution.rs +++ b/libs/@local/hashql/eval/tests/orchestrator/execution.rs @@ -1,18 +1,19 @@ -use alloc::alloc::Global; use core::mem; use hashql_compiletest::pipeline::Pipeline; -use hashql_core::{heap::ResetAllocator as _, span::SpanId}; +use hashql_core::{ + heap::{Heap, ResetAllocator as _}, + span::SpanId, +}; use hashql_diagnostics::{Diagnostic, diagnostic::BoxedDiagnostic}; use hashql_eval::{ - context::EvalContext, + context::{CodeExecutionContext, CodeGenerationContext}, orchestrator::{AppendEventLog, Event, Orchestrator}, postgres::PostgresCompiler, }; use hashql_mir::{ body::Body, def::{DefId, DefIdSlice, DefIdVec}, - intern::Interner, interpret::{Inputs, value::Value}, }; use tokio::runtime; @@ -23,7 +24,7 @@ use tokio_postgres::Client; /// Holds the MIR artifacts needed to build typed inputs (via the decoder /// and the environment) before proceeding to execution. pub(crate) struct Lowered<'heap> { - pub interner: Interner<'heap>, + pub interner: hashql_mir::intern::Interner<'heap>, pub entry: DefId, pub bodies: DefIdVec>, } @@ -65,16 +66,16 @@ pub(crate) fn run<'heap>( runtime: &runtime::Runtime, client: &Client, - inputs: &Inputs<'heap, Global>, + inputs: &Inputs<'heap, &'heap Heap>, - lowered: &mut Lowered<'heap>, -) -> Result<(Value<'heap, Global>, Vec), BoxedDiagnostic<'static, SpanId>> { + mut lowered: Lowered<'heap>, +) -> Result<(Value<'heap, &'heap Heap>, Vec), BoxedDiagnostic<'static, SpanId>> { run_impl( pipeline, runtime, client, inputs, - &lowered.interner, + lowered.interner, lowered.entry, &mut lowered.bodies, ) @@ -94,12 +95,12 @@ pub(crate) fn execute<'heap>( runtime: &runtime::Runtime, client: &Client, - inputs: &Inputs<'heap, Global>, + inputs: &Inputs<'heap, &'heap Heap>, - interner: &Interner<'heap>, + interner: hashql_mir::intern::Interner<'heap>, entry: DefId, bodies: &mut DefIdSlice>, -) -> Result<(Value<'heap, Global>, Vec), BoxedDiagnostic<'static, SpanId>> { +) -> Result<(Value<'heap, &'heap Heap>, Vec), BoxedDiagnostic<'static, SpanId>> { run_impl(pipeline, runtime, client, inputs, interner, entry, bodies) } @@ -116,18 +117,19 @@ fn run_impl<'heap>( runtime: &runtime::Runtime, client: &Client, - inputs: &Inputs<'heap, Global>, + inputs: &Inputs<'heap, &'heap Heap>, - interner: &Interner<'heap>, + interner: hashql_mir::intern::Interner<'heap>, entry: DefId, bodies: &mut DefIdSlice>, -) -> Result<(Value<'heap, Global>, Vec), BoxedDiagnostic<'static, SpanId>> { - pipeline.transform(interner, bodies)?; - let analysis = pipeline.prepare(interner, bodies)?; +) -> Result<(Value<'heap, &'heap Heap>, Vec), BoxedDiagnostic<'static, SpanId>> { + pipeline.transform(&interner, bodies)?; + let analysis = pipeline.prepare(&interner, bodies)?; - let mut context = EvalContext::new_in( + let interner = interner.into(); + let mut context = CodeGenerationContext::new_in( &pipeline.env, - interner, + &interner, bodies, &analysis, pipeline.heap, @@ -141,11 +143,12 @@ fn run_impl<'heap>( pipeline.diagnostics.append(&mut diagnostics.boxed()); let event_log = AppendEventLog::new(); + let context = CodeExecutionContext::from(context); let orchestrator = Orchestrator::new(PostgresClient(client), &queries, &context).with_event_log(&event_log); let value = runtime - .block_on(orchestrator.run(inputs, entry, [])) + .block_on(orchestrator.run_in(inputs, entry, [], pipeline.heap)) .map_err(Diagnostic::boxed)?; Ok((value, event_log.take())) diff --git a/libs/@local/hashql/eval/tests/orchestrator/inputs.rs b/libs/@local/hashql/eval/tests/orchestrator/inputs.rs index d2276c909c8..423b7ae8f65 100644 --- a/libs/@local/hashql/eval/tests/orchestrator/inputs.rs +++ b/libs/@local/hashql/eval/tests/orchestrator/inputs.rs @@ -1,23 +1,19 @@ -use alloc::alloc::Global; +use alloc::rc::Rc; -use hashql_compiletest::pipeline::Pipeline; use hashql_core::{ - heap::Heap, - module::std_lib::graph::types::{ - knowledge::entity, principal::actor_group::web::types as web_types, - }, - symbol::sym, - r#type::TypeBuilder, + heap::{FromIn as _, Heap}, + intern::InternSet, + symbol::{Symbol, sym}, }; -use hashql_eval::orchestrator::codec::{Decoder, JsonValueRef}; -use hashql_mir::{ - intern::Interner, - interpret::{ - Inputs, - value::{self, Value}, - }, +use hashql_mir::interpret::{ + Inputs, + value::{self, StructBuilder, Value}, }; -use type_system::knowledge::entity::id::EntityUuid; +use type_system::{ + knowledge::entity::id::EntityUuid, + principal::actor_group::{ActorGroupEntityUuid, WebId}, +}; +use uuid::Uuid; use crate::{ directives::{AxisBound, AxisDirectives, AxisInterval}, @@ -25,26 +21,26 @@ use crate::{ }; /// Constructs `Opaque(Timestamp, Integer(ms))`. -fn timestamp_value(ms: i128) -> Value<'static, Global> { +fn timestamp_value(heap: &Heap, ms: i128) -> Value<'_, &Heap> { Value::Opaque(value::Opaque::new( sym::path::Timestamp, - Value::Integer(value::Int::from(ms)), + Rc::new_in(Value::Integer(value::Int::from(ms)), heap), )) } /// Constructs `Opaque(UnboundedTemporalBound, Unit)`. -fn unbounded_bound() -> Value<'static, Global> { +fn unbounded_bound(heap: &Heap) -> Value<'_, &Heap> { Value::Opaque(value::Opaque::new( sym::path::UnboundedTemporalBound, - Value::Unit, + Rc::new_in(Value::Unit, heap), )) } /// Constructs `Opaque(ExclusiveTemporalBound, Timestamp(ms))`. -fn exclusive_bound(ms: i128) -> Value<'static, Global> { +fn exclusive_bound(heap: &Heap, ms: i128) -> Value<'_, &Heap> { Value::Opaque(value::Opaque::new( sym::path::ExclusiveTemporalBound, - timestamp_value(ms), + Rc::new_in(timestamp_value(heap, ms), heap), )) } @@ -52,43 +48,48 @@ fn exclusive_bound(ms: i128) -> Value<'static, Global> { /// /// Fields are sorted lexicographically (`end` before `start`). fn interval_value<'heap>( - interner: &Interner<'heap>, - start: Value<'heap, Global>, - end: Value<'heap, Global>, -) -> Value<'heap, Global> { + heap: &'heap Heap, + symbols: &InternSet<'heap, [Symbol<'heap>]>, + start: Value<'heap, &'heap Heap>, + end: Value<'heap, &'heap Heap>, +) -> Value<'heap, &'heap Heap> { // Fields sorted: "end" < "start" - let fields = interner.symbols.intern_slice(&[sym::end, sym::start]); - let values = vec![end, start]; + let mut builder = StructBuilder::<_, 2>::new(); + builder.push(sym::end, end); + builder.push(sym::start, start); + + let inner = builder.finish(symbols, heap); Value::Opaque(value::Opaque::new( sym::path::Interval, - Value::Struct(value::Struct::new(fields, values).expect("interval struct is valid")), + Rc::new_in(Value::Struct(inner), heap), )) } /// Converts an [`AxisInterval`] to a `Value` representing a temporal /// interval: `Opaque(Interval, {start: , end: })`. fn axis_interval_to_value<'heap>( - interner: &Interner<'heap>, + heap: &'heap Heap, + symbols: &InternSet<'heap, [Symbol<'heap>]>, interval: &AxisInterval, -) -> Value<'heap, Global> { +) -> Value<'heap, &'heap Heap> { let start = match interval.start { - AxisBound::Unbounded => unbounded_bound(), + AxisBound::Unbounded => unbounded_bound(heap), AxisBound::Included(ms) => Value::Opaque(value::Opaque::new( sym::path::InclusiveTemporalBound, - timestamp_value(ms), + Rc::new_in(timestamp_value(heap, ms), heap), )), - AxisBound::Excluded(ms) => exclusive_bound(ms), + AxisBound::Excluded(ms) => exclusive_bound(heap, ms), }; let end = match interval.end { - AxisBound::Unbounded => unbounded_bound(), + AxisBound::Unbounded => unbounded_bound(heap), AxisBound::Included(ms) => Value::Opaque(value::Opaque::new( sym::path::InclusiveTemporalBound, - timestamp_value(ms), + Rc::new_in(timestamp_value(heap, ms), heap), )), - AxisBound::Excluded(ms) => exclusive_bound(ms), + AxisBound::Excluded(ms) => exclusive_bound(heap, ms), }; - interval_value(interner, start, end) + interval_value(heap, symbols, start, end) } /// Returns `true` if the interval is a point (both bounds are Included with @@ -108,9 +109,10 @@ fn is_point(interval: &AxisInterval) -> Option { /// determines which axis is pinned (a point `(T)`) and which is variable /// (a range `[a, b)` or defaulting to unbounded). fn temporal_axes_from_directives<'heap>( - interner: &Interner<'heap>, + heap: &'heap Heap, + symbols: &InternSet<'heap, [Symbol<'heap>]>, directives: &AxisDirectives, -) -> Value<'heap, Global> { +) -> Value<'heap, &'heap Heap> { let far_future_ms: i128 = 4_102_444_800_000; // 2100-01-01T00:00:00Z let default_variable = || AxisInterval { start: AxisBound::Unbounded, @@ -167,15 +169,23 @@ fn temporal_axes_from_directives<'heap>( } }; - let pinned = Value::Opaque(value::Opaque::new(pinned_axis, timestamp_value(pinned_ms))); + let pinned = Value::Opaque(value::Opaque::new( + pinned_axis, + Rc::new_in(timestamp_value(heap, pinned_ms), heap), + )); let variable = Value::Opaque(value::Opaque::new( variable_axis_name, - axis_interval_to_value(interner, &variable_interval), + Rc::new_in( + axis_interval_to_value(heap, symbols, &variable_interval), + heap, + ), )); - // "pinned" < "variable" lexicographically. - let fields = interner.symbols.intern_slice(&[sym::pinned, sym::variable]); - let values = vec![pinned, variable]; + let mut builder = value::StructBuilder::<_, 2>::new(); + builder.push(sym::pinned, pinned); + builder.push(sym::variable, variable); + + let inner = builder.finish(symbols, heap); let wrapper_name = if pinned_axis == sym::path::TransactionTime { sym::path::PinnedTransactionTimeTemporalAxes @@ -185,10 +195,31 @@ fn temporal_axes_from_directives<'heap>( Value::Opaque(value::Opaque::new( wrapper_name, - Value::Struct(value::Struct::new(fields, values).expect("axes struct is valid")), + Rc::new_in(Value::Struct(inner), heap), )) } +fn option<'heap, T>( + heap: &'heap Heap, + value: Option, + on_value: impl FnOnce(&'heap Heap, T) -> value::Value<'heap, &'heap Heap>, +) -> value::Value<'heap, &'heap Heap> { + value.map_or_else( + || { + value::Value::Opaque(value::Opaque::new( + sym::path::None, + Rc::new_in(value::Value::Unit, heap), + )) + }, + |value| { + value::Value::Opaque(value::Opaque::new( + sym::path::Some, + Rc::new_in(on_value(heap, value), heap), + )) + }, + ) +} + /// Builds the shared input set from seeded entity data and axis directives. /// /// Uses the decoder and the post-lowering type environment to construct @@ -196,86 +227,106 @@ fn temporal_axes_from_directives<'heap>( /// match what J-Expr test files reference via `["input", "", ""]`. pub(crate) fn build_inputs<'heap>( heap: &'heap Heap, - pipeline: &Pipeline<'heap>, - interner: &Interner<'heap>, + symbols: &InternSet<'heap, [Symbol<'heap>]>, entities: &SeededEntities, directives: &AxisDirectives, -) -> Inputs<'heap, Global> { - let mut inputs = Inputs::new(); - let decoder = Decoder::new(&pipeline.env, interner, Global); - let ty = TypeBuilder::synthetic(&pipeline.env); - let entity_uuid_type = entity::types::entity_uuid(&ty, None); - let entity_id_type = entity::types::entity_id(&ty, None); +) -> Inputs<'heap, &'heap Heap> { + let mut inputs = Inputs::new_in(heap); - // Insert an EntityUuid-typed input. - let insert_uuid = |inputs: &mut Inputs<'heap, Global>, name: &str, uuid: &EntityUuid| { - let uuid_str = uuid.to_string(); - let value = decoder - .decode(entity_uuid_type, JsonValueRef::String(&uuid_str)) - .expect("could not decode EntityUuid input"); + let string = |value: &str| value::Value::String(value::Str::from(Rc::from_in(value, heap))); + + let uuid = |value: Uuid| { + value::Value::Opaque(value::Opaque::new( + sym::path::Uuid, + Rc::new_in(string(value.to_string().as_str()), heap), + )) + }; - inputs.insert(heap.intern_symbol(name), value); + let entity_uuid = |value: EntityUuid| { + value::Value::Opaque(value::Opaque::new( + sym::path::EntityUuid, + Rc::new_in(uuid(value.into()), heap), + )) }; + let actor_group_entity_uuid = |value: ActorGroupEntityUuid| { + value::Value::Opaque(value::Opaque::new( + sym::path::ActorGroupEntityUuid, + Rc::new_in(uuid(value.into()), heap), + )) + }; + + let web_id = |value: WebId| { + value::Value::Opaque(value::Opaque::new( + sym::path::WebId, + Rc::new_in(actor_group_entity_uuid(value.into()), heap), + )) + }; + + let draft_id = |value: Option| { + option(heap, value, |heap, value| { + value::Value::Opaque(value::Opaque::new( + sym::path::DraftId, + Rc::new_in(uuid(value.into()), heap), + )) + }) + }; + + let entity_id = |value: type_system::knowledge::entity::id::EntityId| { + let mut builder = StructBuilder::<_, 3>::new(); + builder.push(sym::web_id, web_id(value.web_id)); + builder.push(sym::entity_uuid, entity_uuid(value.entity_uuid)); + builder.push(sym::draft_id, draft_id(value.draft_id)); + + let r#struct = builder.finish(symbols, heap); + value::Value::Struct(r#struct) + }; + + // Insert an EntityUuid-typed input. + let insert_entity_uuid = + |inputs: &mut Inputs<'heap, &'heap Heap>, name: &str, uuid: EntityUuid| { + inputs.insert(heap.intern_symbol(name), entity_uuid(uuid)); + }; + // Insert a full EntityId-typed input. let insert_entity_id = - |inputs: &mut Inputs<'heap, Global>, + |inputs: &mut Inputs<'heap, &'heap Heap>, name: &str, - id: &type_system::knowledge::entity::EntityId| { - let json = serde_json::json!({ - "web_id": id.web_id.to_string(), - "entity_uuid": id.entity_uuid.to_string(), - "draft_id": id.draft_id.map(|draft| draft.to_string()), - }); - let value = decoder - .decode(entity_id_type, JsonValueRef::from(&json)) - .expect("could not decode EntityId input"); - - inputs.insert(heap.intern_symbol(name), value); + id: type_system::knowledge::entity::EntityId| { + inputs.insert(heap.intern_symbol(name), entity_id(id)); }; - insert_uuid(&mut inputs, "alice_uuid", &entities.alice.entity_uuid); - insert_uuid(&mut inputs, "bob_uuid", &entities.bob.entity_uuid); - insert_uuid(&mut inputs, "org_uuid", &entities.organization.entity_uuid); - insert_uuid( + insert_entity_uuid(&mut inputs, "alice_uuid", entities.alice.entity_uuid); + insert_entity_uuid(&mut inputs, "bob_uuid", entities.bob.entity_uuid); + insert_entity_uuid(&mut inputs, "org_uuid", entities.organization.entity_uuid); + insert_entity_uuid( &mut inputs, "friend_link_uuid", - &entities.friend_link.entity_uuid, + entities.friend_link.entity_uuid, ); - insert_uuid( + insert_entity_uuid( &mut inputs, "draft_alice_uuid", - &entities.draft_alice.entity_uuid, + entities.draft_alice.entity_uuid, ); - insert_entity_id(&mut inputs, "alice_id", &entities.alice); - insert_entity_id(&mut inputs, "bob_id", &entities.bob); - insert_entity_id(&mut inputs, "org_id", &entities.organization); - insert_entity_id(&mut inputs, "friend_link_id", &entities.friend_link); - insert_entity_id(&mut inputs, "draft_alice_id", &entities.draft_alice); + insert_entity_id(&mut inputs, "alice_id", entities.alice); + insert_entity_id(&mut inputs, "bob_id", entities.bob); + insert_entity_id(&mut inputs, "org_id", entities.organization); + insert_entity_id(&mut inputs, "friend_link_id", entities.friend_link); + insert_entity_id(&mut inputs, "draft_alice_id", entities.draft_alice); // WebId input (all seeded entities share the same web). - let web_id_type = web_types::web_id(&ty, None); - let web_id_value = decoder - .decode( - web_id_type, - JsonValueRef::String(&entities.alice.web_id.to_string()), - ) - .expect("could not decode WebId input"); - inputs.insert(heap.intern_symbol("web_id"), web_id_value); + inputs.insert(heap.intern_symbol("web_id"), web_id(entities.alice.web_id)); // String inputs for property-based filtering. - let string_type = ty.string(); - let alice_name = decoder - .decode(string_type, JsonValueRef::String("Alice")) - .expect("could not decode string input"); - inputs.insert(heap.intern_symbol("alice_name"), alice_name); + inputs.insert(heap.intern_symbol("alice_name"), string("Alice")); // Temporal axes from directives (or default: unbounded decision time, // far-future transaction pin). inputs.insert( heap.intern_symbol("temporal_axes"), - temporal_axes_from_directives(interner, directives), + temporal_axes_from_directives(heap, symbols, directives), ); inputs diff --git a/libs/@local/hashql/eval/tests/orchestrator/main.rs b/libs/@local/hashql/eval/tests/orchestrator/main.rs index 08301562a3c..6e8a34976ec 100644 --- a/libs/@local/hashql/eval/tests/orchestrator/main.rs +++ b/libs/@local/hashql/eval/tests/orchestrator/main.rs @@ -109,7 +109,7 @@ fn run_jexpr_test( let mut pipeline = Pipeline::new(&heap); // Lower first so the type environment is populated, then build inputs. - let mut lowered = match execution::lower(&mut pipeline, &bytes) { + let lowered = match execution::lower(&mut pipeline, &bytes) { Ok(lowered) => lowered, Err(diagnostic) => { let rendered = render_failure(&source, &pipeline, &diagnostic); @@ -119,8 +119,7 @@ fn run_jexpr_test( let inputs = build_inputs( &heap, - &pipeline, - &lowered.interner, + &lowered.interner.symbols, &context.entities, &axis_directives, ); @@ -130,7 +129,7 @@ fn run_jexpr_test( runtime, context.store.as_client(), &inputs, - &mut lowered, + lowered, ) { Ok((value, events)) => { let rendered = render_success(&source, &value, &events, &pipeline)?; @@ -162,8 +161,7 @@ fn run_programmatic_test( let inputs = build_inputs( &heap, - &pipeline, - &interner, + &interner.symbols, &context.entities, &AxisDirectives::default(), ); @@ -177,7 +175,7 @@ fn run_programmatic_test( runtime, context.store.as_client(), &inputs, - &interner, + interner, entry, &mut bodies, ) { diff --git a/libs/@local/hashql/eval/tests/orchestrator/output.rs b/libs/@local/hashql/eval/tests/orchestrator/output.rs index 0573c093acb..398507803cd 100644 --- a/libs/@local/hashql/eval/tests/orchestrator/output.rs +++ b/libs/@local/hashql/eval/tests/orchestrator/output.rs @@ -1,4 +1,4 @@ -use alloc::alloc::Global; +use core::alloc::Allocator; use std::{collections::HashMap, fs, path::Path, sync::LazyLock}; use error_stack::{Report, ResultExt as _}; @@ -108,9 +108,9 @@ fn normalize(input: &str) -> String { /// /// Returns [`TestError::Serialization`] if the value cannot be serialized to /// JSON. -pub(crate) fn render_success( +pub(crate) fn render_success( source: &str, - value: &Value<'_, Global>, + value: &Value<'_, A>, events: &[Event], pipeline: &Pipeline<'_>, ) -> Result> { diff --git a/libs/@local/hashql/mir/src/interpret/value/struct.rs b/libs/@local/hashql/mir/src/interpret/value/struct.rs index 4e265b00983..6a3718ff5bb 100644 --- a/libs/@local/hashql/mir/src/interpret/value/struct.rs +++ b/libs/@local/hashql/mir/src/interpret/value/struct.rs @@ -9,10 +9,15 @@ use core::{ ptr, }; -use hashql_core::{algorithms::co_sort, id::Id as _, intern::Interned, symbol::Symbol}; +use hashql_core::{ + algorithms::co_sort, + id::Id as _, + intern::{InternSet, Interned}, + symbol::Symbol, +}; use super::Value; -use crate::{body::place::FieldIndex, intern::Interner}; +use crate::body::place::FieldIndex; /// A named-field struct value. /// @@ -289,7 +294,11 @@ impl<'heap, A: Allocator, const N: usize> StructBuilder<'heap, A, N> { } /// Consumes the builder and produces a [`Struct`]. - pub fn finish(mut self, interner: &Interner<'heap>, alloc: A) -> Struct<'heap, A> { + pub fn finish( + mut self, + symbols: &InternSet<'heap, [Symbol<'heap>]>, + alloc: A, + ) -> Struct<'heap, A> { // SAFETY: `fields[..initialized]` is fully initialized by invariant. let fields_mut = unsafe { self.fields[..self.initialized].assume_init_mut() }; // SAFETY: `values[..initialized]` is fully initialized by invariant. @@ -300,7 +309,7 @@ impl<'heap, A: Allocator, const N: usize> StructBuilder<'heap, A, N> { // initialization invariant is preserved even if it were to unwind. co_sort(fields_mut, values_mut); - let fields = interner.symbols.intern_slice(self.fields()); + let fields = symbols.intern_slice(self.fields()); // Allocate an uninitialized Rc slice for the values. // @@ -385,7 +394,7 @@ mod tests { builder.push(sym_b, int(2)); builder.push(sym_a, int(1)); - let result = builder.finish(&interner, Global); + let result = builder.finish(&interner.symbols, Global); // Fields should be sorted: a before b. assert_eq!(result.fields().len(), 2); @@ -399,7 +408,7 @@ mod tests { let interner = Interner::new(&heap); let builder = StructBuilder::<'_, Global, 0>::new(); - let result = builder.finish(&interner, Global); + let result = builder.finish(&interner.symbols, Global); assert!(result.is_empty()); assert_eq!(result.len(), 0); @@ -415,7 +424,7 @@ mod tests { let mut builder = StructBuilder::<'_, Global, 1>::new(); builder.push(sym, int(42)); - let result = builder.finish(&interner, Global); + let result = builder.finish(&interner.symbols, Global); assert_eq!(result.len(), 1); assert_eq!(result.get_by_name(sym), Some(&int(42))); @@ -470,7 +479,7 @@ mod tests { builder.push(sym_b, string("beta")); // finish moves values into Rc; builder Drop must not re-drop them. - let result = builder.finish(&interner, Global); + let result = builder.finish(&interner.symbols, Global); assert_eq!(result.len(), 2); // Verify values survived the move. @@ -497,7 +506,7 @@ mod tests { builder.push(sym_a, string("alpha")); builder.push(sym_b, string("bravo")); - let result = builder.finish(&interner, Global); + let result = builder.finish(&interner.symbols, Global); // After sorting: a, b, c. let pairs: Vec<_> = result.iter().collect(); diff --git a/libs/@local/hashql/mir/src/pass/mod.rs b/libs/@local/hashql/mir/src/pass/mod.rs index a71f27bd006..09806905b96 100644 --- a/libs/@local/hashql/mir/src/pass/mod.rs +++ b/libs/@local/hashql/mir/src/pass/mod.rs @@ -19,15 +19,26 @@ use core::{ alloc::Allocator, + mem, ops::{BitOr, BitOrAssign}, }; -use hashql_core::heap::BumpAllocator; +use hashql_core::{ + heap::{BumpAllocator, Heap, ResetAllocator as _, Scratch}, + span::SpanId, +}; +use hashql_diagnostics::Status; +use self::{ + analysis::SizeEstimationAnalysis, + execution::{ExecutionAnalysis, ExecutionAnalysisResidual}, + transform::{Inline, InlineConfig, PostInline, PreInline}, +}; use crate::{ body::Body, context::MirContext, def::{DefId, DefIdSlice, DefIdVec}, + error::MirDiagnosticCategory, }; pub mod analysis; @@ -467,6 +478,102 @@ pub trait GlobalAnalysisPass<'env, 'heap> { } } +/// Configuration for the MIR lowering pipeline. +#[derive(Debug, Clone, Default)] +pub struct LowerConfig { + pub inline: InlineConfig, +} + +/// Runs the MIR lowering pipeline over all bodies. +/// +/// Produces optimized, fully inlined MIR ready for execution placement. The +/// pipeline runs three phases: +/// +/// 1. **Pre-inline canonicalization**: simplifies each body in isolation (constant folding, dead +/// code elimination, CFG cleanup) so that the inliner sees clean callees and makes better +/// decisions. +/// 2. **Inlining**: inter-procedural pass that substitutes callees into call sites based on cost +/// heuristics, with aggressive inlining for filter bodies. +/// 3. **Post-inline canonicalization**: cleans up redundancy introduced by inlining (duplicate +/// code, dead stores, unreachable blocks). +/// +/// # Errors +/// +/// Returns `Err` if any pass emits a critical diagnostic (`Error` or `Bug` +/// severity). This indicates a compiler invariant violation, since transform +/// passes operate on well-typed, well-formed MIR. +pub fn lower<'heap>( + context: &mut MirContext<'_, 'heap>, + scratch: &mut Scratch, + bodies: &mut DefIdSlice>, + config: &LowerConfig, +) -> Status<(), MirDiagnosticCategory, SpanId> { + scratch.reset(); + + let mut state = GlobalTransformState::new_in(&*bodies, context.heap); + + let mut pass = PreInline::new_in(&mut *scratch); + let _: Changed = pass.run(context, &mut state, bodies); + scratch.reset(); + + let mut pass = Inline::new_in(config.inline, &mut *scratch); + let _: Changed = pass.run(context, &mut state, bodies); + scratch.reset(); + + let mut pass = PostInline::new_in(&mut *scratch); + let _: Changed = pass.run(context, &mut state, bodies); + scratch.reset(); + + let issues = mem::take(&mut context.diagnostics); + issues.into_status(()) +} + +/// Determines which execution backend each basic block runs on. +/// +/// Only [`GraphReadFilter`] bodies are analyzed; all other bodies receive +/// `None` in the result. The pipeline runs two phases: +/// +/// 1. **Size estimation**: computes per-body footprints used to estimate data transfer costs at +/// island boundaries. +/// 2. **Execution analysis**: for each filter body, computes per-target statement and terminator +/// costs, solves the placement problem, fuses adjacent same-backend blocks, and builds the +/// island graph. +/// +/// # Errors +/// +/// Returns `Err` if the placement solver emits a critical diagnostic. The +/// interpreter is a universal execution target, so a valid assignment +/// should always exist; a solver failure indicates a bug in domain pruning +/// or target construction. +/// +/// [`GraphReadFilter`]: crate::body::Source::GraphReadFilter +pub fn place<'heap>( + context: &mut MirContext<'_, 'heap>, + scratch: &mut Scratch, + bodies: &mut DefIdSlice>, +) -> Status< + DefIdVec>, &'heap Heap>, + MirDiagnosticCategory, + SpanId, +> { + let heap = context.heap; + + let mut pass = SizeEstimationAnalysis::new_in(&*scratch); + pass.run(context, bodies); + let footprints = pass.finish(); + scratch.reset(); + + let pass = ExecutionAnalysis { + footprints: &footprints, + scratch: &mut *scratch, + }; + let residual = pass.run_all_in(context, bodies, heap); + scratch.reset(); + + let issues = mem::take(&mut context.diagnostics); + issues.into_status(residual) +} + #[cfg(test)] mod tests { use super::Changed; From 9a1772f717bf1692c8ecd81c0193978ae3a0ff6f Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:44:21 +0200 Subject: [PATCH 06/25] chore: checkpoint --- libs/@local/graph/api/src/rest/entity/query.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/@local/graph/api/src/rest/entity/query.rs b/libs/@local/graph/api/src/rest/entity/query.rs index 8f397158226..e795cb37f6e 100644 --- a/libs/@local/graph/api/src/rest/entity/query.rs +++ b/libs/@local/graph/api/src/rest/entity/query.rs @@ -15,6 +15,7 @@ use hashql_diagnostics::{ use hashql_eval::{ context::CodeGenerationContext, error::EvalDiagnosticCategory, + orchestrator::Orchestrator, postgres::{PostgresCompiler, PreparedQueries}, }; use hashql_hir::error::HirDiagnosticCategory; From 562626cb7b5cd5ad718a28567fbf0df22f0c460d Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:26:45 +0200 Subject: [PATCH 07/25] feat: checkpoint --- libs/@local/graph/api/src/lib.rs | 1 + .../@local/graph/api/src/rest/entity/query.rs | 87 +++++++- .../postgres-store/src/store/postgres/mod.rs | 11 +- libs/@local/hashql/core/src/graph/linked.rs | 12 +- libs/@local/hashql/eval/src/context.rs | 15 +- .../hashql/eval/src/orchestrator/error.rs | 28 +-- .../eval/tests/orchestrator/execution.rs | 1 + libs/@local/hashql/mir/src/interpret/error.rs | 187 ++++++++++-------- .../hashql/mir/src/interpret/value/struct.rs | 5 +- .../src/pass/execution/island/graph/mod.rs | 11 ++ .../hashql/mir/src/pass/execution/mod.rs | 11 +- 11 files changed, 261 insertions(+), 108 deletions(-) diff --git a/libs/@local/graph/api/src/lib.rs b/libs/@local/graph/api/src/lib.rs index 7b4903ec731..6eced828a20 100644 --- a/libs/@local/graph/api/src/lib.rs +++ b/libs/@local/graph/api/src/lib.rs @@ -9,6 +9,7 @@ // Library Features error_generic_member_access, + allocator_api )] extern crate alloc; diff --git a/libs/@local/graph/api/src/rest/entity/query.rs b/libs/@local/graph/api/src/rest/entity/query.rs index e795cb37f6e..a87322d79b8 100644 --- a/libs/@local/graph/api/src/rest/entity/query.rs +++ b/libs/@local/graph/api/src/rest/entity/query.rs @@ -1,6 +1,10 @@ use alloc::borrow::Cow; use core::mem; +use std::{alloc::Global, sync::Arc}; +use hash_graph_postgres_store::store::{PostgresStore, PostgresStorePool}; +use hash_graph_store::pool::StorePool; +use hash_temporal_client::TemporalClient; use hashql_ast::error::AstDiagnosticCategory; use hashql_core::{ heap::{Heap, HeapPool, ResetAllocator, Scratch, ScratchPool}, @@ -13,7 +17,7 @@ use hashql_diagnostics::{ StatusExt as _, Success, category::canonical_category_id, severity::Critical, source::SourceId, }; use hashql_eval::{ - context::CodeGenerationContext, + context::{CodeExecutionContext, CodeGenerationContext}, error::EvalDiagnosticCategory, orchestrator::Orchestrator, postgres::{PostgresCompiler, PreparedQueries}, @@ -23,17 +27,23 @@ use hashql_mir::{ body::Body, def::{DefId, DefIdVec}, error::MirDiagnosticCategory, - pass::LowerConfig, + interpret::{CallStack, Inputs}, + pass::{LowerConfig, execution::ExecutionAnalysisResidual}, }; use hashql_syntax_jexpr::{error::JExprDiagnosticCategory, span::Span}; use serde_json::value::RawValue; -use tokio::task::LocalSet; +use tokio::{runtime::Runtime, task::LocalSet}; -struct QueryContext { +struct CompilerContext { scratches: ScratchPool, heaps: HeapPool, } +struct ExecutionContext<'ctx> { + postgres: &'ctx PostgresStorePool, + temporal: Option>, +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] enum HashQlDiagnosticCategory { JExpr(JExprDiagnosticCategory), @@ -93,19 +103,72 @@ where } } -fn query(ctx: &QueryContext, query: &RawValue) { +struct Task { + context: CompilerContext, +} + +impl Task { + fn spawn(&self) { + let rt = Runtime::new + + } +} + +#[expect(clippy::future_not_send)] +async fn query_local( + ctx: Arc, + client: ::Store<'static>, + query: Arc, +) -> Status<(), HashQlDiagnosticCategory, SpanId> { + // We're hitting a borrow checker limitation here, we cannot prove that scratch and heap are + // still alive by the time that we `spawn_local`, and due to the fact that it requires `'static` + // it means that we need to build the query inside here, and only then delegate, otherwise we'd + // be able to run it outside. let mut scratch = ctx.scratches.get(); let heap = ctx.heaps.get(); - let compilation = Compilation::compile(&heap, &mut scratch, query.get()); + let inputs = Inputs::new(); // TODO: https://linear.app/hash/issue/BE-41/hashql-expose-input-in-graph-api + + let Success { + value: compilation, + advisories, + } = tokio::task::block_in_place(|| Compilation::compile(&heap, &mut scratch, query.get()))?; + + let context = compilation.context(); + + let orchestrator = Orchestrator::new(client, &compilation.artifact.postgres, &context); + + orchestrator + .run(&inputs, compilation.entrypoint, []) + .await + .unwrap(); + + Status::success(()) +} + +async fn query( + ctx: Arc, + exec: ExecutionContext<'_>, + query: Arc, +) -> Status<(), HashQlDiagnosticCategory, SpanId> { + let client = exec.postgres.acquire(exec.temporal).await.unwrap(); + + let local = LocalSet::new(); + let result = local.run_until(query_local(ctx, client, query)).await; + + todo!() } struct CodeCompilationArtifact<'heap> { + assignment: DefIdVec>, &'heap Heap>, + interpreter: DefIdVec, &'heap Heap>, postgres: PreparedQueries<'heap, &'heap Heap>, } struct Compilation<'heap, 'query> { + heap: &'heap Heap, + sources: Sources<'query>, spans: SpanTable, @@ -248,15 +311,27 @@ impl<'heap, 'query> Compilation<'heap, 'query> { .with_diagnostics(advisories)?; Status::success(Self { + heap, sources, spans, env, interner, entrypoint, artifact: CodeCompilationArtifact { + assignment: execution, interpreter: bodies, postgres: queries, }, }) } + + fn context(&self) -> CodeExecutionContext<'_, 'heap, &'heap Heap> { + CodeExecutionContext { + env: &self.env, + interner: &self.interner, + bodies: &self.artifact.interpreter, + execution: &self.artifact.assignment, + alloc: self.heap, + } + } } diff --git a/libs/@local/graph/postgres-store/src/store/postgres/mod.rs b/libs/@local/graph/postgres-store/src/store/postgres/mod.rs index 5fd8a492611..89d6f84cc77 100644 --- a/libs/@local/graph/postgres-store/src/store/postgres/mod.rs +++ b/libs/@local/graph/postgres-store/src/store/postgres/mod.rs @@ -51,7 +51,7 @@ use hash_status::StatusCode; use hash_temporal_client::TemporalClient; use postgres_types::{Json, ToSql}; use time::OffsetDateTime; -use tokio_postgres::{GenericClient as _, error::SqlState}; +use tokio_postgres::{Client, GenericClient as _, error::SqlState}; use tracing::Instrument as _; use type_system::{ Valid, @@ -112,6 +112,15 @@ pub struct PostgresStore { pub settings: Arc, } +impl AsRef for PostgresStore +where + C: AsClient, +{ + fn as_ref(&self) -> &Client { + self.client.as_client() + } +} + impl PostgresStore> { /// Inserts multiple policies into the database. /// diff --git a/libs/@local/hashql/core/src/graph/linked.rs b/libs/@local/hashql/core/src/graph/linked.rs index 569139dbb00..e26222bc8e1 100644 --- a/libs/@local/hashql/core/src/graph/linked.rs +++ b/libs/@local/hashql/core/src/graph/linked.rs @@ -50,6 +50,7 @@ use alloc::alloc::Global; use core::{ alloc::Allocator, + fmt, ops::{Index, IndexMut}, }; @@ -213,7 +214,7 @@ impl Edge { /// } /// # else { unreachable!() } /// ``` -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct LinkedGraph { /// All nodes in the graph, indexed by [`NodeId`]. nodes: IdVec, A>, @@ -541,6 +542,15 @@ impl Default for LinkedGraph { } } +impl fmt::Debug for LinkedGraph { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("LinkedGraph") + .field("nodes", &self.nodes) + .field("edges", &self.edges) + .finish() + } +} + impl IndexMut for LinkedGraph { fn index_mut(&mut self, index: NodeId) -> &mut Self::Output { &mut self.nodes[index] diff --git a/libs/@local/hashql/eval/src/context.rs b/libs/@local/hashql/eval/src/context.rs index c4b171cf2d5..de7df9e75fa 100644 --- a/libs/@local/hashql/eval/src/context.rs +++ b/libs/@local/hashql/eval/src/context.rs @@ -1,4 +1,4 @@ -use core::{alloc::Allocator, ops::Index}; +use core::{alloc::Allocator, fmt, ops::Index}; use hashql_core::{ heap::BumpAllocator, id::bit_vec::DenseBitSet, r#type::environment::Environment, @@ -57,6 +57,7 @@ pub struct CodeGenerationContext<'ctx, 'heap, A: Allocator> { pub live_out: LiveOut, pub diagnostics: EvalDiagnosticIssues, + pub alloc: A, } @@ -121,6 +122,7 @@ impl<'ctx, 'heap, A: Allocator> CodeGenerationContext<'ctx, 'heap, A> { } } +#[derive(Copy, Clone)] pub struct CodeExecutionContext<'ctx, 'heap, A: Allocator> { pub env: &'ctx Environment<'heap>, pub interner: &'ctx Interner<'heap>, @@ -131,6 +133,17 @@ pub struct CodeExecutionContext<'ctx, 'heap, A: Allocator> { pub alloc: A, } +impl fmt::Debug for CodeExecutionContext<'_, '_, A> { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("CodeExecutionContext") + .field("env", &self.env) + .field("interner", &self.interner) + .field("bodies", &self.bodies) + .field("execution", &self.execution) + .finish_non_exhaustive() + } +} + impl<'ctx, 'heap, A: Allocator> From> for CodeExecutionContext<'ctx, 'heap, A> { diff --git a/libs/@local/hashql/eval/src/orchestrator/error.rs b/libs/@local/hashql/eval/src/orchestrator/error.rs index a3c5471b4b2..d5f3306e9ae 100644 --- a/libs/@local/hashql/eval/src/orchestrator/error.rs +++ b/libs/@local/hashql/eval/src/orchestrator/error.rs @@ -28,7 +28,7 @@ use hashql_diagnostics::{ Diagnostic, Label, category::{DiagnosticCategory, TerminalDiagnosticCategory}, diagnostic::Message, - severity::Severity, + severity::Critical, }; use hashql_mir::{ body::{basic_block::BasicBlockId, local::Local}, @@ -102,7 +102,7 @@ const VALUE_SERIALIZATION: TerminalDiagnosticCategory = TerminalDiagnosticCatego /// Type alias for orchestrator diagnostics. /// /// The default severity kind is [`Severity`], which allows any severity level. -pub type OrchestratorDiagnostic = +pub type OrchestratorDiagnostic = Diagnostic; /// Diagnostic subcategory for errors that occur while fulfilling a suspension. @@ -459,7 +459,7 @@ fn query_execution( sql: &str, error: &tokio_postgres::Error, ) -> OrchestratorDiagnostic { - let mut diagnostic = Diagnostic::new(category(&QUERY_EXECUTION), Severity::Bug).primary( + let mut diagnostic = Diagnostic::new(category(&QUERY_EXECUTION), Critical::BUG).primary( Label::new(span, "compiled query was rejected by the database"), ); @@ -483,7 +483,7 @@ fn row_hydration( source: &tokio_postgres::Error, ) -> OrchestratorDiagnostic { let mut diagnostic = - Diagnostic::new(category(&ROW_HYDRATION), Severity::Bug).primary(Label::new( + Diagnostic::new(category(&ROW_HYDRATION), Critical::BUG).primary(Label::new( span, format!("cannot decode result column {index} ({column})"), )); @@ -615,7 +615,7 @@ fn value_deserialization( env: &Environment<'_>, ) -> OrchestratorDiagnostic { let mut diagnostic = - Diagnostic::new(category(&VALUE_DESERIALIZATION), Severity::Bug).primary(Label::new( + Diagnostic::new(category(&VALUE_DESERIALIZATION), Critical::BUG).primary(Label::new( span, format!("cannot deserialize result column {index} ({column})"), )); @@ -636,7 +636,7 @@ fn continuation_deserialization( source: &DecodeError<'_>, env: &Environment<'_>, ) -> OrchestratorDiagnostic { - let mut diagnostic = Diagnostic::new(category(&CONTINUATION_DESERIALIZATION), Severity::Bug) + let mut diagnostic = Diagnostic::new(category(&CONTINUATION_DESERIALIZATION), Critical::BUG) .primary(Label::new( span, format!("cannot deserialize continuation local {local} in definition {body}"), @@ -658,7 +658,7 @@ fn invalid_continuation_block_id( block_id: i32, ) -> OrchestratorDiagnostic { let mut diagnostic = - Diagnostic::new(category(&INVALID_CONTINUATION_BLOCK_ID), Severity::Bug).primary( + Diagnostic::new(category(&INVALID_CONTINUATION_BLOCK_ID), Critical::BUG).primary( Label::new(span, "continuation returned an invalid block ID"), ); @@ -674,7 +674,7 @@ fn invalid_continuation_block_id( } fn invalid_continuation_local(span: SpanId, body: DefId, local: i32) -> OrchestratorDiagnostic { - let mut diagnostic = Diagnostic::new(category(&INVALID_CONTINUATION_LOCAL), Severity::Bug) + let mut diagnostic = Diagnostic::new(category(&INVALID_CONTINUATION_LOCAL), Critical::BUG) .primary(Label::new(span, "continuation returned an invalid local")); diagnostic.add_message(Message::note(format!( @@ -694,7 +694,7 @@ fn parameter_encoding( error: &(dyn core::error::Error + Send + Sync), ) -> OrchestratorDiagnostic { let mut diagnostic = - Diagnostic::new(category(&PARAMETER_ENCODING), Severity::Bug).primary(Label::new( + Diagnostic::new(category(&PARAMETER_ENCODING), Critical::BUG).primary(Label::new( span, format!( "cannot encode parameter ${} for the database", @@ -712,7 +712,7 @@ fn parameter_encoding( } fn query_lookup(span: SpanId, body: DefId, block: BasicBlockId) -> OrchestratorDiagnostic { - let mut diagnostic = Diagnostic::new(category(&QUERY_LOOKUP), Severity::Bug).primary( + let mut diagnostic = Diagnostic::new(category(&QUERY_LOOKUP), Critical::BUG).primary( Label::new(span, "no compiled query found for this data access"), ); @@ -728,7 +728,7 @@ fn query_lookup(span: SpanId, body: DefId, block: BasicBlockId) -> OrchestratorD } fn incomplete_continuation(span: SpanId, body: DefId, field: &str) -> OrchestratorDiagnostic { - let mut diagnostic = Diagnostic::new(category(&INCOMPLETE_CONTINUATION), Severity::Bug) + let mut diagnostic = Diagnostic::new(category(&INCOMPLETE_CONTINUATION), Critical::BUG) .primary(Label::new( span, "continuation state is missing required columns", @@ -746,7 +746,7 @@ fn incomplete_continuation(span: SpanId, body: DefId, field: &str) -> Orchestrat } fn missing_execution_residual(span: SpanId, body: DefId) -> OrchestratorDiagnostic { - let mut diagnostic = Diagnostic::new(category(&MISSING_EXECUTION_RESIDUAL), Severity::Bug) + let mut diagnostic = Diagnostic::new(category(&MISSING_EXECUTION_RESIDUAL), Critical::BUG) .primary(Label::new( span, "no execution residual found for this definition", @@ -764,7 +764,7 @@ fn missing_execution_residual(span: SpanId, body: DefId) -> OrchestratorDiagnost } fn invalid_filter_return(span: SpanId, body: DefId) -> OrchestratorDiagnostic { - let mut diagnostic = Diagnostic::new(category(&INVALID_FILTER_RETURN), Severity::Bug) + let mut diagnostic = Diagnostic::new(category(&INVALID_FILTER_RETURN), Critical::BUG) .primary(Label::new(span, "filter body returned a non-boolean value")); diagnostic.add_message(Message::note(format!( @@ -779,7 +779,7 @@ fn invalid_filter_return(span: SpanId, body: DefId) -> OrchestratorDiagnostic { } fn value_serialization(span: SpanId, error: &serde_json::Error) -> OrchestratorDiagnostic { - let mut diagnostic = Diagnostic::new(category(&VALUE_SERIALIZATION), Severity::Bug) + let mut diagnostic = Diagnostic::new(category(&VALUE_SERIALIZATION), Critical::BUG) .primary(Label::new(span, "cannot serialize runtime value to JSON")); diagnostic.add_message(Message::note(format!("serialization failed: {error}"))); diff --git a/libs/@local/hashql/eval/tests/orchestrator/execution.rs b/libs/@local/hashql/eval/tests/orchestrator/execution.rs index ced37221692..5bad3bba6ab 100644 --- a/libs/@local/hashql/eval/tests/orchestrator/execution.rs +++ b/libs/@local/hashql/eval/tests/orchestrator/execution.rs @@ -149,6 +149,7 @@ fn run_impl<'heap>( let value = runtime .block_on(orchestrator.run_in(inputs, entry, [], pipeline.heap)) + .map_err(Diagnostic::generalize) .map_err(Diagnostic::boxed)?; Ok((value, event_log.take())) diff --git a/libs/@local/hashql/mir/src/interpret/error.rs b/libs/@local/hashql/mir/src/interpret/error.rs index bc94c731995..f315cd98f42 100644 --- a/libs/@local/hashql/mir/src/interpret/error.rs +++ b/libs/@local/hashql/mir/src/interpret/error.rs @@ -14,7 +14,7 @@ use hashql_diagnostics::{ Diagnostic, Label, category::{DiagnosticCategory, TerminalDiagnosticCategory}, diagnostic::Message, - severity::Severity, + severity::Critical, }; use super::value::{Int, Ptr, Value, ValueTypeName}; @@ -27,7 +27,7 @@ use crate::body::{ /// Type alias for interpreter diagnostics. /// /// The default severity kind is [`Severity`], which allows any severity level. -pub type InterpretDiagnostic = Diagnostic; +pub type InterpretDiagnostic = Diagnostic; // Terminal categories for ICEs const LOCAL_ACCESS: TerminalDiagnosticCategory = TerminalDiagnosticCategory { @@ -374,11 +374,14 @@ impl RuntimeError<'_, E, A> { /// /// For callers that need a different output category (e.g. an orchestrator /// wrapping interpreter errors), use [`into_diagnostic_with`](Self::into_diagnostic_with). - pub fn into_diagnostic( + pub fn into_diagnostic( self, callstack: impl IntoIterator, - on_suspension: impl FnOnce(E) -> InterpretDiagnostic, - ) -> InterpretDiagnostic { + on_suspension: impl FnOnce(E) -> InterpretDiagnostic, + ) -> InterpretDiagnostic + where + Critical: Into, + { self.into_diagnostic_with(callstack, on_suspension, core::convert::identity) } @@ -389,12 +392,15 @@ impl RuntimeError<'_, E, A> { /// to embed [`InterpretDiagnosticCategory`] inside a broader category /// hierarchy. The `on_suspension` closure produces a diagnostic with the /// same output category `C`. - pub fn into_diagnostic_with( + pub fn into_diagnostic_with( self, callstack: impl IntoIterator, - on_suspension: impl FnOnce(E) -> Diagnostic, + on_suspension: impl FnOnce(E) -> Diagnostic, on_otherwise: impl FnOnce(InterpretDiagnosticCategory) -> C, - ) -> Diagnostic { + ) -> Diagnostic + where + Critical: Into, + { let mut spans = callstack.into_iter(); let primary_span = spans.next().unwrap_or(SpanId::SYNTHETIC); @@ -408,70 +414,85 @@ impl RuntimeError<'_, E, A> { diagnostic } - fn make_diagnostic( + fn make_diagnostic( self, span: SpanId, - on_suspension: impl FnOnce(E) -> Diagnostic, + on_suspension: impl FnOnce(E) -> Diagnostic, on_otherwise: impl FnOnce(InterpretDiagnosticCategory) -> C, - ) -> Diagnostic { + ) -> Diagnostic + where + Critical: Into, + { match self { - Self::UninitializedLocal { local, decl } => { - uninitialized_local(span, local, decl).map_category(on_otherwise) - } - Self::InvalidIndexType { base, index } => { - invalid_index_type(span, &base, &index).map_category(on_otherwise) - } - Self::InvalidSubscriptType { base } => { - invalid_subscript_type(span, &base).map_category(on_otherwise) - } - Self::InvalidProjectionType { base } => { - invalid_projection_type(span, &base).map_category(on_otherwise) - } + Self::UninitializedLocal { local, decl } => uninitialized_local(span, local, decl) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::InvalidIndexType { base, index } => invalid_index_type(span, &base, &index) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::InvalidSubscriptType { base } => invalid_subscript_type(span, &base) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::InvalidProjectionType { base } => invalid_projection_type(span, &base) + .map_category(on_otherwise) + .map_severity(Into::into), Self::InvalidProjectionByNameType { base } => { - invalid_projection_by_name_type(span, &base).map_category(on_otherwise) - } - Self::UnknownField { base, field } => { - unknown_field(span, &base, field).map_category(on_otherwise) - } - Self::UnknownFieldByName { base, field } => { - unknown_field_by_name(span, &base, field).map_category(on_otherwise) + invalid_projection_by_name_type(span, &base) + .map_category(on_otherwise) + .map_severity(Into::into) } + Self::UnknownField { base, field } => unknown_field(span, &base, field) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::UnknownFieldByName { base, field } => unknown_field_by_name(span, &base, field) + .map_category(on_otherwise) + .map_severity(Into::into), Self::StructFieldLengthMismatch { values, fields } => { - struct_field_length_mismatch(span, values, fields).map_category(on_otherwise) - } - Self::InvalidDiscriminantType { r#type } => { - invalid_discriminant_type(span, &r#type).map_category(on_otherwise) - } - Self::InvalidDiscriminant { value } => { - invalid_discriminant(span, value).map_category(on_otherwise) - } - Self::UnreachableReached => unreachable_reached(span).map_category(on_otherwise), - Self::BinaryTypeMismatch(mismatch) => { - binary_type_mismatch(span, *mismatch).map_category(on_otherwise) - } - Self::UnaryTypeMismatch(mismatch) => { - unary_type_mismatch(span, *mismatch).map_category(on_otherwise) - } - Self::ApplyNonPointer { r#type } => { - apply_non_pointer(span, &r#type).map_category(on_otherwise) - } - Self::CallstackEmpty => callstack_empty(span).map_category(on_otherwise), - Self::OutOfRange { length, index } => { - out_of_range(span, length, index).map_category(on_otherwise) - } - Self::InputNotFound { name } => input_not_found(span, name).map_category(on_otherwise), - Self::RecursionLimitExceeded { limit } => { - recursion_limit_exceeded(span, limit).map_category(on_otherwise) - } - Self::IntegerOverflow { operation } => { - integer_overflow(span, operation).map_category(on_otherwise) + struct_field_length_mismatch(span, values, fields) + .map_category(on_otherwise) + .map_severity(Into::into) } + Self::InvalidDiscriminantType { r#type } => invalid_discriminant_type(span, &r#type) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::InvalidDiscriminant { value } => invalid_discriminant(span, value) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::UnreachableReached => unreachable_reached(span) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::BinaryTypeMismatch(mismatch) => binary_type_mismatch(span, *mismatch) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::UnaryTypeMismatch(mismatch) => unary_type_mismatch(span, *mismatch) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::ApplyNonPointer { r#type } => apply_non_pointer(span, &r#type) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::CallstackEmpty => callstack_empty(span) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::OutOfRange { length, index } => out_of_range(span, length, index) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::InputNotFound { name } => input_not_found(span, name) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::RecursionLimitExceeded { limit } => recursion_limit_exceeded(span, limit) + .map_category(on_otherwise) + .map_severity(Into::into), + Self::IntegerOverflow { operation } => integer_overflow(span, operation) + .map_category(on_otherwise) + .map_severity(Into::into), Self::UnexpectedValueType { expected, actual } => { - unexpected_value_type(span, &expected, &actual).map_category(on_otherwise) - } - Self::InvalidConstructor { name } => { - invalid_constructor(span, name).map_category(on_otherwise) + unexpected_value_type(span, &expected, &actual) + .map_category(on_otherwise) + .map_severity(Into::into) } + Self::InvalidConstructor { name } => invalid_constructor(span, name) + .map_category(on_otherwise) + .map_severity(Into::into), Self::Suspension(suspension) => on_suspension(suspension), } } @@ -538,7 +559,7 @@ fn uninitialized_local(span: SpanId, local: Local, decl: LocalDecl) -> Interpret }); let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::LocalAccess, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::LocalAccess, Critical::BUG).primary( Label::new(span, format!("local `{name}` used before initialization")), ); @@ -557,7 +578,7 @@ fn uninitialized_local(span: SpanId, local: Local, decl: LocalDecl) -> Interpret fn invalid_index_type(span: SpanId, base: &TypeName, index: &TypeName) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("cannot index `{base}` with `{index}`")), ); @@ -569,7 +590,7 @@ fn invalid_index_type(span: SpanId, base: &TypeName, index: &TypeName) -> Interp } fn invalid_subscript_type(span: SpanId, base: &TypeName) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG) .primary(Label::new(span, format!("cannot subscript `{base}`"))); diagnostic.add_message(Message::help( @@ -581,7 +602,7 @@ fn invalid_subscript_type(span: SpanId, base: &TypeName) -> InterpretDiagnostic fn invalid_projection_type(span: SpanId, base: &TypeName) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("cannot project field from `{base}`")), ); @@ -594,7 +615,7 @@ fn invalid_projection_type(span: SpanId, base: &TypeName) -> InterpretDiagnostic fn invalid_projection_by_name_type(span: SpanId, base: &TypeName) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("cannot project named field from `{base}`")), ); @@ -606,7 +627,7 @@ fn invalid_projection_by_name_type(span: SpanId, base: &TypeName) -> InterpretDi } fn unknown_field(span: SpanId, base: &TypeName, field: FieldIndex) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG) .primary(Label::new( span, format!("field index {field} does not exist on `{base}`"), @@ -621,7 +642,7 @@ fn unknown_field(span: SpanId, base: &TypeName, field: FieldIndex) -> InterpretD fn unknown_field_by_name(span: SpanId, base: &TypeName, field: Symbol) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("field `{field}` does not exist on `{base}`")), ); @@ -633,7 +654,7 @@ fn unknown_field_by_name(span: SpanId, base: &TypeName, field: Symbol) -> Interp } fn invalid_discriminant_type(span: SpanId, r#type: &TypeName) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG) .primary(Label::new( span, format!("switch discriminant has type `{type}`, expected `Integer`"), @@ -656,7 +677,7 @@ fn binary_type_mismatch( rhs, }: BinaryTypeMismatch, ) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG) .primary(Label::new( span, format!( @@ -686,7 +707,7 @@ fn unary_type_mismatch( value, }: UnaryTypeMismatch, ) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG) .primary(Label::new( span, format!("cannot apply `{}` to `{}`", op.as_str(), value.type_name()), @@ -703,7 +724,7 @@ fn unary_type_mismatch( fn apply_non_pointer(span: SpanId, r#type: &TypeName) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("cannot call `{type}` as a function")), ); @@ -720,7 +741,7 @@ fn unexpected_value_type( actual: &TypeName, ) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("expected `{expected}`, found `{actual}`")), ); @@ -733,7 +754,7 @@ fn unexpected_value_type( fn invalid_constructor(span: SpanId, name: Symbol) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Severity::Bug).primary( + Diagnostic::new(InterpretDiagnosticCategory::TypeInvariant, Critical::BUG).primary( Label::new(span, format!("unrecognized opaque constructor `{name}`")), ); @@ -751,7 +772,7 @@ fn invalid_constructor(span: SpanId, name: Symbol) -> InterpretDiagnostic { fn struct_field_length_mismatch(span: SpanId, values: usize, fields: usize) -> InterpretDiagnostic { let mut diagnostic = Diagnostic::new( InterpretDiagnosticCategory::StructuralInvariant, - Severity::Bug, + Critical::BUG, ) .primary(Label::new( span, @@ -770,7 +791,7 @@ fn struct_field_length_mismatch(span: SpanId, values: usize, fields: usize) -> I // ============================================================================= fn invalid_discriminant(span: SpanId, value: Int) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Critical::BUG) .primary(Label::new( span, format!("switch discriminant `{value}` has no matching branch"), @@ -784,7 +805,7 @@ fn invalid_discriminant(span: SpanId, value: Int) -> InterpretDiagnostic { } fn unreachable_reached(span: SpanId) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Critical::BUG) .primary(Label::new(span, "reached unreachable code")); diagnostic.add_message(Message::help( @@ -796,7 +817,7 @@ fn unreachable_reached(span: SpanId) -> InterpretDiagnostic { #[coverage(off)] fn callstack_empty(span: SpanId) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Severity::Bug) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::ControlFlow, Critical::BUG) .primary(Label::new(span, "attempted to step with empty callstack")); diagnostic.add_message(Message::help( @@ -811,7 +832,7 @@ fn callstack_empty(span: SpanId) -> InterpretDiagnostic { // ============================================================================= fn out_of_range(span: SpanId, length: usize, index: Int) -> InterpretDiagnostic { - let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::BoundsCheck, Severity::Error) + let mut diagnostic = Diagnostic::new(InterpretDiagnosticCategory::BoundsCheck, Critical::ERROR) .primary(Label::new( span, format!("index `{index}` is out of bounds for length {length}"), @@ -829,7 +850,7 @@ fn out_of_range(span: SpanId, length: usize, index: Int) -> InterpretDiagnostic fn input_not_found(span: SpanId, name: Symbol) -> InterpretDiagnostic { let mut diagnostic = Diagnostic::new( InterpretDiagnosticCategory::InputResolution, - Severity::Error, + Critical::ERROR, ) .primary(Label::new(span, format!("input `{name}` not found"))); @@ -844,7 +865,7 @@ fn input_not_found(span: SpanId, name: Symbol) -> InterpretDiagnostic { fn recursion_limit_exceeded(span: SpanId, limit: usize) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::RuntimeLimit, Severity::Error).primary( + Diagnostic::new(InterpretDiagnosticCategory::RuntimeLimit, Critical::ERROR).primary( Label::new(span, format!("recursion limit of {limit} exceeded")), ); @@ -857,7 +878,7 @@ fn recursion_limit_exceeded(span: SpanId, limit: usize) -> InterpretDiagnostic { fn integer_overflow(span: SpanId, operation: &str) -> InterpretDiagnostic { let mut diagnostic = - Diagnostic::new(InterpretDiagnosticCategory::RuntimeLimit, Severity::Error).primary( + Diagnostic::new(InterpretDiagnosticCategory::RuntimeLimit, Critical::ERROR).primary( Label::new( span, format!("integer {operation} produced a result outside the supported range"), diff --git a/libs/@local/hashql/mir/src/interpret/value/struct.rs b/libs/@local/hashql/mir/src/interpret/value/struct.rs index 6a3718ff5bb..50a7fc4e8d4 100644 --- a/libs/@local/hashql/mir/src/interpret/value/struct.rs +++ b/libs/@local/hashql/mir/src/interpret/value/struct.rs @@ -371,7 +371,10 @@ mod tests { use hashql_core::heap::Heap; use super::*; - use crate::interpret::value::{Int, Str, Value}; + use crate::{ + intern::Interner, + interpret::value::{Int, Str, Value}, + }; fn int(value: i128) -> Value<'static> { Value::Integer(Int::from(value)) diff --git a/libs/@local/hashql/mir/src/pass/execution/island/graph/mod.rs b/libs/@local/hashql/mir/src/pass/execution/island/graph/mod.rs index c3c4f6a54dc..97170c90c46 100644 --- a/libs/@local/hashql/mir/src/pass/execution/island/graph/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/island/graph/mod.rs @@ -23,6 +23,7 @@ pub(crate) mod tests; use alloc::alloc::Global; use core::{ alloc::Allocator, + fmt, ops::{Index, IndexMut}, }; @@ -287,6 +288,16 @@ impl IslandGraph { } } +impl fmt::Debug for IslandGraph { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("IslandGraph") + .field("vertex", &self.vertex) + .field("inner", &self.inner) + .field("lookup", &self.lookup) + .finish() + } +} + impl DirectedGraph for IslandGraph { type Edge<'this> = &'this Edge diff --git a/libs/@local/hashql/mir/src/pass/execution/mod.rs b/libs/@local/hashql/mir/src/pass/execution/mod.rs index ce0bddb23ef..81408178561 100644 --- a/libs/@local/hashql/mir/src/pass/execution/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/mod.rs @@ -19,7 +19,7 @@ mod terminator_placement; pub mod traversal; mod vertex; -use core::{alloc::Allocator, assert_matches}; +use core::{alloc::Allocator, assert_matches, fmt}; use hashql_core::heap::{BumpAllocator, Heap}; @@ -57,6 +57,15 @@ pub struct ExecutionAnalysisResidual { pub islands: IslandGraph, } +impl core::fmt::Debug for ExecutionAnalysisResidual { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("ExecutionAnalysisResidual") + .field("assignment", &self.assignment) + .field("islands", &self.islands) + .finish() + } +} + pub struct ExecutionAnalysis<'ctx, 'heap, S: Allocator> { pub footprints: &'ctx DefIdSlice>, pub scratch: S, From 6ee00eca47fdb837ac38c00b71e1102a2b676a34 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:01:52 +0200 Subject: [PATCH 08/25] feat: checkpoint --- .../@local/graph/api/src/rest/entity/query.rs | 19 +++-- libs/@local/hashql/diagnostics/src/lib.rs | 2 +- libs/@local/hashql/diagnostics/src/status.rs | 76 +++++++++++++++++++ 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/libs/@local/graph/api/src/rest/entity/query.rs b/libs/@local/graph/api/src/rest/entity/query.rs index a87322d79b8..746a3e6eb90 100644 --- a/libs/@local/graph/api/src/rest/entity/query.rs +++ b/libs/@local/graph/api/src/rest/entity/query.rs @@ -13,7 +13,7 @@ use hashql_core::{ r#type::environment::Environment, }; use hashql_diagnostics::{ - Diagnostic, DiagnosticCategory, DiagnosticIssues, Failure, Source, Sources, Status, + Diagnostic, DiagnosticCategory, DiagnosticIssues, Failure, IntoStatus, Source, Sources, Status, StatusExt as _, Success, category::canonical_category_id, severity::Critical, source::SourceId, }; use hashql_eval::{ @@ -33,10 +33,12 @@ use hashql_mir::{ use hashql_syntax_jexpr::{error::JExprDiagnosticCategory, span::Span}; use serde_json::value::RawValue; use tokio::{runtime::Runtime, task::LocalSet}; +use tokio_util::task::LocalPoolHandle; struct CompilerContext { scratches: ScratchPool, heaps: HeapPool, + pool: LocalPoolHandle, } struct ExecutionContext<'ctx> { @@ -109,8 +111,7 @@ struct Task { impl Task { fn spawn(&self) { - let rt = Runtime::new - + todo!() } } @@ -138,10 +139,14 @@ async fn query_local( let orchestrator = Orchestrator::new(client, &compilation.artifact.postgres, &context); - orchestrator + let Success { value, advisories } = orchestrator .run(&inputs, compilation.entrypoint, []) .await - .unwrap(); + .into_status() + .map_category(|category| { + HashQlDiagnosticCategory::Eval(EvalDiagnosticCategory::Orchestrator(category)) + }) + .with_diagnostics(advisories)?; Status::success(()) } @@ -153,8 +158,8 @@ async fn query( ) -> Status<(), HashQlDiagnosticCategory, SpanId> { let client = exec.postgres.acquire(exec.temporal).await.unwrap(); - let local = LocalSet::new(); - let result = local.run_until(query_local(ctx, client, query)).await; + let pool = ctx.pool.clone(); + let result = pool.spawn_pinned(|| query_local(ctx, client, query)).await; todo!() } diff --git a/libs/@local/hashql/diagnostics/src/lib.rs b/libs/@local/hashql/diagnostics/src/lib.rs index 83e416cf5f3..28f03988520 100644 --- a/libs/@local/hashql/diagnostics/src/lib.rs +++ b/libs/@local/hashql/diagnostics/src/lib.rs @@ -36,5 +36,5 @@ pub use self::{ issues::{DiagnosticIssues, DiagnosticSink}, severity::Severity, source::{Source, Sources}, - status::{Failure, Status, StatusExt, Success}, + status::{Failure, IntoStatus, Status, StatusExt, Success}, }; diff --git a/libs/@local/hashql/diagnostics/src/status.rs b/libs/@local/hashql/diagnostics/src/status.rs index 29f9d3d15e8..fb1c36f611f 100644 --- a/libs/@local/hashql/diagnostics/src/status.rs +++ b/libs/@local/hashql/diagnostics/src/status.rs @@ -925,3 +925,79 @@ impl StatusExt for Status { } } } + +/// Conversion into a [`Status`] from result types that carry either a value or +/// a critical diagnostic, but have no accumulated advisories or secondary +/// diagnostics. +/// +/// This is useful at boundaries where a fallible operation returns +/// `Result>` and the caller needs to feed that +/// into a [`Status`]-based pipeline. +pub trait IntoStatus { + /// Converts into a [`Status`]. + /// + /// The resulting status has no advisories on success and no secondary + /// diagnostics on failure. + /// + /// # Errors + /// + /// Returns [`Failure`] when the source value represents a critical + /// diagnostic. + /// + /// # Examples + /// + /// Successful conversion: + /// + /// ``` + /// use hashql_diagnostics::{IntoStatus, Status, StatusExt as _}; + /// + /// let result: Result = Ok(42); + /// let status: Status<_, (), ()> = result.into_status(); + /// + /// match status { + /// Ok(success) => { + /// assert_eq!(success.value, 42); + /// assert!(success.advisories.is_empty()); + /// } + /// Err(_) => panic!("should be successful"), + /// } + /// ``` + /// + /// Failed conversion: + /// + /// ``` + /// use hashql_diagnostics::{ + /// Diagnostic, IntoStatus, Label, Severity, Status, StatusExt as _, severity::SeverityKind, + /// }; + /// # use hashql_diagnostics::category::TerminalDiagnosticCategory; + /// # const CATEGORY: TerminalDiagnosticCategory = TerminalDiagnosticCategory { + /// # id: "example", name: "Example" + /// # }; + /// + /// let diagnostic = Diagnostic::new(CATEGORY, Severity::Error) + /// .primary(Label::new(10..15, "something went wrong")) + /// .specialize() + /// .expect_err("should be critical"); + /// + /// let result: Result = Err(diagnostic); + /// let status: Status = result.into_status(); + /// + /// match status { + /// Ok(_) => panic!("should be a failure"), + /// Err(failure) => { + /// assert!(failure.primary.severity.is_critical()); + /// assert!(failure.secondary.is_empty()); + /// } + /// } + /// ``` + fn into_status(self) -> Status; +} + +impl IntoStatus for Result> { + fn into_status(self) -> Status { + match self { + Ok(value) => Status::success(value), + Err(primary) => Status::failure(primary), + } + } +} From 88cfc6255970b8ab180e8f357c99499baa33a81d Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:04:36 +0200 Subject: [PATCH 09/25] chore: remove old value module --- Cargo.lock | 1 - libs/@local/hashql/core/Cargo.toml | 1 - libs/@local/hashql/core/src/value/dict.rs | 327 -------------------- libs/@local/hashql/core/src/value/list.rs | 279 ----------------- libs/@local/hashql/core/src/value/mod.rs | 292 +---------------- libs/@local/hashql/core/src/value/opaque.rs | 102 ------ libs/@local/hashql/core/src/value/struct.rs | 291 ----------------- libs/@local/hashql/core/src/value/tuple.rs | 220 ------------- 8 files changed, 1 insertion(+), 1512 deletions(-) delete mode 100644 libs/@local/hashql/core/src/value/dict.rs delete mode 100644 libs/@local/hashql/core/src/value/list.rs delete mode 100644 libs/@local/hashql/core/src/value/opaque.rs delete mode 100644 libs/@local/hashql/core/src/value/struct.rs delete mode 100644 libs/@local/hashql/core/src/value/tuple.rs diff --git a/Cargo.lock b/Cargo.lock index 5000af8f238..3bf72da1cbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4138,7 +4138,6 @@ dependencies = [ "proptest", "rapidfuzz", "roaring", - "rpds", "rstest", "serde", "simple-mermaid", diff --git a/libs/@local/hashql/core/Cargo.toml b/libs/@local/hashql/core/Cargo.toml index c1c1f638fc5..5cef818ed51 100644 --- a/libs/@local/hashql/core/Cargo.toml +++ b/libs/@local/hashql/core/Cargo.toml @@ -32,7 +32,6 @@ lexical = { workspace = true, features = ["parse-integers", "parse- memchr = { workspace = true } rapidfuzz = { workspace = true } roaring = { workspace = true, features = ["std"] } -rpds = { workspace = true, features = ["std"] } serde = { workspace = true, optional = true, features = ["alloc", "derive"] } simple-mermaid = { workspace = true } tracing = { workspace = true } diff --git a/libs/@local/hashql/core/src/value/dict.rs b/libs/@local/hashql/core/src/value/dict.rs deleted file mode 100644 index fcd6368c89f..00000000000 --- a/libs/@local/hashql/core/src/value/dict.rs +++ /dev/null @@ -1,327 +0,0 @@ -use rpds::RedBlackTreeMap; - -use super::Value; - -/// A persistent key-value mapping. -/// -/// All operations return new [`Dict`] instances without modifying the original. -/// -/// # Examples -/// -/// ``` -/// use hashql_core::{ -/// heap::Heap, -/// value::{Integer, Primitive, String}, -/// value::{Dict, Value}, -/// }; -/// -/// let heap = Heap::new(); -/// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); -/// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); -/// -/// // Create a dict from key-value pairs -/// let person = Dict::from_entries([ -/// (string("name"), string("Alice")), -/// (string("age"), integer("30")) -/// ]); -/// -/// // Access values -/// if let Some(name) = person.get(&string("name")) { -/// println!("Name: {:?}", name); -/// } -/// -/// // Insert returns a new dict -/// let updated_person = person.insert(string("email"), string("alice@example.com")); -/// -/// assert_eq!(person.len(), 2); // Original unchanged -/// assert_eq!(updated_person.len(), 3); // New dict has additional entry -/// ``` -#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] -pub struct Dict<'heap> { - // We use a `RedBlackTreeMap` here, because otherwise the type would be unhashable, another - // possibility would be to use an immutable chunkmap. - values: RedBlackTreeMap, Value<'heap>>, -} - -impl<'heap> Dict<'heap> { - /// Creates a new [`Dict`] from key-value pairs. - /// - /// If duplicate keys are provided, the last value for each key is retained. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Integer, Primitive, String}, - /// value::{Dict, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); - /// - /// let entries = [ - /// (string("b"), integer("2")), - /// (string("a"), integer("1")), - /// (string("c"), integer("3")), - /// ]; - /// - /// let dict = Dict::from_entries(entries); - /// assert_eq!(dict.len(), 3); - /// // Dict contains all entries - /// ``` - pub fn from_entries(entries: impl IntoIterator, Value<'heap>)>) -> Self { - Self { - values: entries.into_iter().collect(), - } - } - - /// Returns the value for the given key. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Primitive, String}, - /// value::{Dict, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let key = string("username"); - /// let value = string("alice"); - /// let dict = Dict::from_entries([(key.clone(), value.clone())]); - /// - /// assert_eq!(dict.get(&key), Some(&value)); - /// assert_eq!(dict.get(&string("nonexistent")), None); - /// ``` - #[must_use] - pub fn get(&self, key: &Value<'heap>) -> Option<&Value<'heap>> { - self.values.get(key) - } - - /// Returns both the stored key and value for the given key. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Primitive, String}, - /// value::{Dict, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let dict = Dict::from_entries([ - /// (string("config"), string("enabled")) - /// ]); - /// - /// if let Some((stored_key, stored_value)) = dict.get_key_value(&string("config")) { - /// assert_eq!(stored_key, &string("config")); - /// assert_eq!(stored_value, &string("enabled")); - /// } - /// ``` - #[must_use] - pub fn get_key_value(&self, key: &Value<'heap>) -> Option<(&Value<'heap>, &Value<'heap>)> { - self.values.get_key_value(key) - } - - /// Returns a new [`Dict`] with the given key-value pair inserted. - /// - /// If the key already exists, its value is replaced. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Integer, Primitive, String}, - /// value::{Dict, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); - /// - /// let original = Dict::from_entries([ - /// (string("a"), integer("1")) - /// ]); - /// - /// let updated = original.insert(string("b"), integer("2")); - /// - /// assert_eq!(original.len(), 1); // Original unchanged - /// assert_eq!(updated.len(), 2); // New dict has both entries - /// assert!(updated.contains_key(&string("b"))); - /// ``` - #[must_use] - pub fn insert(&self, key: Value<'heap>, value: Value<'heap>) -> Self { - Self { - values: self.values.insert(key, value), - } - } - - /// Returns a new [`Dict`] with the given key removed. - /// - /// If the key doesn't exist, returns an equivalent dict. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Integer, Primitive, String}, - /// value::{Dict, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); - /// - /// let original = Dict::from_entries([ - /// (string("a"), integer("1")), - /// (string("b"), integer("2")), - /// ]); - /// - /// let updated = original.remove(&string("a")); - /// - /// assert_eq!(original.len(), 2); // Original unchanged - /// assert_eq!(updated.len(), 1); // New dict has one less entry - /// assert!(!updated.contains_key(&string("a"))); - /// ``` - #[must_use] - pub fn remove(&self, key: &Value<'heap>) -> Self { - let values = self.values.remove(key); - - Self { values } - } - - /// Returns `true` if the dict contains the given key. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Primitive, String}, - /// value::{Dict, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let dict = Dict::from_entries([(string("key1"), string("value1"))]); - /// - /// assert!(dict.contains_key(&string("key1"))); - /// assert!(!dict.contains_key(&string("key2"))); - /// ``` - #[must_use] - pub fn contains_key(&self, key: &Value<'heap>) -> bool { - self.values.contains_key(key) - } - - /// Returns the number of key-value pairs in the dict. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Integer, Primitive, String}, - /// value::{Dict, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); - /// - /// let empty_dict = Dict::from_entries([]); - /// assert_eq!(empty_dict.len(), 0); - /// - /// let dict = Dict::from_entries([ - /// (string("a"), integer("1")), - /// (string("b"), integer("2")), - /// ]); - /// assert_eq!(dict.len(), 2); - /// ``` - #[must_use] - pub fn len(&self) -> usize { - self.values.size() - } - - /// Returns `true` if the dict contains no key-value pairs. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Primitive, String}, - /// value::{Dict, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let empty_dict = Dict::from_entries([]); - /// assert!(empty_dict.is_empty()); - /// - /// let dict = Dict::from_entries([ - /// (string("key"), string("value")), - /// ]); - /// assert!(!dict.is_empty()); - /// ``` - #[must_use] - pub fn is_empty(&self) -> bool { - self.values.is_empty() - } - - /// Returns an iterator over the key-value pairs. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Integer, Primitive, String}, - /// value::{Dict, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); - /// - /// let dict = Dict::from_entries([ - /// (string("name"), string("Alice")), - /// (string("age"), integer("30")), - /// (string("city"), string("Boston")), - /// ]); - /// - /// // Iterate over all key-value pairs - /// let pairs: Vec<_> = dict.iter().collect(); - /// assert_eq!(pairs.len(), 3); - /// - /// // Check that we can find specific entries - /// assert!(pairs.iter().any(|(k, v)| **k == string("name") && **v == string("Alice"))); - /// - /// // Use with for loop - /// for (key, value) in dict.iter() { - /// println!("{:?}: {:?}", key, value); - /// } - /// ``` - pub fn iter(&self) -> impl Iterator, &Value<'heap>)> { - self.values.iter() - } -} - -impl<'heap> FromIterator<(Value<'heap>, Value<'heap>)> for Dict<'heap> { - fn from_iter, Value<'heap>)>>(iter: T) -> Self { - Self { - values: iter.into_iter().collect(), - } - } -} diff --git a/libs/@local/hashql/core/src/value/list.rs b/libs/@local/hashql/core/src/value/list.rs deleted file mode 100644 index ccaa1f74bfa..00000000000 --- a/libs/@local/hashql/core/src/value/list.rs +++ /dev/null @@ -1,279 +0,0 @@ -use super::Value; - -/// A persistent sequence of values. -/// -/// All operations return new [`List`] instances without modifying the original. -/// -/// # Examples -/// -/// ``` -/// use hashql_core::{ -/// heap::Heap, -/// value::{List, Primitive, String, Value}, -/// }; -/// -/// let heap = Heap::new(); -/// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); -/// -/// // Create a list of user IDs -/// let user_ids = List::from_values([ -/// string("user_123"), -/// string("user_456"), -/// string("user_789"), -/// ]); -/// -/// // Add a new user ID (returns new list) -/// let extended_ids = user_ids.push(string("user_999")); -/// -/// assert_eq!(user_ids.len(), 3); // Original unchanged -/// assert_eq!(extended_ids.len(), 4); // New list has additional element -/// -/// // Access elements by index -/// if let Some(first_id) = extended_ids.get(0) { -/// println!("First user: {:?}", first_id); -/// } -/// ``` -#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] -pub struct List<'heap> { - values: rpds::Vector>, -} - -impl<'heap> List<'heap> { - /// Creates a new [`List`] from values. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{List, Primitive, String, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let values = vec![ - /// string("first"), - /// string("second"), - /// string("third"), - /// ]; - /// - /// let list = List::from_values(values); - /// assert_eq!(list.len(), 3); - /// assert_eq!(list.get(0), Some(&string("first"))); - /// ``` - pub fn from_values(values: impl IntoIterator>) -> Self { - Self { - values: values.into_iter().collect(), - } - } - - /// Returns a new [`List`] with the given value appended. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{List, Primitive, String, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let original = List::from_values([string("a"), string("b")]); - /// let extended = original.push(string("c")); - /// - /// assert_eq!(original.len(), 2); // Original unchanged - /// assert_eq!(extended.len(), 3); // New list has additional element - /// assert_eq!(extended.get(2), Some(&string("c"))); - /// ``` - #[must_use] - pub fn push(&self, value: Value<'heap>) -> Self { - Self { - values: self.values.push_back(value), - } - } - - /// Returns a new [`List`] with the last element removed. - /// - /// # Returns - /// - /// [`None`] if the list is empty. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{List, Primitive, String, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let original = List::from_values([string("a"), string("b")]); - /// let popped = original.pop().unwrap(); - /// - /// assert_eq!(original.len(), 2); // Original unchanged - /// assert_eq!(popped.len(), 1); // New list has one less element - /// assert_eq!(popped.get(0), Some(&string("a"))); - /// - /// let empty = List::from_values([]); - /// assert_eq!(empty.pop(), None); - /// ``` - #[must_use] - pub fn pop(&self) -> Option { - let values = self.values.drop_last()?; - - Some(Self { values }) - } - - /// Returns a reference to the value at the given index. - /// - /// # Returns - /// - /// [`None`] if the index is out of bounds. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{List, Primitive, String, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let list = List::from_values([string("zero"), string("one"), string("two")]); - /// - /// assert_eq!(list.get(0), Some(&string("zero"))); - /// assert_eq!(list.get(1), Some(&string("one"))); - /// assert_eq!(list.get(10), None); // Out of bounds - /// ``` - #[must_use] - pub fn get(&self, index: usize) -> Option<&Value<'heap>> { - self.values.get(index) - } - - /// Returns a new [`List`] with the value at the given index replaced. - /// - /// # Returns - /// - /// [`None`] if the index is out of bounds. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{List, Primitive, String, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let original = List::from_values([string("old_value"), string("keep_this")]); - /// - /// let updated = original.set(0, string("new_value")).unwrap(); - /// - /// assert_eq!(original.get(0), Some(&string("old_value"))); // Original unchanged - /// assert_eq!(updated.get(0), Some(&string("new_value"))); - /// assert_eq!(updated.get(1), Some(&string("keep_this"))); // Other values preserved - /// - /// // Out of bounds returns None - /// assert_eq!(original.set(10, string("invalid")), None); - /// ``` - #[must_use] - pub fn set(&self, index: usize, value: Value<'heap>) -> Option { - let values = self.values.set(index, value)?; - - Some(Self { values }) - } - - /// Returns the number of elements in the list. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{List, Primitive, String, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let empty_list = List::from_values([]); - /// assert_eq!(empty_list.len(), 0); - /// - /// let list = List::from_values([string("a"), string("b"), string("c")]); - /// assert_eq!(list.len(), 3); - /// ``` - #[must_use] - pub fn len(&self) -> usize { - self.values.len() - } - - /// Returns `true` if the list contains no elements. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{List, Primitive, String, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let empty_list = List::from_values([]); - /// assert!(empty_list.is_empty()); - /// - /// let list = List::from_values([string("element")]); - /// assert!(!list.is_empty()); - /// ``` - #[must_use] - pub fn is_empty(&self) -> bool { - self.values.is_empty() - } - - /// Returns an iterator over the values. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{List, Primitive, String, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let list = List::from_values([string("first"), string("second"), string("third")]); - /// - /// // Iterate over all values - /// let values: Vec<_> = list.iter().collect(); - /// assert_eq!(values.len(), 3); - /// assert_eq!(values[0], &string("first")); - /// - /// // Use with for loop - /// for (index, value) in list.iter().enumerate() { - /// println!("Element {}: {:?}", index, value); - /// } - /// ``` - pub fn iter(&self) -> impl Iterator> { - self.values.iter() - } -} - -impl<'heap> FromIterator> for List<'heap> { - fn from_iter>>(iter: T) -> Self { - Self { - values: iter.into_iter().collect(), - } - } -} diff --git a/libs/@local/hashql/core/src/value/mod.rs b/libs/@local/hashql/core/src/value/mod.rs index f8b0cc393bf..6a38edf3a9e 100644 --- a/libs/@local/hashql/core/src/value/mod.rs +++ b/libs/@local/hashql/core/src/value/mod.rs @@ -1,293 +1,3 @@ -mod dict; -mod list; - -mod opaque; mod primitive; -mod r#struct; -mod tuple; - -pub use self::{ - dict::Dict, - list::List, - opaque::Opaque, - primitive::{Float, Integer, Primitive, String}, - r#struct::{Struct, StructError}, - tuple::{Tuple, TupleError}, -}; -use crate::symbol::Symbol; - -/// Errors that can occur when accessing fields on values. -#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] -pub enum FieldAccessError<'heap> { - /// A struct field access error. - Struct(StructError<'heap>), - /// A tuple field access error. - Tuple(TupleError<'heap>), - /// The value type does not support field access. - #[display("Cannot access field `{_1}` on `{_0}`")] - UnableToAccess(&'static str, Symbol<'heap>), -} - -impl core::error::Error for FieldAccessError<'_> {} - -/// Errors that can occur when accessing values by index. -#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] -pub enum IndexAccessError { - /// The provided index type is not valid for list access. - #[display("Unable to access list with index type `{_0}`")] - InvalidListIndexType(&'static str), - /// The requested key was not found in the collection. - #[display("Key not found")] - KeyNotFound, - /// The value type does not support index access. - #[display("Unable to index `{_0}`")] - UnableToAccess(&'static str), -} - -impl core::error::Error for IndexAccessError {} - -/// A value in HashQL. -/// -/// Values are immutable and can be primitives (null, boolean, integer, float, string) or -/// collections (struct, tuple, list, dict, opaque). -/// -/// # Examples -/// -/// ``` -/// use hashql_core::{ -/// heap::Heap, -/// value::{Dict, Integer, List, Primitive, String, Struct, Tuple, Value}, -/// }; -/// -/// let heap = Heap::new(); -/// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); -/// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); -/// # let boolean = |value: bool| Value::Primitive(Primitive::Boolean(value)); -/// -/// // Primitive values -/// let number = integer("42"); -/// let text = string("hello"); -/// let flag = boolean(true); -/// -/// // Collections -/// let list = Value::List(List::from_values([number.clone(), text.clone()])); -/// let tuple = Value::Tuple(Tuple::from_values([flag.clone(), number.clone()])); -/// -/// // Structured data -/// let person = Value::Struct(Struct::from_fields( -/// &heap, -/// [ -/// (heap.intern_symbol("name"), text.clone()), -/// (heap.intern_symbol("age"), number.clone()), -/// ], -/// )); -/// -/// let config = Value::Dict(Dict::from_entries([ -/// (string("debug"), flag), -/// (string("port"), number), -/// ])); -/// ``` -#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, derive_more::From)] -pub enum Value<'heap> { - /// A primitive literal value (null, boolean, integer, float, or string). - Primitive(Primitive<'heap>), - /// A structured value with named fields. - Struct(Struct<'heap>), - /// A fixed-size sequence of values accessed by position. - Tuple(Tuple<'heap>), - /// A variable-size sequence of values. - List(List<'heap>), - /// A key-value mapping. - Dict(Dict<'heap>), - /// An opaque nominal value with a name and a value. - Opaque(Opaque<'heap>), -} - -impl<'heap> Value<'heap> { - /// Returns the type name of this value. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Integer, Primitive, String, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); - /// - /// let number = integer("42"); - /// assert_eq!(number.type_name(), "integer"); - /// - /// let text = string("hello"); - /// assert_eq!(text.type_name(), "string"); - /// - /// let flag = Value::Primitive(Primitive::Boolean(true)); - /// assert_eq!(flag.type_name(), "boolean"); - /// ``` - #[must_use] - pub const fn type_name(&self) -> &'static str { - match self { - Self::Primitive(Primitive::Null) => "null", - Self::Primitive(Primitive::Boolean(_)) => "boolean", - Self::Primitive(Primitive::Integer(_)) => "integer", - Self::Primitive(Primitive::Float(_)) => "float", - Self::Primitive(Primitive::String(_)) => "string", - Self::Struct(_) => "struct", - Self::Tuple(_) => "tuple", - Self::List(_) => "list", - Self::Dict(_) => "dict", - Self::Opaque(_) => "opaque", - } - } - - /// Accesses a field by symbol name. - /// - /// For structs, accesses named fields. For tuples, parses the symbol as an integer index. - /// - /// # Errors - /// - /// Returns an error if the field doesn't exist or the value type doesn't support field access. - /// - /// # Examples - /// - /// ``` - /// # #![feature(assert_matches)] - /// # use core::assert_matches; - /// use hashql_core::{ - /// heap::Heap, - /// value::{FieldAccessError, Integer, Primitive, String, Struct, Tuple, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); - /// - /// // Struct field access - /// let person = Value::Struct(Struct::from_fields( - /// &heap, - /// [(heap.intern_symbol("name"), string("Alice"))], - /// )); - /// let name_field = heap.intern_symbol("name"); - /// assert_eq!( - /// person.access_by_field(name_field).unwrap(), - /// &string("Alice") - /// ); - /// - /// // Tuple field access (using string index) - /// let point = Value::Tuple(Tuple::from_values([integer("1"), integer("2")])); - /// let index_0 = heap.intern_symbol("0"); - /// assert_eq!(point.access_by_field(index_0).unwrap(), &integer("1")); - /// - /// // Error case - field access on primitive - /// let number = integer("42"); - /// let field = heap.intern_symbol("invalid"); - /// assert_matches!( - /// number.access_by_field(field), - /// Err(FieldAccessError::UnableToAccess("integer", _)) - /// ); - /// ``` - pub fn access_by_field(&self, field: Symbol<'heap>) -> Result<&Self, FieldAccessError<'heap>> { - match self { - Self::Struct(r#struct) => r#struct.get(field).map_err(FieldAccessError::Struct), - Self::Tuple(tuple) => tuple.get(field).map_err(FieldAccessError::Tuple), - Self::Opaque(opaque) => opaque.value().access_by_field(field), - Self::Primitive(_) | Self::List(_) | Self::Dict(_) => { - Err(FieldAccessError::UnableToAccess(self.type_name(), field)) - } - } - } - - /// Accesses an element by index or key. - /// - /// For lists, the index must be an integer. For dicts, any value can be used as a key. - /// - /// # Errors - /// - /// Returns an error if the index type is invalid or the value type doesn't support indexing. - /// - /// # Examples - /// - /// ``` - /// # #![feature(assert_matches)] - /// # use core::assert_matches; - /// use hashql_core::{ - /// heap::Heap, - /// value::{Dict, IndexAccessError, Integer, List, Primitive, String, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); - /// - /// // List index access - /// let list = Value::List(List::from_values([ - /// string("first"), - /// string("second"), - /// ])); - /// let index = integer("0"); - /// assert_eq!( - /// list.access_by_index(&index).unwrap(), - /// Some(&string("first")) - /// ); - /// - /// // Dict key access - /// let dict = Value::Dict(Dict::from_entries([ - /// (string("key"), string("value")) - /// ])); - /// let key = string("key"); - /// assert_eq!( - /// dict.access_by_index(&key).unwrap(), - /// Some(&string("value")) - /// ); - /// - /// // Error case - invalid index type for list - /// let invalid_index = string("not_a_number"); - /// assert_matches!( - /// list.access_by_index(&invalid_index), - /// Err(IndexAccessError::InvalidListIndexType(_)) - /// ); - /// - /// // Error case - index access on primitive - /// let number = integer("42"); - /// assert_matches!( - /// number.access_by_index(&index), - /// Err(IndexAccessError::UnableToAccess("integer")) - /// ); - /// ``` - pub fn access_by_index(&self, index: &Self) -> Result, IndexAccessError> { - match self { - Value::List(list) => { - let integer = match index { - &Self::Primitive(Primitive::Integer(integer)) => integer, - Self::Primitive(Primitive::Float(float)) - if let Some(integer) = float.as_integer() => - { - integer - } - Self::Primitive(_) - | Self::Struct(_) - | Self::Tuple(_) - | Self::List(_) - | Self::Dict(_) - | Self::Opaque(_) => { - return Err(IndexAccessError::InvalidListIndexType(index.type_name())); - } - }; - - let Some(index) = integer.as_usize() else { - return Ok(None); - }; - Ok(list.get(index)) - } - Value::Dict(dict) => Ok(dict.get(index)), - Value::Opaque(opaque) => opaque.value().access_by_index(index), - Value::Primitive(_) | Value::Struct(_) | Value::Tuple(_) => { - Err(IndexAccessError::UnableToAccess(self.type_name())) - } - } - } -} +pub use self::primitive::{Float, Integer, Primitive, String}; diff --git a/libs/@local/hashql/core/src/value/opaque.rs b/libs/@local/hashql/core/src/value/opaque.rs deleted file mode 100644 index 6804498635c..00000000000 --- a/libs/@local/hashql/core/src/value/opaque.rs +++ /dev/null @@ -1,102 +0,0 @@ -use alloc::rc::Rc; - -use super::Value; -use crate::symbol::Symbol; - -/// A nominal type that wraps a value with a type name. -/// -/// Opaque values create distinct types from the same underlying representation. -/// Two opaque values are equal only if both their names and wrapped values are equal. -/// -/// # Examples -/// -/// ``` -/// use hashql_core::{ -/// heap::Heap, -/// value::{Integer, Primitive, String}, -/// symbol::Symbol, -/// value::{Opaque, Value}, -/// }; -/// -/// let heap = Heap::new(); -/// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); -/// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); -/// -/// let user_id = Opaque::new(heap.intern_symbol("UserId"), string("user_12345")); -/// let score = Opaque::new(heap.intern_symbol("Score"), integer("95")); -/// -/// assert_eq!(user_id.name(), heap.intern_symbol("UserId")); -/// assert_eq!(user_id.value(), &string("user_12345")); -/// ``` -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Opaque<'heap> { - name: Symbol<'heap>, - value: Rc>, -} - -impl<'heap> Opaque<'heap> { - /// Creates a new nominal type with the given name and value. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Opaque, Primitive, String, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let email = Opaque::new(heap.intern_symbol("Email"), string("alice@example.com")); - /// # let _email = email; - /// ``` - pub fn new(name: Symbol<'heap>, value: impl Into>>) -> Self { - Self { - name, - value: value.into(), - } - } - - /// Returns the type name of this nominal type. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Opaque, Primitive, String, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let username = Opaque::new(heap.intern_symbol("Username"), string("alice")); - /// assert_eq!(username.name(), heap.intern_symbol("Username")); - /// ``` - #[must_use] - pub const fn name(&self) -> Symbol<'heap> { - self.name - } - - /// Returns the wrapped value. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Integer, Opaque, Primitive, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); - /// - /// let temperature = Opaque::new(heap.intern_symbol("Temperature"), integer("72")); - /// assert_eq!(temperature.value(), &integer("72")); - /// ``` - #[must_use] - pub fn value(&self) -> &Value<'heap> { - &self.value - } -} diff --git a/libs/@local/hashql/core/src/value/struct.rs b/libs/@local/hashql/core/src/value/struct.rs deleted file mode 100644 index e42d24d7c0f..00000000000 --- a/libs/@local/hashql/core/src/value/struct.rs +++ /dev/null @@ -1,291 +0,0 @@ -use core::ops::Index; - -use super::{Tuple, Value}; -use crate::{ - collections::SmallVec, - heap::{Heap, TransferInto as _}, - symbol::Symbol, -}; - -/// Errors that can occur when working with [`Struct`] fields. -#[derive(Debug, Clone, PartialEq, Eq, Hash, derive_more::Display)] -pub enum StructError<'heap> { - /// The requested field was not found in the struct. - #[display("Field `{_0}` not found")] - FieldNotFound(Symbol<'heap>), -} - -impl core::error::Error for StructError<'_> {} - -/// A named tuple with field-based access to values. -/// -/// Fields can be accessed by name. The internal order of fields is not guaranteed. -/// -/// # Examples -/// -/// ``` -/// use hashql_core::{ -/// heap::Heap, -/// value::{Integer, Primitive, String, Struct, Value}, -/// symbol::Symbol, -/// }; -/// -/// let heap = Heap::new(); -/// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); -/// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); -/// -/// // Create a struct representing a person -/// let name_field = heap.intern_symbol("name"); -/// let age_field = heap.intern_symbol("age"); -/// let email_field = heap.intern_symbol("email"); -/// -/// let person = Struct::from_fields( -/// &heap, -/// [ -/// ( -/// name_field, -/// string("Alice"), -/// ), -/// ( -/// age_field, -/// integer("30"), -/// ), -/// ( -/// email_field, -/// string("alice@example.com"), -/// ), -/// ], -/// ); -/// -/// // Access fields by name -/// assert_eq!( -/// person.get(name_field).unwrap(), -/// &string("Alice") -/// ); -/// assert_eq!( -/// person.get(age_field).unwrap(), -/// &integer("30") -/// ); -/// -/// // Using index syntax -/// assert_eq!( -/// person[name_field], -/// string("Alice") -/// ); -/// ``` -#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] -pub struct Struct<'heap> { - /// Field names associated with the underlying tuple of values. - fields: &'heap [Symbol<'heap>], - values: Tuple<'heap>, -} - -impl<'heap> Struct<'heap> { - /// Creates a new [`Struct`] from field-value pairs. - /// - /// # Panics - /// - /// Panics if there are duplicate fields if debug assertions are enabled. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Float, Integer, Primitive, String, Struct, Value}, - /// symbol::Symbol, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); - /// # let float = |value: &'static str| Value::Primitive(Primitive::Float(Float::new_unchecked(heap.intern_symbol(value)))); - /// - /// let fields = [ - /// (heap.intern_symbol("id"), integer("42")), - /// (heap.intern_symbol("name"), string("Product")), - /// (heap.intern_symbol("price"), float("19.99")), - /// ]; - /// - /// let product = Struct::from_fields(&heap, fields); - /// assert_eq!(product.get(heap.intern_symbol("id")).unwrap(), &integer("42")); - /// ``` - pub fn from_fields( - heap: &'heap Heap, - fields: impl IntoIterator, Value<'heap>)>, - ) -> Self { - let fields = fields.into_iter(); - - let (fields, values): (SmallVec<_>, SmallVec<_>) = fields.collect(); - - // This is an assert, as previous stages in the compilation should have ensured that there - // are no duplicate fields. - if cfg!(debug_assertions) { - let mut seen = crate::collections::fast_hash_set_with_capacity(fields.len()); - for field in &fields { - assert!(seen.insert(*field), "Duplicate field: {field}"); - } - } - - let fields = fields.transfer_into(heap); - let values = Tuple::from_values(values); - - Self { fields, values } - } - - /// Returns the value for the given field. - /// - /// # Errors - /// - /// Returns [`StructError::FieldNotFound`] if the field doesn't exist. - /// - /// # Examples - /// - /// ``` - /// # #![feature(assert_matches)] - /// # use core::assert_matches; - /// use hashql_core::{ - /// heap::Heap, - /// value::{Integer, Primitive, String, Struct, StructError, Value}, - /// symbol::Symbol, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); - /// - /// let name_field = heap.intern_symbol("name"); - /// let age_field = heap.intern_symbol("age"); - /// - /// let person = Struct::from_fields( - /// &heap, - /// [ - /// (name_field, string("Bob")), - /// (age_field, integer("25")), - /// ], - /// ); - /// - /// // Successful field access - /// assert_eq!(person.get(name_field), Ok(&string("Bob"))); - /// - /// // Field not found - /// let unknown_field = heap.intern_symbol("unknown"); - /// assert_matches!( - /// person.get(unknown_field), - /// Err(StructError::FieldNotFound(field)) if field == unknown_field - /// ); - /// ``` - pub fn get(&self, field: Symbol<'heap>) -> Result<&Value<'heap>, StructError<'heap>> { - let Some(position) = self.fields.iter().position(|name| *name == field) else { - return Err(StructError::FieldNotFound(field)); - }; - - Ok(&self.values[position]) - } - - /// Returns the number of fields in the struct. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Integer, Primitive, String, Struct, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); - /// - /// let empty_struct = Struct::from_fields(&heap, []); - /// assert_eq!(empty_struct.len(), 0); - /// - /// let person = Struct::from_fields( - /// &heap, - /// [ - /// (heap.intern_symbol("name"), string("Alice")), - /// (heap.intern_symbol("age"), integer("30")), - /// (heap.intern_symbol("city"), string("Boston")), - /// ], - /// ); - /// assert_eq!(person.len(), 3); - /// ``` - #[must_use] - pub const fn len(&self) -> usize { - self.fields.len() - } - - /// Returns `true` if the struct contains no fields. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Primitive, String, Struct, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let empty_struct = Struct::from_fields(&heap, []); - /// assert!(empty_struct.is_empty()); - /// - /// let person = Struct::from_fields( - /// &heap, - /// [(heap.intern_symbol("name"), string("Alice"))], - /// ); - /// assert!(!person.is_empty()); - /// ``` - #[must_use] - pub const fn is_empty(&self) -> bool { - self.fields.is_empty() - } - - /// Returns an iterator over the field-value pairs. - /// - /// The order is unspecified but stable. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Integer, Primitive, String, Struct, Value}, - /// symbol::Symbol, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// # let integer = |value: &'static str| Value::Primitive(Primitive::Integer(Integer::new_unchecked(heap.intern_symbol(value)))); - /// - /// let person = Struct::from_fields( - /// &heap, - /// [ - /// (heap.intern_symbol("name"), string("Alice")), - /// (heap.intern_symbol("age"), integer("30")), - /// (heap.intern_symbol("city"), string("Boston")), - /// ], - /// ); - /// - /// // Iterate over all field-value pairs - /// for (field, value) in person.iter() { - /// println!("{}: {:?}", field, value); - /// } - /// - /// // Collect into a vector - /// let pairs: Vec<_> = person.iter().collect(); - /// assert_eq!(pairs.len(), 3); - /// ``` - pub fn iter(&self) -> impl Iterator, &Value<'heap>)> { - self.fields.iter().copied().zip(self.values.iter()) - } -} - -impl<'heap> Index> for Struct<'heap> { - type Output = Value<'heap>; - - fn index(&self, index: Symbol<'heap>) -> &Self::Output { - self.get(index).expect("struct field not found") - } -} diff --git a/libs/@local/hashql/core/src/value/tuple.rs b/libs/@local/hashql/core/src/value/tuple.rs deleted file mode 100644 index 2a1d9f44f64..00000000000 --- a/libs/@local/hashql/core/src/value/tuple.rs +++ /dev/null @@ -1,220 +0,0 @@ -use alloc::rc::Rc; -use core::{num::ParseIntError, ops::Index}; - -use super::Value; -use crate::symbol::Symbol; - -/// Errors that can occur when working with [`Tuple`] field access. -#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] -pub enum TupleError<'heap> { - /// The provided symbol could not be parsed as a valid integer index. - #[display("`{_0}` is not a valid integer: {_1}")] - InvalidInteger(Symbol<'heap>, ParseIntError), - /// The provided index is out of bounds for the tuple. - #[display("`{_0}` is out of bounds, the tuple has {_1} elements")] - OutOfBounds(Symbol<'heap>, usize), -} - -impl core::error::Error for TupleError<'_> {} - -/// A fixed-size collection of values accessed by position. -/// -/// Tuples store values where each element's position has semantic meaning. -/// Elements are accessed by index and tuples cannot be modified after creation. -/// -/// # Examples -/// -/// ``` -/// use hashql_core::{ -/// heap::Heap, -/// value::{Float, Primitive, Tuple, Value}, -/// }; -/// -/// let heap = Heap::new(); -/// # let float = |value: &'static str| Value::Primitive(Primitive::Float(Float::new_unchecked(heap.intern_symbol(value)))); -/// -/// // A 3D point represented as a tuple -/// let point = Tuple::from_values([float("1.23"), float("4.56"), float("7.89")]); -/// -/// // Access elements by index -/// assert_eq!(point[0], float("1.23")); -/// assert_eq!(point[1], float("4.56")); -/// assert_eq!(point.len(), 3); -/// ``` -#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] -pub struct Tuple<'heap> { - values: Rc<[Value<'heap>]>, -} - -impl<'heap> Tuple<'heap> { - /// Creates a new [`Tuple`] from values. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Primitive, String, Tuple, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let values = [ - /// string("red"), - /// string("green"), - /// string("blue"), - /// ]; - /// - /// let color_tuple = Tuple::from_values(values); - /// assert_eq!(color_tuple.len(), 3); - /// assert_eq!(color_tuple[0], string("red")); - /// ``` - pub fn from_values(values: impl IntoIterator>) -> Self { - Self { - values: values.into_iter().collect(), - } - } - - /// Returns the value at the given index. - /// - /// The symbol must represent a valid non-negative integer that is within - /// the bounds of the tuple. - /// - /// # Errors - /// - /// Returns an error if the symbol is not a valid integer or is out of bounds. - /// - /// # Examples - /// - /// ``` - /// # #![feature(assert_matches)] - /// # use core::assert_matches; - /// use hashql_core::{ - /// heap::Heap, - /// value::{Primitive, String, Tuple, TupleError, Value}, - /// symbol::Symbol, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let tuple = Tuple::from_values([string("first"), string("second")]); - /// - /// // Valid index access - /// let index_0 = heap.intern_symbol("0"); - /// assert_eq!(tuple.get(index_0).unwrap(), &string("first")); - /// - /// // Invalid integer - /// let invalid = heap.intern_symbol("not_a_number"); - /// assert_matches!( - /// tuple.get(invalid), - /// Err(TupleError::InvalidInteger(_, _)) - /// ); - /// - /// // Out of bounds - /// let out_of_bounds = heap.intern_symbol("10"); - /// assert_matches!( - /// tuple.get(out_of_bounds), - /// Err(TupleError::OutOfBounds(_, _)) - /// ); - /// ``` - pub fn get(&self, field: Symbol<'heap>) -> Result<&Value<'heap>, TupleError<'heap>> { - let index = field - .as_str() - .parse::() - .map_err(|error| TupleError::InvalidInteger(field, error))?; - - self.values - .get(index) - .ok_or_else(|| TupleError::OutOfBounds(field, self.values.len())) - } - - /// Returns the number of elements in the tuple. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Primitive, String, Tuple, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let empty_tuple = Tuple::from_values([]); - /// assert_eq!(empty_tuple.len(), 0); - /// - /// let tuple = Tuple::from_values([string("a"), string("b"), string("c")]); - /// assert_eq!(tuple.len(), 3); - /// ``` - #[must_use] - pub fn len(&self) -> usize { - self.values.len() - } - - /// Returns `true` if the tuple contains no elements. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Primitive, String, Tuple, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let empty_tuple = Tuple::from_values([]); - /// assert!(empty_tuple.is_empty()); - /// - /// let tuple = Tuple::from_values([string("element")]); - /// assert!(!tuple.is_empty()); - /// ``` - #[must_use] - pub fn is_empty(&self) -> bool { - self.values.is_empty() - } - - /// Returns an iterator over the values. - /// - /// # Examples - /// - /// ``` - /// use hashql_core::{ - /// heap::Heap, - /// value::{Primitive, String, Tuple, Value}, - /// }; - /// - /// let heap = Heap::new(); - /// # let string = |value: &'static str| Value::Primitive(Primitive::String(String::new(heap.intern_symbol(value)))); - /// - /// let tuple = Tuple::from_values([string("first"), string("second"), string("third")]); - /// - /// for (index, value) in tuple.iter().enumerate() { - /// println!("Element {}: {:?}", index, value); - /// } - /// - /// assert_eq!( - /// tuple.iter().collect::>(), - /// [ - /// &string("first"), - /// &string("second"), - /// &string("third") - /// ] - /// ); - /// ``` - pub fn iter(&self) -> impl Iterator> { - self.values.iter() - } -} - -impl<'heap> Index for Tuple<'heap> { - type Output = Value<'heap>; - - fn index(&self, index: usize) -> &Self::Output { - &self.values[index] - } -} From e395aea22cd9e0d9e831e0ae1bd31dfc7a630732 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:39:21 +0200 Subject: [PATCH 10/25] feat: checkpoint --- .../@local/graph/api/src/rest/entity/query.rs | 86 ++++++++++++++++++- .../hashql/mir/src/interpret/value/dict.rs | 29 +++++++ .../hashql/mir/src/interpret/value/list.rs | 42 +++++++++ .../hashql/mir/src/interpret/value/mod.rs | 23 +++++ .../hashql/mir/src/interpret/value/num.rs | 1 + .../hashql/mir/src/interpret/value/opaque.rs | 53 +++++++++++- .../hashql/mir/src/interpret/value/ptr.rs | 3 + .../hashql/mir/src/interpret/value/str.rs | 54 +++++++++++- .../hashql/mir/src/interpret/value/struct.rs | 10 +++ .../hashql/mir/src/interpret/value/tuple.rs | 4 + 10 files changed, 293 insertions(+), 12 deletions(-) diff --git a/libs/@local/graph/api/src/rest/entity/query.rs b/libs/@local/graph/api/src/rest/entity/query.rs index 746a3e6eb90..ecb4751e379 100644 --- a/libs/@local/graph/api/src/rest/entity/query.rs +++ b/libs/@local/graph/api/src/rest/entity/query.rs @@ -1,13 +1,17 @@ use alloc::borrow::Cow; -use core::mem; -use std::{alloc::Global, sync::Arc}; +use core::{mem, str::FromStr}; +use std::{ + alloc::{Allocator, Global}, + collections::BTreeMap, + sync::Arc, +}; use hash_graph_postgres_store::store::{PostgresStore, PostgresStorePool}; use hash_graph_store::pool::StorePool; use hash_temporal_client::TemporalClient; use hashql_ast::error::AstDiagnosticCategory; use hashql_core::{ - heap::{Heap, HeapPool, ResetAllocator, Scratch, ScratchPool}, + heap::{CloneIn, Heap, HeapPool, ResetAllocator, Scratch, ScratchPool}, module::ModuleRegistry, span::{SpanId, SpanTable}, r#type::environment::Environment, @@ -27,7 +31,10 @@ use hashql_mir::{ body::Body, def::{DefId, DefIdVec}, error::MirDiagnosticCategory, - interpret::{CallStack, Inputs}, + interpret::{ + CallStack, Inputs, + value::{Int, Num, Ptr, Str, Value}, + }, pass::{LowerConfig, execution::ExecutionAnalysisResidual}, }; use hashql_syntax_jexpr::{error::JExprDiagnosticCategory, span::Span}; @@ -115,6 +122,77 @@ impl Task { } } +// This is only here until https://linear.app/hash/issue/BE-540/hashql-register-based-bytecode-vm +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +enum OwnedValue { + /// The unit value. + Unit, + /// An integer value (also represents booleans). + Integer(Int), + /// A floating-point number. + Number(Num), + /// A string value. + String(Arc), + /// A function pointer. + Pointer(Ptr), + + /// An opaque/newtype wrapper. + Opaque(Arc, Box), + /// A named-field struct. + Struct(Vec<(Arc, Self)>), + /// A positional tuple. + Tuple(Vec), + /// An ordered list. + List(Vec), + /// An ordered dictionary. + Dict(BTreeMap), +} + +impl<'heap, A: Allocator + Clone> From> for OwnedValue { + fn from(value: Value<'heap, A>) -> Self { + match value { + Value::Unit => Self::Unit, + Value::Integer(int) => Self::Integer(int), + Value::Number(num) => Self::Number(num), + Value::String(str) => Self::String(Arc::from(str.as_str())), + Value::Pointer(ptr) => Self::Pointer(ptr), + Value::Opaque(opaque) => Self::Opaque( + Arc::from(opaque.name().as_str()), + Box::new(opaque.into_value().into()), + ), + Value::Struct(r#struct) => { + debug_assert_eq!(r#struct.fields().len(), r#struct.values().len()); + + Self::Struct( + r#struct + .fields() + .iter() + .zip(r#struct.values()) + .map(|(field, value)| { + (Arc::from(field.as_str()), Self::from(value.clone())) + }) + .collect(), + ) + } + Value::Tuple(tuple) => Self::Tuple( + tuple + .values() + .iter() + .map(|value| Self::from(value.clone())) + .collect(), + ), + Value::List(list) => { + Self::List(list.iter().map(|value| Self::from(value.clone())).collect()) + } + Value::Dict(dict) => Self::Dict( + dict.iter() + .map(|(key, value)| (Self::from(key.clone()), Self::from(value.clone()))) + .collect(), + ), + } + } +} + #[expect(clippy::future_not_send)] async fn query_local( ctx: Arc, diff --git a/libs/@local/hashql/mir/src/interpret/value/dict.rs b/libs/@local/hashql/mir/src/interpret/value/dict.rs index 7f3e9cbeb8b..33c3b800fcb 100644 --- a/libs/@local/hashql/mir/src/interpret/value/dict.rs +++ b/libs/@local/hashql/mir/src/interpret/value/dict.rs @@ -5,6 +5,31 @@ use core::{alloc::Allocator, cmp}; use super::Value; /// An ordered dictionary mapping values to values. +/// +/// Keys are ordered by [`Value`]'s [`Ord`] implementation. +/// +/// # Examples +/// +/// ``` +/// # #![feature(allocator_api)] +/// # extern crate alloc; +/// use alloc::alloc::Global; +/// +/// use hashql_mir::interpret::value::{Dict, Value}; +/// +/// let mut dict: Dict<'_, Global> = Dict::new(); +/// dict.insert(Value::Integer(1.into()), Value::Integer(100.into())); +/// dict.insert(Value::Integer(2.into()), Value::Integer(200.into())); +/// +/// assert_eq!(dict.len(), 2); +/// assert_eq!( +/// dict.get(&Value::Integer(1.into())), +/// Some(&Value::Integer(100.into())), +/// ); +/// +/// // Missing keys return None +/// assert_eq!(dict.get(&Value::Integer(99.into())), None); +/// ``` #[derive(Debug, Clone)] pub struct Dict<'heap, A: Allocator> { inner: rpds::RedBlackTreeMap, Value<'heap, A>>, @@ -12,6 +37,7 @@ pub struct Dict<'heap, A: Allocator> { impl<'heap, A: Allocator> Dict<'heap, A> { /// Creates a new empty dictionary. + #[inline] #[must_use] pub fn new() -> Self { Self { @@ -20,12 +46,14 @@ impl<'heap, A: Allocator> Dict<'heap, A> { } /// Returns the number of key-value pairs in the dictionary. + #[inline] #[must_use] pub fn len(&self) -> usize { self.inner.size() } /// Returns `true` if the dictionary contains no elements. + #[inline] #[must_use] pub fn is_empty(&self) -> bool { self.inner.is_empty() @@ -91,6 +119,7 @@ impl Ord for Dict<'_, A> { } impl Default for Dict<'_, A> { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/interpret/value/list.rs b/libs/@local/hashql/mir/src/interpret/value/list.rs index ce457e1ad3f..78f8fa98b9f 100644 --- a/libs/@local/hashql/mir/src/interpret/value/list.rs +++ b/libs/@local/hashql/mir/src/interpret/value/list.rs @@ -5,6 +5,41 @@ use core::{alloc::Allocator, cmp}; use super::{Int, Value}; /// An ordered list of values. +/// +/// Supports negative indexing: `-1` is the last element, `-2` is +/// second-to-last, and so on (see [`get`](Self::get)). +/// +/// # Examples +/// +/// ``` +/// # #![feature(allocator_api)] +/// # extern crate alloc; +/// use alloc::alloc::Global; +/// +/// use hashql_mir::interpret::value::{Int, List, Value}; +/// +/// let mut list: List<'_, Global> = List::new(); +/// list.push_back(Value::Integer(10.into())); +/// list.push_back(Value::Integer(20.into())); +/// list.push_back(Value::Integer(30.into())); +/// +/// // Forward indexing +/// assert_eq!(list.get(Int::from(0_i32)), Some(&Value::Integer(10.into()))); +/// +/// // Negative indexing counts from the end +/// assert_eq!( +/// list.get(Int::from(-1_i32)), +/// Some(&Value::Integer(30.into())) +/// ); +/// assert_eq!( +/// list.get(Int::from(-3_i32)), +/// Some(&Value::Integer(10.into())) +/// ); +/// +/// // Out of bounds +/// assert_eq!(list.get(Int::from(3_i32)), None); +/// assert_eq!(list.get(Int::from(-4_i32)), None); +/// ``` #[derive(Debug, Clone)] pub struct List<'heap, A: Allocator> { inner: rpds::Vector>, @@ -12,6 +47,7 @@ pub struct List<'heap, A: Allocator> { impl<'heap, A: Allocator> List<'heap, A> { /// Creates a new empty list. + #[inline] #[must_use] pub fn new() -> Self { Self { @@ -20,12 +56,14 @@ impl<'heap, A: Allocator> List<'heap, A> { } /// Returns the number of elements in the list. + #[inline] #[must_use] pub fn len(&self) -> usize { self.inner.len() } /// Returns `true` if the list contains no elements. + #[inline] #[must_use] pub fn is_empty(&self) -> bool { self.inner.is_empty() @@ -40,6 +78,9 @@ impl<'heap, A: Allocator> List<'heap, A> { } /// Returns a reference to the element at the given `index`. + /// + /// Supports negative indexing: `-1` is the last element, `-2` is + /// second-to-last, etc. #[must_use] pub fn get(&self, index: Int) -> Option<&Value<'heap, A>> { let index = isize::try_from(index.as_int()).ok()?; @@ -108,6 +149,7 @@ impl Ord for List<'_, A> { } impl Default for List<'_, A> { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/interpret/value/mod.rs b/libs/@local/hashql/mir/src/interpret/value/mod.rs index aba5483c64c..d33773a293e 100644 --- a/libs/@local/hashql/mir/src/interpret/value/mod.rs +++ b/libs/@local/hashql/mir/src/interpret/value/mod.rs @@ -125,6 +125,29 @@ pub enum Value<'heap, A: Allocator = Global> { impl<'heap, A: Allocator> Value<'heap, A> { const UNIT: Self = Self::Unit; + /// Returns a displayable representation of this value's runtime type. + /// + /// Primitives produce their type name (`"Integer"`, `"String"`), + /// aggregates include their structure (`"(x: Integer, y: String)"` + /// for structs, `"(Integer, String)"` for tuples), and opaques + /// include their wrapper name (`"UserId(Integer)"`). + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::Value; + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// + /// assert_eq!(Value::<'_, Global>::Unit.type_name().to_string(), "()"); + /// assert_eq!( + /// Value::<'_, Global>::Integer(42.into()) + /// .type_name() + /// .to_string(), + /// "Integer" + /// ); + /// ``` pub fn type_name(&self) -> ValueTypeName<'_, 'heap, A> { ValueTypeName::from(self) } diff --git a/libs/@local/hashql/mir/src/interpret/value/num.rs b/libs/@local/hashql/mir/src/interpret/value/num.rs index 5ec17d7a585..9e859b57751 100644 --- a/libs/@local/hashql/mir/src/interpret/value/num.rs +++ b/libs/@local/hashql/mir/src/interpret/value/num.rs @@ -22,6 +22,7 @@ pub struct Num { impl Num { /// Returns the underlying [`f64`] value. + #[inline] #[must_use] pub const fn as_f64(self) -> f64 { self.value diff --git a/libs/@local/hashql/mir/src/interpret/value/opaque.rs b/libs/@local/hashql/mir/src/interpret/value/opaque.rs index dfbae62c0af..86ecd671971 100644 --- a/libs/@local/hashql/mir/src/interpret/value/opaque.rs +++ b/libs/@local/hashql/mir/src/interpret/value/opaque.rs @@ -11,11 +11,28 @@ use hashql_core::symbol::Symbol; use super::Value; -/// An opaque wrapper around a value. +/// An opaque wrapper around a [`Value`]. /// -/// Wraps a value with a named type tag, representing nominal types or -/// newtype wrappers. The name distinguishes different opaque types even -/// when their underlying values are structurally identical. +/// Pairs a [`Symbol`] name with an inner value to represent nominal types +/// (newtypes, branded types). Two opaques with the same inner value but +/// different names are not equal. +/// +/// # Examples +/// +/// ``` +/// use hashql_mir::interpret::value::{Opaque, Value}; +/// # use hashql_core::heap::Heap; +/// # extern crate alloc; +/// # use alloc::rc::Rc; +/// +/// let heap = Heap::new(); +/// let name = heap.intern_symbol("UserId"); +/// let inner = Rc::new(Value::Integer(42.into())); +/// +/// let opaque = Opaque::new(name, inner); +/// assert_eq!(opaque.name().as_str(), "UserId"); +/// assert_eq!(opaque.value(), &Value::Integer(42.into())); +/// ``` #[derive(Debug, Clone)] pub struct Opaque<'heap, A: Allocator> { name: Symbol<'heap>, @@ -24,6 +41,7 @@ pub struct Opaque<'heap, A: Allocator> { impl<'heap, A: Allocator> Opaque<'heap, A> { /// Creates a new opaque value with the given `name` and wrapped `value`. + #[inline] #[must_use] pub fn new(name: Symbol<'heap>, value: impl Into, A>>) -> Self { Self { @@ -33,17 +51,20 @@ impl<'heap, A: Allocator> Opaque<'heap, A> { } /// Returns the type name of this opaque value. + #[inline] #[must_use] pub const fn name(&self) -> Symbol<'heap> { self.name } /// Returns a reference to the wrapped value. + #[inline] #[must_use] pub fn value(&self) -> &Value<'heap, A> { &self.value } + /// Returns a mutable reference to the wrapped value. #[must_use] pub fn value_mut(&mut self) -> &mut Value<'heap, A> where @@ -52,6 +73,30 @@ impl<'heap, A: Allocator> Opaque<'heap, A> { Rc::make_mut(&mut self.value) } + /// Extracts the inner [`Value`], discarding the name. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Opaque, Value}; + /// # use hashql_core::heap::Heap; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let heap = Heap::new(); + /// let name = heap.intern_symbol("UserId"); + /// let opaque = Opaque::new(name, Rc::new(Value::Integer(42.into()))); + /// + /// let inner = opaque.into_value(); + /// assert_eq!(inner, Value::Integer(42.into())); + /// ``` + pub fn into_value(self) -> Value<'heap, A> + where + A: Clone, + { + Rc::unwrap_or_clone(self.value) + } + /// Returns a displayable representation of this opaque type's name. pub fn type_name(&self) -> impl Display { fmt::from_fn(|fmt| { diff --git a/libs/@local/hashql/mir/src/interpret/value/ptr.rs b/libs/@local/hashql/mir/src/interpret/value/ptr.rs index 9349dc09bbe..04aa0f54fa6 100644 --- a/libs/@local/hashql/mir/src/interpret/value/ptr.rs +++ b/libs/@local/hashql/mir/src/interpret/value/ptr.rs @@ -15,12 +15,14 @@ pub struct Ptr { impl Ptr { /// Creates a new function pointer from a [`DefId`]. + #[inline] #[must_use] pub const fn new(value: DefId) -> Self { Self { value } } /// Returns the [`DefId`] this pointer references. + #[inline] #[must_use] pub const fn def(self) -> DefId { self.value @@ -28,6 +30,7 @@ impl Ptr { } impl From for Ptr { + #[inline] fn from(value: DefId) -> Self { Self::new(value) } diff --git a/libs/@local/hashql/mir/src/interpret/value/str.rs b/libs/@local/hashql/mir/src/interpret/value/str.rs index ee211df869d..85ba6eecbb1 100644 --- a/libs/@local/hashql/mir/src/interpret/value/str.rs +++ b/libs/@local/hashql/mir/src/interpret/value/str.rs @@ -2,8 +2,13 @@ use alloc::{alloc::Global, rc::Rc}; use core::{alloc::Allocator, cmp, fmt}; +use std::alloc::AllocError; -use hashql_core::{symbol::Symbol, value::String}; +use hashql_core::{ + heap::{CloneIn, FromIn, TryCloneIn}, + symbol::Symbol, + value::String, +}; /// Internal storage for string values. #[derive(Clone)] @@ -43,9 +48,22 @@ impl Ord for StrInner<'_, A> { /// A string value. /// -/// Supports both owned strings (via [`Rc`]) and borrowed interned -/// symbols. This dual representation allows efficient handling of both -/// dynamically created strings and compile-time literals. +/// Use [`as_str`](Self::as_str) to access the content. Strings that +/// originate from a [`Heap`](hashql_core::heap::Heap) can be detached +/// with [`into_owned_in`](Self::into_owned_in) to outlive that heap. +/// +/// # Examples +/// +/// ``` +/// use hashql_mir::interpret::value::Str; +/// # extern crate alloc; +/// # use alloc::rc::Rc; +/// +/// let a = Str::from(Rc::::from("hello")); +/// let b = Str::from(Rc::::from("hello")); +/// assert_eq!(a.as_str(), "hello"); +/// assert_eq!(a, b); +/// ``` #[derive(Clone)] pub struct Str<'heap, A: Allocator = Global> { inner: StrInner<'heap, A>, @@ -53,6 +71,7 @@ pub struct Str<'heap, A: Allocator = Global> { impl Str<'_, A> { /// Returns this string as a string slice. + #[inline] #[must_use] pub fn as_str(&self) -> &str { match &self.inner { @@ -61,6 +80,11 @@ impl Str<'_, A> { } } + /// Converts this string into an owned representation with an independent + /// lifetime. + /// + /// The returned [`Str`] is not tied to the original heap, so it can + /// outlive the heap the string was interned on. pub fn into_owned_in<'lifetime>(self, alloc: A) -> Str<'lifetime, A> { match self.inner { StrInner::Owned(value) => Str { @@ -73,7 +97,16 @@ impl Str<'_, A> { } } +impl<'heap, A: Allocator> From> for Str<'heap, A> { + fn from(value: Symbol<'heap>) -> Self { + Self { + inner: StrInner::Interned(value), + } + } +} + impl<'heap, A: Allocator> From> for Str<'heap, A> { + #[inline] fn from(value: String<'heap>) -> Self { Self { inner: StrInner::Interned(value.as_symbol()), @@ -82,6 +115,7 @@ impl<'heap, A: Allocator> From> for Str<'heap, A> { } impl<'heap, A: Allocator> From<&String<'heap>> for Str<'heap, A> { + #[inline] fn from(value: &String<'heap>) -> Self { Self { inner: StrInner::Interned(value.as_symbol()), @@ -90,6 +124,7 @@ impl<'heap, A: Allocator> From<&String<'heap>> for Str<'heap, A> { } impl From> for Str<'_, A> { + #[inline] fn from(value: Rc) -> Self { Self { inner: StrInner::Owned(value), @@ -125,3 +160,14 @@ impl Ord for Str<'_, A> { inner.cmp(&other.inner) } } + +impl<'heap, A: Allocator, B: Allocator> TryCloneIn for Str<'heap, A> { + type Cloned = Str<'heap, B>; + + fn try_clone_in(&self, allocator: B) -> Result { + match &self.inner { + StrInner::Owned(value) => Ok(Str::from(Rc::from_in(&**value, allocator))), + &StrInner::Interned(symbol) => Ok(Str::from(symbol)), + } + } +} diff --git a/libs/@local/hashql/mir/src/interpret/value/struct.rs b/libs/@local/hashql/mir/src/interpret/value/struct.rs index 50a7fc4e8d4..ff935fde68d 100644 --- a/libs/@local/hashql/mir/src/interpret/value/struct.rs +++ b/libs/@local/hashql/mir/src/interpret/value/struct.rs @@ -38,6 +38,7 @@ impl<'heap, A: Allocator> Struct<'heap, A> { /// Creates a new struct without checking invariants. /// /// The caller must ensure that `fields` and `values` have the same length. + #[inline] pub fn new_unchecked( fields: Interned<'heap, [Symbol<'heap>]>, values: Rc<[Value<'heap, A>], A>, @@ -62,30 +63,35 @@ impl<'heap, A: Allocator> Struct<'heap, A> { } /// Returns the field names. + #[inline] #[must_use] pub const fn fields(&self) -> &Interned<'heap, [Symbol<'heap>]> { &self.fields } /// Returns the field values. + #[inline] #[must_use] pub fn values(&self) -> &[Value<'heap, A>] { &self.values } /// Returns the number of fields. + #[inline] #[must_use] pub fn len(&self) -> usize { self.fields.len() } /// Returns `true` if the struct has no fields. + #[inline] #[must_use] pub fn is_empty(&self) -> bool { self.fields.is_empty() } /// Returns the value for the given `field` name. + #[inline] #[must_use] pub fn get_by_name(&self, field: Symbol<'heap>) -> Option<&Value<'heap, A>> { self.fields @@ -108,6 +114,7 @@ impl<'heap, A: Allocator> Struct<'heap, A> { } /// Returns a reference to the value at the given field `index`. + #[inline] #[must_use] pub fn get_by_index(&self, index: FieldIndex) -> Option<&Value<'heap, A>> { self.values.get(index.as_usize()) @@ -227,6 +234,7 @@ pub struct StructBuilder<'heap, A: Allocator, const N: usize> { #[expect(unsafe_code)] impl<'heap, A: Allocator, const N: usize> StructBuilder<'heap, A, N> { /// Creates an empty builder with capacity for `N` fields. + #[inline] #[must_use] pub const fn new() -> Self { Self { @@ -251,12 +259,14 @@ impl<'heap, A: Allocator, const N: usize> StructBuilder<'heap, A, N> { } /// Returns the number of fields pushed so far. + #[inline] #[must_use] pub const fn len(&self) -> usize { self.initialized } /// Returns `true` if no fields have been pushed. + #[inline] #[must_use] pub const fn is_empty(&self) -> bool { self.initialized == 0 diff --git a/libs/@local/hashql/mir/src/interpret/value/tuple.rs b/libs/@local/hashql/mir/src/interpret/value/tuple.rs index 5605f1ac604..e002d07925b 100644 --- a/libs/@local/hashql/mir/src/interpret/value/tuple.rs +++ b/libs/@local/hashql/mir/src/interpret/value/tuple.rs @@ -33,6 +33,7 @@ impl<'heap, A: Allocator> Tuple<'heap, A> { /// Creates a new tuple without checking invariants. /// /// The caller must ensure that `values` is non-empty. + #[inline] pub fn new_unchecked(values: Rc<[Value<'heap, A>], A>) -> Self { debug_assert!(!values.is_empty(), "tuple is non-empty by construction"); @@ -50,12 +51,14 @@ impl<'heap, A: Allocator> Tuple<'heap, A> { } /// Returns the tuple's values. + #[inline] #[must_use] pub fn values(&self) -> &[Value<'heap, A>] { &self.values } /// Returns the number of elements. + #[inline] #[must_use] pub fn len(&self) -> NonZero { NonZero::new(self.values.len()) @@ -63,6 +66,7 @@ impl<'heap, A: Allocator> Tuple<'heap, A> { } /// Returns a reference to the element at the given `index`. + #[inline] #[must_use] pub fn get(&self, index: FieldIndex) -> Option<&Value<'heap, A>> { self.values.get(index.as_usize()) From eacbb11041c12c375fb42d47591619ebb11287ac Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:56:43 +0200 Subject: [PATCH 11/25] feat: checkpoint --- .../@local/graph/api/src/rest/entity/query.rs | 49 ++- .../@local/hashql/mir/src/interpret/locals.rs | 6 +- .../hashql/mir/src/interpret/value/dict.rs | 123 ++++++- .../hashql/mir/src/interpret/value/list.rs | 111 ++++++ .../hashql/mir/src/interpret/value/mod.rs | 84 ++++- .../hashql/mir/src/interpret/value/num.rs | 24 +- .../hashql/mir/src/interpret/value/opaque.rs | 82 +++++ .../hashql/mir/src/interpret/value/ptr.rs | 28 ++ .../hashql/mir/src/interpret/value/str.rs | 34 +- .../hashql/mir/src/interpret/value/struct.rs | 322 +++++++++++++++++- .../hashql/mir/src/interpret/value/tuple.rs | 131 ++++++- 11 files changed, 959 insertions(+), 35 deletions(-) diff --git a/libs/@local/graph/api/src/rest/entity/query.rs b/libs/@local/graph/api/src/rest/entity/query.rs index ecb4751e379..b3e129617e4 100644 --- a/libs/@local/graph/api/src/rest/entity/query.rs +++ b/libs/@local/graph/api/src/rest/entity/query.rs @@ -12,6 +12,7 @@ use hash_temporal_client::TemporalClient; use hashql_ast::error::AstDiagnosticCategory; use hashql_core::{ heap::{CloneIn, Heap, HeapPool, ResetAllocator, Scratch, ScratchPool}, + id::Id, module::ModuleRegistry, span::{SpanId, SpanTable}, r#type::environment::Environment, @@ -112,16 +113,6 @@ where } } -struct Task { - context: CompilerContext, -} - -impl Task { - fn spawn(&self) { - todo!() - } -} - // This is only here until https://linear.app/hash/issue/BE-540/hashql-register-based-bytecode-vm #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] enum OwnedValue { @@ -193,12 +184,41 @@ impl<'heap, A: Allocator + Clone> From> for OwnedValue { } } +#[derive(Copy, Clone)] +struct JsonValueSerialize<'value>(&'value OwnedValue); + +impl serde::Serialize for JsonValueSerialize<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self.0 { + OwnedValue::Unit => serializer.serialize_unit(), + OwnedValue::Integer(int) => serializer.serialize_i128(int.as_int()), + OwnedValue::Number(num) => serializer.serialize_f64(num.as_f64()), + OwnedValue::String(str) => serializer.serialize_str(str.as_ref()), + OwnedValue::Pointer(ptr) => serializer.serialize_u64(ptr.def().as_u64()), + OwnedValue::Opaque(_, owned_value) => { + serde::Serialize::serialize(&JsonValueSerialize(&**owned_value), serializer) + } + OwnedValue::Struct(items) => serializer.collect_map( + items + .iter() + .map(|(key, value)| (key, JsonValueSerialize(value))), + ), + OwnedValue::Tuple(owned_values) => todo!(), + OwnedValue::List(owned_values) => todo!(), + OwnedValue::Dict(btree_map) => todo!(), + } + } +} + #[expect(clippy::future_not_send)] async fn query_local( ctx: Arc, client: ::Store<'static>, query: Arc, -) -> Status<(), HashQlDiagnosticCategory, SpanId> { +) -> Status { // We're hitting a borrow checker limitation here, we cannot prove that scratch and heap are // still alive by the time that we `spawn_local`, and due to the fact that it requires `'static` // it means that we need to build the query inside here, and only then delegate, otherwise we'd @@ -217,16 +237,15 @@ async fn query_local( let orchestrator = Orchestrator::new(client, &compilation.artifact.postgres, &context); - let Success { value, advisories } = orchestrator + orchestrator .run(&inputs, compilation.entrypoint, []) .await .into_status() .map_category(|category| { HashQlDiagnosticCategory::Eval(EvalDiagnosticCategory::Orchestrator(category)) }) - .with_diagnostics(advisories)?; - - Status::success(()) + .with_diagnostics(advisories) + .map_value(OwnedValue::from) } async fn query( diff --git a/libs/@local/hashql/mir/src/interpret/locals.rs b/libs/@local/hashql/mir/src/interpret/locals.rs index f35dc1dccf4..41e88e0c9d7 100644 --- a/libs/@local/hashql/mir/src/interpret/locals.rs +++ b/libs/@local/hashql/mir/src/interpret/locals.rs @@ -323,7 +323,11 @@ impl<'ctx, 'heap, A: Allocator> Locals<'ctx, 'heap, A> { // SAFETY: We have just filled the slice with values, and no errors have occurred. let values = unsafe { values.assume_init() }; - Ok(Value::Struct(Struct::new_unchecked(fields, values))) + // SAFETY: `fields` comes from `AggregateKind::Struct`, where MIR construction + // guarantees sorted field order. `values` has the same length by construction. + Ok(Value::Struct(unsafe { + Struct::new_unchecked(fields, values) + })) } /// Constructs an aggregate value (tuple, struct, list, dict, opaque, closure). diff --git a/libs/@local/hashql/mir/src/interpret/value/dict.rs b/libs/@local/hashql/mir/src/interpret/value/dict.rs index 33c3b800fcb..286564ef75c 100644 --- a/libs/@local/hashql/mir/src/interpret/value/dict.rs +++ b/libs/@local/hashql/mir/src/interpret/value/dict.rs @@ -37,6 +37,18 @@ pub struct Dict<'heap, A: Allocator> { impl<'heap, A: Allocator> Dict<'heap, A> { /// Creates a new empty dictionary. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::Dict; + /// + /// let dict: Dict<'_, Global> = Dict::new(); + /// assert!(dict.is_empty()); + /// ``` #[inline] #[must_use] pub fn new() -> Self { @@ -46,6 +58,21 @@ impl<'heap, A: Allocator> Dict<'heap, A> { } /// Returns the number of key-value pairs in the dictionary. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Value}; + /// + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// assert_eq!(dict.len(), 0); + /// + /// dict.insert(Value::Integer(1.into()), Value::Unit); + /// assert_eq!(dict.len(), 1); + /// ``` #[inline] #[must_use] pub fn len(&self) -> usize { @@ -53,6 +80,21 @@ impl<'heap, A: Allocator> Dict<'heap, A> { } /// Returns `true` if the dictionary contains no elements. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Value}; + /// + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// assert!(dict.is_empty()); + /// + /// dict.insert(Value::Integer(1.into()), Value::Unit); + /// assert!(!dict.is_empty()); + /// ``` #[inline] #[must_use] pub fn is_empty(&self) -> bool { @@ -60,6 +102,27 @@ impl<'heap, A: Allocator> Dict<'heap, A> { } /// Inserts a key-value pair into the dictionary. + /// + /// If the key already exists, the value is replaced. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Value}; + /// + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// dict.insert(Value::Integer(1.into()), Value::Integer(100.into())); + /// dict.insert(Value::Integer(1.into()), Value::Integer(200.into())); + /// + /// assert_eq!(dict.len(), 1); + /// assert_eq!( + /// dict.get(&Value::Integer(1.into())), + /// Some(&Value::Integer(200.into())), + /// ); + /// ``` pub fn insert(&mut self, key: Value<'heap, A>, value: Value<'heap, A>) where A: Clone, @@ -68,12 +131,48 @@ impl<'heap, A: Allocator> Dict<'heap, A> { } /// Returns a reference to the value associated with the `key`. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Value}; + /// + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// dict.insert(Value::Integer(1.into()), Value::Integer(100.into())); + /// + /// assert_eq!( + /// dict.get(&Value::Integer(1.into())), + /// Some(&Value::Integer(100.into())) + /// ); + /// assert_eq!(dict.get(&Value::Integer(2.into())), None); + /// ``` #[must_use] pub fn get(&self, key: &Value<'heap, A>) -> Option<&Value<'heap, A>> { self.inner.get(key) } - /// Returns a mutable reference to the value for `key`, inserting [`Value::Unit`] if absent. + /// Returns a mutable reference to the value for `key`, inserting + /// [`Value::Unit`] if absent. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Value}; + /// + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// + /// // Accessing a missing key inserts Unit, then returns a mutable reference + /// let key = Value::Integer(1.into()); + /// *dict.get_mut(&key) = Value::Integer(42.into()); + /// + /// assert_eq!(dict.get(&key), Some(&Value::Integer(42.into()))); + /// ``` pub fn get_mut(&mut self, key: &Value<'heap, A>) -> &mut Value<'heap, A> where A: Clone, @@ -86,6 +185,28 @@ impl<'heap, A: Allocator> Dict<'heap, A> { } /// Returns an iterator over key-value pairs. + /// + /// Pairs are yielded in key order. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Value}; + /// + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// dict.insert(Value::Integer(2.into()), Value::Unit); + /// dict.insert(Value::Integer(1.into()), Value::Unit); + /// + /// let keys: Vec<_> = dict.iter().map(|(k, _)| k.clone()).collect(); + /// // Keys are yielded in sorted order + /// assert_eq!( + /// keys, + /// vec![Value::Integer(1.into()), Value::Integer(2.into())] + /// ); + /// ``` #[must_use] pub fn iter( &self, diff --git a/libs/@local/hashql/mir/src/interpret/value/list.rs b/libs/@local/hashql/mir/src/interpret/value/list.rs index 78f8fa98b9f..4c19f87111e 100644 --- a/libs/@local/hashql/mir/src/interpret/value/list.rs +++ b/libs/@local/hashql/mir/src/interpret/value/list.rs @@ -47,6 +47,18 @@ pub struct List<'heap, A: Allocator> { impl<'heap, A: Allocator> List<'heap, A> { /// Creates a new empty list. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::List; + /// + /// let list: List<'_, Global> = List::new(); + /// assert!(list.is_empty()); + /// ``` #[inline] #[must_use] pub fn new() -> Self { @@ -56,6 +68,21 @@ impl<'heap, A: Allocator> List<'heap, A> { } /// Returns the number of elements in the list. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{List, Value}; + /// + /// let mut list: List<'_, Global> = List::new(); + /// assert_eq!(list.len(), 0); + /// + /// list.push_back(Value::Integer(1.into())); + /// assert_eq!(list.len(), 1); + /// ``` #[inline] #[must_use] pub fn len(&self) -> usize { @@ -63,6 +90,21 @@ impl<'heap, A: Allocator> List<'heap, A> { } /// Returns `true` if the list contains no elements. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{List, Value}; + /// + /// let mut list: List<'_, Global> = List::new(); + /// assert!(list.is_empty()); + /// + /// list.push_back(Value::Unit); + /// assert!(!list.is_empty()); + /// ``` #[inline] #[must_use] pub fn is_empty(&self) -> bool { @@ -70,6 +112,22 @@ impl<'heap, A: Allocator> List<'heap, A> { } /// Appends a value to the end of the list. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Int, List, Value}; + /// + /// let mut list: List<'_, Global> = List::new(); + /// list.push_back(Value::Integer(10.into())); + /// list.push_back(Value::Integer(20.into())); + /// + /// assert_eq!(list.get(Int::from(0_i32)), Some(&Value::Integer(10.into()))); + /// assert_eq!(list.get(Int::from(1_i32)), Some(&Value::Integer(20.into()))); + /// ``` pub fn push_back(&mut self, value: Value<'heap, A>) where A: Clone, @@ -81,6 +139,26 @@ impl<'heap, A: Allocator> List<'heap, A> { /// /// Supports negative indexing: `-1` is the last element, `-2` is /// second-to-last, etc. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Int, List, Value}; + /// + /// let mut list: List<'_, Global> = List::new(); + /// list.push_back(Value::Integer(10.into())); + /// list.push_back(Value::Integer(20.into())); + /// + /// assert_eq!(list.get(Int::from(0_i32)), Some(&Value::Integer(10.into()))); + /// assert_eq!( + /// list.get(Int::from(-1_i32)), + /// Some(&Value::Integer(20.into())) + /// ); + /// assert_eq!(list.get(Int::from(2_i32)), None); + /// ``` #[must_use] pub fn get(&self, index: Int) -> Option<&Value<'heap, A>> { let index = isize::try_from(index.as_int()).ok()?; @@ -100,6 +178,23 @@ impl<'heap, A: Allocator> List<'heap, A> { /// Returns a mutable reference to the element at the given `index`. /// /// Supports negative indexing: `-1` is the last element, `-2` is second-to-last, etc. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Int, List, Value}; + /// + /// let mut list: List<'_, Global> = List::new(); + /// list.push_back(Value::Integer(1.into())); + /// list.push_back(Value::Integer(2.into())); + /// + /// // Mutate the last element via negative index + /// *list.get_mut(Int::from(-1_i32)).unwrap() = Value::Integer(99.into()); + /// assert_eq!(list.get(Int::from(1_i32)), Some(&Value::Integer(99.into()))); + /// ``` pub fn get_mut(&mut self, index: Int) -> Option<&mut Value<'heap, A>> where A: Clone, @@ -119,6 +214,22 @@ impl<'heap, A: Allocator> List<'heap, A> { } /// Returns an iterator over the list's elements. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{List, Value}; + /// + /// let mut list: List<'_, Global> = List::new(); + /// list.push_back(Value::Integer(1.into())); + /// list.push_back(Value::Integer(2.into())); + /// + /// let values: Vec<_> = list.iter().collect(); + /// assert_eq!(values.len(), 2); + /// ``` #[must_use] pub fn iter(&self) -> impl ExactSizeIterator> + DoubleEndedIterator { self.inner.iter() diff --git a/libs/@local/hashql/mir/src/interpret/value/mod.rs b/libs/@local/hashql/mir/src/interpret/value/mod.rs index d33773a293e..6ed8e74d1a0 100644 --- a/libs/@local/hashql/mir/src/interpret/value/mod.rs +++ b/libs/@local/hashql/mir/src/interpret/value/mod.rs @@ -169,13 +169,45 @@ impl<'heap, A: Allocator> Value<'heap, A> { /// Indexes into this value using another value as the index. /// - /// For lists, the index must be an integer. For dicts, any value can be used as a key. + /// For lists, the index must be an integer (supports negative indexing). + /// For dicts, any value can be used as a key. /// Returns [`Value::Unit`] if the index is not found. /// /// # Errors /// /// Returns an error if this value is not subscriptable (not a list or dict), /// or if the index type is invalid for the collection type. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::{Dict, Int, List, Value}; + /// + /// // List subscript with integer index + /// let mut list: List<'_, Global> = List::new(); + /// list.push_back(Value::Integer(10.into())); + /// let list_val = Value::List(list); + /// + /// let result = list_val.subscript::<()>(&Value::Integer(0.into())).unwrap(); + /// assert_eq!(result, &Value::Integer(10.into())); + /// + /// // Dict subscript with any key type + /// let mut dict: Dict<'_, Global> = Dict::new(); + /// dict.insert(Value::Integer(1.into()), Value::Integer(100.into())); + /// let dict_val = Value::Dict(dict); + /// + /// let result = dict_val.subscript::<()>(&Value::Integer(1.into())).unwrap(); + /// assert_eq!(result, &Value::Integer(100.into())); + /// + /// // Missing key returns Unit + /// let result = dict_val + /// .subscript::<()>(&Value::Integer(99.into())) + /// .unwrap(); + /// assert_eq!(result, &Value::Unit); + /// ``` #[inline] pub fn subscript<'this, 'index, E>( &'this self, @@ -249,11 +281,31 @@ impl<'heap, A: Allocator> Value<'heap, A> { /// Projects a field from this value by index. /// - /// Works on structs and tuples. + /// Works on structs, tuples, and opaques (projects through the wrapper). /// /// # Errors /// /// Returns an error if this value is not projectable or the field index is invalid. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::{ + /// body::place::FieldIndex, + /// interpret::value::{Tuple, Value}, + /// }; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Integer(10.into()), Value::Integer(20.into())]); + /// let tuple = Value::Tuple(Tuple::new(values).unwrap()); + /// + /// let field = tuple.project::<()>(FieldIndex::new(1)).unwrap(); + /// assert_eq!(field, &Value::Integer(20.into())); + /// + /// // Out-of-bounds index returns an error + /// assert!(tuple.project::<()>(FieldIndex::new(5)).is_err()); + /// ``` #[inline] pub fn project<'this, E>( &'this self, @@ -331,11 +383,37 @@ impl<'heap, A: Allocator> Value<'heap, A> { /// Projects a field from this value by name. /// - /// Only works on structs. + /// Only works on structs and opaques (projects through the wrapper). /// /// # Errors /// /// Returns an error if this value is not a struct or the field name is not found. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// let heap = Heap::new(); + /// let interner = Interner::new(&heap); + /// + /// let mut builder = StructBuilder::<'_, Global, 1>::new(); + /// let name = heap.intern_symbol("x"); + /// builder.push(name, Value::Integer(42.into())); + /// let s = Value::Struct(builder.finish(&interner.symbols, Global)); + /// + /// let field = s.project_by_name::<()>(name).unwrap(); + /// assert_eq!(field, &Value::Integer(42.into())); + /// + /// // Unknown field returns an error + /// let unknown = heap.intern_symbol("z"); + /// assert!(s.project_by_name::<()>(unknown).is_err()); + /// ``` pub fn project_by_name<'this, E>( &'this self, index: Symbol<'heap>, diff --git a/libs/@local/hashql/mir/src/interpret/value/num.rs b/libs/@local/hashql/mir/src/interpret/value/num.rs index 9e859b57751..f30fcc22b13 100644 --- a/libs/@local/hashql/mir/src/interpret/value/num.rs +++ b/libs/@local/hashql/mir/src/interpret/value/num.rs @@ -14,7 +14,20 @@ use crate::macros::{forward_ref_binop, forward_ref_unop}; /// A floating-point number value with total ordering semantics. /// /// Wraps an [`f64`] and implements [`Ord`] using [`f64::total_cmp`], which follows -/// the IEEE 754 `totalOrder` predicate. +/// the IEEE 754 `totalOrder` predicate. Negative zero and positive zero are +/// treated as equal. +/// +/// # Examples +/// +/// ``` +/// use hashql_mir::interpret::value::Num; +/// +/// let n = Num::from(3.14); +/// assert_eq!(n.as_f64(), 3.14); +/// +/// // Negative and positive zero are equal +/// assert_eq!(Num::from(-0.0), Num::from(0.0)); +/// ``` #[derive(Debug, Copy, Clone)] pub struct Num { value: f64, @@ -22,6 +35,15 @@ pub struct Num { impl Num { /// Returns the underlying [`f64`] value. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::Num; + /// + /// let n = Num::from(2.5); + /// assert_eq!(n.as_f64(), 2.5); + /// ``` #[inline] #[must_use] pub const fn as_f64(self) -> f64 { diff --git a/libs/@local/hashql/mir/src/interpret/value/opaque.rs b/libs/@local/hashql/mir/src/interpret/value/opaque.rs index 86ecd671971..8c23bd33c78 100644 --- a/libs/@local/hashql/mir/src/interpret/value/opaque.rs +++ b/libs/@local/hashql/mir/src/interpret/value/opaque.rs @@ -41,6 +41,22 @@ pub struct Opaque<'heap, A: Allocator> { impl<'heap, A: Allocator> Opaque<'heap, A> { /// Creates a new opaque value with the given `name` and wrapped `value`. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Opaque, Value}; + /// # use hashql_core::heap::Heap; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let heap = Heap::new(); + /// let opaque = Opaque::new( + /// heap.intern_symbol("Meters"), + /// Rc::new(Value::Integer(100.into())), + /// ); + /// assert_eq!(opaque.name().as_str(), "Meters"); + /// ``` #[inline] #[must_use] pub fn new(name: Symbol<'heap>, value: impl Into, A>>) -> Self { @@ -51,6 +67,22 @@ impl<'heap, A: Allocator> Opaque<'heap, A> { } /// Returns the type name of this opaque value. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Opaque, Value}; + /// # use hashql_core::heap::Heap; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let heap = Heap::new(); + /// let opaque = Opaque::new( + /// heap.intern_symbol("UserId"), + /// Rc::new(Value::Integer(1.into())), + /// ); + /// assert_eq!(opaque.name().as_str(), "UserId"); + /// ``` #[inline] #[must_use] pub const fn name(&self) -> Symbol<'heap> { @@ -58,6 +90,22 @@ impl<'heap, A: Allocator> Opaque<'heap, A> { } /// Returns a reference to the wrapped value. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Opaque, Value}; + /// # use hashql_core::heap::Heap; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let heap = Heap::new(); + /// let opaque = Opaque::new( + /// heap.intern_symbol("Tag"), + /// Rc::new(Value::Integer(42.into())), + /// ); + /// assert_eq!(opaque.value(), &Value::Integer(42.into())); + /// ``` #[inline] #[must_use] pub fn value(&self) -> &Value<'heap, A> { @@ -65,6 +113,22 @@ impl<'heap, A: Allocator> Opaque<'heap, A> { } /// Returns a mutable reference to the wrapped value. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Opaque, Value}; + /// # use hashql_core::heap::Heap; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let heap = Heap::new(); + /// let name = heap.intern_symbol("Counter"); + /// let mut opaque = Opaque::new(name, Rc::new(Value::Integer(0.into()))); + /// + /// *opaque.value_mut() = Value::Integer(42.into()); + /// assert_eq!(opaque.value(), &Value::Integer(42.into())); + /// ``` #[must_use] pub fn value_mut(&mut self) -> &mut Value<'heap, A> where @@ -98,6 +162,24 @@ impl<'heap, A: Allocator> Opaque<'heap, A> { } /// Returns a displayable representation of this opaque type's name. + /// + /// The format is `Name(InnerType)` for most inner values, or + /// `Name(field: Type, ...)` when the inner value is a struct or tuple + /// (parentheses from the inner type are reused). + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Opaque, Value}; + /// # use hashql_core::heap::Heap; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let heap = Heap::new(); + /// let name = heap.intern_symbol("UserId"); + /// let opaque = Opaque::new(name, Rc::new(Value::Integer(42.into()))); + /// assert_eq!(opaque.type_name().to_string(), "UserId(Integer)"); + /// ``` pub fn type_name(&self) -> impl Display { fmt::from_fn(|fmt| { // check if the inner type is a struct or tuple, in which case we elide the `()` diff --git a/libs/@local/hashql/mir/src/interpret/value/ptr.rs b/libs/@local/hashql/mir/src/interpret/value/ptr.rs index 04aa0f54fa6..32379cdb026 100644 --- a/libs/@local/hashql/mir/src/interpret/value/ptr.rs +++ b/libs/@local/hashql/mir/src/interpret/value/ptr.rs @@ -8,6 +8,16 @@ use crate::def::DefId; /// /// Points to a function definition identified by its [`DefId`]. Used to /// represent first-class functions and closures in the interpreter. +/// +/// # Examples +/// +/// ``` +/// use hashql_mir::{def::DefId, interpret::value::Ptr}; +/// +/// let ptr = Ptr::new(DefId::DICT_INSERT); +/// assert_eq!(ptr.def(), DefId::DICT_INSERT); +/// assert_eq!(ptr.to_string(), format!("*{}", DefId::DICT_INSERT)); +/// ``` #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Ptr { value: DefId, @@ -15,6 +25,15 @@ pub struct Ptr { impl Ptr { /// Creates a new function pointer from a [`DefId`]. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::{def::DefId, interpret::value::Ptr}; + /// + /// let ptr = Ptr::new(DefId::DICT_INSERT); + /// assert_eq!(ptr.def(), DefId::DICT_INSERT); + /// ``` #[inline] #[must_use] pub const fn new(value: DefId) -> Self { @@ -22,6 +41,15 @@ impl Ptr { } /// Returns the [`DefId`] this pointer references. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::{def::DefId, interpret::value::Ptr}; + /// + /// let ptr = Ptr::from(DefId::DICT_INSERT); + /// assert_eq!(ptr.def(), DefId::DICT_INSERT); + /// ``` #[inline] #[must_use] pub const fn def(self) -> DefId { diff --git a/libs/@local/hashql/mir/src/interpret/value/str.rs b/libs/@local/hashql/mir/src/interpret/value/str.rs index 85ba6eecbb1..f077b7bd039 100644 --- a/libs/@local/hashql/mir/src/interpret/value/str.rs +++ b/libs/@local/hashql/mir/src/interpret/value/str.rs @@ -1,11 +1,13 @@ //! String representation for the MIR interpreter. use alloc::{alloc::Global, rc::Rc}; -use core::{alloc::Allocator, cmp, fmt}; -use std::alloc::AllocError; +use core::{ + alloc::{AllocError, Allocator}, + cmp, fmt, +}; use hashql_core::{ - heap::{CloneIn, FromIn, TryCloneIn}, + heap::{FromIn as _, TryCloneIn}, symbol::Symbol, value::String, }; @@ -71,6 +73,17 @@ pub struct Str<'heap, A: Allocator = Global> { impl Str<'_, A> { /// Returns this string as a string slice. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::Str; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let s = Str::from(Rc::::from("hello")); + /// assert_eq!(s.as_str(), "hello"); + /// ``` #[inline] #[must_use] pub fn as_str(&self) -> &str { @@ -85,6 +98,21 @@ impl Str<'_, A> { /// /// The returned [`Str`] is not tied to the original heap, so it can /// outlive the heap the string was interned on. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// use alloc::alloc::Global; + /// + /// use hashql_mir::interpret::value::Str; + /// # use alloc::rc::Rc; + /// + /// let s = Str::from(Rc::::from("hello")); + /// let owned: Str<'static, Global> = s.into_owned_in(Global); + /// assert_eq!(owned.as_str(), "hello"); + /// ``` pub fn into_owned_in<'lifetime>(self, alloc: A) -> Str<'lifetime, A> { match self.inner { StrInner::Owned(value) => Str { diff --git a/libs/@local/hashql/mir/src/interpret/value/struct.rs b/libs/@local/hashql/mir/src/interpret/value/struct.rs index ff935fde68d..77b2c653f04 100644 --- a/libs/@local/hashql/mir/src/interpret/value/struct.rs +++ b/libs/@local/hashql/mir/src/interpret/value/struct.rs @@ -21,13 +21,42 @@ use crate::body::place::FieldIndex; /// A named-field struct value. /// -/// Contains field names (interned symbols) and their corresponding values. -/// Field order is preserved and significant for comparison. +/// Contains field names (interned [`Symbol`]s) and their corresponding values. +/// Fields are sorted by symbol and accessed by name or positional index. +/// +/// [`StructBuilder`] is the ergonomic way to construct structs, as it +/// handles sorting and interning automatically. /// /// # Invariants /// /// - `fields.len() == values.len()` /// - Field names should be unique (not enforced at construction) +/// +/// # Examples +/// +/// ``` +/// # #![feature(allocator_api)] +/// # extern crate alloc; +/// use alloc::alloc::Global; +/// +/// use hashql_mir::interpret::value::{Struct, StructBuilder, Value}; +/// # use hashql_core::heap::Heap; +/// # use hashql_mir::intern::Interner; +/// +/// let heap = Heap::new(); +/// let interner = Interner::new(&heap); +/// +/// let mut builder = StructBuilder::<'_, Global, 2>::new(); +/// builder.push(heap.intern_symbol("x"), Value::Integer(1.into())); +/// builder.push(heap.intern_symbol("y"), Value::Integer(2.into())); +/// let s = builder.finish(&interner.symbols, Global); +/// +/// assert_eq!(s.len(), 2); +/// assert_eq!( +/// s.get_by_name(heap.intern_symbol("x")), +/// Some(&Value::Integer(1.into())), +/// ); +/// ``` #[derive(Debug, Clone)] pub struct Struct<'heap, A: Allocator> { fields: Interned<'heap, [Symbol<'heap>]>, @@ -37,9 +66,17 @@ pub struct Struct<'heap, A: Allocator> { impl<'heap, A: Allocator> Struct<'heap, A> { /// Creates a new struct without checking invariants. /// - /// The caller must ensure that `fields` and `values` have the same length. + /// # Safety + /// + /// The caller must ensure that: + /// - `fields` and `values` have the same length + /// - `fields` is sorted by [`Symbol`] ordering + #[expect( + unsafe_code, + reason = "callers must uphold the sorted-fields invariant" + )] #[inline] - pub fn new_unchecked( + pub unsafe fn new_unchecked( fields: Interned<'heap, [Symbol<'heap>]>, values: Rc<[Value<'heap, A>], A>, ) -> Self { @@ -51,7 +88,11 @@ impl<'heap, A: Allocator> Struct<'heap, A> { /// Creates a new struct from field names and values. /// - /// Returns [`None`] if `fields` and `values` have different lengths. + /// Returns [`None`] if `fields` and `values` have different lengths, + /// or if `fields` is not sorted. + /// + /// Prefer [`StructBuilder`] for constructing structs, as it handles + /// sorting fields correctly. #[must_use] pub fn new( fields: Interned<'heap, [Symbol<'heap>]>, @@ -59,10 +100,37 @@ impl<'heap, A: Allocator> Struct<'heap, A> { ) -> Option { let values = values.into(); - (fields.len() == values.len()).then(|| Self::new_unchecked(fields, values)) + if fields.len() != values.len() || !fields.is_sorted() { + return None; + } + + // SAFETY: we just verified length equality and sort order. + #[expect(unsafe_code, reason = "invariants checked above")] + Some(unsafe { Self::new_unchecked(fields, values) }) } /// Returns the field names. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let mut builder = StructBuilder::<'_, Global, 2>::new(); + /// builder.push(heap.intern_symbol("a"), Value::Unit); + /// builder.push(heap.intern_symbol("b"), Value::Unit); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// let names: Vec<_> = s.fields().iter().map(|sym| sym.as_str()).collect(); + /// assert_eq!(names, vec!["a", "b"]); + /// ``` #[inline] #[must_use] pub const fn fields(&self) -> &Interned<'heap, [Symbol<'heap>]> { @@ -70,6 +138,27 @@ impl<'heap, A: Allocator> Struct<'heap, A> { } /// Returns the field values. + /// + /// Values are in the same order as [`fields`](Self::fields). + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let mut builder = StructBuilder::<'_, Global, 1>::new(); + /// builder.push(heap.intern_symbol("x"), Value::Integer(5.into())); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// assert_eq!(s.values(), &[Value::Integer(5.into())]); + /// ``` #[inline] #[must_use] pub fn values(&self) -> &[Value<'heap, A>] { @@ -77,6 +166,26 @@ impl<'heap, A: Allocator> Struct<'heap, A> { } /// Returns the number of fields. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let mut builder = StructBuilder::<'_, Global, 2>::new(); + /// builder.push(heap.intern_symbol("a"), Value::Unit); + /// builder.push(heap.intern_symbol("b"), Value::Unit); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// assert_eq!(s.len(), 2); + /// ``` #[inline] #[must_use] pub fn len(&self) -> usize { @@ -84,6 +193,23 @@ impl<'heap, A: Allocator> Struct<'heap, A> { } /// Returns `true` if the struct has no fields. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let builder = StructBuilder::<'_, Global, 0>::new(); + /// let s = builder.finish(&interner.symbols, Global); + /// assert!(s.is_empty()); + /// ``` #[inline] #[must_use] pub fn is_empty(&self) -> bool { @@ -91,6 +217,27 @@ impl<'heap, A: Allocator> Struct<'heap, A> { } /// Returns the value for the given `field` name. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let name = heap.intern_symbol("age"); + /// let mut builder = StructBuilder::<'_, Global, 1>::new(); + /// builder.push(name, Value::Integer(30.into())); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// assert_eq!(s.get_by_name(name), Some(&Value::Integer(30.into()))); + /// assert_eq!(s.get_by_name(heap.intern_symbol("missing")), None); + /// ``` #[inline] #[must_use] pub fn get_by_name(&self, field: Symbol<'heap>) -> Option<&Value<'heap, A>> { @@ -101,6 +248,27 @@ impl<'heap, A: Allocator> Struct<'heap, A> { } /// Returns a mutable reference to the value for the given `field` name. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let name = heap.intern_symbol("count"); + /// let mut builder = StructBuilder::<'_, Global, 1>::new(); + /// builder.push(name, Value::Integer(0.into())); + /// let mut s = builder.finish(&interner.symbols, Global); + /// + /// *s.get_by_name_mut(name).unwrap() = Value::Integer(10.into()); + /// assert_eq!(s.get_by_name(name), Some(&Value::Integer(10.into()))); + /// ``` #[must_use] pub fn get_by_name_mut(&mut self, field: Symbol<'heap>) -> Option<&mut Value<'heap, A>> where @@ -114,6 +282,40 @@ impl<'heap, A: Allocator> Struct<'heap, A> { } /// Returns a reference to the value at the given field `index`. + /// + /// Fields are sorted by symbol, so the positional index depends on + /// the sort order of the field names. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::{ + /// body::place::FieldIndex, + /// interpret::value::{StructBuilder, Value}, + /// }; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let mut builder = StructBuilder::<'_, Global, 2>::new(); + /// builder.push(heap.intern_symbol("b"), Value::Integer(2.into())); + /// builder.push(heap.intern_symbol("a"), Value::Integer(1.into())); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// // After sorting: "a" is at index 0, "b" is at index 1 + /// assert_eq!( + /// s.get_by_index(FieldIndex::new(0)), + /// Some(&Value::Integer(1.into())) + /// ); + /// assert_eq!( + /// s.get_by_index(FieldIndex::new(1)), + /// Some(&Value::Integer(2.into())) + /// ); + /// ``` #[inline] #[must_use] pub fn get_by_index(&self, index: FieldIndex) -> Option<&Value<'heap, A>> { @@ -121,6 +323,32 @@ impl<'heap, A: Allocator> Struct<'heap, A> { } /// Returns a mutable reference to the value at the given field `index`. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::{ + /// body::place::FieldIndex, + /// interpret::value::{StructBuilder, Value}, + /// }; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let mut builder = StructBuilder::<'_, Global, 1>::new(); + /// builder.push(heap.intern_symbol("x"), Value::Integer(0.into())); + /// let mut s = builder.finish(&interner.symbols, Global); + /// + /// *s.get_by_index_mut(FieldIndex::new(0)).unwrap() = Value::Integer(99.into()); + /// assert_eq!( + /// s.get_by_index(FieldIndex::new(0)), + /// Some(&Value::Integer(99.into())) + /// ); + /// ``` pub fn get_by_index_mut(&mut self, index: FieldIndex) -> Option<&mut Value<'heap, A>> where A: Clone, @@ -130,6 +358,27 @@ impl<'heap, A: Allocator> Struct<'heap, A> { } /// Returns an iterator over (field name, value) pairs. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// # let interner = Interner::new(&heap); + /// let mut builder = StructBuilder::<'_, Global, 2>::new(); + /// builder.push(heap.intern_symbol("x"), Value::Integer(1.into())); + /// builder.push(heap.intern_symbol("y"), Value::Integer(2.into())); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// let names: Vec<_> = s.iter().map(|(name, _)| name.as_str().to_owned()).collect(); + /// assert_eq!(names, vec!["x", "y"]); + /// ``` pub fn iter(&self) -> StructIter<'_, 'heap, A> { StructIter { fields: self.fields.iter().copied(), @@ -234,6 +483,19 @@ pub struct StructBuilder<'heap, A: Allocator, const N: usize> { #[expect(unsafe_code)] impl<'heap, A: Allocator, const N: usize> StructBuilder<'heap, A, N> { /// Creates an empty builder with capacity for `N` fields. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// use hashql_mir::interpret::value::StructBuilder; + /// + /// let builder = StructBuilder::<'_, Global, 3>::new(); + /// assert!(builder.is_empty()); + /// assert_eq!(builder.len(), 0); + /// ``` #[inline] #[must_use] pub const fn new() -> Self { @@ -293,6 +555,23 @@ impl<'heap, A: Allocator, const N: usize> StructBuilder<'heap, A, N> { /// /// - If the builder is full (`initialized == N`) /// - If `field` has already been pushed + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// # let heap = Heap::new(); + /// let mut builder = StructBuilder::<'_, Global, 2>::new(); + /// builder.push(heap.intern_symbol("x"), Value::Integer(1.into())); + /// builder.push(heap.intern_symbol("y"), Value::Integer(2.into())); + /// + /// assert_eq!(builder.len(), 2); + /// ``` pub fn push(&mut self, field: Symbol<'heap>, value: Value<'heap, A>) { assert_ne!(self.initialized, N, "struct is full"); assert!(!self.fields().contains(&field), "field already exists"); @@ -304,6 +583,37 @@ impl<'heap, A: Allocator, const N: usize> StructBuilder<'heap, A, N> { } /// Consumes the builder and produces a [`Struct`]. + /// + /// Fields are sorted by symbol before interning, so the resulting + /// struct's field order may differ from the push order. + /// + /// # Examples + /// + /// ``` + /// # #![feature(allocator_api)] + /// # extern crate alloc; + /// # use alloc::alloc::Global; + /// # use hashql_core::heap::Heap; + /// # use hashql_mir::intern::Interner; + /// use hashql_mir::interpret::value::{StructBuilder, Value}; + /// + /// let heap = Heap::new(); + /// let interner = Interner::new(&heap); + /// + /// let mut builder = StructBuilder::<'_, Global, 2>::new(); + /// builder.push(heap.intern_symbol("b"), Value::Integer(2.into())); + /// builder.push(heap.intern_symbol("a"), Value::Integer(1.into())); + /// let s = builder.finish(&interner.symbols, Global); + /// + /// // Fields are sorted: "a" comes first + /// let names: Vec<_> = s.fields().iter().map(|sym| sym.as_str()).collect(); + /// assert_eq!(names, vec!["a", "b"]); + /// // Values follow their fields + /// assert_eq!( + /// s.values(), + /// &[Value::Integer(1.into()), Value::Integer(2.into())] + /// ); + /// ``` pub fn finish( mut self, symbols: &InternSet<'heap, [Symbol<'heap>]>, diff --git a/libs/@local/hashql/mir/src/interpret/value/tuple.rs b/libs/@local/hashql/mir/src/interpret/value/tuple.rs index e002d07925b..fc53a59cf0b 100644 --- a/libs/@local/hashql/mir/src/interpret/value/tuple.rs +++ b/libs/@local/hashql/mir/src/interpret/value/tuple.rs @@ -15,14 +15,28 @@ use crate::body::place::FieldIndex; /// A positional tuple value. /// -/// Contains an ordered sequence of values accessed by index. Unlike unit -/// (represented by [`Value::Unit`]), a tuple always contains at least one -/// element. +/// Contains an ordered sequence of values accessed by [`FieldIndex`]. Unlike +/// unit (represented by [`Value::Unit`]), a tuple always contains at least +/// one element. /// /// # Invariants /// /// - Must be non-empty (empty tuples should use [`Value::Unit`]) /// +/// # Examples +/// +/// ``` +/// use hashql_mir::interpret::value::{Tuple, Value}; +/// # extern crate alloc; +/// # use alloc::rc::Rc; +/// +/// let values: Rc<[Value]> = Rc::from(vec![Value::Integer(1.into()), Value::Integer(2.into())]); +/// let tuple = Tuple::new(values).expect("non-empty"); +/// +/// assert_eq!(tuple.len().get(), 2); +/// assert_eq!(tuple.values()[0], Value::Integer(1.into())); +/// ``` +/// /// [`Value::Unit`]: super::Value::Unit #[derive(Debug, Clone)] pub struct Tuple<'heap, A: Allocator> { @@ -43,14 +57,41 @@ impl<'heap, A: Allocator> Tuple<'heap, A> { /// Creates a new tuple from a slice of values. /// /// Returns [`None`] if `values` is empty. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Tuple, Value}; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Unit, Value::Integer(1.into())]); + /// assert!(Tuple::new(values).is_some()); + /// + /// let empty: Rc<[Value]> = Rc::from(vec![]); + /// assert!(Tuple::new(empty).is_none()); + /// ``` #[must_use] pub fn new(values: impl Into], A>>) -> Option { let values = values.into(); - (!values.is_empty()).then_some(Self::new_unchecked(values)) + (!values.is_empty()).then(|| Self::new_unchecked(values)) } - /// Returns the tuple's values. + /// Returns the tuple's values as a slice. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Tuple, Value}; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Integer(1.into()), Value::Unit]); + /// let tuple = Tuple::new(values).unwrap(); + /// + /// assert_eq!(tuple.values(), &[Value::Integer(1.into()), Value::Unit]); + /// ``` #[inline] #[must_use] pub fn values(&self) -> &[Value<'heap, A>] { @@ -58,6 +99,22 @@ impl<'heap, A: Allocator> Tuple<'heap, A> { } /// Returns the number of elements. + /// + /// Always at least 1 (empty tuples are represented by [`Value::Unit`]). + /// + /// [`Value::Unit`]: super::Value::Unit + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Tuple, Value}; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Unit, Value::Unit, Value::Unit]); + /// let tuple = Tuple::new(values).unwrap(); + /// assert_eq!(tuple.len().get(), 3); + /// ``` #[inline] #[must_use] pub fn len(&self) -> NonZero { @@ -66,6 +123,30 @@ impl<'heap, A: Allocator> Tuple<'heap, A> { } /// Returns a reference to the element at the given `index`. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::{ + /// body::place::FieldIndex, + /// interpret::value::{Tuple, Value}, + /// }; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Integer(10.into()), Value::Integer(20.into())]); + /// let tuple = Tuple::new(values).unwrap(); + /// + /// assert_eq!( + /// tuple.get(FieldIndex::new(0)), + /// Some(&Value::Integer(10.into())) + /// ); + /// assert_eq!( + /// tuple.get(FieldIndex::new(1)), + /// Some(&Value::Integer(20.into())) + /// ); + /// assert_eq!(tuple.get(FieldIndex::new(2)), None); + /// ``` #[inline] #[must_use] pub fn get(&self, index: FieldIndex) -> Option<&Value<'heap, A>> { @@ -73,6 +154,26 @@ impl<'heap, A: Allocator> Tuple<'heap, A> { } /// Returns a mutable reference to the element at the given `index`. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::{ + /// body::place::FieldIndex, + /// interpret::value::{Tuple, Value}, + /// }; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Integer(1.into()), Value::Integer(2.into())]); + /// let mut tuple = Tuple::new(values).unwrap(); + /// + /// *tuple.get_mut(FieldIndex::new(0)).unwrap() = Value::Integer(99.into()); + /// assert_eq!( + /// tuple.get(FieldIndex::new(0)), + /// Some(&Value::Integer(99.into())) + /// ); + /// ``` #[must_use] pub fn get_mut(&mut self, index: FieldIndex) -> Option<&mut Value<'heap, A>> where @@ -83,6 +184,26 @@ impl<'heap, A: Allocator> Tuple<'heap, A> { } /// Returns an iterator over the tuple's elements. + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::{Tuple, Value}; + /// # extern crate alloc; + /// # use alloc::rc::Rc; + /// + /// let values: Rc<[Value]> = Rc::from(vec![Value::Integer(1.into()), Value::Integer(2.into())]); + /// let tuple = Tuple::new(values).unwrap(); + /// + /// let sum: i128 = tuple + /// .iter() + /// .filter_map(|v| match v { + /// Value::Integer(i) => Some(i.as_int()), + /// _ => None, + /// }) + /// .sum(); + /// assert_eq!(sum, 3); + /// ``` pub fn iter(&self) -> core::slice::Iter<'_, Value<'heap, A>> { self.values.iter() } From 0ed6fc3f9085822c19f1556c613270e914c48a94 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:13:29 +0200 Subject: [PATCH 12/25] feat: checkpoint --- .../@local/graph/api/src/rest/entity/query.rs | 47 +++++++++++++------ .../ast/src/lowering/node_renumberer.rs | 1 + .../hashql/ast/src/lowering/sanitizer.rs | 1 + .../hashql/core/src/collections/pool.rs | 1 + .../graph/algorithms/dominators/frontier.rs | 1 + libs/@local/hashql/core/src/graph/linked.rs | 2 + libs/@local/hashql/core/src/heap/mod.rs | 1 + libs/@local/hashql/core/src/heap/pool.rs | 2 + libs/@local/hashql/core/src/heap/scratch.rs | 5 +- libs/@local/hashql/core/src/id/bit_vec/mod.rs | 2 + .../hashql/core/src/module/namespace.rs | 1 + .../hashql/core/src/pretty/formatter.rs | 1 + .../core/src/type/inference/solver/graph.rs | 1 + .../core/src/type/inference/solver/mod.rs | 1 + libs/@local/hashql/core/src/type/recursion.rs | 1 + libs/@local/hashql/core/src/type/visit.rs | 1 + libs/@local/hashql/diagnostics/src/issues.rs | 1 + .../hashql/diagnostics/src/source/span.rs | 2 + .../hashql/eval/src/orchestrator/events.rs | 1 + .../hashql/eval/src/postgres/parameters.rs | 1 + .../hashql/eval/src/postgres/projections.rs | 1 + libs/@local/hashql/hir/src/context.rs | 2 + .../hashql/hir/src/lower/normalization.rs | 1 + libs/@local/hashql/hir/src/map.rs | 1 + libs/@local/hashql/mir/src/body/operand.rs | 2 + .../@local/hashql/mir/src/interpret/inputs.rs | 1 + .../hashql/mir/src/interpret/runtime.rs | 1 + .../mir/src/interpret/suspension/temporal.rs | 2 + libs/@local/hashql/mir/src/interpret/tests.rs | 8 +++- .../src/pass/analysis/data_dependency/mod.rs | 1 + .../hashql/mir/src/pass/execution/cost/mod.rs | 1 + .../mir/src/pass/execution/island/mod.rs | 2 + .../mir/src/pass/execution/splitting/mod.rs | 1 + .../execution/terminator_placement/mod.rs | 1 + .../mir/src/pass/execution/traversal/mod.rs | 1 + libs/@local/hashql/mir/src/pass/mod.rs | 1 + .../src/pass/transform/canonicalization.rs | 1 + .../src/pass/transform/cfg_simplify/mod.rs | 1 + .../pass/transform/copy_propagation/mod.rs | 1 + .../hashql/mir/src/pass/transform/dse/mod.rs | 1 + .../pass/transform/forward_substitution.rs | 1 + .../src/pass/transform/inst_simplify/mod.rs | 1 + .../mir/src/pass/transform/ssa_repair/mod.rs | 1 + libs/@local/hashql/mir/src/reify/current.rs | 2 + .../syntax-jexpr/src/lexer/syntax_kind_set.rs | 2 + .../syntax-jexpr/src/parser/array/error.rs | 1 + .../hashql/syntax-jexpr/src/parser/error.rs | 4 ++ .../syntax-jexpr/src/parser/object/error.rs | 1 + .../hashql/syntax-jexpr/src/parser/state.rs | 2 + 49 files changed, 101 insertions(+), 19 deletions(-) diff --git a/libs/@local/graph/api/src/rest/entity/query.rs b/libs/@local/graph/api/src/rest/entity/query.rs index b3e129617e4..569680840ad 100644 --- a/libs/@local/graph/api/src/rest/entity/query.rs +++ b/libs/@local/graph/api/src/rest/entity/query.rs @@ -39,6 +39,7 @@ use hashql_mir::{ pass::{LowerConfig, execution::ExecutionAnalysisResidual}, }; use hashql_syntax_jexpr::{error::JExprDiagnosticCategory, span::Span}; +use serde::Serialize as _; use serde_json::value::RawValue; use tokio::{runtime::Runtime, task::LocalSet}; use tokio_util::task::LocalPoolHandle; @@ -113,19 +114,31 @@ where } } +fn serialize_int(int: &Int, serializer: S) -> Result { + int.as_int().serialize(serializer) +} + +fn serialize_num(num: &Num, serializer: S) -> Result { + num.as_f64().serialize(serializer) +} + +fn serialize_ptr(ptr: &Ptr, serializer: S) -> Result { + ptr.def().as_u32().serialize(serializer) +} + // This is only here until https://linear.app/hash/issue/BE-540/hashql-register-based-bytecode-vm -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)] enum OwnedValue { /// The unit value. Unit, /// An integer value (also represents booleans). - Integer(Int), + Integer(#[serde(serialize_with = "serialize_int")] Int), /// A floating-point number. - Number(Num), + Number(#[serde(serialize_with = "serialize_num")] Num), /// A string value. String(Arc), /// A function pointer. - Pointer(Ptr), + Pointer(#[serde(serialize_with = "serialize_ptr")] Ptr), /// An opaque/newtype wrapper. Opaque(Arc, Box), @@ -194,21 +207,25 @@ impl serde::Serialize for JsonValueSerialize<'_> { { match self.0 { OwnedValue::Unit => serializer.serialize_unit(), - OwnedValue::Integer(int) => serializer.serialize_i128(int.as_int()), - OwnedValue::Number(num) => serializer.serialize_f64(num.as_f64()), - OwnedValue::String(str) => serializer.serialize_str(str.as_ref()), - OwnedValue::Pointer(ptr) => serializer.serialize_u64(ptr.def().as_u64()), + OwnedValue::Integer(int) => serialize_int(int, serializer), + OwnedValue::Number(num) => serialize_num(num, serializer), + OwnedValue::String(str) => serde::Serialize::serialize(str.as_ref(), serializer), + OwnedValue::Pointer(ptr) => serialize_ptr(ptr, serializer), OwnedValue::Opaque(_, owned_value) => { - serde::Serialize::serialize(&JsonValueSerialize(&**owned_value), serializer) + serde::Serialize::serialize(&Self(owned_value), serializer) + } + OwnedValue::Struct(items) => { + serializer.collect_map(items.iter().map(|(key, value)| (key, Self(value)))) + } + OwnedValue::Tuple(owned_values) => { + serializer.collect_seq(owned_values.iter().map(Self)) } - OwnedValue::Struct(items) => serializer.collect_map( - items + OwnedValue::List(owned_values) => serializer.collect_seq(owned_values.iter().map(Self)), + OwnedValue::Dict(btree_map) => serializer.collect_map( + btree_map .iter() - .map(|(key, value)| (key, JsonValueSerialize(value))), + .map(|(key, value)| (Self(key), Self(value))), ), - OwnedValue::Tuple(owned_values) => todo!(), - OwnedValue::List(owned_values) => todo!(), - OwnedValue::Dict(btree_map) => todo!(), } } } diff --git a/libs/@local/hashql/ast/src/lowering/node_renumberer.rs b/libs/@local/hashql/ast/src/lowering/node_renumberer.rs index d1f3c1857d0..cda3d37fa62 100644 --- a/libs/@local/hashql/ast/src/lowering/node_renumberer.rs +++ b/libs/@local/hashql/ast/src/lowering/node_renumberer.rs @@ -26,6 +26,7 @@ impl Visitor<'_> for NodeRenumberer { } impl Default for NodeRenumberer { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/ast/src/lowering/sanitizer.rs b/libs/@local/hashql/ast/src/lowering/sanitizer.rs index b394d1eca65..1be747aba94 100644 --- a/libs/@local/hashql/ast/src/lowering/sanitizer.rs +++ b/libs/@local/hashql/ast/src/lowering/sanitizer.rs @@ -204,6 +204,7 @@ impl<'heap> Visitor<'heap> for Sanitizer { } impl Default for Sanitizer { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/core/src/collections/pool.rs b/libs/@local/hashql/core/src/collections/pool.rs index 410fdfc6e16..2b9e730a1f4 100644 --- a/libs/@local/hashql/core/src/collections/pool.rs +++ b/libs/@local/hashql/core/src/collections/pool.rs @@ -344,6 +344,7 @@ where /// let pool = VecPool::::new(15); /// assert_eq!(pool.capacity(), 15); /// ``` + #[inline] pub const fn capacity(&self) -> usize { self.capacity } diff --git a/libs/@local/hashql/core/src/graph/algorithms/dominators/frontier.rs b/libs/@local/hashql/core/src/graph/algorithms/dominators/frontier.rs index c00f04189f8..bf411ca8b46 100644 --- a/libs/@local/hashql/core/src/graph/algorithms/dominators/frontier.rs +++ b/libs/@local/hashql/core/src/graph/algorithms/dominators/frontier.rs @@ -177,6 +177,7 @@ where } /// Returns the underlying [`RowRef`] if the frontier is non-empty. + #[inline] #[must_use] pub const fn as_inner(&self) -> Option> { self.inner diff --git a/libs/@local/hashql/core/src/graph/linked.rs b/libs/@local/hashql/core/src/graph/linked.rs index e26222bc8e1..a5bd8a05afb 100644 --- a/libs/@local/hashql/core/src/graph/linked.rs +++ b/libs/@local/hashql/core/src/graph/linked.rs @@ -138,11 +138,13 @@ impl HasId for Edge { impl Edge { /// Returns the source node of this edge. + #[inline] pub const fn source(&self) -> NodeId { self.source } /// Returns the target node of this edge. + #[inline] pub const fn target(&self) -> NodeId { self.target } diff --git a/libs/@local/hashql/core/src/heap/mod.rs b/libs/@local/hashql/core/src/heap/mod.rs index f1e2cd1116f..525c955fc70 100644 --- a/libs/@local/hashql/core/src/heap/mod.rs +++ b/libs/@local/hashql/core/src/heap/mod.rs @@ -288,6 +288,7 @@ impl Heap { } impl Default for Heap { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/core/src/heap/pool.rs b/libs/@local/hashql/core/src/heap/pool.rs index 358c1136629..c0ffe494b68 100644 --- a/libs/@local/hashql/core/src/heap/pool.rs +++ b/libs/@local/hashql/core/src/heap/pool.rs @@ -70,6 +70,7 @@ impl ScratchPool { } impl Default for ScratchPool { + #[inline] fn default() -> Self { Self::new() } @@ -170,6 +171,7 @@ impl HeapPool { } impl Default for HeapPool { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/core/src/heap/scratch.rs b/libs/@local/hashql/core/src/heap/scratch.rs index ce87a754e75..53f69d2aafd 100644 --- a/libs/@local/hashql/core/src/heap/scratch.rs +++ b/libs/@local/hashql/core/src/heap/scratch.rs @@ -33,16 +33,16 @@ pub struct Scratch { impl Scratch { /// Creates a new scratch allocator. - #[must_use] #[inline] + #[must_use] pub fn new() -> Self { Self { inner: Allocator::new(), } } - #[must_use] #[inline] + #[must_use] pub fn with_capacity(capacity: usize) -> Self { Self { inner: Allocator::with_capacity(capacity), @@ -51,6 +51,7 @@ impl Scratch { } impl Default for Scratch { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/core/src/id/bit_vec/mod.rs b/libs/@local/hashql/core/src/id/bit_vec/mod.rs index bbc332ca59d..030dc3fe579 100644 --- a/libs/@local/hashql/core/src/id/bit_vec/mod.rs +++ b/libs/@local/hashql/core/src/id/bit_vec/mod.rs @@ -108,6 +108,7 @@ pub struct DenseBitSet { impl DenseBitSet { /// Gets the domain size. + #[inline] #[must_use] pub const fn domain_size(&self) -> usize { self.domain_size @@ -581,6 +582,7 @@ enum Chunk { const _: () = assert!(size_of::() == 16); impl ChunkedBitSet { + #[inline] #[must_use] pub const fn domain_size(&self) -> usize { self.domain_size diff --git a/libs/@local/hashql/core/src/module/namespace.rs b/libs/@local/hashql/core/src/module/namespace.rs index f2570859aad..607c489b555 100644 --- a/libs/@local/hashql/core/src/module/namespace.rs +++ b/libs/@local/hashql/core/src/module/namespace.rs @@ -60,6 +60,7 @@ impl<'env, 'heap> ModuleNamespace<'env, 'heap> { } } + #[inline] #[must_use] pub const fn registry(&self) -> &'env ModuleRegistry<'heap> { self.registry diff --git a/libs/@local/hashql/core/src/pretty/formatter.rs b/libs/@local/hashql/core/src/pretty/formatter.rs index dfaf182b4e5..621658edb3a 100644 --- a/libs/@local/hashql/core/src/pretty/formatter.rs +++ b/libs/@local/hashql/core/src/pretty/formatter.rs @@ -35,6 +35,7 @@ impl FormatterOptions { } impl Default for FormatterOptions { + #[inline] fn default() -> Self { Self { indent: 4 } } diff --git a/libs/@local/hashql/core/src/type/inference/solver/graph.rs b/libs/@local/hashql/core/src/type/inference/solver/graph.rs index 35fd34131fb..1038c34b32a 100644 --- a/libs/@local/hashql/core/src/type/inference/solver/graph.rs +++ b/libs/@local/hashql/core/src/type/inference/solver/graph.rs @@ -113,6 +113,7 @@ impl Edges { } impl Default for Edges { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/core/src/type/inference/solver/mod.rs b/libs/@local/hashql/core/src/type/inference/solver/mod.rs index 7dfab9901a6..89ce41e14a3 100644 --- a/libs/@local/hashql/core/src/type/inference/solver/mod.rs +++ b/libs/@local/hashql/core/src/type/inference/solver/mod.rs @@ -220,6 +220,7 @@ struct VariableConstraintSatisfiability { } impl Default for VariableConstraintSatisfiability { + #[inline] fn default() -> Self { Self { upper: true } } diff --git a/libs/@local/hashql/core/src/type/recursion.rs b/libs/@local/hashql/core/src/type/recursion.rs index 79b6116914b..ae095f82b9a 100644 --- a/libs/@local/hashql/core/src/type/recursion.rs +++ b/libs/@local/hashql/core/src/type/recursion.rs @@ -58,6 +58,7 @@ pub struct RecursionBoundary<'heap, A: Allocator = Global> { } impl Default for RecursionBoundary<'_> { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/core/src/type/visit.rs b/libs/@local/hashql/core/src/type/visit.rs index 228cf2c42f0..773b553fc82 100644 --- a/libs/@local/hashql/core/src/type/visit.rs +++ b/libs/@local/hashql/core/src/type/visit.rs @@ -709,6 +709,7 @@ impl<'heap, A: Allocator> RecursiveVisitorGuard<'heap, A> { } impl Default for RecursiveVisitorGuard<'_> { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/diagnostics/src/issues.rs b/libs/@local/hashql/diagnostics/src/issues.rs index 065f8ded9c4..42c2e68d86f 100644 --- a/libs/@local/hashql/diagnostics/src/issues.rs +++ b/libs/@local/hashql/diagnostics/src/issues.rs @@ -231,6 +231,7 @@ impl DiagnosticIssues { /// ); /// assert_eq!(issues.critical(), 1); // Warnings are not critical /// ``` + #[inline] #[must_use] pub const fn critical(&self) -> usize { self.critical diff --git a/libs/@local/hashql/diagnostics/src/source/span.rs b/libs/@local/hashql/diagnostics/src/source/span.rs index 02f1a0b955d..b3a5a4b7870 100644 --- a/libs/@local/hashql/diagnostics/src/source/span.rs +++ b/libs/@local/hashql/diagnostics/src/source/span.rs @@ -166,12 +166,14 @@ impl SourceSpan { } /// Returns the source file identifier for this span. + #[inline] #[must_use] pub const fn source(&self) -> SourceId { self.source } /// Returns the text range within the source file. + #[inline] #[must_use] pub const fn range(&self) -> TextRange { self.range diff --git a/libs/@local/hashql/eval/src/orchestrator/events.rs b/libs/@local/hashql/eval/src/orchestrator/events.rs index e2c6b4cd3b6..dd9e3ce337c 100644 --- a/libs/@local/hashql/eval/src/orchestrator/events.rs +++ b/libs/@local/hashql/eval/src/orchestrator/events.rs @@ -146,6 +146,7 @@ impl AppendEventLog { } impl Default for AppendEventLog { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/eval/src/postgres/parameters.rs b/libs/@local/hashql/eval/src/postgres/parameters.rs index 411a93544d9..e929f18fb9e 100644 --- a/libs/@local/hashql/eval/src/postgres/parameters.rs +++ b/libs/@local/hashql/eval/src/postgres/parameters.rs @@ -36,6 +36,7 @@ impl Display for ParameterIndex { } impl From for Expression { + #[inline] fn from(value: ParameterIndex) -> Self { Self::Parameter(value.as_usize() + 1) } diff --git a/libs/@local/hashql/eval/src/postgres/projections.rs b/libs/@local/hashql/eval/src/postgres/projections.rs index 59dbdafd577..f40f6752dcc 100644 --- a/libs/@local/hashql/eval/src/postgres/projections.rs +++ b/libs/@local/hashql/eval/src/postgres/projections.rs @@ -20,6 +20,7 @@ enum ComputedColumn { } impl From for ColumnName<'_> { + #[inline] fn from(value: ComputedColumn) -> Self { match value { ComputedColumn::EntityTypeIds => ColumnName::from(Identifier::from("entity_type_ids")), diff --git a/libs/@local/hashql/hir/src/context.rs b/libs/@local/hashql/hir/src/context.rs index bfeca6b0aaf..8015b7a6cca 100644 --- a/libs/@local/hashql/hir/src/context.rs +++ b/libs/@local/hashql/hir/src/context.rs @@ -23,6 +23,7 @@ impl SymbolRegistry<'_> { } impl Default for SymbolRegistry<'_> { + #[inline] fn default() -> Self { Self::new() } @@ -44,6 +45,7 @@ impl Counter { } impl Default for Counter { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/hir/src/lower/normalization.rs b/libs/@local/hashql/hir/src/lower/normalization.rs index 2d64b2148fe..23572291a2a 100644 --- a/libs/@local/hashql/hir/src/lower/normalization.rs +++ b/libs/@local/hashql/hir/src/lower/normalization.rs @@ -210,6 +210,7 @@ pub struct NormalizationState<'heap> { } impl Default for NormalizationState<'_> { + #[inline] fn default() -> Self { Self { recycler: VecPool::new(4), diff --git a/libs/@local/hashql/hir/src/map.rs b/libs/@local/hashql/hir/src/map.rs index 9e36cf11238..e7e911e3b1d 100644 --- a/libs/@local/hashql/hir/src/map.rs +++ b/libs/@local/hashql/hir/src/map.rs @@ -169,6 +169,7 @@ impl<'heap> HirMap<'heap> { } impl Default for HirMap<'_> { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/body/operand.rs b/libs/@local/hashql/mir/src/body/operand.rs index 4190f286b7d..4dc7f6dc794 100644 --- a/libs/@local/hashql/mir/src/body/operand.rs +++ b/libs/@local/hashql/mir/src/body/operand.rs @@ -60,12 +60,14 @@ impl<'heap> Operand<'heap> { } impl From for Operand<'_> { + #[inline] fn from(value: !) -> Self { value } } impl From for Operand<'_> { + #[inline] fn from(local: Local) -> Self { Operand::Place(Place::local(local)) } diff --git a/libs/@local/hashql/mir/src/interpret/inputs.rs b/libs/@local/hashql/mir/src/interpret/inputs.rs index 134567cee8c..5b8d282831e 100644 --- a/libs/@local/hashql/mir/src/interpret/inputs.rs +++ b/libs/@local/hashql/mir/src/interpret/inputs.rs @@ -94,6 +94,7 @@ impl Inputs<'_> { } impl Default for Inputs<'_> { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/interpret/runtime.rs b/libs/@local/hashql/mir/src/interpret/runtime.rs index 8d827100982..5238308e9a5 100644 --- a/libs/@local/hashql/mir/src/interpret/runtime.rs +++ b/libs/@local/hashql/mir/src/interpret/runtime.rs @@ -294,6 +294,7 @@ pub struct RuntimeConfig { } impl Default for RuntimeConfig { + #[inline] fn default() -> Self { Self { recursion_limit: 1024, diff --git a/libs/@local/hashql/mir/src/interpret/suspension/temporal.rs b/libs/@local/hashql/mir/src/interpret/suspension/temporal.rs index 5a9f27e6659..55875f4dd34 100644 --- a/libs/@local/hashql/mir/src/interpret/suspension/temporal.rs +++ b/libs/@local/hashql/mir/src/interpret/suspension/temporal.rs @@ -17,12 +17,14 @@ use crate::interpret::value::Int; pub struct Timestamp(Int); impl From for Timestamp { + #[inline] fn from(value: Int) -> Self { Self(value) } } impl From for Int { + #[inline] fn from(value: Timestamp) -> Self { value.0 } diff --git a/libs/@local/hashql/mir/src/interpret/tests.rs b/libs/@local/hashql/mir/src/interpret/tests.rs index a14cb7d88c1..ce4f8934ba7 100644 --- a/libs/@local/hashql/mir/src/interpret/tests.rs +++ b/libs/@local/hashql/mir/src/interpret/tests.rs @@ -1735,6 +1735,7 @@ fn ice_struct_field_length_mismatch() { /// /// where `pinned` = `Opaque(TransactionTime, Opaque(Timestamp, Integer(pinned_ms)))` and /// `variable` wraps an interval with inclusive start and unbounded end. +#[expect(unsafe_code)] fn make_temporal_axes<'heap>( interner: &Interner<'heap>, pinned_ms: i128, @@ -1770,7 +1771,9 @@ fn make_temporal_axes<'heap>( // Interval(Struct { start, end }) let interval_fields = interner.symbols.intern_slice(&[sym::end, sym::start]); - let interval_struct = Struct::new_unchecked(interval_fields, Rc::new([end_bound, start_bound])); + // SAFETY: p is before v in the alphabetical order + let interval_struct = + unsafe { Struct::new_unchecked(interval_fields, Rc::new([end_bound, start_bound])) }; let interval = Value::Opaque(Opaque::new( sym::path::Interval, Rc::new(Value::Struct(interval_struct)), @@ -1781,7 +1784,8 @@ fn make_temporal_axes<'heap>( // PinnedTransactionTimeTemporalAxes(Struct { pinned, variable }) let axes_fields = interner.symbols.intern_slice(&[sym::pinned, sym::variable]); - let axes_struct = Struct::new_unchecked(axes_fields, Rc::new([pinned, variable])); + // SAFETY: p is before v in the alphabetical order + let axes_struct = unsafe { Struct::new_unchecked(axes_fields, Rc::new([pinned, variable])) }; Value::Opaque(Opaque::new( sym::path::PinnedTransactionTimeTemporalAxes, diff --git a/libs/@local/hashql/mir/src/pass/analysis/data_dependency/mod.rs b/libs/@local/hashql/mir/src/pass/analysis/data_dependency/mod.rs index 99c9d311537..eb98209a6d4 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/data_dependency/mod.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/data_dependency/mod.rs @@ -104,6 +104,7 @@ impl DataDependencyAnalysis<'_> { } impl Default for DataDependencyAnalysis<'_> { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/execution/cost/mod.rs b/libs/@local/hashql/mir/src/pass/execution/cost/mod.rs index 470f53f4259..100bf4f2bc2 100644 --- a/libs/@local/hashql/mir/src/pass/execution/cost/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/cost/mod.rs @@ -252,6 +252,7 @@ impl From for ApproxCost { } impl From for ApproxCost { + #[inline] fn from(value: InformationUnit) -> Self { #[expect(clippy::cast_precision_loss)] Self(value.as_u32() as f32) diff --git a/libs/@local/hashql/mir/src/pass/execution/island/mod.rs b/libs/@local/hashql/mir/src/pass/execution/island/mod.rs index fb26e47b8c2..9d9f0446afc 100644 --- a/libs/@local/hashql/mir/src/pass/execution/island/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/island/mod.rs @@ -84,6 +84,7 @@ impl Island { self.members.is_empty() } + #[inline] #[must_use] pub const fn traversals(&self) -> TraversalPathBitSet { self.traversals @@ -114,6 +115,7 @@ impl IslandPlacement { } impl Default for IslandPlacement { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/execution/splitting/mod.rs b/libs/@local/hashql/mir/src/pass/execution/splitting/mod.rs index c3fa52702a9..0cc69483b44 100644 --- a/libs/@local/hashql/mir/src/pass/execution/splitting/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/splitting/mod.rs @@ -401,6 +401,7 @@ impl BasicBlockSplitting { } impl Default for BasicBlockSplitting { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/execution/terminator_placement/mod.rs b/libs/@local/hashql/mir/src/pass/execution/terminator_placement/mod.rs index 87579b11c39..934ff483265 100644 --- a/libs/@local/hashql/mir/src/pass/execution/terminator_placement/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/terminator_placement/mod.rs @@ -201,6 +201,7 @@ impl TransMatrix { } impl Default for TransMatrix { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/execution/traversal/mod.rs b/libs/@local/hashql/mir/src/pass/execution/traversal/mod.rs index 37b5f21404f..76cd0be7d1d 100644 --- a/libs/@local/hashql/mir/src/pass/execution/traversal/mod.rs +++ b/libs/@local/hashql/mir/src/pass/execution/traversal/mod.rs @@ -309,6 +309,7 @@ impl TraversalPathBitMap { } impl From for TraversalPathBitMap { + #[inline] fn from(value: TraversalPathBitSet) -> Self { let mut this = TraversalMapLattice.bottom(); this[value.vertex()] = value; diff --git a/libs/@local/hashql/mir/src/pass/mod.rs b/libs/@local/hashql/mir/src/pass/mod.rs index 09806905b96..c4f8f181f69 100644 --- a/libs/@local/hashql/mir/src/pass/mod.rs +++ b/libs/@local/hashql/mir/src/pass/mod.rs @@ -170,6 +170,7 @@ impl BitOrAssign for Changed { } impl From for Changed { + #[inline] fn from(value: bool) -> Self { if value { Self::Yes } else { Self::No } } diff --git a/libs/@local/hashql/mir/src/pass/transform/canonicalization.rs b/libs/@local/hashql/mir/src/pass/transform/canonicalization.rs index 73adc9f2535..e96a9211bd5 100644 --- a/libs/@local/hashql/mir/src/pass/transform/canonicalization.rs +++ b/libs/@local/hashql/mir/src/pass/transform/canonicalization.rs @@ -26,6 +26,7 @@ pub struct CanonicalizationConfig { } impl Default for CanonicalizationConfig { + #[inline] fn default() -> Self { Self { max_iterations: 16 } } diff --git a/libs/@local/hashql/mir/src/pass/transform/cfg_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/cfg_simplify/mod.rs index a7dd78ff96e..3cce324e4d1 100644 --- a/libs/@local/hashql/mir/src/pass/transform/cfg_simplify/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/cfg_simplify/mod.rs @@ -483,6 +483,7 @@ impl CfgSimplify { } impl Default for CfgSimplify { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/transform/copy_propagation/mod.rs b/libs/@local/hashql/mir/src/pass/transform/copy_propagation/mod.rs index 55e866ff43f..72c793a2adf 100644 --- a/libs/@local/hashql/mir/src/pass/transform/copy_propagation/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/copy_propagation/mod.rs @@ -213,6 +213,7 @@ impl CopyPropagation { } impl Default for CopyPropagation { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/transform/dse/mod.rs b/libs/@local/hashql/mir/src/pass/transform/dse/mod.rs index d14e7d3ad5d..20decb75a15 100644 --- a/libs/@local/hashql/mir/src/pass/transform/dse/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/dse/mod.rs @@ -132,6 +132,7 @@ impl DeadStoreElimination { } impl Default for DeadStoreElimination { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/transform/forward_substitution.rs b/libs/@local/hashql/mir/src/pass/transform/forward_substitution.rs index 145b0c6c70e..ef4a2c3c68b 100644 --- a/libs/@local/hashql/mir/src/pass/transform/forward_substitution.rs +++ b/libs/@local/hashql/mir/src/pass/transform/forward_substitution.rs @@ -157,6 +157,7 @@ impl<'env, 'heap, A: Allocator> TransformPass<'env, 'heap> for ForwardSubstituti } impl Default for ForwardSubstitution { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs index 34ee56a1333..18e1f148ed6 100644 --- a/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/inst_simplify/mod.rs @@ -158,6 +158,7 @@ impl InstSimplify { } impl Default for InstSimplify { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/pass/transform/ssa_repair/mod.rs b/libs/@local/hashql/mir/src/pass/transform/ssa_repair/mod.rs index 46eabd3cbee..49ed23dd800 100644 --- a/libs/@local/hashql/mir/src/pass/transform/ssa_repair/mod.rs +++ b/libs/@local/hashql/mir/src/pass/transform/ssa_repair/mod.rs @@ -156,6 +156,7 @@ impl SsaRepair { } impl Default for SsaRepair { + #[inline] fn default() -> Self { Self::new() } diff --git a/libs/@local/hashql/mir/src/reify/current.rs b/libs/@local/hashql/mir/src/reify/current.rs index f033cc15721..4ea1fdd1870 100644 --- a/libs/@local/hashql/mir/src/reify/current.rs +++ b/libs/@local/hashql/mir/src/reify/current.rs @@ -48,6 +48,7 @@ impl ForwardRef { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) struct EntryBlock(pub BasicBlockId); impl From for BasicBlockId { + #[inline] fn from(entry: EntryBlock) -> Self { entry.0 } @@ -56,6 +57,7 @@ impl From for BasicBlockId { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) struct ExitBlock(pub BasicBlockId); impl From for BasicBlockId { + #[inline] fn from(exit: ExitBlock) -> Self { exit.0 } diff --git a/libs/@local/hashql/syntax-jexpr/src/lexer/syntax_kind_set.rs b/libs/@local/hashql/syntax-jexpr/src/lexer/syntax_kind_set.rs index 405991a2acd..d0a313a2915 100644 --- a/libs/@local/hashql/syntax-jexpr/src/lexer/syntax_kind_set.rs +++ b/libs/@local/hashql/syntax-jexpr/src/lexer/syntax_kind_set.rs @@ -186,6 +186,7 @@ impl FromIterator for SyntaxKindSet { } impl From for SyntaxKindSet { + #[inline] fn from(kind: SyntaxKind) -> Self { Self::from_kind(kind) } @@ -225,6 +226,7 @@ impl BitXor for SyntaxKindSet { } impl Default for SyntaxKindSet { + #[inline] fn default() -> Self { Self::EMPTY } diff --git a/libs/@local/hashql/syntax-jexpr/src/parser/array/error.rs b/libs/@local/hashql/syntax-jexpr/src/parser/array/error.rs index 275c5a19a39..e587f1587d1 100644 --- a/libs/@local/hashql/syntax-jexpr/src/parser/array/error.rs +++ b/libs/@local/hashql/syntax-jexpr/src/parser/array/error.rs @@ -118,6 +118,7 @@ impl DiagnosticCategory for ArrayDiagnosticCategory { } impl From for ArrayDiagnosticCategory { + #[inline] fn from(value: LexerDiagnosticCategory) -> Self { Self::Lexer(value) } diff --git a/libs/@local/hashql/syntax-jexpr/src/parser/error.rs b/libs/@local/hashql/syntax-jexpr/src/parser/error.rs index f3ead4f6158..977576d7e02 100644 --- a/libs/@local/hashql/syntax-jexpr/src/parser/error.rs +++ b/libs/@local/hashql/syntax-jexpr/src/parser/error.rs @@ -76,24 +76,28 @@ impl DiagnosticCategory for ParserDiagnosticCategory { } impl From for ParserDiagnosticCategory { + #[inline] fn from(value: LexerDiagnosticCategory) -> Self { Self::Lexer(value) } } impl From for ParserDiagnosticCategory { + #[inline] fn from(value: StringDiagnosticCategory) -> Self { Self::String(value) } } impl From for ParserDiagnosticCategory { + #[inline] fn from(value: ArrayDiagnosticCategory) -> Self { Self::Array(value) } } impl From for ParserDiagnosticCategory { + #[inline] fn from(value: ObjectDiagnosticCategory) -> Self { Self::Object(value) } diff --git a/libs/@local/hashql/syntax-jexpr/src/parser/object/error.rs b/libs/@local/hashql/syntax-jexpr/src/parser/object/error.rs index 60b6e7f3ad3..49dbfb2aa8b 100644 --- a/libs/@local/hashql/syntax-jexpr/src/parser/object/error.rs +++ b/libs/@local/hashql/syntax-jexpr/src/parser/object/error.rs @@ -230,6 +230,7 @@ impl DiagnosticCategory for ObjectDiagnosticCategory { } impl From for ObjectDiagnosticCategory { + #[inline] fn from(value: LexerDiagnosticCategory) -> Self { Self::Lexer(value) } diff --git a/libs/@local/hashql/syntax-jexpr/src/parser/state.rs b/libs/@local/hashql/syntax-jexpr/src/parser/state.rs index cd917bd0886..2f1b0c410e6 100644 --- a/libs/@local/hashql/syntax-jexpr/src/parser/state.rs +++ b/libs/@local/hashql/syntax-jexpr/src/parser/state.rs @@ -182,12 +182,14 @@ impl Expected { } impl From for Expected { + #[inline] fn from(value: SyntaxKindSet) -> Self { Self::Validate(value) } } impl From for Expected { + #[inline] fn from(value: SyntaxKind) -> Self { Self::Validate(SyntaxKindSet::from(value)) } From d55a4f092ead77e8c3f7bbc0da4e1a2c8ab133ec Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:22:28 +0200 Subject: [PATCH 13/25] feat: checkpoint --- .../@local/graph/api/src/rest/entity/query.rs | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/libs/@local/graph/api/src/rest/entity/query.rs b/libs/@local/graph/api/src/rest/entity/query.rs index 569680840ad..bcd1afd037c 100644 --- a/libs/@local/graph/api/src/rest/entity/query.rs +++ b/libs/@local/graph/api/src/rest/entity/query.rs @@ -1,25 +1,20 @@ -use alloc::borrow::Cow; -use core::{mem, str::FromStr}; -use std::{ - alloc::{Allocator, Global}, - collections::BTreeMap, - sync::Arc, -}; +use alloc::{borrow::Cow, collections::BTreeMap, sync::Arc}; +use core::alloc::Allocator; -use hash_graph_postgres_store::store::{PostgresStore, PostgresStorePool}; +use hash_graph_postgres_store::store::PostgresStorePool; use hash_graph_store::pool::StorePool; use hash_temporal_client::TemporalClient; use hashql_ast::error::AstDiagnosticCategory; use hashql_core::{ - heap::{CloneIn, Heap, HeapPool, ResetAllocator, Scratch, ScratchPool}, - id::Id, + heap::{Heap, HeapPool, ResetAllocator as _, Scratch, ScratchPool}, + id::Id as _, module::ModuleRegistry, span::{SpanId, SpanTable}, r#type::environment::Environment, }; use hashql_diagnostics::{ - Diagnostic, DiagnosticCategory, DiagnosticIssues, Failure, IntoStatus, Source, Sources, Status, - StatusExt as _, Success, category::canonical_category_id, severity::Critical, source::SourceId, + Diagnostic, DiagnosticCategory, DiagnosticIssues, Failure, IntoStatus as _, Source, Sources, + Status, StatusExt as _, Success, category::canonical_category_id, severity::Critical, }; use hashql_eval::{ context::{CodeExecutionContext, CodeGenerationContext}, @@ -33,15 +28,14 @@ use hashql_mir::{ def::{DefId, DefIdVec}, error::MirDiagnosticCategory, interpret::{ - CallStack, Inputs, - value::{Int, Num, Ptr, Str, Value}, + Inputs, + value::{Int, Num, Ptr, Value}, }, pass::{LowerConfig, execution::ExecutionAnalysisResidual}, }; use hashql_syntax_jexpr::{error::JExprDiagnosticCategory, span::Span}; use serde::Serialize as _; use serde_json::value::RawValue; -use tokio::{runtime::Runtime, task::LocalSet}; use tokio_util::task::LocalPoolHandle; struct CompilerContext { @@ -118,10 +112,12 @@ fn serialize_int(int: &Int, serializer: S) -> Result(num: &Num, serializer: S) -> Result { num.as_f64().serialize(serializer) } +#[expect(clippy::trivially_copy_pass_by_ref)] fn serialize_ptr(ptr: &Ptr, serializer: S) -> Result { ptr.def().as_u32().serialize(serializer) } From 07970548d3173f8ea333137ad33023b104b105e5 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:53:43 +0200 Subject: [PATCH 14/25] feat: checkpoint --- .../@local/graph/api/src/rest/entity/query.rs | 219 +++++++++++++----- libs/@local/hashql/core/src/span/mod.rs | 15 +- libs/@local/hashql/core/src/span/table.rs | 86 +++---- libs/@local/hashql/diagnostics/src/issues.rs | 78 +++++++ libs/@local/hashql/diagnostics/src/status.rs | 69 +++++- 5 files changed, 354 insertions(+), 113 deletions(-) diff --git a/libs/@local/graph/api/src/rest/entity/query.rs b/libs/@local/graph/api/src/rest/entity/query.rs index bcd1afd037c..9529d8e76c9 100644 --- a/libs/@local/graph/api/src/rest/entity/query.rs +++ b/libs/@local/graph/api/src/rest/entity/query.rs @@ -1,6 +1,7 @@ use alloc::{borrow::Cow, collections::BTreeMap, sync::Arc}; -use core::alloc::Allocator; +use core::{alloc::Allocator, ops::Range}; +use axum::response::{Html, IntoResponse}; use hash_graph_postgres_store::store::PostgresStorePool; use hash_graph_store::pool::StorePool; use hash_temporal_client::TemporalClient; @@ -13,8 +14,11 @@ use hashql_core::{ r#type::environment::Environment, }; use hashql_diagnostics::{ - Diagnostic, DiagnosticCategory, DiagnosticIssues, Failure, IntoStatus as _, Source, Sources, - Status, StatusExt as _, Success, category::canonical_category_id, severity::Critical, + Diagnostic, DiagnosticCategory, DiagnosticIssues, Failure, IntoStatus as _, Label, Message, + Source, Sources, Status, StatusExt as _, Success, + category::{TerminalDiagnosticCategory, canonical_category_id}, + diagnostic::render::{Format, RenderOptions}, + severity::Critical, }; use hashql_eval::{ context::{CodeExecutionContext, CodeGenerationContext}, @@ -34,21 +38,34 @@ use hashql_mir::{ pass::{LowerConfig, execution::ExecutionAnalysisResidual}, }; use hashql_syntax_jexpr::{error::JExprDiagnosticCategory, span::Span}; +use http::StatusCode; use serde::Serialize as _; use serde_json::value::RawValue; use tokio_util::task::LocalPoolHandle; +use crate::rest::{json::Json, status::BoxedResponse}; + struct CompilerContext { scratches: ScratchPool, heaps: HeapPool, pool: LocalPoolHandle, } -struct ExecutionContext<'ctx> { - postgres: &'ctx PostgresStorePool, +struct ExecutionContext { + postgres: PostgresStorePool, temporal: Option>, } +struct CompilationOptions { + interactive: bool, + json_compat: bool, +} + +const INFRASTRUCTURE_CATEGORY: TerminalDiagnosticCategory = TerminalDiagnosticCategory { + id: "infrastructure", + name: "Infrastructure", +}; + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] enum HashQlDiagnosticCategory { JExpr(JExprDiagnosticCategory), @@ -56,6 +73,7 @@ enum HashQlDiagnosticCategory { Hir(HirDiagnosticCategory), Mir(MirDiagnosticCategory), Eval(EvalDiagnosticCategory), + Infrastructure, } impl serde::Serialize for HashQlDiagnosticCategory { @@ -83,27 +101,7 @@ impl DiagnosticCategory for HashQlDiagnosticCategory { Self::Hir(hir) => Some(hir), Self::Mir(mir) => Some(mir), Self::Eval(eval) => Some(eval), - } - } -} - -fn process_status( - diagnostics: &mut DiagnosticIssues, - status: Status, -) -> Result>> -where - C: DiagnosticCategory + 'static, -{ - match status { - Ok(Success { value, advisories }) => { - diagnostics.append(&mut advisories.generalize()); - - Ok(value) - } - Err(Failure { primary, secondary }) => { - diagnostics.append(&mut secondary.generalize()); - - Err(primary) + Self::Infrastructure => Some(&INFRASTRUCTURE_CATEGORY), } } } @@ -226,11 +224,92 @@ impl serde::Serialize for JsonValueSerialize<'_> { } } +#[derive(Debug, serde::Serialize)] +struct PointerSpan { + pub range: Range, + pub pointer: Option, +} + +impl PointerSpan { + fn resolve(id: SpanId, spans: &SpanTable) -> Option { + let absolute = spans.absolute(id)?; + + let mut pointer = None; + + for ancestor in spans.ancestors(id) { + let Some(span) = spans.get(ancestor) else { + continue; + }; + + if let Some(span_pointer) = &span.pointer { + pointer = Some(span_pointer.as_str().to_owned()); + break; + } + } + + Some(Self { + range: absolute.range().into(), + pointer, + }) + } +} + +fn status_to_response( + status: Status, + sources: &Sources<'_>, + mut spans: &SpanTable, + options: &CompilationOptions, +) -> BoxedResponse { + match status { + Ok(Success { value, advisories }) => { + let advisories = advisories.map_spans(|span| PointerSpan::resolve(span, spans)); + + if options.json_compat { + Json(Success { + value: JsonValueSerialize(&value), + advisories, + }) + .into_response() + .into() + } else { + Json(value).into_response().into() + } + } + Err(Failure { primary, secondary }) => { + let severity = primary.severity; + let status_code = if severity == Critical::ERROR { + StatusCode::BAD_REQUEST + } else { + StatusCode::INTERNAL_SERVER_ERROR + }; + + let mut response = if options.interactive { + let mut diagnostics = secondary.generalize(); + diagnostics.insert_front(primary.generalize()); + + let output = + diagnostics.render(RenderOptions::new(Format::Html, sources), &mut spans); + Html(output).into_response() + } else { + Json(Failure { + primary: Box::new(primary.map_spans(|span| PointerSpan::resolve(span, spans))), + secondary: secondary.map_spans(|span| PointerSpan::resolve(span, spans)), + }) + .into_response() + }; + + *response.status_mut() = status_code; + response.into() + } + } +} + #[expect(clippy::future_not_send)] -async fn query_local( +async fn query_local_impl( ctx: Arc, - client: ::Store<'static>, - query: Arc, + exec: ExecutionContext, + spans: &mut SpanTable, + query: &[u8], ) -> Status { // We're hitting a borrow checker limitation here, we cannot prove that scratch and heap are // still alive by the time that we `spawn_local`, and due to the fact that it requires `'static` @@ -244,12 +323,30 @@ async fn query_local( let Success { value: compilation, advisories, - } = tokio::task::block_in_place(|| Compilation::compile(&heap, &mut scratch, query.get()))?; + } = tokio::task::block_in_place(|| Compilation::compile(&heap, &mut scratch, spans, query))?; let context = compilation.context(); - let orchestrator = Orchestrator::new(client, &compilation.artifact.postgres, &context); + let Success { + value: client, + advisories, + } = exec + .postgres + .acquire(exec.temporal) + .await + .map_err(|report| { + let mut diagnostic = + Diagnostic::new(HashQlDiagnosticCategory::Infrastructure, Critical::BUG).primary( + Label::new(compilation.root_span, "failed to acquire postgres client"), + ); + + diagnostic.add_message(Message::note(format!("{report:?}"))); + diagnostic + }) + .into_status() + .with_diagnostics(advisories)?; + let orchestrator = Orchestrator::new(client, &compilation.artifact.postgres, &context); orchestrator .run(&inputs, compilation.entrypoint, []) .await @@ -261,17 +358,38 @@ async fn query_local( .map_value(OwnedValue::from) } -async fn query( +#[expect(clippy::future_not_send)] +async fn query_local( ctx: Arc, - exec: ExecutionContext<'_>, + exec: ExecutionContext, query: Arc, -) -> Status<(), HashQlDiagnosticCategory, SpanId> { - let client = exec.postgres.acquire(exec.temporal).await.unwrap(); + options: CompilationOptions, +) -> BoxedResponse { + let mut sources = Sources::new(); + let source_id = sources.push(Source::new(query.get())); - let pool = ctx.pool.clone(); - let result = pool.spawn_pinned(|| query_local(ctx, client, query)).await; + let mut spans = SpanTable::new(source_id); - todo!() + let status = query_local_impl(ctx, exec, &mut spans, query.get().as_bytes()).await; + status_to_response(status, &sources, &spans, &options) +} + +async fn query( + ctx: Arc, + exec: ExecutionContext, + query: Arc, + options: CompilationOptions, +) -> BoxedResponse { + let pool = ctx.pool.clone(); + let result = pool + .spawn_pinned(|| query_local(ctx, exec, query, options)) + .await; + + result.unwrap_or_else(|_| { + Json(serde_json::json!({"fatal": "internal error: query execution failed"})) + .into_response() + .into() + }) } struct CodeCompilationArtifact<'heap> { @@ -281,11 +399,10 @@ struct CodeCompilationArtifact<'heap> { postgres: PreparedQueries<'heap, &'heap Heap>, } -struct Compilation<'heap, 'query> { +struct Compilation<'heap> { heap: &'heap Heap, - sources: Sources<'query>, - spans: SpanTable, + root_span: SpanId, interner: hashql_eval::intern::Interner<'heap>, env: Environment<'heap>, @@ -294,20 +411,16 @@ struct Compilation<'heap, 'query> { artifact: CodeCompilationArtifact<'heap>, } -impl<'heap, 'query> Compilation<'heap, 'query> { +impl<'heap> Compilation<'heap> { pub fn compile( heap: &'heap Heap, scratch: &mut Scratch, - query: &'query str, + spans: &mut SpanTable, + query: &[u8], ) -> Status { - let mut sources = Sources::new(); - let source_id = sources.push(Source::new(query)); - - let mut spans = SpanTable::new(source_id); - // Parse the query - let mut parser = hashql_syntax_jexpr::Parser::new(heap, &mut spans); - let mut ast = parser.parse_expr(query.as_bytes()).map_err(|diagnostic| { + let mut parser = hashql_syntax_jexpr::Parser::new(heap, spans); + let mut ast = parser.parse_expr(query).map_err(|diagnostic| { Failure::new( diagnostic .map_category(HashQlDiagnosticCategory::JExpr) @@ -323,6 +436,8 @@ impl<'heap, 'query> Compilation<'heap, 'query> { ) })?; + let root_span = ast.span; + let mut env = Environment::new(heap); let modules = ModuleRegistry::new(&env); @@ -427,8 +542,8 @@ impl<'heap, 'query> Compilation<'heap, 'query> { Status::success(Self { heap, - sources, - spans, + + root_span, env, interner, entrypoint, diff --git a/libs/@local/hashql/core/src/span/mod.rs b/libs/@local/hashql/core/src/span/mod.rs index 875140f8d5e..9d3da1b043f 100644 --- a/libs/@local/hashql/core/src/span/mod.rs +++ b/libs/@local/hashql/core/src/span/mod.rs @@ -275,6 +275,10 @@ pub use self::table::SpanTable; /// Some(TextRange::new(0.into(), 10.into())) /// ); /// ``` +#[expect( + clippy::unsafe_derive_deserialize, + reason = "id() is safe to use with serde" +)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SpanId(u32); @@ -311,8 +315,10 @@ impl SpanId { SourceId::new_unchecked(self.0 >> Self::SOURCE_OFFSET) } - pub(crate) const fn id(self) -> u32 { - self.0 & Self::ID_MASK + #[expect(unsafe_code)] + pub(crate) const fn id(self) -> LocalSpanId { + // SAFETY: The mask ensures that the value does not exceed the maximum span index. + unsafe { LocalSpanId::new_unchecked(self.0 & Self::ID_MASK) } } } @@ -322,6 +328,11 @@ impl Display for SpanId { } } +crate::id::newtype! { + #[id(crate = crate)] + pub(crate) struct LocalSpanId(u32 is 0..=SpanId::MAX_ID) +} + /// Determines how multiple ancestor spans should be combined during span resolution. /// /// When a span has multiple ancestors (parent spans), [`SpanResolutionMode`] controls diff --git a/libs/@local/hashql/core/src/span/table.rs b/libs/@local/hashql/core/src/span/table.rs index 100e53e3678..554f790ff78 100644 --- a/libs/@local/hashql/core/src/span/table.rs +++ b/libs/@local/hashql/core/src/span/table.rs @@ -4,6 +4,7 @@ use hashql_diagnostics::source::{SourceId, SourceSpan}; use text_size::{TextRange, TextSize}; use super::{Span, SpanAncestors, SpanAncestorsMut, SpanId, SpanResolutionMode}; +use crate::id::{Id as _, bit_vec::DenseBitSet}; #[derive(Debug)] struct SpanEntry { @@ -244,7 +245,7 @@ impl SpanTable { return false; } - let index = span.id() as usize; + let index = span.id().as_usize(); let Some(element) = self.spans.get_mut(index) else { return false; @@ -265,8 +266,7 @@ impl SpanTable { return None; } - let index = span.id() as usize; - + let index = span.id().as_usize(); self.spans.get(index) } @@ -487,34 +487,14 @@ impl SpanTable { self.absolute_impl(span, 0) } - /// Computes all ancestor spans for a given span in linearized order. - /// - /// Returns a flattened list of all spans that are ancestors of the given span, - /// traversing the entire ancestor tree and removing duplicates. The result - /// includes both direct ancestors and ancestors-of-ancestors. - /// - /// # Returns - /// - /// A [`Vec`] containing all unique ancestor spans. The order is - /// determined by the traversal algorithm and may not be hierarchical. - /// - /// # Algorithm + /// Returns an iterator over the given span and all of its ancestors. /// - /// Uses a depth-first search with a stack to traverse the ancestor tree: - /// 1. Start with the given span - /// 2. For each span, add its direct ancestors to the result and stack - /// 3. Continue until all ancestors are processed - /// 4. Duplicates are automatically filtered during traversal - /// - /// # Performance - /// - /// Time complexity is O(n) where n is the total number of unique ancestors. - /// Space complexity is also O(n) for the result vector and traversal stack. + /// The first element yielded is `span` itself, followed by its transitive + /// ancestors. Each [`SpanId`] appears at most once. Spans that cannot be + /// resolved in this table are skipped. /// /// # Examples /// - /// Simple hierarchy: - /// /// ```rust /// use hashql_core::span::{SpanAncestors, SpanTable, TextRange}; /// use hashql_diagnostics::source::SourceId; @@ -541,14 +521,15 @@ impl SpanTable { /// }; /// let child_id = table.insert(child, SpanAncestors::union(&[parent_id])); /// - /// let ancestors = table.ancestors(child_id); - /// // Result contains both parent_id and grandparent_id - /// assert_eq!(ancestors.len(), 2); + /// let ancestors: Vec<_> = table.ancestors(child_id).into_iter().collect(); + /// // First element is the span itself, followed by its ancestors + /// assert_eq!(ancestors[0], child_id); /// assert!(ancestors.contains(&parent_id)); /// assert!(ancestors.contains(&grandparent_id)); + /// assert_eq!(ancestors.len(), 3); /// ``` /// - /// Complex hierarchy with multiple branches: + /// Diamond-shaped hierarchy with shared ancestors: /// /// ```rust /// use hashql_core::span::{SpanAncestors, SpanTable, TextRange}; @@ -560,7 +541,6 @@ impl SpanTable { /// # } /// let mut table = SpanTable::new(SourceId::new_unchecked(0)); /// - /// // Create diamond-shaped hierarchy /// let root = MySpan { /// range: TextRange::new(0.into(), 100.into()), /// }; @@ -581,36 +561,32 @@ impl SpanTable { /// }; /// let merge_id = table.insert(merge, SpanAncestors::union(&[left_id, right_id])); /// - /// let ancestors = table.ancestors(merge_id); - /// // Result contains all unique ancestors: left_id, right_id, root_id - /// assert_eq!(ancestors.len(), 3); + /// let ancestors: Vec<_> = table.ancestors(merge_id).into_iter().collect(); + /// // Contains merge_id, left_id, right_id, and root_id (deduplicated) + /// assert_eq!(ancestors.len(), 4); + /// assert_eq!(ancestors[0], merge_id); /// ``` #[must_use] - pub fn ancestors(&self, span: SpanId) -> Vec { - let mut ancestors = Vec::new(); + pub fn ancestors(&self, span: SpanId) -> impl IntoIterator { let mut stack = vec![span]; + let mut visited = DenseBitSet::new_empty(self.spans.len()); - while let Some(current) = stack.pop() { - let Some(entry) = self.get_entry(current) else { - continue; - }; - - let direct_ancestors = &self.ancestors[entry.ancestors.clone()]; - - if direct_ancestors.is_empty() { - continue; - } - - for &ancestor in direct_ancestors { - if ancestors.contains(&ancestor) { + core::iter::from_fn(move || { + loop { + let span = stack.pop()?; + if !visited.insert(span.id()) { continue; } - ancestors.push(ancestor); - stack.push(ancestor); - } - } + let Some(entry) = self.get_entry(span) else { + continue; + }; - ancestors + let ancestors = &self.ancestors[entry.ancestors.clone()]; + stack.extend(ancestors); + + return Some(span); + } + }) } } diff --git a/libs/@local/hashql/diagnostics/src/issues.rs b/libs/@local/hashql/diagnostics/src/issues.rs index 42c2e68d86f..da862a1e2a2 100644 --- a/libs/@local/hashql/diagnostics/src/issues.rs +++ b/libs/@local/hashql/diagnostics/src/issues.rs @@ -145,6 +145,41 @@ impl DiagnosticIssues { } } + /// Transforms the span type of all diagnostics in the collection. + /// + /// Applies the function to every span in labels, suggestions, and patches. + /// This is useful when converting between span representations, such as + /// resolving relative spans to absolute positions for rendering. + /// + /// # Examples + /// + /// ``` + /// use hashql_diagnostics::{Diagnostic, DiagnosticIssues, Label, Severity}; + /// # use hashql_diagnostics::category::TerminalDiagnosticCategory; + /// # const CATEGORY: TerminalDiagnosticCategory = TerminalDiagnosticCategory { + /// # id: "example", name: "Example" + /// # }; + /// + /// let mut issues = DiagnosticIssues::new(); + /// issues.push( + /// Diagnostic::new(CATEGORY, Severity::Warning).primary(Label::new(10..15, "warning here")), + /// ); + /// + /// // Convert Range spans to string descriptions + /// let converted = issues.map_spans(|range| format!("{}..{}", range.start, range.end)); + /// assert_eq!(converted.len(), 1); + /// ``` + pub fn map_spans(self, mut func: impl FnMut(S) -> S2) -> DiagnosticIssues { + DiagnosticIssues { + diagnostics: self + .diagnostics + .into_iter() + .map(|diagnostic| diagnostic.map_spans(&mut func)) + .collect(), + critical: self.critical, + } + } + /// Converts to a collection with type-erased categories. /// /// When combining diagnostics from different compilation phases that use @@ -805,12 +840,55 @@ impl IntoIterator for DiagnosticIssues { } } +impl From>> for DiagnosticIssues +where + K: SeverityKind, +{ + fn from(diagnostics: Vec>) -> Self { + let critical = diagnostics + .iter() + .filter(|diagnostic| diagnostic.severity.is_critical()) + .count(); + + Self { + diagnostics, + critical, + } + } +} + impl Default for DiagnosticIssues { fn default() -> Self { Self::new() } } +#[cfg(feature = "serde")] +impl serde::Serialize for DiagnosticIssues +where + C: serde::Serialize, + S: serde::Serialize, + K: serde::Serialize, +{ + fn serialize(&self, serializer: Ser) -> Result { + self.diagnostics.serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de, C, S, K> serde::Deserialize<'de> for DiagnosticIssues +where + C: serde::Deserialize<'de>, + S: serde::Deserialize<'de>, + K: serde::Deserialize<'de> + SeverityKind, +{ + fn deserialize>(deserializer: De) -> Result { + let inner = Vec::deserialize(deserializer)?; + + Ok(Self::from(inner)) + } +} + /// A trait for collecting diagnostic information from fallible operations. /// /// `DiagnosticSink` provides a way to process [`Result`] values that may contain diff --git a/libs/@local/hashql/diagnostics/src/status.rs b/libs/@local/hashql/diagnostics/src/status.rs index fb1c36f611f..f2ad9092801 100644 --- a/libs/@local/hashql/diagnostics/src/status.rs +++ b/libs/@local/hashql/diagnostics/src/status.rs @@ -40,16 +40,14 @@ use crate::{ /// assert_eq!(result.advisories.critical(), 0); /// ``` #[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Success { pub value: T, pub advisories: DiagnosticIssues, } impl Success { - /// Transforms the value in the success result while preserving diagnostics. - /// - /// This method applies a function to the success value, creating a new [`Success`] with - /// the transformed value and the same advisory diagnostics. + /// Transforms the value while preserving advisory diagnostics. /// /// # Examples /// @@ -168,6 +166,7 @@ impl Success { /// assert_eq!(error.secondary.len(), 1); /// ``` #[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Failure { // boxed to reduce memory footprint pub primary: Box>, @@ -175,6 +174,27 @@ pub struct Failure { } impl Failure { + /// Creates a [`Failure`] from a primary critical diagnostic. + /// + /// The resulting failure has no secondary diagnostics. + /// + /// # Examples + /// + /// ``` + /// use hashql_diagnostics::{Diagnostic, Failure, Label, Severity}; + /// # use hashql_diagnostics::category::TerminalDiagnosticCategory; + /// # const CATEGORY: TerminalDiagnosticCategory = TerminalDiagnosticCategory { + /// # id: "error", name: "Error" + /// # }; + /// + /// let critical = Diagnostic::new(CATEGORY, Severity::Error) + /// .primary(Label::new(10..15, "error here")) + /// .specialize() + /// .expect_err("should be critical"); + /// + /// let failure = Failure::new(critical); + /// assert!(failure.secondary.is_empty()); + /// ``` pub fn new(primary: Diagnostic) -> Self { Self { primary: Box::new(primary), @@ -503,6 +523,34 @@ pub trait StatusExt: sealed::Sealed { #[expect(clippy::missing_errors_doc, reason = "This is a trait on Result")] fn map_category(self, func: impl FnMut(C) -> C2) -> Status; + /// Transforms the span type of all diagnostics in the status. + /// + /// Applies the function to every span in labels, suggestions, and patches + /// across both success advisories and failure diagnostics. This is useful + /// when converting between span representations at compilation phase + /// boundaries, such as resolving relative spans to absolute positions. + /// + /// # Examples + /// + /// ``` + /// use hashql_diagnostics::{Diagnostic, Label, Severity, Status, StatusExt as _}; + /// # use hashql_diagnostics::category::TerminalDiagnosticCategory; + /// # const CATEGORY: TerminalDiagnosticCategory = TerminalDiagnosticCategory { + /// # id: "example", name: "Example" + /// # }; + /// + /// let mut result = Status::success(42); + /// result.push_diagnostic( + /// Diagnostic::new(CATEGORY, Severity::Warning).primary(Label::new(10..15, "warning here")), + /// ); + /// + /// // Convert Range spans to (start, end) tuples + /// let converted = result.map_spans(|range| (range.start, range.end)); + /// assert!(converted.is_ok()); + /// ``` + #[expect(clippy::missing_errors_doc, reason = "This is a trait on Result")] + fn map_spans(self, func: impl FnMut(S) -> S2) -> Status; + /// Converts to a result with type-erased diagnostic categories. /// /// When combining diagnostics from different compilation phases that use different category @@ -809,6 +857,19 @@ impl StatusExt for Status { } } + fn map_spans(self, mut func: impl FnMut(S) -> S2) -> Status { + match self { + Ok(Success { value, advisories }) => Ok(Success { + value, + advisories: advisories.map_spans(func), + }), + Err(Failure { primary, secondary }) => Err(Failure { + primary: Box::new(primary.map_spans(&mut func)), + secondary: secondary.map_spans(func), + }), + } + } + fn boxed<'category>(self) -> Self::Boxed<'category> where C: DiagnosticCategory + 'category, From 46c42b60ad4a36236ea061441aaffc15c73fb7ea Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:30:26 +0200 Subject: [PATCH 15/25] chore: checkpoint --- libs/@local/graph/api/src/rest/entity.rs | 45 +- .../@local/graph/api/src/rest/entity/query.rs | 567 ------------------ .../graph/api/src/rest/entity/query/filter.rs | 1 + .../graph/api/src/rest/entity/query/mod.rs | 2 + .../api/src/rest/entity/query/request.rs | 292 +++++++++ .../api/src/rest/entity_query_request.rs | 119 +--- .../graph/api/src/rest/hashql/compile.rs | 189 ++++++ .../@local/graph/api/src/rest/hashql/error.rs | 148 +++++ libs/@local/graph/api/src/rest/hashql/mod.rs | 155 +++++ .../@local/graph/api/src/rest/hashql/value.rs | 123 ++++ libs/@local/hashql/syntax-jexpr/src/error.rs | 21 +- .../hashql/syntax-jexpr/src/lexer/error.rs | 18 +- .../hashql/syntax-jexpr/src/lexer/mod.rs | 19 +- .../syntax-jexpr/src/parser/array/error.rs | 18 +- .../syntax-jexpr/src/parser/array/visit.rs | 6 +- .../syntax-jexpr/src/parser/complex/mod.rs | 6 +- .../hashql/syntax-jexpr/src/parser/error.rs | 6 +- .../syntax-jexpr/src/parser/object/error.rs | 42 +- .../syntax-jexpr/src/parser/object/visit.rs | 10 +- .../syntax-jexpr/src/parser/string/error.rs | 6 +- libs/@local/hashql/syntax-jexpr/src/test.rs | 6 +- 21 files changed, 1002 insertions(+), 797 deletions(-) delete mode 100644 libs/@local/graph/api/src/rest/entity/query.rs create mode 100644 libs/@local/graph/api/src/rest/entity/query/filter.rs create mode 100644 libs/@local/graph/api/src/rest/entity/query/mod.rs create mode 100644 libs/@local/graph/api/src/rest/entity/query/request.rs create mode 100644 libs/@local/graph/api/src/rest/hashql/compile.rs create mode 100644 libs/@local/graph/api/src/rest/hashql/error.rs create mode 100644 libs/@local/graph/api/src/rest/hashql/mod.rs create mode 100644 libs/@local/graph/api/src/rest/hashql/value.rs diff --git a/libs/@local/graph/api/src/rest/entity.rs b/libs/@local/graph/api/src/rest/entity.rs index 2b332db1b7a..c4e625ffbdf 100644 --- a/libs/@local/graph/api/src/rest/entity.rs +++ b/libs/@local/graph/api/src/rest/entity.rs @@ -77,12 +77,9 @@ use type_system::{ }; use utoipa::{OpenApi, ToSchema}; -pub use crate::rest::entity_query_request::{ - EntityQuery, EntityQueryOptions, QueryEntitiesRequest, QueryEntitySubgraphRequest, -}; +use self::query::request::{QueryEntitiesRequest, QueryEntitySubgraphRequest}; use crate::rest::{ ApiConfig, AuthenticatedUserHeader, InteractiveHeader, OpenApiQuery, QueryLogger, - entity_query_request::CompilationOptions, json::Json, status::{BoxedResponse, report_to_response}, utoipa_typedef::subgraph::Subgraph, @@ -123,7 +120,6 @@ use crate::rest::{ HasPermissionForEntitiesParams, - EntityQueryOptions, QueryEntitiesRequest, QueryEntitySubgraphRequest, EntityQueryCursor, @@ -452,24 +448,8 @@ where .map_err(Report::from) .map_err(report_to_response)?; - let (query, options) = request.into_parts(); - - // TODO: https://linear.app/hash/issue/H-5351/reuse-parts-between-compilation-units - let mut heap = Heap::uninitialized(); - let mut scratch = Scratch::new(); - - if matches!(query, EntityQuery::Query { .. }) { - // The heap is going to be used in the compilation of the query and therefore needs to be - // primed. - // Doing this in a separate step allows us to be allocation free when not using HashQL - // queries. - heap.prime(); - } - - let filter = query.compile(&heap, &mut scratch, CompilationOptions { interactive })?; - - let params = options - .into_params(filter, api_config) + let params = request + .into_params(api_config) .attach(hash_status::StatusCode::InvalidArgument) .map_err(report_to_response)?; @@ -566,24 +546,9 @@ where let request = QueryEntitySubgraphRequest::deserialize(&request) .map_err(Report::from) .map_err(report_to_response)?; - let (query, options, traversal) = request.into_parts(); - - // TODO: https://linear.app/hash/issue/H-5351/reuse-parts-between-compilation-units - let mut heap = Heap::uninitialized(); - let mut scratch = Scratch::new(); - - if matches!(query, EntityQuery::Query { .. }) { - // The heap is going to be used in the compilation of the query and therefore needs to be - // primed. - // Doing this in a separate step allows us to be allocation free when not using HashQL - // queries. - heap.prime(); - } - - let filter = query.compile(&heap, &mut scratch, CompilationOptions { interactive })?; - let params = options - .into_traversal_params(filter, traversal, api_config) + let params = request + .into_traversal_params(api_config) .attach(hash_status::StatusCode::InvalidArgument) .map_err(report_to_response)?; diff --git a/libs/@local/graph/api/src/rest/entity/query.rs b/libs/@local/graph/api/src/rest/entity/query.rs deleted file mode 100644 index 9529d8e76c9..00000000000 --- a/libs/@local/graph/api/src/rest/entity/query.rs +++ /dev/null @@ -1,567 +0,0 @@ -use alloc::{borrow::Cow, collections::BTreeMap, sync::Arc}; -use core::{alloc::Allocator, ops::Range}; - -use axum::response::{Html, IntoResponse}; -use hash_graph_postgres_store::store::PostgresStorePool; -use hash_graph_store::pool::StorePool; -use hash_temporal_client::TemporalClient; -use hashql_ast::error::AstDiagnosticCategory; -use hashql_core::{ - heap::{Heap, HeapPool, ResetAllocator as _, Scratch, ScratchPool}, - id::Id as _, - module::ModuleRegistry, - span::{SpanId, SpanTable}, - r#type::environment::Environment, -}; -use hashql_diagnostics::{ - Diagnostic, DiagnosticCategory, DiagnosticIssues, Failure, IntoStatus as _, Label, Message, - Source, Sources, Status, StatusExt as _, Success, - category::{TerminalDiagnosticCategory, canonical_category_id}, - diagnostic::render::{Format, RenderOptions}, - severity::Critical, -}; -use hashql_eval::{ - context::{CodeExecutionContext, CodeGenerationContext}, - error::EvalDiagnosticCategory, - orchestrator::Orchestrator, - postgres::{PostgresCompiler, PreparedQueries}, -}; -use hashql_hir::error::HirDiagnosticCategory; -use hashql_mir::{ - body::Body, - def::{DefId, DefIdVec}, - error::MirDiagnosticCategory, - interpret::{ - Inputs, - value::{Int, Num, Ptr, Value}, - }, - pass::{LowerConfig, execution::ExecutionAnalysisResidual}, -}; -use hashql_syntax_jexpr::{error::JExprDiagnosticCategory, span::Span}; -use http::StatusCode; -use serde::Serialize as _; -use serde_json::value::RawValue; -use tokio_util::task::LocalPoolHandle; - -use crate::rest::{json::Json, status::BoxedResponse}; - -struct CompilerContext { - scratches: ScratchPool, - heaps: HeapPool, - pool: LocalPoolHandle, -} - -struct ExecutionContext { - postgres: PostgresStorePool, - temporal: Option>, -} - -struct CompilationOptions { - interactive: bool, - json_compat: bool, -} - -const INFRASTRUCTURE_CATEGORY: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "infrastructure", - name: "Infrastructure", -}; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -enum HashQlDiagnosticCategory { - JExpr(JExprDiagnosticCategory), - Ast(AstDiagnosticCategory), - Hir(HirDiagnosticCategory), - Mir(MirDiagnosticCategory), - Eval(EvalDiagnosticCategory), - Infrastructure, -} - -impl serde::Serialize for HashQlDiagnosticCategory { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.collect_str(&canonical_category_id(self)) - } -} - -impl DiagnosticCategory for HashQlDiagnosticCategory { - fn id(&self) -> Cow<'_, str> { - Cow::Borrowed("hashql") - } - - fn name(&self) -> Cow<'_, str> { - Cow::Borrowed("HashQL") - } - - fn subcategory(&self) -> Option<&dyn DiagnosticCategory> { - match self { - Self::JExpr(jexpr) => Some(jexpr), - Self::Ast(ast) => Some(ast), - Self::Hir(hir) => Some(hir), - Self::Mir(mir) => Some(mir), - Self::Eval(eval) => Some(eval), - Self::Infrastructure => Some(&INFRASTRUCTURE_CATEGORY), - } - } -} - -fn serialize_int(int: &Int, serializer: S) -> Result { - int.as_int().serialize(serializer) -} - -#[expect(clippy::trivially_copy_pass_by_ref)] -fn serialize_num(num: &Num, serializer: S) -> Result { - num.as_f64().serialize(serializer) -} - -#[expect(clippy::trivially_copy_pass_by_ref)] -fn serialize_ptr(ptr: &Ptr, serializer: S) -> Result { - ptr.def().as_u32().serialize(serializer) -} - -// This is only here until https://linear.app/hash/issue/BE-540/hashql-register-based-bytecode-vm -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)] -enum OwnedValue { - /// The unit value. - Unit, - /// An integer value (also represents booleans). - Integer(#[serde(serialize_with = "serialize_int")] Int), - /// A floating-point number. - Number(#[serde(serialize_with = "serialize_num")] Num), - /// A string value. - String(Arc), - /// A function pointer. - Pointer(#[serde(serialize_with = "serialize_ptr")] Ptr), - - /// An opaque/newtype wrapper. - Opaque(Arc, Box), - /// A named-field struct. - Struct(Vec<(Arc, Self)>), - /// A positional tuple. - Tuple(Vec), - /// An ordered list. - List(Vec), - /// An ordered dictionary. - Dict(BTreeMap), -} - -impl<'heap, A: Allocator + Clone> From> for OwnedValue { - fn from(value: Value<'heap, A>) -> Self { - match value { - Value::Unit => Self::Unit, - Value::Integer(int) => Self::Integer(int), - Value::Number(num) => Self::Number(num), - Value::String(str) => Self::String(Arc::from(str.as_str())), - Value::Pointer(ptr) => Self::Pointer(ptr), - Value::Opaque(opaque) => Self::Opaque( - Arc::from(opaque.name().as_str()), - Box::new(opaque.into_value().into()), - ), - Value::Struct(r#struct) => { - debug_assert_eq!(r#struct.fields().len(), r#struct.values().len()); - - Self::Struct( - r#struct - .fields() - .iter() - .zip(r#struct.values()) - .map(|(field, value)| { - (Arc::from(field.as_str()), Self::from(value.clone())) - }) - .collect(), - ) - } - Value::Tuple(tuple) => Self::Tuple( - tuple - .values() - .iter() - .map(|value| Self::from(value.clone())) - .collect(), - ), - Value::List(list) => { - Self::List(list.iter().map(|value| Self::from(value.clone())).collect()) - } - Value::Dict(dict) => Self::Dict( - dict.iter() - .map(|(key, value)| (Self::from(key.clone()), Self::from(value.clone()))) - .collect(), - ), - } - } -} - -#[derive(Copy, Clone)] -struct JsonValueSerialize<'value>(&'value OwnedValue); - -impl serde::Serialize for JsonValueSerialize<'_> { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self.0 { - OwnedValue::Unit => serializer.serialize_unit(), - OwnedValue::Integer(int) => serialize_int(int, serializer), - OwnedValue::Number(num) => serialize_num(num, serializer), - OwnedValue::String(str) => serde::Serialize::serialize(str.as_ref(), serializer), - OwnedValue::Pointer(ptr) => serialize_ptr(ptr, serializer), - OwnedValue::Opaque(_, owned_value) => { - serde::Serialize::serialize(&Self(owned_value), serializer) - } - OwnedValue::Struct(items) => { - serializer.collect_map(items.iter().map(|(key, value)| (key, Self(value)))) - } - OwnedValue::Tuple(owned_values) => { - serializer.collect_seq(owned_values.iter().map(Self)) - } - OwnedValue::List(owned_values) => serializer.collect_seq(owned_values.iter().map(Self)), - OwnedValue::Dict(btree_map) => serializer.collect_map( - btree_map - .iter() - .map(|(key, value)| (Self(key), Self(value))), - ), - } - } -} - -#[derive(Debug, serde::Serialize)] -struct PointerSpan { - pub range: Range, - pub pointer: Option, -} - -impl PointerSpan { - fn resolve(id: SpanId, spans: &SpanTable) -> Option { - let absolute = spans.absolute(id)?; - - let mut pointer = None; - - for ancestor in spans.ancestors(id) { - let Some(span) = spans.get(ancestor) else { - continue; - }; - - if let Some(span_pointer) = &span.pointer { - pointer = Some(span_pointer.as_str().to_owned()); - break; - } - } - - Some(Self { - range: absolute.range().into(), - pointer, - }) - } -} - -fn status_to_response( - status: Status, - sources: &Sources<'_>, - mut spans: &SpanTable, - options: &CompilationOptions, -) -> BoxedResponse { - match status { - Ok(Success { value, advisories }) => { - let advisories = advisories.map_spans(|span| PointerSpan::resolve(span, spans)); - - if options.json_compat { - Json(Success { - value: JsonValueSerialize(&value), - advisories, - }) - .into_response() - .into() - } else { - Json(value).into_response().into() - } - } - Err(Failure { primary, secondary }) => { - let severity = primary.severity; - let status_code = if severity == Critical::ERROR { - StatusCode::BAD_REQUEST - } else { - StatusCode::INTERNAL_SERVER_ERROR - }; - - let mut response = if options.interactive { - let mut diagnostics = secondary.generalize(); - diagnostics.insert_front(primary.generalize()); - - let output = - diagnostics.render(RenderOptions::new(Format::Html, sources), &mut spans); - Html(output).into_response() - } else { - Json(Failure { - primary: Box::new(primary.map_spans(|span| PointerSpan::resolve(span, spans))), - secondary: secondary.map_spans(|span| PointerSpan::resolve(span, spans)), - }) - .into_response() - }; - - *response.status_mut() = status_code; - response.into() - } - } -} - -#[expect(clippy::future_not_send)] -async fn query_local_impl( - ctx: Arc, - exec: ExecutionContext, - spans: &mut SpanTable, - query: &[u8], -) -> Status { - // We're hitting a borrow checker limitation here, we cannot prove that scratch and heap are - // still alive by the time that we `spawn_local`, and due to the fact that it requires `'static` - // it means that we need to build the query inside here, and only then delegate, otherwise we'd - // be able to run it outside. - let mut scratch = ctx.scratches.get(); - let heap = ctx.heaps.get(); - - let inputs = Inputs::new(); // TODO: https://linear.app/hash/issue/BE-41/hashql-expose-input-in-graph-api - - let Success { - value: compilation, - advisories, - } = tokio::task::block_in_place(|| Compilation::compile(&heap, &mut scratch, spans, query))?; - - let context = compilation.context(); - - let Success { - value: client, - advisories, - } = exec - .postgres - .acquire(exec.temporal) - .await - .map_err(|report| { - let mut diagnostic = - Diagnostic::new(HashQlDiagnosticCategory::Infrastructure, Critical::BUG).primary( - Label::new(compilation.root_span, "failed to acquire postgres client"), - ); - - diagnostic.add_message(Message::note(format!("{report:?}"))); - diagnostic - }) - .into_status() - .with_diagnostics(advisories)?; - - let orchestrator = Orchestrator::new(client, &compilation.artifact.postgres, &context); - orchestrator - .run(&inputs, compilation.entrypoint, []) - .await - .into_status() - .map_category(|category| { - HashQlDiagnosticCategory::Eval(EvalDiagnosticCategory::Orchestrator(category)) - }) - .with_diagnostics(advisories) - .map_value(OwnedValue::from) -} - -#[expect(clippy::future_not_send)] -async fn query_local( - ctx: Arc, - exec: ExecutionContext, - query: Arc, - options: CompilationOptions, -) -> BoxedResponse { - let mut sources = Sources::new(); - let source_id = sources.push(Source::new(query.get())); - - let mut spans = SpanTable::new(source_id); - - let status = query_local_impl(ctx, exec, &mut spans, query.get().as_bytes()).await; - status_to_response(status, &sources, &spans, &options) -} - -async fn query( - ctx: Arc, - exec: ExecutionContext, - query: Arc, - options: CompilationOptions, -) -> BoxedResponse { - let pool = ctx.pool.clone(); - let result = pool - .spawn_pinned(|| query_local(ctx, exec, query, options)) - .await; - - result.unwrap_or_else(|_| { - Json(serde_json::json!({"fatal": "internal error: query execution failed"})) - .into_response() - .into() - }) -} - -struct CodeCompilationArtifact<'heap> { - assignment: DefIdVec>, &'heap Heap>, - - interpreter: DefIdVec, &'heap Heap>, - postgres: PreparedQueries<'heap, &'heap Heap>, -} - -struct Compilation<'heap> { - heap: &'heap Heap, - - root_span: SpanId, - - interner: hashql_eval::intern::Interner<'heap>, - env: Environment<'heap>, - - entrypoint: DefId, - artifact: CodeCompilationArtifact<'heap>, -} - -impl<'heap> Compilation<'heap> { - pub fn compile( - heap: &'heap Heap, - scratch: &mut Scratch, - spans: &mut SpanTable, - query: &[u8], - ) -> Status { - // Parse the query - let mut parser = hashql_syntax_jexpr::Parser::new(heap, spans); - let mut ast = parser.parse_expr(query).map_err(|diagnostic| { - Failure::new( - diagnostic - .map_category(HashQlDiagnosticCategory::JExpr) - .map_severity(|severity| { - Critical::try_new(severity).unwrap_or_else(|| { - tracing::error!( - ?severity, - "JExpr returned an error of non-critical severity" - ); - Critical::ERROR - }) - }), - ) - })?; - - let root_span = ast.span; - - let mut env = Environment::new(heap); - let modules = ModuleRegistry::new(&env); - - // Lower the AST - let Success { - value: types, - advisories, - } = hashql_ast::lowering::lower(heap.intern_symbol("main"), &mut ast, &env, &modules) - .map_category(|category| { - HashQlDiagnosticCategory::Ast(AstDiagnosticCategory::Lowering(category)) - })?; - - let interner = hashql_hir::intern::Interner::new(heap); - let mut hir_context = hashql_hir::context::HirContext::new(&interner, &modules); - - // Reify the HIR from the AST - let Success { - value: hir, - advisories, - } = hashql_hir::node::NodeData::from_ast(ast, &mut hir_context, &types) - .map_category(|category| { - HashQlDiagnosticCategory::Hir(HirDiagnosticCategory::Reification(category)) - }) - .with_diagnostics(advisories)?; - - // Lower the HIR - let Success { - value: hir, - advisories, - } = hashql_hir::lower::lower(hir, &types, &mut env, &mut hir_context) - .map_category(|category| { - HashQlDiagnosticCategory::Hir(HirDiagnosticCategory::Lowering(category)) - }) - .with_diagnostics(advisories)?; - - let interner = hashql_mir::intern::Interner::new(heap); - let mut bodies = DefIdVec::new_in(heap); - let mut mir_context = hashql_mir::context::MirContext { - heap, - env: &env, - interner: &interner, - diagnostics: DiagnosticIssues::new(), - }; - let mut reify_context = hashql_mir::reify::ReifyContext { - bodies: &mut bodies, - mir: &mut mir_context, - hir: &hir_context, - scratch: &*scratch, - }; - - // Reify the MIR from the HIR - let Success { - value: entrypoint, - advisories, - } = hashql_mir::reify::from_hir(hir, &mut reify_context) - .map_category(|category| { - HashQlDiagnosticCategory::Mir(MirDiagnosticCategory::Reify(category)) - }) - .with_diagnostics(advisories)?; - - // Lower the MIR - let Success { - value: (), - advisories, - } = hashql_mir::pass::lower( - &mut mir_context, - scratch, - &mut bodies, - &LowerConfig::default(), - ) - .map_category(HashQlDiagnosticCategory::Mir) - .with_diagnostics(advisories)?; - - // Plan the execution - let Success { - value: execution, - advisories, - } = hashql_mir::pass::place(&mut mir_context, scratch, &mut bodies) - .map_category(HashQlDiagnosticCategory::Mir) - .with_diagnostics(advisories)?; - - // Build the postgres fragments - let interner = interner.into(); - let mut context = CodeGenerationContext::new_in( - &env, - &interner, - &bodies, - &execution, - heap, - &mut *scratch, - ); - - let mut postgres = PostgresCompiler::new_in(&mut context, &mut *scratch); - let queries = postgres.compile(); - scratch.reset(); - - context - .diagnostics - .into_status(()) - .map_category(HashQlDiagnosticCategory::Eval) - .with_diagnostics(advisories)?; - - Status::success(Self { - heap, - - root_span, - env, - interner, - entrypoint, - artifact: CodeCompilationArtifact { - assignment: execution, - interpreter: bodies, - postgres: queries, - }, - }) - } - - fn context(&self) -> CodeExecutionContext<'_, 'heap, &'heap Heap> { - CodeExecutionContext { - env: &self.env, - interner: &self.interner, - bodies: &self.artifact.interpreter, - execution: &self.artifact.assignment, - alloc: self.heap, - } - } -} diff --git a/libs/@local/graph/api/src/rest/entity/query/filter.rs b/libs/@local/graph/api/src/rest/entity/query/filter.rs new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/libs/@local/graph/api/src/rest/entity/query/filter.rs @@ -0,0 +1 @@ + diff --git a/libs/@local/graph/api/src/rest/entity/query/mod.rs b/libs/@local/graph/api/src/rest/entity/query/mod.rs new file mode 100644 index 00000000000..032e5be832a --- /dev/null +++ b/libs/@local/graph/api/src/rest/entity/query/mod.rs @@ -0,0 +1,2 @@ +pub mod filter; +pub mod request; diff --git a/libs/@local/graph/api/src/rest/entity/query/request.rs b/libs/@local/graph/api/src/rest/entity/query/request.rs new file mode 100644 index 00000000000..5cb89770dea --- /dev/null +++ b/libs/@local/graph/api/src/rest/entity/query/request.rs @@ -0,0 +1,292 @@ +use error_stack::Report; +use hash_graph_store::{ + entity::{ + EntityQueryCursor, EntityQueryPath, EntityQuerySorting, EntityQuerySortingRecord, + QueryConversion, QueryEntitiesParams, QueryEntitySubgraphParams, + }, + entity_type::IncludeEntityTypeOption, + filter::Filter, + query::Ordering, + subgraph::{ + edges::{EntityTraversalPath, GraphResolveDepths, SubgraphTraversalParams, TraversalPath}, + temporal_axes::QueryTemporalAxesUnresolved, + }, +}; +use type_system::knowledge::Entity; + +use crate::rest::{ApiConfig, LimitExceededError, resolve_limit}; + +#[tracing::instrument(level = "info", skip_all)] +fn generate_sorting_paths( + paths: Option>>, + temporal_axes: &QueryTemporalAxesUnresolved, +) -> Vec> { + let temporal_axes_sorting_path = match temporal_axes { + QueryTemporalAxesUnresolved::TransactionTime { .. } => &EntityQueryPath::TransactionTime, + QueryTemporalAxesUnresolved::DecisionTime { .. } => &EntityQueryPath::DecisionTime, + }; + + paths + .map_or_else( + || { + vec![ + EntityQuerySortingRecord { + path: temporal_axes_sorting_path.clone(), + ordering: Ordering::Descending, + nulls: None, + }, + EntityQuerySortingRecord { + path: EntityQueryPath::Uuid, + ordering: Ordering::Ascending, + nulls: None, + }, + EntityQuerySortingRecord { + path: EntityQueryPath::WebId, + ordering: Ordering::Ascending, + nulls: None, + }, + ] + }, + |mut paths| { + let mut has_temporal_axis = false; + let mut has_uuid = false; + let mut has_web_id = false; + + for path in &paths { + if path.path == EntityQueryPath::TransactionTime + || path.path == EntityQueryPath::DecisionTime + { + has_temporal_axis = true; + } + if path.path == EntityQueryPath::Uuid { + has_uuid = true; + } + if path.path == EntityQueryPath::WebId { + has_web_id = true; + } + } + + if !has_temporal_axis { + paths.push(EntityQuerySortingRecord { + path: temporal_axes_sorting_path.clone(), + ordering: Ordering::Descending, + nulls: None, + }); + } + if !has_uuid { + paths.push(EntityQuerySortingRecord { + path: EntityQueryPath::Uuid, + ordering: Ordering::Ascending, + nulls: None, + }); + } + if !has_web_id { + paths.push(EntityQuerySortingRecord { + path: EntityQueryPath::WebId, + ordering: Ordering::Ascending, + nulls: None, + }); + } + + paths + }, + ) + .into_iter() + .map(EntityQuerySortingRecord::into_owned) + .collect() +} + +#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] +#[expect( + clippy::struct_excessive_bools, + reason = "Parameter struct deserialized from JSON" +)] +#[serde(rename_all = "camelCase")] +pub struct QueryEntitiesRequest<'q, 's, 'p> { + #[serde(borrow)] + filter: Filter<'q, Entity>, + + temporal_axes: QueryTemporalAxesUnresolved, + include_drafts: bool, + limit: Option, + #[serde(borrow, default)] + conversions: Vec>, + #[serde(borrow)] + sorting_paths: Option>>, + #[serde(borrow)] + cursor: Option>, + #[serde(default)] + include_count: bool, + #[serde(default)] + include_entity_types: Option, + #[serde(default)] + include_web_ids: bool, + #[serde(default)] + include_created_by_ids: bool, + #[serde(default)] + include_edition_created_by_ids: bool, + #[serde(default)] + include_type_ids: bool, + #[serde(default)] + include_type_titles: bool, + include_permissions: bool, +} + +impl<'q, 's, 'p> QueryEntitiesRequest<'q, 's, 'p> { + /// # Errors + /// + /// Returns [`LimitExceededError`] if the requested limit exceeds the configured maximum in + /// [`ApiConfig::query_entity_limit`]. + pub fn into_params( + self, + config: ApiConfig, + ) -> Result, Report> + where + 'p: 'q, + { + let limit = resolve_limit(self.limit, config.query_entity_limit)?; + + Ok(QueryEntitiesParams { + filter: self.filter, + sorting: EntityQuerySorting { + paths: generate_sorting_paths(self.sorting_paths, &self.temporal_axes), + cursor: self.cursor.map(EntityQueryCursor::into_owned), + }, + limit, + conversions: self.conversions, + include_drafts: self.include_drafts, + include_count: self.include_count, + include_entity_types: self.include_entity_types, + temporal_axes: self.temporal_axes, + include_web_ids: self.include_web_ids, + include_created_by_ids: self.include_created_by_ids, + include_edition_created_by_ids: self.include_edition_created_by_ids, + include_type_ids: self.include_type_ids, + include_type_titles: self.include_type_titles, + include_permissions: self.include_permissions, + }) + } + + /// # Errors + /// + /// Returns [`LimitExceededError`] if the requested limit exceeds the configured maximum in + /// [`ApiConfig::query_entity_limit`]. + pub fn into_traversal_params( + self, + traversal: SubgraphTraversalParams, + config: ApiConfig, + ) -> Result, Report> + where + 'p: 'q, + { + match traversal { + SubgraphTraversalParams::Paths { traversal_paths } => { + Ok(QueryEntitySubgraphParams::Paths { + traversal_paths, + request: self.into_params(config)?, + }) + } + SubgraphTraversalParams::ResolveDepths { + traversal_paths, + graph_resolve_depths, + } => Ok(QueryEntitySubgraphParams::ResolveDepths { + traversal_paths, + graph_resolve_depths, + request: self.into_params(config)?, + }), + } + } +} + +#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] +#[serde(untagged, deny_unknown_fields)] +pub enum QueryEntitySubgraphRequest<'q, 's, 'p> { + #[serde(rename_all = "camelCase")] + ResolveDepths { + traversal_paths: Vec, + graph_resolve_depths: GraphResolveDepths, + #[serde(borrow, flatten)] + request: QueryEntitiesRequest<'q, 's, 'p>, + }, + #[serde(rename_all = "camelCase")] + Paths { + traversal_paths: Vec, + #[serde(borrow, flatten)] + request: QueryEntitiesRequest<'q, 's, 'p>, + }, +} + +impl<'q, 's, 'p> QueryEntitySubgraphRequest<'q, 's, 'p> { + #[must_use] + pub fn from_parts( + request: QueryEntitiesRequest<'q, 's, 'p>, + traversal_params: SubgraphTraversalParams, + ) -> Self { + match traversal_params { + SubgraphTraversalParams::Paths { traversal_paths } => Self::Paths { + traversal_paths, + request, + }, + SubgraphTraversalParams::ResolveDepths { + traversal_paths, + graph_resolve_depths, + } => Self::ResolveDepths { + request, + traversal_paths, + graph_resolve_depths, + }, + } + } + + #[must_use] + pub fn into_parts(self) -> (QueryEntitiesRequest<'q, 's, 'p>, SubgraphTraversalParams) { + match self { + QueryEntitySubgraphRequest::Paths { + traversal_paths, + request: options, + } => (options, SubgraphTraversalParams::Paths { traversal_paths }), + QueryEntitySubgraphRequest::ResolveDepths { + traversal_paths, + graph_resolve_depths, + request: options, + } => ( + options, + SubgraphTraversalParams::ResolveDepths { + traversal_paths, + graph_resolve_depths, + }, + ), + } + } + + /// # Errors + /// + /// Returns [`LimitExceededError`] if the requested limit exceeds the configured maximum in + /// [`ApiConfig::query_entity_limit`]. + pub fn into_traversal_params( + self, + config: ApiConfig, + ) -> Result, Report> + where + 'p: 'q, + { + let (request, params) = self.into_parts(); + + match params { + SubgraphTraversalParams::Paths { traversal_paths } => { + Ok(QueryEntitySubgraphParams::Paths { + traversal_paths, + request: request.into_params(config)?, + }) + } + SubgraphTraversalParams::ResolveDepths { + traversal_paths, + graph_resolve_depths, + } => Ok(QueryEntitySubgraphParams::ResolveDepths { + traversal_paths, + graph_resolve_depths, + request: request.into_params(config)?, + }), + } + } +} diff --git a/libs/@local/graph/api/src/rest/entity_query_request.rs b/libs/@local/graph/api/src/rest/entity_query_request.rs index c5a07712f56..33f0dd18e40 100644 --- a/libs/@local/graph/api/src/rest/entity_query_request.rs +++ b/libs/@local/graph/api/src/rest/entity_query_request.rs @@ -335,124 +335,7 @@ impl<'q> EntityQuery<'q> { spans: &mut SpanTable, query: &RawJsonValue, ) -> Status, HashQLDiagnosticCategory, SpanId> { - // Parse the query - let mut parser = hashql_syntax_jexpr::Parser::new(heap, spans); - let mut ast = parser - .parse_expr(query.get().as_bytes()) - .map_err(|diagnostic| { - Failure::new( - diagnostic - .map_category(HashQLDiagnosticCategory::JExpr) - .map_severity(|severity| { - Critical::try_new(severity).unwrap_or_else(|| { - tracing::error!( - ?severity, - "JExpr returned an error of non-critical severity" - ); - Critical::ERROR - }) - }), - ) - })?; - - let mut env = Environment::new(heap); - let modules = ModuleRegistry::new(&env); - - // Lower the AST - let Success { - value: types, - advisories, - } = hashql_ast::lowering::lower(heap.intern_symbol("main"), &mut ast, &env, &modules) - .map_category(|category| { - HashQLDiagnosticCategory::Ast(AstDiagnosticCategory::Lowering(category)) - })?; - - let interner = hashql_hir::intern::Interner::new(heap); - let mut hir_context = hashql_hir::context::HirContext::new(&interner, &modules); - - // Reify the HIR from the AST - let Success { - value: hir, - advisories, - } = hashql_hir::node::NodeData::from_ast(ast, &mut hir_context, &types) - .map_category(|category| { - HashQLDiagnosticCategory::Hir(HirDiagnosticCategory::Reification(category)) - }) - .with_diagnostics(advisories)?; - - // Lower the HIR - let Success { - value: hir, - advisories, - } = hashql_hir::lower::lower(hir, &types, &mut env, &mut hir_context) - .map_category(|category| { - HashQLDiagnosticCategory::Hir(HirDiagnosticCategory::Lowering(category)) - }) - .with_diagnostics(advisories)?; - - let interner = hashql_mir::intern::Interner::new(heap); - let mut bodies = DefIdVec::new_in(heap); - let mut mir_context = hashql_mir::context::MirContext { - heap, - env: &env, - interner: &interner, - diagnostics: DiagnosticIssues::new(), - }; - let mut reify_context = hashql_mir::reify::ReifyContext { - bodies: &mut bodies, - mir: &mut mir_context, - hir: &hir_context, - scratch: &*scratch, - }; - - // Reify the MIR from the HIR - let Success { - value: mir, - advisories, - } = hashql_mir::reify::from_hir(hir, &mut reify_context) - .map_category(|category| { - HashQLDiagnosticCategory::Mir(MirDiagnosticCategory::Reify(category)) - }) - .with_diagnostics(advisories)?; - - todo!("lower the MIR"); - - // Evaluate the HIR - // TODO: https://linear.app/hash/issue/BE-41/hashql-expose-input-in-graph-api - // let inputs = fast_hash_map_with_capacity(0); - // let mut compiler = hashql_eval::graph::read::GraphReadCompiler::new(heap, &inputs); - - // compiler.visit_node(hir); - - // let Success { - // value: result, - // advisories, - // } = compiler - // .finish() - // .map_category(|category| { - // HashQLDiagnosticCategory::Eval(EvalDiagnosticCategory::Graph( - // GraphCompilerDiagnosticCategory::Read(category), - // )) - // }) - // .with_diagnostics(advisories)?; - - // let output = result.output.get(&hir.id).expect("TODO"); - - // // Compile the Filter into one - // let filters = match output { - // FilterSlice::Entity { range } => result.filters.entity(range.clone()), - // }; - - // let filter = match filters { - // [] => Filter::All(Vec::new()), - // [filter] => filter.clone(), - // _ => Filter::All(filters.to_vec()), - // }; - - // Ok(Success { - // value: filter, - // advisories, - // }) + unimplemented!() } /// Compiles a query into an executable entity filter. diff --git a/libs/@local/graph/api/src/rest/hashql/compile.rs b/libs/@local/graph/api/src/rest/hashql/compile.rs new file mode 100644 index 00000000000..9ba8a323794 --- /dev/null +++ b/libs/@local/graph/api/src/rest/hashql/compile.rs @@ -0,0 +1,189 @@ +use hashql_ast::error::AstDiagnosticCategory; +use hashql_core::{ + heap::{Heap, ResetAllocator as _, Scratch}, + module::ModuleRegistry, + span::{SpanId, SpanTable}, + r#type::environment::Environment, +}; +use hashql_diagnostics::{DiagnosticIssues, IntoStatus as _, Status, StatusExt as _, Success}; +use hashql_eval::{ + context::{CodeExecutionContext, CodeGenerationContext}, + postgres::{PostgresCompiler, PreparedQueries}, +}; +use hashql_hir::error::HirDiagnosticCategory; +use hashql_mir::{ + body::Body, + def::{DefId, DefIdVec}, + error::MirDiagnosticCategory, + pass::{LowerConfig, execution::ExecutionAnalysisResidual}, +}; +use hashql_syntax_jexpr::span::Span; + +use super::error::HashQlDiagnosticCategory; + +pub(crate) struct CodeCompilationArtifact<'heap> { + pub assignment: DefIdVec>, &'heap Heap>, + + pub interpreter: DefIdVec, &'heap Heap>, + pub postgres: PreparedQueries<'heap, &'heap Heap>, +} + +pub(crate) struct Compilation<'heap> { + pub heap: &'heap Heap, + + pub root_span: SpanId, + + pub interner: hashql_eval::intern::Interner<'heap>, + pub env: Environment<'heap>, + + pub entrypoint: DefId, + pub artifact: CodeCompilationArtifact<'heap>, +} + +impl<'heap> Compilation<'heap> { + pub fn compile( + heap: &'heap Heap, + scratch: &mut Scratch, + spans: &mut SpanTable, + query: &[u8], + ) -> Status { + // Parse the query + let mut parser = hashql_syntax_jexpr::Parser::new(heap, spans); + let Success { + value: mut ast, + advisories, + } = parser + .parse_expr(query) + .into_status() + .map_category(HashQlDiagnosticCategory::JExpr)?; + + let root_span = ast.span; + + let mut env = Environment::new(heap); + let modules = ModuleRegistry::new(&env); + + // Lower the AST + let Success { + value: types, + advisories, + } = hashql_ast::lowering::lower(heap.intern_symbol("main"), &mut ast, &env, &modules) + .map_category(|category| { + HashQlDiagnosticCategory::Ast(AstDiagnosticCategory::Lowering(category)) + }) + .with_diagnostics(advisories)?; + + let interner = hashql_hir::intern::Interner::new(heap); + let mut hir_context = hashql_hir::context::HirContext::new(&interner, &modules); + + // Reify the HIR from the AST + let Success { + value: hir, + advisories, + } = hashql_hir::node::NodeData::from_ast(ast, &mut hir_context, &types) + .map_category(|category| { + HashQlDiagnosticCategory::Hir(HirDiagnosticCategory::Reification(category)) + }) + .with_diagnostics(advisories)?; + + // Lower the HIR + let Success { + value: hir, + advisories, + } = hashql_hir::lower::lower(hir, &types, &mut env, &mut hir_context) + .map_category(|category| { + HashQlDiagnosticCategory::Hir(HirDiagnosticCategory::Lowering(category)) + }) + .with_diagnostics(advisories)?; + + let interner = hashql_mir::intern::Interner::new(heap); + let mut bodies = DefIdVec::new_in(heap); + let mut mir_context = hashql_mir::context::MirContext { + heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }; + let mut reify_context = hashql_mir::reify::ReifyContext { + bodies: &mut bodies, + mir: &mut mir_context, + hir: &hir_context, + scratch: &*scratch, + }; + + // Reify the MIR from the HIR + let Success { + value: entrypoint, + advisories, + } = hashql_mir::reify::from_hir(hir, &mut reify_context) + .map_category(|category| { + HashQlDiagnosticCategory::Mir(MirDiagnosticCategory::Reify(category)) + }) + .with_diagnostics(advisories)?; + + // Lower the MIR + let Success { + value: (), + advisories, + } = hashql_mir::pass::lower( + &mut mir_context, + scratch, + &mut bodies, + &LowerConfig::default(), + ) + .map_category(HashQlDiagnosticCategory::Mir) + .with_diagnostics(advisories)?; + + // Plan the execution + let Success { + value: execution, + advisories, + } = hashql_mir::pass::place(&mut mir_context, scratch, &mut bodies) + .map_category(HashQlDiagnosticCategory::Mir) + .with_diagnostics(advisories)?; + + // Build the postgres artifacts + let interner = interner.into(); + let mut context = CodeGenerationContext::new_in( + &env, + &interner, + &bodies, + &execution, + heap, + &mut *scratch, + ); + + let mut postgres = PostgresCompiler::new_in(&mut context, &mut *scratch); + let queries = postgres.compile(); + scratch.reset(); + + context + .diagnostics + .into_status(()) + .map_category(HashQlDiagnosticCategory::Eval) + .with_diagnostics(advisories)?; + + Status::success(Self { + heap, + + root_span, + env, + interner, + entrypoint, + artifact: CodeCompilationArtifact { + assignment: execution, + interpreter: bodies, + postgres: queries, + }, + }) + } + + pub(crate) fn context(&self) -> CodeExecutionContext<'_, 'heap, &'heap Heap> { + CodeExecutionContext { + env: &self.env, + interner: &self.interner, + bodies: &self.artifact.interpreter, + execution: &self.artifact.assignment, + alloc: self.heap, + } + } +} diff --git a/libs/@local/graph/api/src/rest/hashql/error.rs b/libs/@local/graph/api/src/rest/hashql/error.rs new file mode 100644 index 00000000000..fc60da91247 --- /dev/null +++ b/libs/@local/graph/api/src/rest/hashql/error.rs @@ -0,0 +1,148 @@ +use alloc::borrow::Cow; +use core::ops::Range; + +use axum::response::{Html, IntoResponse as _}; +use hashql_ast::error::AstDiagnosticCategory; +use hashql_core::span::{SpanId, SpanTable}; +use hashql_diagnostics::{ + DiagnosticCategory, Failure, Sources, Status, Success, + category::{TerminalDiagnosticCategory, canonical_category_id}, + diagnostic::render::{Format, RenderOptions}, + severity::Critical, +}; +use hashql_eval::error::EvalDiagnosticCategory; +use hashql_hir::error::HirDiagnosticCategory; +use hashql_mir::error::MirDiagnosticCategory; +use hashql_syntax_jexpr::{error::JExprDiagnosticCategory, span::Span}; +use http::StatusCode; + +use super::{ + CompilationOutputOptions, + value::{JsonValueSerialize, OwnedValue}, +}; +use crate::rest::{json::Json, status::BoxedResponse}; + +const INFRASTRUCTURE_CATEGORY: TerminalDiagnosticCategory = TerminalDiagnosticCategory { + id: "infrastructure", + name: "Infrastructure", +}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) enum HashQlDiagnosticCategory { + JExpr(JExprDiagnosticCategory), + Ast(AstDiagnosticCategory), + Hir(HirDiagnosticCategory), + Mir(MirDiagnosticCategory), + Eval(EvalDiagnosticCategory), + Infrastructure, +} + +impl serde::Serialize for HashQlDiagnosticCategory { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_str(&canonical_category_id(self)) + } +} + +impl DiagnosticCategory for HashQlDiagnosticCategory { + fn id(&self) -> Cow<'_, str> { + Cow::Borrowed("hashql") + } + + fn name(&self) -> Cow<'_, str> { + Cow::Borrowed("HashQL") + } + + fn subcategory(&self) -> Option<&dyn DiagnosticCategory> { + match self { + Self::JExpr(jexpr) => Some(jexpr), + Self::Ast(ast) => Some(ast), + Self::Hir(hir) => Some(hir), + Self::Mir(mir) => Some(mir), + Self::Eval(eval) => Some(eval), + Self::Infrastructure => Some(&INFRASTRUCTURE_CATEGORY), + } + } +} + +#[derive(Debug, serde::Serialize)] +struct PointerSpan { + pub range: Range, + pub pointer: Option, +} + +impl PointerSpan { + fn resolve(id: SpanId, spans: &SpanTable) -> Option { + let absolute = spans.absolute(id)?; + + let mut pointer = None; + + for ancestor in spans.ancestors(id) { + let Some(span) = spans.get(ancestor) else { + continue; + }; + + if let Some(span_pointer) = &span.pointer { + pointer = Some(span_pointer.as_str().to_owned()); + break; + } + } + + Some(Self { + range: absolute.range().into(), + pointer, + }) + } +} + +pub(crate) fn status_to_response( + status: Status, + sources: &Sources<'_>, + mut spans: &SpanTable, + options: &CompilationOutputOptions, +) -> BoxedResponse { + match status { + Ok(Success { value, advisories }) => { + let advisories = advisories.map_spans(|span| PointerSpan::resolve(span, spans)); + + if options.json_compat { + Json(Success { + value: JsonValueSerialize(&value), + advisories, + }) + .into_response() + .into() + } else { + Json(value).into_response().into() + } + } + Err(Failure { primary, secondary }) => { + let severity = primary.severity; + let status_code = if severity == Critical::ERROR { + StatusCode::BAD_REQUEST + } else { + StatusCode::INTERNAL_SERVER_ERROR + }; + + let mut response = if options.interactive { + let mut diagnostics = secondary.generalize(); + diagnostics.insert_front(primary.generalize()); + + let output = + diagnostics.render(RenderOptions::new(Format::Html, sources), &mut spans); + Html(output).into_response() + } else { + Json(Failure { + primary: Box::new(primary.map_spans(|span| PointerSpan::resolve(span, spans))), + secondary: secondary.map_spans(|span| PointerSpan::resolve(span, spans)), + }) + .into_response() + }; + + *response.status_mut() = status_code; + response.into() + } + } +} diff --git a/libs/@local/graph/api/src/rest/hashql/mod.rs b/libs/@local/graph/api/src/rest/hashql/mod.rs new file mode 100644 index 00000000000..79968d00a32 --- /dev/null +++ b/libs/@local/graph/api/src/rest/hashql/mod.rs @@ -0,0 +1,155 @@ +mod compile; +mod error; +mod value; + +use alloc::sync::Arc; +use core::num::NonZero; +use std::thread::available_parallelism; + +use axum::response::IntoResponse; +use hash_graph_postgres_store::store::PostgresStorePool; +use hash_graph_store::pool::StorePool; +use hash_temporal_client::TemporalClient; +use hashql_core::{ + heap::{HeapPool, ScratchPool}, + span::{SpanId, SpanTable}, +}; +use hashql_diagnostics::{ + Diagnostic, IntoStatus as _, Label, Message, Source, Sources, Status, StatusExt as _, Success, + severity::Critical, +}; +use hashql_eval::{error::EvalDiagnosticCategory, orchestrator::Orchestrator}; +use hashql_mir::interpret::Inputs; +use hashql_syntax_jexpr::span::Span; +use serde_json::value::RawValue; +use tokio_util::task::LocalPoolHandle; + +use self::{ + compile::Compilation, + error::{HashQlDiagnosticCategory, status_to_response}, + value::OwnedValue, +}; +use crate::rest::{json::Json, status::BoxedResponse}; + +pub struct CompilerContext { + pub scratches: ScratchPool, + pub heaps: HeapPool, + pub pool: LocalPoolHandle, +} + +impl CompilerContext { + pub fn new(memory_pool_size: Option, exec_pool_size: Option) -> Self { + let scratches = + memory_pool_size.map_or_else(|| ScratchPool::new(), |size| ScratchPool::bounded(size)); + let heaps = + memory_pool_size.map_or_else(|| HeapPool::new(), |size| HeapPool::bounded(size)); + + let thread_count = exec_pool_size + .unwrap_or_else(|| available_parallelism().map(NonZero::get).unwrap_or(4)); + + let pool = LocalPoolHandle::new(thread_count); + Self { + scratches, + heaps, + pool, + } + } +} + +struct ExecutionContext { + postgres: PostgresStorePool, + temporal: Option>, +} + +pub(crate) struct CompilationOutputOptions { + pub interactive: bool, + pub json_compat: bool, +} + +#[expect(clippy::future_not_send)] +async fn query_local_impl( + ctx: Arc, + exec: ExecutionContext, + spans: &mut SpanTable, + query: &[u8], +) -> Status { + // We're hitting a borrow checker limitation here, we cannot prove that scratch and heap are + // still alive by the time that we `spawn_local`, and due to the fact that it requires `'static` + // it means that we need to build the query inside here, and only then delegate, otherwise we'd + // be able to run it outside. + let mut scratch = ctx.scratches.get(); + let heap = ctx.heaps.get(); + + let inputs = Inputs::new(); // TODO: https://linear.app/hash/issue/BE-41/hashql-expose-input-in-graph-api + + let Success { + value: compilation, + advisories, + } = tokio::task::block_in_place(|| Compilation::compile(&heap, &mut scratch, spans, query))?; + + let context = compilation.context(); + + let Success { + value: client, + advisories, + } = exec + .postgres + .acquire(exec.temporal) + .await + .map_err(|report| { + let mut diagnostic = + Diagnostic::new(HashQlDiagnosticCategory::Infrastructure, Critical::BUG).primary( + Label::new(compilation.root_span, "failed to acquire postgres client"), + ); + + diagnostic.add_message(Message::note(format!("{report:?}"))); + diagnostic + }) + .into_status() + .with_diagnostics(advisories)?; + + let orchestrator = Orchestrator::new(client, &compilation.artifact.postgres, &context); + orchestrator + .run(&inputs, compilation.entrypoint, []) + .await + .into_status() + .map_category(|category| { + HashQlDiagnosticCategory::Eval(EvalDiagnosticCategory::Orchestrator(category)) + }) + .with_diagnostics(advisories) + .map_value(OwnedValue::from) +} + +#[expect(clippy::future_not_send)] +async fn query_local( + ctx: Arc, + exec: ExecutionContext, + query: Arc, + options: CompilationOutputOptions, +) -> BoxedResponse { + let mut sources = Sources::new(); + let source_id = sources.push(Source::new(query.get())); + + let mut spans = SpanTable::new(source_id); + + let status = query_local_impl(ctx, exec, &mut spans, query.get().as_bytes()).await; + status_to_response(status, &sources, &spans, &options) +} + +async fn query( + ctx: Arc, + exec: ExecutionContext, + query: Arc, + options: CompilationOutputOptions, +) -> BoxedResponse { + let pool = ctx.pool.clone(); + let result = pool + .spawn_pinned(|| query_local(ctx, exec, query, options)) + .await; + + result.unwrap_or_else(|_| { + Json(serde_json::json!({"fatal": "internal error: query execution failed"})) + .into_response() + .into() + }) +} diff --git a/libs/@local/graph/api/src/rest/hashql/value.rs b/libs/@local/graph/api/src/rest/hashql/value.rs new file mode 100644 index 00000000000..969fc32ede0 --- /dev/null +++ b/libs/@local/graph/api/src/rest/hashql/value.rs @@ -0,0 +1,123 @@ +use std::{alloc::Allocator, collections::BTreeMap, sync::Arc}; + +use hashql_core::id::Id as _; +use hashql_mir::interpret::value::{Int, Num, Ptr, Value}; +use serde::Serialize as _; + +fn serialize_int(int: &Int, serializer: S) -> Result { + int.as_int().serialize(serializer) +} + +#[expect(clippy::trivially_copy_pass_by_ref)] +fn serialize_num(num: &Num, serializer: S) -> Result { + num.as_f64().serialize(serializer) +} + +#[expect(clippy::trivially_copy_pass_by_ref)] +fn serialize_ptr(ptr: &Ptr, serializer: S) -> Result { + ptr.def().as_u32().serialize(serializer) +} + +// This is only here until https://linear.app/hash/issue/BE-540/hashql-register-based-bytecode-vm +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)] +pub(crate) enum OwnedValue { + /// The unit value. + Unit, + /// An integer value (also represents booleans). + Integer(#[serde(serialize_with = "serialize_int")] Int), + /// A floating-point number. + Number(#[serde(serialize_with = "serialize_num")] Num), + /// A string value. + String(Arc), + /// A function pointer. + Pointer(#[serde(serialize_with = "serialize_ptr")] Ptr), + + /// An opaque/newtype wrapper. + Opaque(Arc, Box), + /// A named-field struct. + Struct(Vec<(Arc, Self)>), + /// A positional tuple. + Tuple(Vec), + /// An ordered list. + List(Vec), + /// An ordered dictionary. + Dict(BTreeMap), +} + +impl<'heap, A: Allocator + Clone> From> for OwnedValue { + fn from(value: Value<'heap, A>) -> Self { + match value { + Value::Unit => Self::Unit, + Value::Integer(int) => Self::Integer(int), + Value::Number(num) => Self::Number(num), + Value::String(str) => Self::String(Arc::from(str.as_str())), + Value::Pointer(ptr) => Self::Pointer(ptr), + Value::Opaque(opaque) => Self::Opaque( + Arc::from(opaque.name().as_str()), + Box::new(opaque.into_value().into()), + ), + Value::Struct(r#struct) => { + debug_assert_eq!(r#struct.fields().len(), r#struct.values().len()); + + Self::Struct( + r#struct + .fields() + .iter() + .zip(r#struct.values()) + .map(|(field, value)| { + (Arc::from(field.as_str()), Self::from(value.clone())) + }) + .collect(), + ) + } + Value::Tuple(tuple) => Self::Tuple( + tuple + .values() + .iter() + .map(|value| Self::from(value.clone())) + .collect(), + ), + Value::List(list) => { + Self::List(list.iter().map(|value| Self::from(value.clone())).collect()) + } + Value::Dict(dict) => Self::Dict( + dict.iter() + .map(|(key, value)| (Self::from(key.clone()), Self::from(value.clone()))) + .collect(), + ), + } + } +} + +#[derive(Copy, Clone)] +pub(crate) struct JsonValueSerialize<'value>(pub &'value OwnedValue); + +impl serde::Serialize for JsonValueSerialize<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self.0 { + OwnedValue::Unit => serializer.serialize_unit(), + OwnedValue::Integer(int) => serialize_int(int, serializer), + OwnedValue::Number(num) => serialize_num(num, serializer), + OwnedValue::String(str) => serde::Serialize::serialize(str.as_ref(), serializer), + OwnedValue::Pointer(ptr) => serialize_ptr(ptr, serializer), + OwnedValue::Opaque(_, owned_value) => { + serde::Serialize::serialize(&Self(owned_value), serializer) + } + OwnedValue::Struct(items) => { + serializer.collect_map(items.iter().map(|(key, value)| (key, Self(value)))) + } + OwnedValue::Tuple(owned_values) => { + serializer.collect_seq(owned_values.iter().map(Self)) + } + OwnedValue::List(owned_values) => serializer.collect_seq(owned_values.iter().map(Self)), + OwnedValue::Dict(btree_map) => serializer.collect_map( + btree_map + .iter() + .map(|(key, value)| (Self(key), Self(value))), + ), + } + } +} diff --git a/libs/@local/hashql/syntax-jexpr/src/error.rs b/libs/@local/hashql/syntax-jexpr/src/error.rs index 068c09189ad..0647bf69d96 100644 --- a/libs/@local/hashql/syntax-jexpr/src/error.rs +++ b/libs/@local/hashql/syntax-jexpr/src/error.rs @@ -1,11 +1,15 @@ use alloc::borrow::Cow; use hashql_core::span::SpanId; -use hashql_diagnostics::{Diagnostic, category::DiagnosticCategory}; +use hashql_diagnostics::{ + Diagnostic, + category::DiagnosticCategory, + severity::{Critical, SeverityKind}, +}; use crate::parser::error::ParserDiagnosticCategory; -pub type JExprDiagnostic = Diagnostic; +pub type JExprDiagnostic = Diagnostic; #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum JExprDiagnosticCategory { @@ -29,7 +33,7 @@ impl DiagnosticCategory for JExprDiagnosticCategory { } /// Extension trait for changing diagnostic categories in results. -pub(crate) trait ResultExt { +pub(crate) trait ResultExt { type Ok; type DiagnosticCategory; type Span; @@ -38,10 +42,15 @@ pub(crate) trait ResultExt { fn change_category( self, category: impl FnOnce(Self::DiagnosticCategory) -> C, - ) -> Result>; + ) -> Result> + where + K: SeverityKind; } -impl ResultExt for Result> { +impl ResultExt for Result> +where + K: SeverityKind, +{ type DiagnosticCategory = C; type Ok = T; type Span = S; @@ -49,7 +58,7 @@ impl ResultExt for Result> { fn change_category( self, category: impl FnOnce(Self::DiagnosticCategory) -> D, - ) -> Result> { + ) -> Result> { self.map_err(|diagnostic| diagnostic.map_category(category)) } } diff --git a/libs/@local/hashql/syntax-jexpr/src/lexer/error.rs b/libs/@local/hashql/syntax-jexpr/src/lexer/error.rs index 365fd51e802..49d6eece405 100644 --- a/libs/@local/hashql/syntax-jexpr/src/lexer/error.rs +++ b/libs/@local/hashql/syntax-jexpr/src/lexer/error.rs @@ -5,7 +5,7 @@ use hashql_diagnostics::{ Diagnostic, Label, category::{DiagnosticCategory, TerminalDiagnosticCategory}, diagnostic::Message, - severity::Severity, + severity::Critical, }; use text_size::TextRange; @@ -16,7 +16,7 @@ use super::{ }; use crate::lexer::syntax_kind_set::Conjunction; -pub(crate) type LexerDiagnostic = Diagnostic; +pub(crate) type LexerDiagnostic = Diagnostic; const INVALID_STRING: TerminalDiagnosticCategory = TerminalDiagnosticCategory { id: "invalid-string", @@ -85,7 +85,7 @@ const CONTROL_CHAR_HELP: &str = pub(crate) fn from_hifijson_str_error( error: &hifijson::str::Error, span: SpanId, -) -> Diagnostic { +) -> LexerDiagnostic { let (message, help) = match error { hifijson::str::Error::Control => ( "Control character not allowed", @@ -102,7 +102,7 @@ pub(crate) fn from_hifijson_str_error( ), }; - let mut diagnostic = Diagnostic::new(LexerDiagnosticCategory::InvalidString, Severity::Error) + let mut diagnostic = Diagnostic::new(LexerDiagnosticCategory::InvalidString, Critical::ERROR) .primary(Label::new(span, message)); if let Some(help) = help { @@ -123,7 +123,7 @@ pub(crate) fn unexpected_eof(span: SpanId, expected: SyntaxKindSet) -> LexerDiag ) }; - let mut diagnostic = Diagnostic::new(LexerDiagnosticCategory::UnexpectedEof, Severity::Error) + let mut diagnostic = Diagnostic::new(LexerDiagnosticCategory::UnexpectedEof, Critical::ERROR) .primary(Label::new(span, label)); // Provide specific help based on what was expected @@ -165,7 +165,7 @@ pub(crate) fn unexpected_token( ) }; - let mut diagnostic = Diagnostic::new(LexerDiagnosticCategory::UnexpectedToken, Severity::Error) + let mut diagnostic = Diagnostic::new(LexerDiagnosticCategory::UnexpectedToken, Critical::ERROR) .primary(Label::new(span, label)); // Provide specific help based on common syntax errors @@ -197,7 +197,7 @@ const INVALID_NUMBER_HELP: &str = "JSON numbers must contain digits and follow t `[-]digits[.digits][(e|E)[+|-]digits]`"; pub(crate) fn from_number_error(error: ParseNumberErrorKind, span: SpanId) -> LexerDiagnostic { - let mut diagnostic = Diagnostic::new(LexerDiagnosticCategory::InvalidNumber, Severity::Error) + let mut diagnostic = Diagnostic::new(LexerDiagnosticCategory::InvalidNumber, Critical::ERROR) .primary(Label::new(span, error.to_string())); diagnostic.add_message(Message::help(INVALID_NUMBER_HELP)); @@ -210,7 +210,7 @@ const UNRECOGNIZED_CHAR_HELP: &str = "J-Expr only supports standard JSON syntax. pub(crate) fn from_unrecognized_character_error(span: SpanId) -> LexerDiagnostic { let mut diagnostic = - Diagnostic::new(LexerDiagnosticCategory::InvalidCharacter, Severity::Error) + Diagnostic::new(LexerDiagnosticCategory::InvalidCharacter, Critical::ERROR) .primary(Label::new(span, "Unrecognized character")); diagnostic.add_message(Message::help(UNRECOGNIZED_CHAR_HELP)); @@ -222,7 +222,7 @@ const INVALID_UTF8_HELP: &str = "J-Expr requires valid UTF-8 encoded input. Chec characters or ensure your source is properly encoded as UTF-8."; pub(crate) fn from_invalid_utf8_error(span: SpanId) -> LexerDiagnostic { - let mut diagnostic = Diagnostic::new(LexerDiagnosticCategory::InvalidUtf8, Severity::Error) + let mut diagnostic = Diagnostic::new(LexerDiagnosticCategory::InvalidUtf8, Critical::ERROR) .primary(Label::new(span, "Invalid UTF-8 byte sequence")); diagnostic.add_message(Message::help(INVALID_UTF8_HELP)); diff --git a/libs/@local/hashql/syntax-jexpr/src/lexer/mod.rs b/libs/@local/hashql/syntax-jexpr/src/lexer/mod.rs index a2b4d0a4851..b664b6a9449 100644 --- a/libs/@local/hashql/syntax-jexpr/src/lexer/mod.rs +++ b/libs/@local/hashql/syntax-jexpr/src/lexer/mod.rs @@ -1,12 +1,11 @@ -use hashql_core::span::{SpanAncestors, SpanId, SpanTable}; -use hashql_diagnostics::Diagnostic; +use hashql_core::span::{SpanAncestors, SpanTable}; use logos::SpannedIter; use text_size::{TextRange, TextSize}; pub(crate) use self::number::Number; use self::{ error::{ - LexerDiagnosticCategory, LexerError, from_hifijson_str_error, from_invalid_utf8_error, + LexerDiagnostic, LexerError, from_hifijson_str_error, from_invalid_utf8_error, from_number_error, from_unrecognized_character_error, }, number::ParseNumberError, @@ -63,7 +62,7 @@ impl<'source> Lexer<'source> { pub(crate) fn advance( &mut self, context: &mut LexerContext, - ) -> Option, Diagnostic>> { + ) -> Option, LexerDiagnostic>> { let (kind, span) = self.inner.next()?; let span = { @@ -131,18 +130,22 @@ impl<'source> Lexer<'source> { #[cfg(test)] mod test { #![expect(clippy::non_ascii_literal)] - use hashql_core::span::{SpanId, SpanTable}; - use hashql_diagnostics::{Diagnostic, source::SourceId}; + use hashql_core::span::SpanTable; + use hashql_diagnostics::source::SourceId; use insta::{assert_snapshot, with_settings}; use text_size::TextRange; - use super::{Lexer, LexerContext, error::LexerDiagnosticCategory, token::Token}; + use super::{ + Lexer, LexerContext, + error::{LexerDiagnostic, LexerDiagnosticCategory}, + token::Token, + }; use crate::{span::Span, test::render_diagnostic}; fn parse<'source>( source: &'source str, spans: &mut SpanTable, - ) -> Result>, Diagnostic> { + ) -> Result>, LexerDiagnostic> { let mut lexer = Lexer::new(source.as_bytes()); let mut tokens = Vec::new(); diff --git a/libs/@local/hashql/syntax-jexpr/src/parser/array/error.rs b/libs/@local/hashql/syntax-jexpr/src/parser/array/error.rs index e587f1587d1..8cab0e3855d 100644 --- a/libs/@local/hashql/syntax-jexpr/src/parser/array/error.rs +++ b/libs/@local/hashql/syntax-jexpr/src/parser/array/error.rs @@ -5,13 +5,13 @@ use hashql_diagnostics::{ Diagnostic, Label, category::{DiagnosticCategory, TerminalDiagnosticCategory}, diagnostic::Message, - severity::Severity, + severity::Critical, }; use winnow::error::ContextError; use crate::{lexer::error::LexerDiagnosticCategory, span::Span}; -pub(crate) type ArrayDiagnostic = Diagnostic; +pub(crate) type ArrayDiagnostic = Diagnostic; const LEADING_COMMA: TerminalDiagnosticCategory = TerminalDiagnosticCategory { id: "leading-comma", @@ -132,7 +132,7 @@ const EMPTY_NOTE: &str = r##"Valid examples: "##; pub(crate) fn empty(span: SpanId) -> ArrayDiagnostic { - let mut diagnostic = Diagnostic::new(ArrayDiagnosticCategory::Empty, Severity::Error) + let mut diagnostic = Diagnostic::new(ArrayDiagnosticCategory::Empty, Critical::ERROR) .primary(Label::new(span, "Empty array not allowed")); diagnostic.add_message(Message::help(EMPTY_HELP)); @@ -147,7 +147,7 @@ const TRAILING_COMMA_HELP: &str = "J-Expr does not support trailing commas in ar pub(crate) fn trailing_commas(spans: &[SpanId]) -> ArrayDiagnostic { let (&first, rest) = spans.split_first().expect("spans must be non-empty"); - let mut diagnostic = Diagnostic::new(ArrayDiagnosticCategory::TrailingComma, Severity::Error) + let mut diagnostic = Diagnostic::new(ArrayDiagnosticCategory::TrailingComma, Critical::ERROR) .primary(Label::new(first, "Remove this trailing comma")); for &span in rest { @@ -167,7 +167,7 @@ const LEADING_COMMA_HELP: &str = pub(crate) fn leading_commas(spans: &[SpanId]) -> ArrayDiagnostic { let (&first, rest) = spans.split_first().expect("spans must be non-empty"); - let mut diagnostic = Diagnostic::new(ArrayDiagnosticCategory::LeadingComma, Severity::Error) + let mut diagnostic = Diagnostic::new(ArrayDiagnosticCategory::LeadingComma, Critical::ERROR) .primary(Label::new(first, "Remove this leading comma")); for &span in rest { @@ -188,7 +188,7 @@ pub(crate) fn consecutive_commas(spans: &[SpanId]) -> ArrayDiagnostic { let (&first, rest) = spans.split_first().expect("spans must be non-empty"); let mut diagnostic = - Diagnostic::new(ArrayDiagnosticCategory::ConsecutiveComma, Severity::Error) + Diagnostic::new(ArrayDiagnosticCategory::ConsecutiveComma, Critical::ERROR) .primary(Label::new(first, "Remove this extra comma")); for &span in rest { @@ -215,7 +215,7 @@ pub(crate) fn labeled_argument_missing_prefix( ) -> ArrayDiagnostic { let mut diagnostic = Diagnostic::new( ArrayDiagnosticCategory::LabeledArgumentMissingPrefix, - Severity::Error, + Critical::ERROR, ) .primary(Label::new(span, "Missing ':' prefix")); @@ -237,7 +237,7 @@ pub(crate) fn labeled_arguments_length_mismatch( ) -> ArrayDiagnostic { let diagnostic = Diagnostic::new( ArrayDiagnosticCategory::LabeledArgumentLengthMismatch, - Severity::Error, + Critical::ERROR, ); let mut diagnostic = if count == 0 { @@ -284,7 +284,7 @@ pub(crate) fn labeled_argument_invalid_identifier( ) -> ArrayDiagnostic { let mut diagnostic = Diagnostic::new( ArrayDiagnosticCategory::LabeledArgumentInvalidIdentifier, - Severity::Error, + Critical::ERROR, ) .primary(Label::new(label_span, "Invalid labeled argument name")); diff --git a/libs/@local/hashql/syntax-jexpr/src/parser/array/visit.rs b/libs/@local/hashql/syntax-jexpr/src/parser/array/visit.rs index 04bf4d0553f..3c224a13c37 100644 --- a/libs/@local/hashql/syntax-jexpr/src/parser/array/visit.rs +++ b/libs/@local/hashql/syntax-jexpr/src/parser/array/visit.rs @@ -1,5 +1,5 @@ use hashql_core::span::SpanId; -use hashql_diagnostics::Diagnostic; +use hashql_diagnostics::{Diagnostic, severity::Critical}; use text_size::TextRange; use crate::{ @@ -38,8 +38,8 @@ pub(crate) fn visit_array<'arena, 'source, 'spans, C>( token: Token<'source>, mut on_item: impl FnMut( &mut ParserState<'arena, 'source, 'spans>, - ) -> Result<(), Diagnostic>, -) -> Result> + ) -> Result<(), Diagnostic>, +) -> Result> where C: From, { diff --git a/libs/@local/hashql/syntax-jexpr/src/parser/complex/mod.rs b/libs/@local/hashql/syntax-jexpr/src/parser/complex/mod.rs index eb3f1e93bf2..0dca8159f52 100644 --- a/libs/@local/hashql/syntax-jexpr/src/parser/complex/mod.rs +++ b/libs/@local/hashql/syntax-jexpr/src/parser/complex/mod.rs @@ -1,5 +1,5 @@ use hashql_core::span::SpanId; -use hashql_diagnostics::Diagnostic; +use hashql_diagnostics::{Diagnostic, severity::Critical}; use crate::{ ParserState, @@ -21,8 +21,8 @@ pub(crate) fn verify_no_repeat( &mut ParserState<'_, '_, '_>, Vec, VerifyState, - ) -> Diagnostic, -) -> Result<(), Diagnostic> + ) -> Diagnostic, +) -> Result<(), Diagnostic> where C: From, { diff --git a/libs/@local/hashql/syntax-jexpr/src/parser/error.rs b/libs/@local/hashql/syntax-jexpr/src/parser/error.rs index 977576d7e02..43f4041fc9d 100644 --- a/libs/@local/hashql/syntax-jexpr/src/parser/error.rs +++ b/libs/@local/hashql/syntax-jexpr/src/parser/error.rs @@ -5,7 +5,7 @@ use hashql_diagnostics::{ Diagnostic, Label, category::{DiagnosticCategory, TerminalDiagnosticCategory}, diagnostic::Message, - severity::Severity, + severity::Critical, }; use super::{ @@ -14,7 +14,7 @@ use super::{ }; use crate::lexer::error::LexerDiagnosticCategory; -pub(crate) type ParserDiagnostic = Diagnostic; +pub(crate) type ParserDiagnostic = Diagnostic; const EXPECTED_LANGUAGE_ITEM: TerminalDiagnosticCategory = TerminalDiagnosticCategory { id: "unexpected-token", @@ -107,7 +107,7 @@ const EXPECTED_EOF_HELP: &str = "Remove this token or check for missing delimiters in the preceding expression"; pub(crate) fn expected_eof(span: SpanId) -> ParserDiagnostic { - let mut diagnostic = Diagnostic::new(ParserDiagnosticCategory::ExpectedEof, Severity::Error) + let mut diagnostic = Diagnostic::new(ParserDiagnosticCategory::ExpectedEof, Critical::ERROR) .primary(Label::new(span, "Extra content after expression")); diagnostic.add_message(Message::help(EXPECTED_EOF_HELP)); diff --git a/libs/@local/hashql/syntax-jexpr/src/parser/object/error.rs b/libs/@local/hashql/syntax-jexpr/src/parser/object/error.rs index 49dbfb2aa8b..8b9174b6299 100644 --- a/libs/@local/hashql/syntax-jexpr/src/parser/object/error.rs +++ b/libs/@local/hashql/syntax-jexpr/src/parser/object/error.rs @@ -5,7 +5,7 @@ use hashql_diagnostics::{ Diagnostic, Label, category::{DiagnosticCategory, TerminalDiagnosticCategory}, diagnostic::Message, - severity::Severity, + severity::Critical, }; use winnow::error::ContextError; @@ -14,7 +14,7 @@ use crate::{ span::Span, }; -pub(crate) type ObjectDiagnostic = Diagnostic; +pub(crate) type ObjectDiagnostic = Diagnostic; const LEADING_COMMA: TerminalDiagnosticCategory = TerminalDiagnosticCategory { id: "leading-comma", @@ -250,7 +250,7 @@ Empty objects don't have semantic meaning in J-Expr. "##; pub(crate) fn empty(span: SpanId) -> ObjectDiagnostic { - let mut diagnostic = Diagnostic::new(ObjectDiagnosticCategory::Empty, Severity::Error) + let mut diagnostic = Diagnostic::new(ObjectDiagnosticCategory::Empty, Critical::ERROR) .primary(Label::new(span, "Add required fields to this object")); diagnostic.add_message(Message::help(EMPTY_HELP)); @@ -264,7 +264,7 @@ const TRAILING_COMMA_HELP: &str = r#"J-Expr does not support trailing commas in pub(crate) fn trailing_commas(spans: &[SpanId]) -> ObjectDiagnostic { let (&first, rest) = spans.split_first().expect("Should have at least one span"); - let mut diagnostic = Diagnostic::new(ObjectDiagnosticCategory::TrailingComma, Severity::Error) + let mut diagnostic = Diagnostic::new(ObjectDiagnosticCategory::TrailingComma, Critical::ERROR) .primary(Label::new(first, "Remove this trailing comma")); for &span in rest { @@ -282,7 +282,7 @@ const LEADING_COMMA_HELP: &str = r#"J-Expr does not support leading commas in ob pub(crate) fn leading_commas(spans: &[SpanId]) -> ObjectDiagnostic { let (&first, rest) = spans.split_first().expect("Should have at least one span"); - let mut diagnostic = Diagnostic::new(ObjectDiagnosticCategory::LeadingComma, Severity::Error) + let mut diagnostic = Diagnostic::new(ObjectDiagnosticCategory::LeadingComma, Critical::ERROR) .primary(Label::new(first, "Remove this leading comma")); for &span in rest { @@ -301,7 +301,7 @@ const CONSECUTIVE_COMMA_HELP: &str = r#"J-Expr requires exactly one comma betwee pub(crate) fn consecutive_commas(spans: &[SpanId]) -> ObjectDiagnostic { let (&first, rest) = spans.split_first().expect("Should have at least one span"); let mut diagnostic = - Diagnostic::new(ObjectDiagnosticCategory::ConsecutiveComma, Severity::Error) + Diagnostic::new(ObjectDiagnosticCategory::ConsecutiveComma, Critical::ERROR) .primary(Label::new(first, "Remove this extra comma")); for &span in rest { @@ -320,7 +320,7 @@ const CONSECUTIVE_COLON_HELP: &str = r#"J-Expr requires exactly one colon betwee pub(crate) fn consecutive_colons(spans: &[SpanId]) -> ObjectDiagnostic { let (&first, rest) = spans.split_first().expect("Expected at least one span"); let mut diagnostic = - Diagnostic::new(ObjectDiagnosticCategory::ConsecutiveColon, Severity::Error) + Diagnostic::new(ObjectDiagnosticCategory::ConsecutiveColon, Critical::ERROR) .primary(Label::new(first, "Remove this extra colon")); for &span in rest { @@ -338,8 +338,8 @@ pub(crate) fn unknown_key( span: SpanId, key: impl AsRef, expected: &[&'static str], -) -> Diagnostic { - let mut diagnostic = Diagnostic::new(ObjectDiagnosticCategory::UnknownKey, Severity::Error) +) -> ObjectDiagnostic { + let mut diagnostic = Diagnostic::new(ObjectDiagnosticCategory::UnknownKey, Critical::ERROR) .primary(Label::new( span, if expected.is_empty() { @@ -386,7 +386,7 @@ pub(crate) fn unknown_key( pub(crate) fn orphaned_type(span: SpanId) -> ObjectDiagnostic { let mut diagnostic = - Diagnostic::new(ObjectDiagnosticCategory::OrphanedType, Severity::Error).primary( + Diagnostic::new(ObjectDiagnosticCategory::OrphanedType, Critical::ERROR).primary( Label::new(span, "Add a primary construct to use with #type"), ); @@ -417,7 +417,7 @@ pub(crate) fn duplicate_key( duplicate_span: SpanId, key: impl AsRef, ) -> ObjectDiagnostic { - let mut diagnostic = Diagnostic::new(ObjectDiagnosticCategory::DuplicateKey, Severity::Error) + let mut diagnostic = Diagnostic::new(ObjectDiagnosticCategory::DuplicateKey, Critical::ERROR) .primary(Label::new(duplicate_span, "Duplicate key")); diagnostic.labels.push(Label::new( @@ -441,7 +441,7 @@ pub(crate) fn struct_key_expected_identifier( ) -> ObjectDiagnostic { let mut diagnostic = Diagnostic::new( ObjectDiagnosticCategory::StructKeyExpectedIdentifier, - Severity::Error, + Critical::ERROR, ) .primary(Label::new(key_span, "Invalid struct field key")); @@ -460,7 +460,7 @@ pub(crate) fn struct_key_expected_identifier( pub(crate) fn dict_entry_expected_array(span: SpanId, found: SyntaxKind) -> ObjectDiagnostic { let mut diagnostic = Diagnostic::new( ObjectDiagnosticCategory::DictEntryExpectedArray, - Severity::Error, + Critical::ERROR, ) .primary(Label::new(span, "Expected an array for dictionary entry")); @@ -483,7 +483,7 @@ const DICT_ENTRY_FORMAT_NOTE: &str = pub(crate) fn dict_entry_too_few_items(span: SpanId, found: usize) -> ObjectDiagnostic { let mut diagnostic = Diagnostic::new( ObjectDiagnosticCategory::DictEntryTooFewItems, - Severity::Error, + Critical::ERROR, ) .primary(Label::new( span, @@ -509,7 +509,7 @@ pub(crate) fn dict_entry_too_many_items(excess_element_spans: &[SpanId]) -> Obje let mut diagnostic = Diagnostic::new( ObjectDiagnosticCategory::DictEntryTooManyItems, - Severity::Error, + Critical::ERROR, ) .primary(Label::new(first, "Remove this element")); @@ -533,7 +533,7 @@ pub(crate) fn dict_entry_too_many_items(excess_element_spans: &[SpanId]) -> Obje pub(crate) fn tuple_expected_array(span: SpanId, found: SyntaxKind) -> ObjectDiagnostic { let mut diagnostic = Diagnostic::new( ObjectDiagnosticCategory::TupleExpectedArray, - Severity::Error, + Critical::ERROR, ) .primary(Label::new(span, "Expected an array here")); @@ -554,7 +554,7 @@ pub(crate) fn tuple_expected_array(span: SpanId, found: SyntaxKind) -> ObjectDia pub(crate) fn list_expected_array(span: SpanId, found: SyntaxKind) -> ObjectDiagnostic { let mut diagnostic = - Diagnostic::new(ObjectDiagnosticCategory::ListExpectedArray, Severity::Error) + Diagnostic::new(ObjectDiagnosticCategory::ListExpectedArray, Critical::ERROR) .primary(Label::new(span, "Expected an array here")); // More specific help with clear guidance @@ -575,7 +575,7 @@ pub(crate) fn list_expected_array(span: SpanId, found: SyntaxKind) -> ObjectDiag pub(crate) fn struct_expected_object(span: SpanId, found: SyntaxKind) -> ObjectDiagnostic { let mut diagnostic = Diagnostic::new( ObjectDiagnosticCategory::StructExpectedObject, - Severity::Error, + Critical::ERROR, ) .primary(Label::new(span, "Expected an object here")); @@ -597,7 +597,7 @@ pub(crate) fn struct_expected_object(span: SpanId, found: SyntaxKind) -> ObjectD pub(crate) fn dict_expected_format(span: SpanId, found: SyntaxKind) -> ObjectDiagnostic { let mut diagnostic = Diagnostic::new( ObjectDiagnosticCategory::DictExpectedFormat, - Severity::Error, + Critical::ERROR, ) .primary(Label::new( span, @@ -623,7 +623,7 @@ pub(crate) fn dict_expected_format(span: SpanId, found: SyntaxKind) -> ObjectDia pub(crate) fn type_expected_string(span: SpanId, found: SyntaxKind) -> ObjectDiagnostic { let mut diagnostic = Diagnostic::new( ObjectDiagnosticCategory::TypeExpectedString, - Severity::Error, + Critical::ERROR, ) .primary(Label::new(span, "Invalid type specification")); @@ -643,7 +643,7 @@ pub(crate) fn type_expected_string(span: SpanId, found: SyntaxKind) -> ObjectDia pub(crate) fn literal_expected_primitive(span: SpanId, found: SyntaxKind) -> ObjectDiagnostic { let mut diagnostic = Diagnostic::new( ObjectDiagnosticCategory::LiteralExpectedPrimitive, - Severity::Error, + Critical::ERROR, ) .primary(Label::new(span, "Invalid literal value in this context")); diff --git a/libs/@local/hashql/syntax-jexpr/src/parser/object/visit.rs b/libs/@local/hashql/syntax-jexpr/src/parser/object/visit.rs index a31534e8c63..bf2d7a4ebb1 100644 --- a/libs/@local/hashql/syntax-jexpr/src/parser/object/visit.rs +++ b/libs/@local/hashql/syntax-jexpr/src/parser/object/visit.rs @@ -1,7 +1,7 @@ use alloc::borrow::Cow; use hashql_core::span::SpanId; -use hashql_diagnostics::Diagnostic; +use hashql_diagnostics::{Diagnostic, severity::Critical}; use text_size::TextRange; use crate::{ @@ -34,8 +34,8 @@ pub(crate) fn visit_object_entry<'arena, 'source, 'spans, C>( on_item: &mut impl FnMut( &mut ParserState<'arena, 'source, 'spans>, Key<'source>, - ) -> Result<(), Diagnostic>, -) -> Result<(), Diagnostic> + ) -> Result<(), Diagnostic>, +) -> Result<(), Diagnostic> where C: From, { @@ -88,8 +88,8 @@ pub(crate) fn visit_object<'arena, 'source, 'spans, C>( mut on_item: impl FnMut( &mut ParserState<'arena, 'source, 'spans>, Key<'source>, - ) -> Result<(), Diagnostic>, -) -> Result> + ) -> Result<(), Diagnostic>, +) -> Result> where C: From, { diff --git a/libs/@local/hashql/syntax-jexpr/src/parser/string/error.rs b/libs/@local/hashql/syntax-jexpr/src/parser/string/error.rs index 9a723b446df..f6ab12b4faa 100644 --- a/libs/@local/hashql/syntax-jexpr/src/parser/string/error.rs +++ b/libs/@local/hashql/syntax-jexpr/src/parser/string/error.rs @@ -6,14 +6,14 @@ use hashql_diagnostics::{ Diagnostic, Label, category::{DiagnosticCategory, TerminalDiagnosticCategory}, diagnostic::Message, - severity::Severity, + severity::Critical, }; use text_size::{TextRange, TextSize}; use winnow::error::{ContextError, StrContext}; use crate::span::Span; -pub(crate) type StringDiagnostic = Diagnostic; +pub(crate) type StringDiagnostic = Diagnostic; const INVALID_EXPR: TerminalDiagnosticCategory = TerminalDiagnosticCategory { id: "invalid-expression", @@ -117,7 +117,7 @@ pub(crate) fn invalid_expr( let (label, expected) = convert_parse_error(spans, parent, (offset, error)); let mut diagnostic = - Diagnostic::new(StringDiagnosticCategory::InvalidExpression, Severity::Error) + Diagnostic::new(StringDiagnosticCategory::InvalidExpression, Critical::ERROR) .primary(label); if let Some(expected) = expected { diff --git a/libs/@local/hashql/syntax-jexpr/src/test.rs b/libs/@local/hashql/syntax-jexpr/src/test.rs index 0575d3c3ff3..793ebb2ea42 100644 --- a/libs/@local/hashql/syntax-jexpr/src/test.rs +++ b/libs/@local/hashql/syntax-jexpr/src/test.rs @@ -3,18 +3,20 @@ use hashql_diagnostics::{ Diagnostic, category::DiagnosticCategory, diagnostic::render::{ColorDepth, Format, RenderOptions}, + severity::SeverityKind, source::{Source, Sources}, }; use crate::span::Span; -pub(crate) fn render_diagnostic( +pub(crate) fn render_diagnostic( source: &str, - diagnostic: &Diagnostic, + diagnostic: &Diagnostic, mut spans: &SpanTable, ) -> String where C: DiagnosticCategory, + K: SeverityKind, { let mut sources = Sources::new(); sources.push(Source::new(source)); From 5f06fd89e9395bfa87a0648072722bc882507f9a Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:52:46 +0200 Subject: [PATCH 16/25] feat: move entity query into its own modul --- libs/@local/graph/api/src/lib.rs | 1 + .../api/src/rest/{entity.rs => entity/mod.rs} | 258 +--------- .../graph/api/src/rest/entity/query/mod.rs | 260 +++++++++- .../api/src/rest/entity/query/request.rs | 447 +++++++++++++++--- 4 files changed, 660 insertions(+), 306 deletions(-) rename libs/@local/graph/api/src/rest/{entity.rs => entity/mod.rs} (66%) diff --git a/libs/@local/graph/api/src/lib.rs b/libs/@local/graph/api/src/lib.rs index 6eced828a20..831c37283c7 100644 --- a/libs/@local/graph/api/src/lib.rs +++ b/libs/@local/graph/api/src/lib.rs @@ -8,6 +8,7 @@ return_type_notation, // Library Features + assert_matches, error_generic_member_access, allocator_api )] diff --git a/libs/@local/graph/api/src/rest/entity.rs b/libs/@local/graph/api/src/rest/entity/mod.rs similarity index 66% rename from libs/@local/graph/api/src/rest/entity.rs rename to libs/@local/graph/api/src/rest/entity/mod.rs index c4e625ffbdf..f06727ba49e 100644 --- a/libs/@local/graph/api/src/rest/entity.rs +++ b/libs/@local/graph/api/src/rest/entity/mod.rs @@ -22,7 +22,6 @@ use hash_graph_store::{ UnexpectedEntityType, UpdateEntityEmbeddingsParams, ValidateEntityComponents, ValidateEntityParams, }, - entity_type::EntityTypeResolveDefinitions, pool::StorePool, query::{NullOrdering, Ordering}, }; @@ -42,9 +41,7 @@ use hash_graph_types::{ }, }; use hash_temporal_client::TemporalClient; -use hashql_core::heap::{Heap, Scratch}; -use serde::{Deserialize as _, Serialize}; -use serde_json::value::RawValue as RawJsonvalue; +use serde::Deserialize as _; use type_system::{ knowledge::{ Confidence, Entity, Property, @@ -68,33 +65,30 @@ use type_system::{ }, value::{ValueMetadata, metadata::ValueProvenance}, }, - ontology::VersionedUrl, - principal::{ - actor::{ActorEntityUuid, ActorType}, - actor_group::WebId, - }, + principal::actor::ActorType, provenance::{Location, OriginProvenance, SourceProvenance, SourceType}, }; -use utoipa::{OpenApi, ToSchema}; -use self::query::request::{QueryEntitiesRequest, QueryEntitySubgraphRequest}; +use self::query::{ + QueryEntitySubgraphResponse, count_entities, query_entities, query_entity_subgraph, + request::{QueryEntitiesRequest, QueryEntitySubgraphRequest}, +}; use crate::rest::{ - ApiConfig, AuthenticatedUserHeader, InteractiveHeader, OpenApiQuery, QueryLogger, + AuthenticatedUserHeader, OpenApiQuery, QueryLogger, json::Json, status::{BoxedResponse, report_to_response}, - utoipa_typedef::subgraph::Subgraph, }; -#[derive(OpenApi)] +#[derive(utoipa::OpenApi)] #[openapi( paths( create_entity, create_entities, validate_entity, has_permission_for_entities, - query_entities, - query_entity_subgraph, - count_entities, + self::query::query_entities, + self::query::query_entity_subgraph, + self::query::count_entities, patch_entity, update_entity_embeddings, diff_entity, @@ -401,236 +395,6 @@ where .map_err(report_to_response) } -#[utoipa::path( - post, - path = "/entities/query", - request_body = QueryEntitiesRequest, - tag = "Entity", - params( - ("X-Authenticated-User-Actor-Id" = ActorEntityUuid, Header, description = "The ID of the actor which is used to authorize the request"), - ("Interactive" = Option, Header, description = "Whether the request is used interactively"), - ("after" = Option, Query, description = "The cursor to start reading from"), - ("limit" = Option, Query, description = "The maximum number of entities to read"), - ), - responses( - ( - status = 200, - content_type = "application/json", - body = QueryEntitiesResponse, - description = "A list of entities that satisfy the given query.", - ), - (status = 422, content_type = "text/plain", description = "Provided query is invalid"), - (status = 500, description = "Store error occurred"), - ) -)] -async fn query_entities( - AuthenticatedUserHeader(actor_id): AuthenticatedUserHeader, - InteractiveHeader(interactive): InteractiveHeader, - store_pool: Extension>, - temporal_client: Extension>>, - Extension(api_config): Extension, - mut query_logger: Option>, - Json(request): Json>, -) -> Result>, BoxedResponse> -where - S: StorePool + Send + Sync, -{ - if let Some(query_logger) = &mut query_logger { - query_logger.capture(actor_id, OpenApiQuery::GetEntities(&request)); - } - - let store = store_pool - .acquire(temporal_client.0) - .await - .map_err(report_to_response)?; - - let request = QueryEntitiesRequest::deserialize(&*request) - .map_err(Report::from) - .map_err(report_to_response)?; - - let params = request - .into_params(api_config) - .attach(hash_status::StatusCode::InvalidArgument) - .map_err(report_to_response)?; - - let response = store - .query_entities(actor_id, params) - .await - .map(Json) - .map_err(report_to_response); - - if let Some(query_logger) = &mut query_logger { - query_logger.send().await.map_err(report_to_response)?; - } - response -} - -#[derive(Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -struct QueryEntitySubgraphResponse<'r> { - subgraph: Subgraph, - #[serde(borrow)] - cursor: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable = false)] - count: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable = false)] - closed_multi_entity_types: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable = false)] - definitions: Option, - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable = false)] - web_ids: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable = false)] - created_by_ids: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable = false)] - edition_created_by_ids: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable = false)] - type_ids: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable = false)] - type_titles: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - #[schema(nullable = false)] - entity_permissions: Option>, -} - -#[utoipa::path( - post, - path = "/entities/query/subgraph", - request_body = QueryEntitySubgraphRequest, - tag = "Entity", - params( - ("X-Authenticated-User-Actor-Id" = ActorEntityUuid, Header, description = "The ID of the actor which is used to authorize the request"), - ("Interactive" = Option, Header, description = "Whether the query is interactive"), - ("after" = Option, Query, description = "The cursor to start reading from"), - ("limit" = Option, Query, description = "The maximum number of entities to read"), - ), - responses( - ( - status = 200, - content_type = "application/json", - body = QueryEntitySubgraphResponse, - description = "A subgraph rooted at entities that satisfy the given query, each resolved to the requested depth.", - ), - (status = 422, content_type = "text/plain", description = "Provided query is invalid"), - (status = 500, description = "Store error occurred"), - ) -)] -async fn query_entity_subgraph( - AuthenticatedUserHeader(actor_id): AuthenticatedUserHeader, - InteractiveHeader(interactive): InteractiveHeader, - store_pool: Extension>, - temporal_client: Extension>>, - Extension(api_config): Extension, - mut query_logger: Option>, - Json(request): Json, -) -> Result>, BoxedResponse> -where - S: StorePool + Send + Sync, -{ - if let Some(query_logger) = &mut query_logger { - query_logger.capture(actor_id, OpenApiQuery::GetEntitySubgraph(&request)); - } - - let store = store_pool - .acquire(temporal_client.0) - .await - .map_err(report_to_response)?; - - let request = QueryEntitySubgraphRequest::deserialize(&request) - .map_err(Report::from) - .map_err(report_to_response)?; - - let params = request - .into_traversal_params(api_config) - .attach(hash_status::StatusCode::InvalidArgument) - .map_err(report_to_response)?; - - let response = store - .query_entity_subgraph(actor_id, params) - .await - .map(|response| { - Json(QueryEntitySubgraphResponse { - subgraph: response.subgraph.into(), - cursor: response.cursor.map(EntityQueryCursor::into_owned), - count: response.count, - closed_multi_entity_types: response.closed_multi_entity_types, - definitions: response.definitions, - web_ids: response.web_ids, - created_by_ids: response.created_by_ids, - edition_created_by_ids: response.edition_created_by_ids, - type_ids: response.type_ids, - type_titles: response.type_titles, - entity_permissions: response.entity_permissions, - }) - }) - .map_err(report_to_response); - if let Some(query_logger) = &mut query_logger { - query_logger.send().await.map_err(report_to_response)?; - } - response -} - -#[utoipa::path( - post, - path = "/entities/query/count", - request_body = CountEntitiesParams, - tag = "Entity", - params( - ("X-Authenticated-User-Actor-Id" = ActorEntityUuid, Header, description = "The ID of the actor which is used to authorize the request"), - - ), - responses( - ( - status = 200, - content_type = "application/json", - body = usize, - ), - (status = 422, content_type = "text/plain", description = "Provided query is invalid"), - (status = 500, description = "Store error occurred"), - ) -)] -async fn count_entities( - AuthenticatedUserHeader(actor_id): AuthenticatedUserHeader, - store_pool: Extension>, - temporal_client: Extension>>, - mut query_logger: Option>, - Json(request): Json, -) -> Result, BoxedResponse> -where - S: StorePool + Send + Sync, -{ - if let Some(query_logger) = &mut query_logger { - query_logger.capture(actor_id, OpenApiQuery::CountEntities(&request)); - } - - let store = store_pool - .acquire(temporal_client.0) - .await - .map_err(report_to_response)?; - - let response = store - .count_entities( - actor_id, - CountEntitiesParams::deserialize(&request) - .map_err(Report::from) - .map_err(report_to_response)?, - ) - .await - .map(Json) - .map_err(report_to_response); - if let Some(query_logger) = &mut query_logger { - query_logger.send().await.map_err(report_to_response)?; - } - response -} - #[utoipa::path( patch, path = "/entities", diff --git a/libs/@local/graph/api/src/rest/entity/query/mod.rs b/libs/@local/graph/api/src/rest/entity/query/mod.rs index 032e5be832a..7b2fafbba0e 100644 --- a/libs/@local/graph/api/src/rest/entity/query/mod.rs +++ b/libs/@local/graph/api/src/rest/entity/query/mod.rs @@ -1,2 +1,258 @@ -pub mod filter; -pub mod request; +pub(crate) mod filter; +pub(crate) mod request; + +use alloc::sync::Arc; +use std::collections::HashMap; + +use axum::Extension; +use error_stack::{Report, ResultExt as _}; +use hash_graph_store::{ + entity::{ + ClosedMultiEntityTypeMap, CountEntitiesParams, EntityPermissions, EntityQueryCursor, + EntityStore as _, QueryEntitiesResponse, + }, + entity_type::EntityTypeResolveDefinitions, + pool::StorePool, +}; +use hash_temporal_client::TemporalClient; +use serde::Deserialize as _; +use serde_json::value::RawValue as RawJsonvalue; +use type_system::{ + knowledge::entity::id::EntityId, + ontology::VersionedUrl, + principal::{actor::ActorEntityUuid, actor_group::WebId}, +}; + +use self::request::{QueryEntitiesRequest, QueryEntitySubgraphRequest}; +use crate::rest::{ + ApiConfig, AuthenticatedUserHeader, OpenApiQuery, QueryLogger, + json::Json, + status::{BoxedResponse, report_to_response}, + utoipa_typedef::subgraph::Subgraph, +}; + +#[utoipa::path( + post, + path = "/entities/query", + request_body = QueryEntitiesRequest, + tag = "Entity", + params( + ("X-Authenticated-User-Actor-Id" = ActorEntityUuid, Header, description = "The ID of the actor which is used to authorize the request"), + ("after" = Option, Query, description = "The cursor to start reading from"), + ("limit" = Option, Query, description = "The maximum number of entities to read"), + ), + responses( + ( + status = 200, + content_type = "application/json", + body = QueryEntitiesResponse, + description = "A list of entities that satisfy the given query.", + ), + (status = 422, content_type = "text/plain", description = "Provided query is invalid"), + (status = 500, description = "Store error occurred"), + ) +)] +pub(super) async fn query_entities( + AuthenticatedUserHeader(actor_id): AuthenticatedUserHeader, + store_pool: Extension>, + temporal_client: Extension>>, + Extension(api_config): Extension, + mut query_logger: Option>, + Json(request): Json>, +) -> Result>, BoxedResponse> +where + S: StorePool + Send + Sync, +{ + if let Some(query_logger) = &mut query_logger { + query_logger.capture(actor_id, OpenApiQuery::GetEntities(&request)); + } + + let store = store_pool + .acquire(temporal_client.0) + .await + .map_err(report_to_response)?; + + let request = QueryEntitiesRequest::deserialize(&*request) + .map_err(Report::from) + .map_err(report_to_response)?; + + let params = request + .into_params(api_config) + .attach(hash_status::StatusCode::InvalidArgument) + .map_err(report_to_response)?; + + let response = store + .query_entities(actor_id, params) + .await + .map(Json) + .map_err(report_to_response); + + if let Some(query_logger) = &mut query_logger { + query_logger.send().await.map_err(report_to_response)?; + } + response +} + +#[derive(serde::Serialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub(super) struct QueryEntitySubgraphResponse<'r> { + subgraph: Subgraph, + #[serde(borrow)] + cursor: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable = false)] + count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable = false)] + closed_multi_entity_types: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable = false)] + definitions: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable = false)] + web_ids: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable = false)] + created_by_ids: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable = false)] + edition_created_by_ids: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable = false)] + type_ids: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable = false)] + type_titles: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(nullable = false)] + entity_permissions: Option>, +} + +#[utoipa::path( + post, + path = "/entities/query/subgraph", + request_body = QueryEntitySubgraphRequest, + tag = "Entity", + params( + ("X-Authenticated-User-Actor-Id" = ActorEntityUuid, Header, description = "The ID of the actor which is used to authorize the request"), + ("after" = Option, Query, description = "The cursor to start reading from"), + ("limit" = Option, Query, description = "The maximum number of entities to read"), + ), + responses( + ( + status = 200, + content_type = "application/json", + body = QueryEntitySubgraphResponse, + description = "A subgraph rooted at entities that satisfy the given query, each resolved to the requested depth.", + ), + (status = 422, content_type = "text/plain", description = "Provided query is invalid"), + (status = 500, description = "Store error occurred"), + ) +)] +pub(super) async fn query_entity_subgraph( + AuthenticatedUserHeader(actor_id): AuthenticatedUserHeader, + store_pool: Extension>, + temporal_client: Extension>>, + Extension(api_config): Extension, + mut query_logger: Option>, + Json(request): Json, +) -> Result>, BoxedResponse> +where + S: StorePool + Send + Sync, +{ + if let Some(query_logger) = &mut query_logger { + query_logger.capture(actor_id, OpenApiQuery::GetEntitySubgraph(&request)); + } + + let store = store_pool + .acquire(temporal_client.0) + .await + .map_err(report_to_response)?; + + let request = QueryEntitySubgraphRequest::deserialize(&request) + .map_err(Report::from) + .map_err(report_to_response)?; + + let params = request + .into_traversal_params(api_config) + .attach(hash_status::StatusCode::InvalidArgument) + .map_err(report_to_response)?; + + let response = store + .query_entity_subgraph(actor_id, params) + .await + .map(|response| { + Json(QueryEntitySubgraphResponse { + subgraph: response.subgraph.into(), + cursor: response.cursor.map(EntityQueryCursor::into_owned), + count: response.count, + closed_multi_entity_types: response.closed_multi_entity_types, + definitions: response.definitions, + web_ids: response.web_ids, + created_by_ids: response.created_by_ids, + edition_created_by_ids: response.edition_created_by_ids, + type_ids: response.type_ids, + type_titles: response.type_titles, + entity_permissions: response.entity_permissions, + }) + }) + .map_err(report_to_response); + if let Some(query_logger) = &mut query_logger { + query_logger.send().await.map_err(report_to_response)?; + } + response +} + +#[utoipa::path( + post, + path = "/entities/query/count", + request_body = CountEntitiesParams, + tag = "Entity", + params( + ("X-Authenticated-User-Actor-Id" = ActorEntityUuid, Header, description = "The ID of the actor which is used to authorize the request"), + + ), + responses( + ( + status = 200, + content_type = "application/json", + body = usize, + ), + (status = 422, content_type = "text/plain", description = "Provided query is invalid"), + (status = 500, description = "Store error occurred"), + ) +)] +pub(super) async fn count_entities( + AuthenticatedUserHeader(actor_id): AuthenticatedUserHeader, + store_pool: Extension>, + temporal_client: Extension>>, + mut query_logger: Option>, + Json(request): Json, +) -> Result, BoxedResponse> +where + S: StorePool + Send + Sync, +{ + if let Some(query_logger) = &mut query_logger { + query_logger.capture(actor_id, OpenApiQuery::CountEntities(&request)); + } + + let store = store_pool + .acquire(temporal_client.0) + .await + .map_err(report_to_response)?; + + let response = store + .count_entities( + actor_id, + CountEntitiesParams::deserialize(&request) + .map_err(Report::from) + .map_err(report_to_response)?, + ) + .await + .map(Json) + .map_err(report_to_response); + if let Some(query_logger) = &mut query_logger { + query_logger.send().await.map_err(report_to_response)?; + } + response +} diff --git a/libs/@local/graph/api/src/rest/entity/query/request.rs b/libs/@local/graph/api/src/rest/entity/query/request.rs index 5cb89770dea..2a7aa25c9a3 100644 --- a/libs/@local/graph/api/src/rest/entity/query/request.rs +++ b/libs/@local/graph/api/src/rest/entity/query/request.rs @@ -102,7 +102,7 @@ fn generate_sorting_paths( reason = "Parameter struct deserialized from JSON" )] #[serde(rename_all = "camelCase")] -pub struct QueryEntitiesRequest<'q, 's, 'p> { +pub(crate) struct QueryEntitiesRequest<'q, 's, 'p> { #[serde(borrow)] filter: Filter<'q, Entity>, @@ -132,12 +132,12 @@ pub struct QueryEntitiesRequest<'q, 's, 'p> { include_permissions: bool, } -impl<'q, 's, 'p> QueryEntitiesRequest<'q, 's, 'p> { +impl<'q, 'p> QueryEntitiesRequest<'q, '_, 'p> { /// # Errors /// /// Returns [`LimitExceededError`] if the requested limit exceeds the configured maximum in /// [`ApiConfig::query_entity_limit`]. - pub fn into_params( + pub(crate) fn into_params( self, config: ApiConfig, ) -> Result, Report> @@ -166,41 +166,11 @@ impl<'q, 's, 'p> QueryEntitiesRequest<'q, 's, 'p> { include_permissions: self.include_permissions, }) } - - /// # Errors - /// - /// Returns [`LimitExceededError`] if the requested limit exceeds the configured maximum in - /// [`ApiConfig::query_entity_limit`]. - pub fn into_traversal_params( - self, - traversal: SubgraphTraversalParams, - config: ApiConfig, - ) -> Result, Report> - where - 'p: 'q, - { - match traversal { - SubgraphTraversalParams::Paths { traversal_paths } => { - Ok(QueryEntitySubgraphParams::Paths { - traversal_paths, - request: self.into_params(config)?, - }) - } - SubgraphTraversalParams::ResolveDepths { - traversal_paths, - graph_resolve_depths, - } => Ok(QueryEntitySubgraphParams::ResolveDepths { - traversal_paths, - graph_resolve_depths, - request: self.into_params(config)?, - }), - } - } } #[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] #[serde(untagged, deny_unknown_fields)] -pub enum QueryEntitySubgraphRequest<'q, 's, 'p> { +pub(crate) enum QueryEntitySubgraphRequest<'q, 's, 'p> { #[serde(rename_all = "camelCase")] ResolveDepths { traversal_paths: Vec, @@ -218,28 +188,7 @@ pub enum QueryEntitySubgraphRequest<'q, 's, 'p> { impl<'q, 's, 'p> QueryEntitySubgraphRequest<'q, 's, 'p> { #[must_use] - pub fn from_parts( - request: QueryEntitiesRequest<'q, 's, 'p>, - traversal_params: SubgraphTraversalParams, - ) -> Self { - match traversal_params { - SubgraphTraversalParams::Paths { traversal_paths } => Self::Paths { - traversal_paths, - request, - }, - SubgraphTraversalParams::ResolveDepths { - traversal_paths, - graph_resolve_depths, - } => Self::ResolveDepths { - request, - traversal_paths, - graph_resolve_depths, - }, - } - } - - #[must_use] - pub fn into_parts(self) -> (QueryEntitiesRequest<'q, 's, 'p>, SubgraphTraversalParams) { + pub(crate) fn into_parts(self) -> (QueryEntitiesRequest<'q, 's, 'p>, SubgraphTraversalParams) { match self { QueryEntitySubgraphRequest::Paths { traversal_paths, @@ -263,7 +212,7 @@ impl<'q, 's, 'p> QueryEntitySubgraphRequest<'q, 's, 'p> { /// /// Returns [`LimitExceededError`] if the requested limit exceeds the configured maximum in /// [`ApiConfig::query_entity_limit`]. - pub fn into_traversal_params( + pub(crate) fn into_traversal_params( self, config: ApiConfig, ) -> Result, Report> @@ -290,3 +239,387 @@ impl<'q, 's, 'p> QueryEntitySubgraphRequest<'q, 's, 'p> { } } } + +#[cfg(test)] +mod tests { + use core::assert_matches; + + use serde_json::json; + + use super::*; + + /// Minimal valid temporal axes for test payloads. + fn temporal_axes() -> serde_json::Value { + json!({ + "pinned": { + "axis": "transactionTime", + "timestamp": null + }, + "variable": { + "axis": "decisionTime", + "interval": { + "start": null, + "end": null + } + } + }) + } + + /// Minimal valid request body shared across tests. + fn base_request() -> String { + json!({ + "filter": { "all": [] }, + "temporalAxes": temporal_axes(), + "includeDrafts": false, + "includePermissions": false + }) + .to_string() + } + + #[test] + fn deserialize_minimal_entity_request() { + let payload = base_request(); + assert_matches!( + serde_json::from_str::>(&payload), + Ok(QueryEntitiesRequest { + include_drafts: false, + include_permissions: false, + limit: None, + sorting_paths: None, + cursor: None, + include_count: false, + include_entity_types: None, + include_web_ids: false, + include_created_by_ids: false, + include_edition_created_by_ids: false, + include_type_ids: false, + include_type_titles: false, + .. + }) + ); + } + + #[test] + fn deserialize_entity_request_with_all_fields() { + let payload = json!({ + "filter": { "all": [] }, + "temporalAxes": temporal_axes(), + "includeDrafts": true, + "includePermissions": true, + "limit": 50, + "includeCount": true, + "includeWebIds": true, + "includeCreatedByIds": true, + "includeEditionCreatedByIds": true, + "includeTypeIds": true, + "includeTypeTitles": true + }) + .to_string(); + assert_matches!( + serde_json::from_str::>(&payload), + Ok(QueryEntitiesRequest { + include_drafts: true, + include_permissions: true, + limit: Some(50), + include_count: true, + include_web_ids: true, + include_created_by_ids: true, + include_edition_created_by_ids: true, + include_type_ids: true, + include_type_titles: true, + .. + }) + ); + } + + #[test] + fn reject_entity_request_missing_filter() { + let payload = json!({ + "temporalAxes": temporal_axes(), + "includeDrafts": false, + "includePermissions": false + }) + .to_string(); + let err = serde_json::from_str::>(&payload) + .expect_err("missing filter should fail") + .to_string(); + assert!(err.starts_with("missing field `filter`"), "{err}"); + } + + #[test] + fn reject_entity_request_missing_temporal_axes() { + let payload = json!({ + "filter": { "all": [] }, + "includeDrafts": false, + "includePermissions": false + }) + .to_string(); + let err = serde_json::from_str::>(&payload) + .expect_err("missing temporalAxes should fail") + .to_string(); + assert!(err.starts_with("missing field `temporalAxes`"), "{err}"); + } + + #[test] + fn reject_entity_request_missing_include_drafts() { + let payload = json!({ + "filter": { "all": [] }, + "temporalAxes": temporal_axes(), + "includePermissions": false + }) + .to_string(); + let err = serde_json::from_str::>(&payload) + .expect_err("missing includeDrafts should fail") + .to_string(); + assert!(err.starts_with("missing field `includeDrafts`"), "{err}"); + } + + #[test] + fn reject_entity_request_missing_include_permissions() { + let payload = json!({ + "filter": { "all": [] }, + "temporalAxes": temporal_axes(), + "includeDrafts": false + }) + .to_string(); + let err = serde_json::from_str::>(&payload) + .expect_err("missing includePermissions should fail") + .to_string(); + assert!( + err.starts_with("missing field `includePermissions`"), + "{err}" + ); + } + + #[test] + fn deserialize_subgraph_paths_variant() { + let payload = json!({ + "traversalPaths": [ + { + "edges": [ + { "kind": "has-left-entity", "direction": "incoming" }, + { "kind": "has-right-entity", "direction": "outgoing" } + ] + } + ], + "filter": { "all": [] }, + "temporalAxes": temporal_axes(), + "includeDrafts": false, + "includePermissions": false + }) + .to_string(); + assert_matches!( + serde_json::from_str::>(&payload), + Ok(QueryEntitySubgraphRequest::Paths { + traversal_paths, + request: QueryEntitiesRequest { include_drafts: false, .. }, + }) if traversal_paths.len() == 1 && traversal_paths[0].edges.len() == 2 + ); + } + + #[test] + fn deserialize_subgraph_resolve_depths_variant() { + let payload = json!({ + "traversalPaths": [], + "graphResolveDepths": { + "inheritsFrom": 0, + "constrainsValuesOn": 0, + "constrainsPropertiesOn": 0, + "constrainsLinksOn": 0, + "constrainsLinkDestinationsOn": 0, + "isOfType": false + }, + "filter": { "all": [] }, + "temporalAxes": temporal_axes(), + "includeDrafts": false, + "includePermissions": false + }) + .to_string(); + assert_matches!( + serde_json::from_str::>(&payload), + Ok(QueryEntitySubgraphRequest::ResolveDepths { + traversal_paths, + graph_resolve_depths: GraphResolveDepths { + inherits_from: 0, + is_of_type: false, + .. + }, + request: QueryEntitiesRequest { include_drafts: false, .. }, + }) if traversal_paths.is_empty() + ); + } + + #[test] + fn reject_subgraph_missing_traversal_paths() { + let payload = json!({ + "filter": { "all": [] }, + "temporalAxes": temporal_axes(), + "includeDrafts": false, + "includePermissions": false + }) + .to_string(); + let err = serde_json::from_str::>(&payload) + .expect_err("missing traversalPaths should fail") + .to_string(); + assert!( + err.starts_with( + "data did not match any variant of untagged enum QueryEntitySubgraphRequest" + ), + "{err}" + ); + } + + #[test] + fn deserialize_filter_request_with_limit_and_count() { + let payload = json!({ + "filter": { "all": [] }, + "temporalAxes": temporal_axes(), + "includeDrafts": false, + "limit": 100, + "includeCount": true, + "includePermissions": false + }) + .to_string(); + assert_matches!( + serde_json::from_str::>(&payload), + Ok(QueryEntitiesRequest { + limit: Some(100), + include_count: true, + .. + }) + ); + } + + #[test] + fn deserialize_subgraph_resolve_depths_with_traversal() { + let payload = json!({ + "filter": { "all": [] }, + "temporalAxes": temporal_axes(), + "graphResolveDepths": { + "inheritsFrom": 255, + "constrainsValuesOn": 255, + "constrainsPropertiesOn": 255, + "constrainsLinksOn": 255, + "constrainsLinkDestinationsOn": 255, + "isOfType": true + }, + "traversalPaths": [ + { + "edges": [ + { "kind": "has-left-entity", "direction": "incoming" }, + { "kind": "has-right-entity", "direction": "outgoing" } + ] + } + ], + "includeDrafts": false, + "includePermissions": false + }) + .to_string(); + assert_matches!( + serde_json::from_str::>(&payload), + Ok(QueryEntitySubgraphRequest::ResolveDepths { + traversal_paths, + graph_resolve_depths: GraphResolveDepths { + inherits_from: 255, + is_of_type: true, + .. + }, + request: QueryEntitiesRequest { include_permissions: false, .. }, + }) if traversal_paths.len() == 1 + ); + } + + #[test] + fn reject_resolve_depths_with_non_entity_edge() { + // If traversalPaths contains an ontology edge (e.g. "is-of-type"), it can't + // deserialize as EntityTraversalPath. The untagged enum must not silently + // fall through to the Paths variant, dropping graphResolveDepths. + let payload = json!({ + "traversalPaths": [ + { + "edges": [ + { "kind": "is-of-type" } + ] + } + ], + "graphResolveDepths": { + "inheritsFrom": 255, + "constrainsValuesOn": 255, + "constrainsPropertiesOn": 255, + "constrainsLinksOn": 255, + "constrainsLinkDestinationsOn": 255, + "isOfType": true + }, + "filter": { "all": [] }, + "temporalAxes": temporal_axes(), + "includeDrafts": false, + "includePermissions": false + }) + .to_string(); + let result = serde_json::from_str::>(&payload); + + match result { + Err(_) => {} // Correctly rejected + Ok(QueryEntitySubgraphRequest::ResolveDepths { .. }) => { + panic!("should not parse ontology edges as EntityTraversalPath"); + } + Ok(QueryEntitySubgraphRequest::Paths { .. }) => { + panic!("silently fell through to Paths variant, dropping graphResolveDepths"); + } + } + } + + #[test] + fn deserialize_paths_with_ontology_edge() { + // Ontology edges (like is-of-type) are valid in TraversalPath but not + // EntityTraversalPath. Without graphResolveDepths, this should parse as Paths. + let payload = json!({ + "traversalPaths": [ + { + "edges": [ + { "kind": "has-left-entity", "direction": "incoming" }, + { "kind": "is-of-type" } + ] + } + ], + "filter": { "all": [] }, + "temporalAxes": temporal_axes(), + "includeDrafts": false, + "includePermissions": false + }) + .to_string(); + assert_matches!( + serde_json::from_str::>(&payload), + Ok(QueryEntitySubgraphRequest::Paths { + traversal_paths, + .. + }) if traversal_paths.len() == 1 && traversal_paths[0].edges.len() == 2 + ); + } + + #[test] + fn deserialize_subgraph_paths_with_traversal() { + let payload = json!({ + "filter": { "all": [] }, + "temporalAxes": temporal_axes(), + "traversalPaths": [ + { + "edges": [ + { "kind": "has-left-entity", "direction": "incoming" }, + { "kind": "has-right-entity", "direction": "outgoing" } + ] + } + ], + "includeDrafts": false, + "includePermissions": false + }) + .to_string(); + assert_matches!( + serde_json::from_str::>(&payload), + Ok(QueryEntitySubgraphRequest::Paths { + traversal_paths, + request: QueryEntitiesRequest { include_permissions: false, .. }, + }) if traversal_paths.len() == 1 && traversal_paths[0].edges.len() == 2 + ); + } +} From e6e3c12cfd34835f6d624bb9120bfe2efac6d242 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:57:05 +0200 Subject: [PATCH 17/25] fix: query request --- libs/@local/graph/api/src/lib.rs | 1 - .../api/src/rest/entity/query/request.rs | 78 +++++++++++++++++-- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/libs/@local/graph/api/src/lib.rs b/libs/@local/graph/api/src/lib.rs index 831c37283c7..6eced828a20 100644 --- a/libs/@local/graph/api/src/lib.rs +++ b/libs/@local/graph/api/src/lib.rs @@ -8,7 +8,6 @@ return_type_notation, // Library Features - assert_matches, error_generic_member_access, allocator_api )] diff --git a/libs/@local/graph/api/src/rest/entity/query/request.rs b/libs/@local/graph/api/src/rest/entity/query/request.rs index 2a7aa25c9a3..286a80bcf38 100644 --- a/libs/@local/graph/api/src/rest/entity/query/request.rs +++ b/libs/@local/graph/api/src/rest/entity/query/request.rs @@ -1,4 +1,4 @@ -use error_stack::Report; +use error_stack::{Report, ResultExt as _}; use hash_graph_store::{ entity::{ EntityQueryCursor, EntityQueryPath, EntityQuerySorting, EntityQuerySortingRecord, @@ -8,7 +8,10 @@ use hash_graph_store::{ filter::Filter, query::Ordering, subgraph::{ - edges::{EntityTraversalPath, GraphResolveDepths, SubgraphTraversalParams, TraversalPath}, + edges::{ + EntityTraversalPath, GraphResolveDepths, MAX_TRAVERSAL_PATHS, SubgraphTraversalParams, + TraversalDepthError, TraversalPath, + }, temporal_axes::QueryTemporalAxesUnresolved, }, }; @@ -16,6 +19,58 @@ use type_system::knowledge::Entity; use crate::rest::{ApiConfig, LimitExceededError, resolve_limit}; +#[derive(Debug, Copy, Clone, PartialEq, Eq, derive_more::Display)] +pub(crate) enum QueryEntitySubgraphError { + #[display("Query limit exceeded")] + Limit, + #[display("Traversal depth exceeded")] + TraversalDepth, + #[display("Resolve depth exceeded")] + ResolveDepth, +} + +impl core::error::Error for QueryEntitySubgraphError {} + +fn validate_traversal( + params: &SubgraphTraversalParams, +) -> Result<(), Report> { + match params { + SubgraphTraversalParams::Paths { traversal_paths } => { + if traversal_paths.len() > MAX_TRAVERSAL_PATHS { + return Err(Report::new(TraversalDepthError::TooManyPaths { + actual: traversal_paths.len(), + max: MAX_TRAVERSAL_PATHS, + }) + .change_context(QueryEntitySubgraphError::TraversalDepth)); + } + for path in traversal_paths { + path.validate() + .change_context(QueryEntitySubgraphError::TraversalDepth)?; + } + } + SubgraphTraversalParams::ResolveDepths { + traversal_paths, + graph_resolve_depths, + } => { + if traversal_paths.len() > MAX_TRAVERSAL_PATHS { + return Err(Report::new(TraversalDepthError::TooManyPaths { + actual: traversal_paths.len(), + max: MAX_TRAVERSAL_PATHS, + }) + .change_context(QueryEntitySubgraphError::TraversalDepth)); + } + for path in traversal_paths { + path.validate() + .change_context(QueryEntitySubgraphError::TraversalDepth)?; + } + graph_resolve_depths + .validate() + .change_context(QueryEntitySubgraphError::ResolveDepth)?; + } + } + Ok(()) +} + #[tracing::instrument(level = "info", skip_all)] fn generate_sorting_paths( paths: Option>>, @@ -210,22 +265,31 @@ impl<'q, 's, 'p> QueryEntitySubgraphRequest<'q, 's, 'p> { /// # Errors /// - /// Returns [`LimitExceededError`] if the requested limit exceeds the configured maximum in - /// [`ApiConfig::query_entity_limit`]. + /// Returns [`QueryEntitySubgraphError`] if: + /// - The requested limit exceeds the configured maximum. + /// - The number of traversal paths exceeds [`MAX_TRAVERSAL_PATHS`]. + /// - Any traversal path exceeds the maximum edge count. + /// - Graph resolve depths exceed the allowed maximum. pub(crate) fn into_traversal_params( self, config: ApiConfig, - ) -> Result, Report> + ) -> Result, Report> where 'p: 'q, { let (request, params) = self.into_parts(); + validate_traversal(¶ms)?; + + let request = request + .into_params(config) + .change_context(QueryEntitySubgraphError::Limit)?; + match params { SubgraphTraversalParams::Paths { traversal_paths } => { Ok(QueryEntitySubgraphParams::Paths { traversal_paths, - request: request.into_params(config)?, + request, }) } SubgraphTraversalParams::ResolveDepths { @@ -234,7 +298,7 @@ impl<'q, 's, 'p> QueryEntitySubgraphRequest<'q, 's, 'p> { } => Ok(QueryEntitySubgraphParams::ResolveDepths { traversal_paths, graph_resolve_depths, - request: request.into_params(config)?, + request, }), } } From 43836676147b76ae1a17c57fe04991663093f6dd Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:05:22 +0200 Subject: [PATCH 18/25] feat: checkpoint (it compiles!) --- libs/@local/graph/api/src/rest/entity/mod.rs | 2 +- .../graph/api/src/rest/entity/query/mod.rs | 4 +- .../api/src/rest/entity/query/request.rs | 65 +- .../api/src/rest/entity_query_request.rs | 906 ------------------ libs/@local/graph/api/src/rest/mod.rs | 1 - .../@local/hashql/compiletest/src/pipeline.rs | 1 + .../manual_queries/entity_queries/mod.rs | 54 +- 7 files changed, 69 insertions(+), 964 deletions(-) delete mode 100644 libs/@local/graph/api/src/rest/entity_query_request.rs diff --git a/libs/@local/graph/api/src/rest/entity/mod.rs b/libs/@local/graph/api/src/rest/entity/mod.rs index f06727ba49e..17e12fb342e 100644 --- a/libs/@local/graph/api/src/rest/entity/mod.rs +++ b/libs/@local/graph/api/src/rest/entity/mod.rs @@ -1,6 +1,6 @@ //! Web routes for CRU operations on entities. -mod query; +pub mod query; use alloc::sync::Arc; use std::collections::HashMap; diff --git a/libs/@local/graph/api/src/rest/entity/query/mod.rs b/libs/@local/graph/api/src/rest/entity/query/mod.rs index 7b2fafbba0e..12c30fdb443 100644 --- a/libs/@local/graph/api/src/rest/entity/query/mod.rs +++ b/libs/@local/graph/api/src/rest/entity/query/mod.rs @@ -23,7 +23,9 @@ use type_system::{ principal::{actor::ActorEntityUuid, actor_group::WebId}, }; -use self::request::{QueryEntitiesRequest, QueryEntitySubgraphRequest}; +pub use self::request::{ + QueryEntitiesRequest, QueryEntitySubgraphError, QueryEntitySubgraphRequest, +}; use crate::rest::{ ApiConfig, AuthenticatedUserHeader, OpenApiQuery, QueryLogger, json::Json, diff --git a/libs/@local/graph/api/src/rest/entity/query/request.rs b/libs/@local/graph/api/src/rest/entity/query/request.rs index 286a80bcf38..4bf9f4292f9 100644 --- a/libs/@local/graph/api/src/rest/entity/query/request.rs +++ b/libs/@local/graph/api/src/rest/entity/query/request.rs @@ -20,7 +20,7 @@ use type_system::knowledge::Entity; use crate::rest::{ApiConfig, LimitExceededError, resolve_limit}; #[derive(Debug, Copy, Clone, PartialEq, Eq, derive_more::Display)] -pub(crate) enum QueryEntitySubgraphError { +pub enum QueryEntitySubgraphError { #[display("Query limit exceeded")] Limit, #[display("Traversal depth exceeded")] @@ -157,34 +157,34 @@ fn generate_sorting_paths( reason = "Parameter struct deserialized from JSON" )] #[serde(rename_all = "camelCase")] -pub(crate) struct QueryEntitiesRequest<'q, 's, 'p> { +pub struct QueryEntitiesRequest<'q, 's, 'p> { #[serde(borrow)] - filter: Filter<'q, Entity>, + pub filter: Filter<'q, Entity>, - temporal_axes: QueryTemporalAxesUnresolved, - include_drafts: bool, - limit: Option, + pub temporal_axes: QueryTemporalAxesUnresolved, + pub include_drafts: bool, + pub limit: Option, #[serde(borrow, default)] - conversions: Vec>, + pub conversions: Vec>, #[serde(borrow)] - sorting_paths: Option>>, + pub sorting_paths: Option>>, #[serde(borrow)] - cursor: Option>, + pub cursor: Option>, #[serde(default)] - include_count: bool, + pub include_count: bool, #[serde(default)] - include_entity_types: Option, + pub include_entity_types: Option, #[serde(default)] - include_web_ids: bool, + pub include_web_ids: bool, #[serde(default)] - include_created_by_ids: bool, + pub include_created_by_ids: bool, #[serde(default)] - include_edition_created_by_ids: bool, + pub include_edition_created_by_ids: bool, #[serde(default)] - include_type_ids: bool, + pub include_type_ids: bool, #[serde(default)] - include_type_titles: bool, - include_permissions: bool, + pub include_type_titles: bool, + pub include_permissions: bool, } impl<'q, 'p> QueryEntitiesRequest<'q, '_, 'p> { @@ -192,7 +192,7 @@ impl<'q, 'p> QueryEntitiesRequest<'q, '_, 'p> { /// /// Returns [`LimitExceededError`] if the requested limit exceeds the configured maximum in /// [`ApiConfig::query_entity_limit`]. - pub(crate) fn into_params( + pub fn into_params( self, config: ApiConfig, ) -> Result, Report> @@ -225,7 +225,7 @@ impl<'q, 'p> QueryEntitiesRequest<'q, '_, 'p> { #[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)] #[serde(untagged, deny_unknown_fields)] -pub(crate) enum QueryEntitySubgraphRequest<'q, 's, 'p> { +pub enum QueryEntitySubgraphRequest<'q, 's, 'p> { #[serde(rename_all = "camelCase")] ResolveDepths { traversal_paths: Vec, @@ -243,7 +243,7 @@ pub(crate) enum QueryEntitySubgraphRequest<'q, 's, 'p> { impl<'q, 's, 'p> QueryEntitySubgraphRequest<'q, 's, 'p> { #[must_use] - pub(crate) fn into_parts(self) -> (QueryEntitiesRequest<'q, 's, 'p>, SubgraphTraversalParams) { + pub fn into_parts(self) -> (QueryEntitiesRequest<'q, 's, 'p>, SubgraphTraversalParams) { match self { QueryEntitySubgraphRequest::Paths { traversal_paths, @@ -263,6 +263,29 @@ impl<'q, 's, 'p> QueryEntitySubgraphRequest<'q, 's, 'p> { } } + #[must_use] + pub fn from_parts( + request: QueryEntitiesRequest<'q, 's, 'p>, + params: SubgraphTraversalParams, + ) -> Self { + match params { + SubgraphTraversalParams::Paths { traversal_paths } => { + QueryEntitySubgraphRequest::Paths { + traversal_paths, + request, + } + } + SubgraphTraversalParams::ResolveDepths { + traversal_paths, + graph_resolve_depths, + } => QueryEntitySubgraphRequest::ResolveDepths { + traversal_paths, + graph_resolve_depths, + request, + }, + } + } + /// # Errors /// /// Returns [`QueryEntitySubgraphError`] if: @@ -270,7 +293,7 @@ impl<'q, 's, 'p> QueryEntitySubgraphRequest<'q, 's, 'p> { /// - The number of traversal paths exceeds [`MAX_TRAVERSAL_PATHS`]. /// - Any traversal path exceeds the maximum edge count. /// - Graph resolve depths exceed the allowed maximum. - pub(crate) fn into_traversal_params( + pub fn into_traversal_params( self, config: ApiConfig, ) -> Result, Report> diff --git a/libs/@local/graph/api/src/rest/entity_query_request.rs b/libs/@local/graph/api/src/rest/entity_query_request.rs deleted file mode 100644 index 33f0dd18e40..00000000000 --- a/libs/@local/graph/api/src/rest/entity_query_request.rs +++ /dev/null @@ -1,906 +0,0 @@ -//! Request types for entity queries. -//! -//! Contains the deserialization structs for both simple entity queries and subgraph requests. -//! Some design choices may look odd due to serde/OpenAPI limitations we need to work around: -//! -//! - Uses proxy structs for deserialization because `RawValue` doesn't play nice with `untagged` + -//! `deny_unknown_fields` (forces intermediate representation). -//! - Subgraph enum has 4 variants instead of nested structs because openapi-generator uses `&` -//! instead of `|` for nested `oneOf` constraints. -//! - Outer enum instead of nested enum because utoipa generates `allOf` constraints (merges all -//! fields into one type). With discriminator on the outer edge we get `oneOf` (proper union), but -//! openapi-generator can't handle nested oneOf and merges them anyway - so we flatten everything -//! - Lots of boolean fields instead of option structs for the same reason -//! -//! When changing any of these types, make sure that the OpenAPI generator types do not degenerate -//! into any of these cases. -use alloc::borrow::Cow; -use core::{cmp, ops::Range}; - -use axum::{ - Json, - response::{Html, IntoResponse as _}, -}; -use error_stack::Report; -use hash_graph_store::{ - entity::{ - EntityQueryCursor, EntityQueryPath, EntityQuerySorting, EntityQuerySortingRecord, - QueryConversion, QueryEntitiesParams, QueryEntitySubgraphParams, - }, - entity_type::IncludeEntityTypeOption, - filter::Filter, - query::Ordering, - subgraph::{ - edges::{ - EntityTraversalPath, GraphResolveDepths, MAX_TRAVERSAL_PATHS, - ResolveDepthExceededError, SubgraphTraversalParams, SubgraphTraversalValidationError, - TraversalDepthError, TraversalPath, TraversalPathConversionError, - }, - temporal_axes::QueryTemporalAxesUnresolved, - }, -}; -use hashql_ast::error::AstDiagnosticCategory; -use hashql_core::{ - heap::{Heap, Scratch}, - module::ModuleRegistry, - span::{SpanId, SpanTable}, - r#type::environment::Environment, -}; -use hashql_diagnostics::{ - DiagnosticIssues, Failure, Severity, Status, StatusExt as _, Success, - category::{DiagnosticCategory, canonical_category_id}, - diagnostic::render::{Format, RenderOptions}, - severity::Critical, - source::{DiagnosticSpan, Source, SourceId, Sources}, -}; -use hashql_eval::error::EvalDiagnosticCategory; -use hashql_hir::error::HirDiagnosticCategory; -use hashql_mir::{def::DefIdVec, error::MirDiagnosticCategory}; -use hashql_syntax_jexpr::{error::JExprDiagnosticCategory, span::Span}; -use http::StatusCode; -use serde::Deserialize; -use serde_json::value::RawValue as RawJsonValue; -use type_system::knowledge::Entity; -use utoipa::ToSchema; - -use super::{ApiConfig, LimitExceededError, resolve_limit, status::BoxedResponse}; - -#[tracing::instrument(level = "info", skip_all)] -fn generate_sorting_paths( - paths: Option>>, - temporal_axes: &QueryTemporalAxesUnresolved, -) -> Vec> { - let temporal_axes_sorting_path = match temporal_axes { - QueryTemporalAxesUnresolved::TransactionTime { .. } => &EntityQueryPath::TransactionTime, - QueryTemporalAxesUnresolved::DecisionTime { .. } => &EntityQueryPath::DecisionTime, - }; - - paths - .map_or_else( - || { - vec![ - EntityQuerySortingRecord { - path: temporal_axes_sorting_path.clone(), - ordering: Ordering::Descending, - nulls: None, - }, - EntityQuerySortingRecord { - path: EntityQueryPath::Uuid, - ordering: Ordering::Ascending, - nulls: None, - }, - EntityQuerySortingRecord { - path: EntityQueryPath::WebId, - ordering: Ordering::Ascending, - nulls: None, - }, - ] - }, - |mut paths| { - let mut has_temporal_axis = false; - let mut has_uuid = false; - let mut has_web_id = false; - - for path in &paths { - if path.path == EntityQueryPath::TransactionTime - || path.path == EntityQueryPath::DecisionTime - { - has_temporal_axis = true; - } - if path.path == EntityQueryPath::Uuid { - has_uuid = true; - } - if path.path == EntityQueryPath::WebId { - has_web_id = true; - } - } - - if !has_temporal_axis { - paths.push(EntityQuerySortingRecord { - path: temporal_axes_sorting_path.clone(), - ordering: Ordering::Descending, - nulls: None, - }); - } - if !has_uuid { - paths.push(EntityQuerySortingRecord { - path: EntityQueryPath::Uuid, - ordering: Ordering::Ascending, - nulls: None, - }); - } - if !has_web_id { - paths.push(EntityQuerySortingRecord { - path: EntityQueryPath::WebId, - ordering: Ordering::Ascending, - nulls: None, - }); - } - - paths - }, - ) - .into_iter() - .map(EntityQuerySortingRecord::into_owned) - .collect() -} - -/// Internal deserialization proxy for `QueryEntitiesRequest`. -/// -/// This struct is necessary because [`RawJsonValue`] cannot be used directly with -/// `#[serde(untagged, deny_unknown_fields)]` - these attributes force deserialization into an -/// intermediate representation, which cannot deserialize into a [`RawJsonValue`] as it materializes -/// the content. -/// -/// See and for more details. -#[derive(Debug, Clone, Deserialize)] -#[expect( - clippy::struct_excessive_bools, - reason = "Parameter struct deserialized from JSON" -)] -#[serde(rename_all = "camelCase")] -struct FlatQueryEntitiesRequestData<'q, 's, 'p> { - // `QueryEntitiesQuery::Filter` - #[serde(borrow)] - filter: Option>, - // `QueryEntitiesQuery::Query`, - #[serde(borrow)] - query: Option<&'q RawJsonValue>, - - // `QueryEntitiesRequest` - temporal_axes: QueryTemporalAxesUnresolved, - include_drafts: bool, - limit: Option, - #[serde(borrow, default)] - conversions: Vec>, - #[serde(borrow)] - sorting_paths: Option>>, - #[serde(borrow)] - cursor: Option>, - #[serde(default)] - include_count: bool, - #[serde(default)] - include_entity_types: Option, - #[serde(default)] - include_web_ids: bool, - #[serde(default)] - include_created_by_ids: bool, - #[serde(default)] - include_edition_created_by_ids: bool, - #[serde(default)] - include_type_ids: bool, - #[serde(default)] - include_type_titles: bool, - include_permissions: bool, - - traversal_paths: Option>, - graph_resolve_depths: Option, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub(crate) struct CompilationOptions { - pub interactive: bool, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -enum HashQLDiagnosticCategory { - JExpr(JExprDiagnosticCategory), - Ast(AstDiagnosticCategory), - Hir(HirDiagnosticCategory), - Mir(MirDiagnosticCategory), - Eval(EvalDiagnosticCategory), -} - -impl serde::Serialize for HashQLDiagnosticCategory { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.collect_str(&canonical_category_id(self)) - } -} - -impl DiagnosticCategory for HashQLDiagnosticCategory { - fn id(&self) -> Cow<'_, str> { - Cow::Borrowed("hashql") - } - - fn name(&self) -> Cow<'_, str> { - Cow::Borrowed("HashQL") - } - - fn subcategory(&self) -> Option<&dyn DiagnosticCategory> { - match self { - Self::JExpr(jexpr) => Some(jexpr), - Self::Ast(ast) => Some(ast), - Self::Hir(hir) => Some(hir), - Self::Mir(mir) => Some(mir), - Self::Eval(eval) => Some(eval), - } - } -} - -#[derive(Debug, serde::Serialize)] -struct ResolvedSpan { - pub range: Range, - pub pointer: Option, -} - -fn resolve_span(id: SpanId, mut spans: &SpanTable) -> Option { - let absolute = DiagnosticSpan::absolute(&id, &mut spans)?; - let mut pointer = spans.get(id)?.pointer.as_ref().map(ToString::to_string); - - for ancestor in spans.ancestors(id) { - let Some(ancestor) = spans.get(ancestor) else { - continue; - }; - - if pointer.is_none() - && let Some(ancestor_pointer) = &ancestor.pointer - { - pointer = Some(ancestor_pointer.to_string()); - } - } - - Some(ResolvedSpan { - range: absolute.range().into(), - pointer, - }) -} - -fn issues_to_response( - issues: DiagnosticIssues, - severity: Severity, - source: &str, - mut spans: &SpanTable, - options: CompilationOptions, -) -> BoxedResponse { - let status_code = match severity { - Severity::Bug | Severity::Fatal => StatusCode::INTERNAL_SERVER_ERROR, - Severity::Error => StatusCode::BAD_REQUEST, - Severity::Warning | Severity::Note | Severity::Debug => StatusCode::CONFLICT, - }; - - let mut sources = Sources::new(); - sources.push(Source::new(source)); - - let mut response = if options.interactive { - let output = issues.render(RenderOptions::new(Format::Html, &sources), &mut spans); - - Html(output).into_response() - } else { - let diagnostics: Vec<_> = issues - .into_iter() - .map(|diagnostic| diagnostic.map_spans(|span| resolve_span(span, spans))) - .collect(); - - Json(diagnostics).into_response() - }; - - *response.status_mut() = status_code; - response.into() -} - -fn failure_to_response( - failure: Failure, - source: &str, - spans: &SpanTable, - options: CompilationOptions, -) -> BoxedResponse { - // Find the highest diagnostic level - let severity = cmp::max( - failure - .secondary - .iter() - .map(|diagnostic| diagnostic.severity) - .max() - .unwrap_or(Severity::Debug), - failure.primary.severity.into(), - ); - - issues_to_response(failure.into_issues(), severity, source, spans, options) -} - -#[derive(Debug, Clone)] -#[expect(clippy::large_enum_variant)] -pub enum EntityQuery<'q> { - Filter { filter: Filter<'q, Entity> }, - Query { query: &'q RawJsonValue }, -} - -impl<'q> EntityQuery<'q> { - fn compile_query<'heap>( - heap: &'heap Heap, - scratch: &mut Scratch, - spans: &mut SpanTable, - query: &RawJsonValue, - ) -> Status, HashQLDiagnosticCategory, SpanId> { - unimplemented!() - } - - /// Compiles a query into an executable entity filter. - /// - /// Transforms the query representation into a [`Filter`] that can be executed - /// against the entity store. For already-compiled filter queries, this returns - /// the filter directly. For raw HashQL queries, it parses and compiles them using - /// the provided `heap` arena allocator. - /// - /// # Errors - /// - /// Returns an error if the HashQL query cannot be compiled. - pub(crate) fn compile( - self, - heap: &'q Heap, - scratch: &mut Scratch, - options: CompilationOptions, - ) -> Result, BoxedResponse> { - match self { - EntityQuery::Filter { filter } => Ok(filter), - EntityQuery::Query { query } => { - let mut spans = SpanTable::new(SourceId::new_unchecked(0x00)); - - let Success { - value: filter, - advisories, - } = Self::compile_query(heap, scratch, &mut spans, query).map_err(|failure| { - failure_to_response(failure, query.get(), &spans, options) - })?; - if !advisories.is_empty() { - // This isn't perfect, what we'd want instead is to return it alongside the - // response, the problem with that approach is just how: we'd need to adjust the - // return type, and respect interactive. Returning warnings before so that user - // can fix them before trying again seems to be the best approach for now. - return Err(issues_to_response( - advisories.generalize(), - Severity::Warning, - query.get(), - &spans, - options, - )); - } - - Ok(filter) - } - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, derive_more::Display)] -pub enum EntityQueryOptionsError { - #[display( - "Field '{field}' is only valid in subgraph requests. Use the subgraph endpoint instead." - )] - InvalidFieldForEntityQuery { field: &'static str }, - #[display( - "Field '{field}' is only valid in entity and subgraph requests. Use the entity endpoint \ - instead." - )] - InvalidFieldForEntityOptions { field: &'static str }, -} - -impl core::error::Error for EntityQueryOptionsError {} - -#[derive(Debug, Clone, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -#[expect( - clippy::struct_excessive_bools, - reason = "Parameter struct deserialized from JSON" -)] -pub struct EntityQueryOptions<'s, 'p> { - pub temporal_axes: QueryTemporalAxesUnresolved, - pub include_drafts: bool, - pub limit: Option, - #[serde(borrow, default)] - pub conversions: Vec>, - #[serde(borrow)] - pub sorting_paths: Option>>, - #[serde(borrow)] - pub cursor: Option>, - #[serde(default)] - pub include_count: bool, - #[serde(default)] - pub include_entity_types: Option, - #[serde(default)] - pub include_web_ids: bool, - #[serde(default)] - pub include_created_by_ids: bool, - #[serde(default)] - pub include_edition_created_by_ids: bool, - #[serde(default)] - pub include_type_ids: bool, - #[serde(default)] - pub include_type_titles: bool, - pub include_permissions: bool, -} - -impl<'q, 's, 'p> TryFrom> for EntityQueryOptions<'s, 'p> { - type Error = EntityQueryOptionsError; - - fn try_from(value: FlatQueryEntitiesRequestData<'q, 's, 'p>) -> Result { - let FlatQueryEntitiesRequestData { - filter, - query, - temporal_axes, - include_drafts, - limit, - conversions, - sorting_paths, - cursor, - include_count, - include_entity_types, - include_web_ids, - include_created_by_ids, - include_edition_created_by_ids, - include_type_ids, - include_type_titles, - include_permissions, - graph_resolve_depths, - traversal_paths, - } = value; - - if filter.is_some() { - return Err(EntityQueryOptionsError::InvalidFieldForEntityOptions { field: "filter" }); - } - - if query.is_some() { - return Err(EntityQueryOptionsError::InvalidFieldForEntityOptions { field: "query" }); - } - - if graph_resolve_depths.is_some() { - return Err(EntityQueryOptionsError::InvalidFieldForEntityQuery { - field: "graphResolveDepths", - }); - } - - if traversal_paths.is_some() { - return Err(EntityQueryOptionsError::InvalidFieldForEntityQuery { - field: "traversalPaths", - }); - } - - Ok(Self { - temporal_axes, - include_drafts, - limit, - conversions, - sorting_paths, - cursor, - include_count, - include_entity_types, - include_web_ids, - include_created_by_ids, - include_edition_created_by_ids, - include_type_ids, - include_type_titles, - include_permissions, - }) - } -} - -impl<'p> EntityQueryOptions<'_, 'p> { - /// # Errors - /// - /// Returns [`LimitExceededError`] if the requested limit exceeds the configured maximum in - /// [`ApiConfig::query_entity_limit`]. - pub fn into_params<'f>( - self, - filter: Filter<'f, Entity>, - config: ApiConfig, - ) -> Result, Report> - where - 'p: 'f, - { - let limit = resolve_limit(self.limit, config.query_entity_limit)?; - - Ok(QueryEntitiesParams { - filter, - sorting: EntityQuerySorting { - paths: generate_sorting_paths(self.sorting_paths, &self.temporal_axes), - cursor: self.cursor.map(EntityQueryCursor::into_owned), - }, - limit, - conversions: self.conversions, - include_drafts: self.include_drafts, - include_count: self.include_count, - include_entity_types: self.include_entity_types, - temporal_axes: self.temporal_axes, - include_web_ids: self.include_web_ids, - include_created_by_ids: self.include_created_by_ids, - include_edition_created_by_ids: self.include_edition_created_by_ids, - include_type_ids: self.include_type_ids, - include_type_titles: self.include_type_titles, - include_permissions: self.include_permissions, - }) - } - - /// # Errors - /// - /// Returns [`LimitExceededError`] if the requested limit exceeds the configured maximum in - /// [`ApiConfig::query_entity_limit`]. - pub fn into_traversal_params<'q>( - self, - filter: Filter<'q, Entity>, - traversal: SubgraphTraversalParams, - config: ApiConfig, - ) -> Result, Report> - where - 'p: 'q, - { - match traversal { - SubgraphTraversalParams::Paths { traversal_paths } => { - Ok(QueryEntitySubgraphParams::Paths { - traversal_paths, - request: self.into_params(filter, config)?, - }) - } - SubgraphTraversalParams::ResolveDepths { - traversal_paths, - graph_resolve_depths, - } => Ok(QueryEntitySubgraphParams::ResolveDepths { - traversal_paths, - graph_resolve_depths, - request: self.into_params(filter, config)?, - }), - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, derive_more::Display, derive_more::From)] -enum QueryEntitiesRequestError { - #[from] - RequestOptions(EntityQueryOptionsError), - #[display("Missing required query parameter. Provide either 'filter' or 'query'.")] - MissingQueryParameter, - #[display("Conflicting query parameters. Provide either 'filter' or 'query', not both.")] - ConflictingQueryParameters, -} - -impl core::error::Error for QueryEntitiesRequestError {} - -#[derive(Debug, Clone, Deserialize, ToSchema)] -#[serde( - untagged, - try_from = "FlatQueryEntitiesRequestData", - deny_unknown_fields -)] -#[expect(clippy::large_enum_variant)] -pub enum QueryEntitiesRequest<'q, 's, 'p> { - #[serde(rename_all = "camelCase")] - Query { - #[serde(borrow)] - #[schema(value_type = utoipa::openapi::schema::Value)] - query: &'q RawJsonValue, - #[serde(borrow, flatten)] - options: EntityQueryOptions<'s, 'p>, - }, - #[serde(rename_all = "camelCase")] - Filter { - #[serde(borrow)] - filter: Filter<'q, Entity>, - #[serde(borrow, flatten)] - options: EntityQueryOptions<'s, 'p>, - }, -} - -impl<'q, 's, 'p> TryFrom> - for QueryEntitiesRequest<'q, 's, 'p> -{ - type Error = QueryEntitiesRequestError; - - fn try_from(mut value: FlatQueryEntitiesRequestData<'q, 's, 'p>) -> Result { - let filter = value.filter.take(); - let query = value.query.take(); - - match (filter, query) { - (None, None) => Err(QueryEntitiesRequestError::MissingQueryParameter), - (Some(_), Some(_)) => Err(QueryEntitiesRequestError::ConflictingQueryParameters), - (Some(filter), None) => Ok(Self::Filter { - filter, - options: value.try_into()?, - }), - (None, Some(query)) => Ok(Self::Query { - query, - options: value.try_into()?, - }), - } - } -} - -impl<'q, 's, 'p> QueryEntitiesRequest<'q, 's, 'p> { - #[must_use] - pub fn from_parts(query: EntityQuery<'q>, options: EntityQueryOptions<'s, 'p>) -> Self { - match query { - EntityQuery::Filter { filter } => Self::Filter { filter, options }, - EntityQuery::Query { query } => Self::Query { query, options }, - } - } - - #[must_use] - pub fn into_parts(self) -> (EntityQuery<'q>, EntityQueryOptions<'s, 'p>) { - match self { - QueryEntitiesRequest::Query { query, options } => { - (EntityQuery::Query { query }, options) - } - QueryEntitiesRequest::Filter { filter, options } => { - (EntityQuery::Filter { filter }, options) - } - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, derive_more::Display, derive_more::From)] -enum QueryEntitySubgraphRequestError { - #[from] - QueryEntityRequest(QueryEntitiesRequestError), - #[from] - UnsupportedGraphTraversalPath(TraversalPathConversionError), - #[display( - "Subgraph request missing traversal parameters. Specify either 'traversalPaths` and \ - optionally `graphResolveDepths'." - )] - MissingSubgraphTraversal, - #[from] - TraversalValidation(SubgraphTraversalValidationError), -} - -impl core::error::Error for QueryEntitySubgraphRequestError {} - -impl From for QueryEntitySubgraphRequestError { - fn from(err: TraversalDepthError) -> Self { - Self::TraversalValidation(err.into()) - } -} - -impl From for QueryEntitySubgraphRequestError { - fn from(err: ResolveDepthExceededError) -> Self { - Self::TraversalValidation(err.into()) - } -} - -#[derive(Debug, Clone, Deserialize, ToSchema)] -#[serde( - untagged, - try_from = "FlatQueryEntitiesRequestData", - deny_unknown_fields -)] -pub enum QueryEntitySubgraphRequest<'q, 's, 'p> { - #[serde(rename_all = "camelCase")] - ResolveDepthsWithQuery { - #[serde(borrow)] - #[schema(value_type = utoipa::openapi::schema::Value)] - query: &'q RawJsonValue, - traversal_paths: Vec, - graph_resolve_depths: GraphResolveDepths, - #[serde(borrow, flatten)] - options: EntityQueryOptions<'s, 'p>, - }, - #[serde(rename_all = "camelCase")] - ResolveDepthsWithFilter { - #[serde(borrow)] - filter: Filter<'q, Entity>, - traversal_paths: Vec, - graph_resolve_depths: GraphResolveDepths, - #[serde(borrow, flatten)] - options: EntityQueryOptions<'s, 'p>, - }, - #[serde(rename_all = "camelCase")] - PathsWithQuery { - #[serde(borrow)] - #[schema(value_type = utoipa::openapi::schema::Value)] - query: &'q RawJsonValue, - traversal_paths: Vec, - #[serde(borrow, flatten)] - options: EntityQueryOptions<'s, 'p>, - }, - #[serde(rename_all = "camelCase")] - PathsWithFilter { - #[serde(borrow)] - filter: Filter<'q, Entity>, - traversal_paths: Vec, - #[serde(borrow, flatten)] - options: EntityQueryOptions<'s, 'p>, - }, -} - -impl<'q, 's, 'p> TryFrom> - for QueryEntitySubgraphRequest<'q, 's, 'p> -{ - type Error = QueryEntitySubgraphRequestError; - - fn try_from(mut value: FlatQueryEntitiesRequestData<'q, 's, 'p>) -> Result { - let graph_resolve_depths = value.graph_resolve_depths.take(); - let traversal_paths = value - .traversal_paths - .take() - .ok_or(QueryEntitySubgraphRequestError::MissingSubgraphTraversal)?; - - if traversal_paths.len() > MAX_TRAVERSAL_PATHS { - return Err(TraversalDepthError::TooManyPaths { - actual: traversal_paths.len(), - max: MAX_TRAVERSAL_PATHS, - } - .into()); - } - - let request = value.try_into()?; - - match graph_resolve_depths { - None => { - for path in &traversal_paths { - path.validate()?; - } - match request { - QueryEntitiesRequest::Filter { filter, options } => { - Ok(QueryEntitySubgraphRequest::PathsWithFilter { - traversal_paths, - filter, - options, - }) - } - QueryEntitiesRequest::Query { query, options } => { - Ok(QueryEntitySubgraphRequest::PathsWithQuery { - traversal_paths, - query, - options, - }) - } - } - } - Some(graph_resolve_depths) => { - let entity_paths: Vec = traversal_paths - .into_iter() - .map(EntityTraversalPath::try_from) - .collect::>()?; - for path in &entity_paths { - path.validate()?; - } - graph_resolve_depths.validate()?; - match request { - QueryEntitiesRequest::Filter { filter, options } => { - Ok(QueryEntitySubgraphRequest::ResolveDepthsWithFilter { - traversal_paths: entity_paths, - graph_resolve_depths, - filter, - options, - }) - } - QueryEntitiesRequest::Query { query, options } => { - Ok(QueryEntitySubgraphRequest::ResolveDepthsWithQuery { - traversal_paths: entity_paths, - graph_resolve_depths, - query, - options, - }) - } - } - } - } - } -} - -impl<'q, 's, 'p> QueryEntitySubgraphRequest<'q, 's, 'p> { - #[must_use] - pub fn from_parts( - query: EntityQuery<'q>, - options: EntityQueryOptions<'s, 'p>, - traversal_params: SubgraphTraversalParams, - ) -> Self { - match (query, traversal_params) { - ( - EntityQuery::Filter { filter }, - SubgraphTraversalParams::Paths { traversal_paths }, - ) => Self::PathsWithFilter { - filter, - options, - traversal_paths, - }, - (EntityQuery::Query { query }, SubgraphTraversalParams::Paths { traversal_paths }) => { - Self::PathsWithQuery { - query, - traversal_paths, - options, - } - } - ( - EntityQuery::Filter { filter }, - SubgraphTraversalParams::ResolveDepths { - traversal_paths, - graph_resolve_depths, - }, - ) => Self::ResolveDepthsWithFilter { - filter, - options, - traversal_paths, - graph_resolve_depths, - }, - ( - EntityQuery::Query { query }, - SubgraphTraversalParams::ResolveDepths { - traversal_paths, - graph_resolve_depths, - }, - ) => Self::ResolveDepthsWithQuery { - query, - options, - traversal_paths, - graph_resolve_depths, - }, - } - } - - #[must_use] - pub fn into_parts( - self, - ) -> ( - EntityQuery<'q>, - EntityQueryOptions<'s, 'p>, - SubgraphTraversalParams, - ) { - match self { - QueryEntitySubgraphRequest::PathsWithQuery { - query, - traversal_paths, - options, - } => ( - EntityQuery::Query { query }, - options, - SubgraphTraversalParams::Paths { traversal_paths }, - ), - QueryEntitySubgraphRequest::PathsWithFilter { - filter, - traversal_paths, - options, - } => ( - EntityQuery::Filter { filter }, - options, - SubgraphTraversalParams::Paths { traversal_paths }, - ), - QueryEntitySubgraphRequest::ResolveDepthsWithQuery { - query, - traversal_paths, - graph_resolve_depths, - options, - } => ( - EntityQuery::Query { query }, - options, - SubgraphTraversalParams::ResolveDepths { - traversal_paths, - graph_resolve_depths, - }, - ), - QueryEntitySubgraphRequest::ResolveDepthsWithFilter { - filter, - traversal_paths, - graph_resolve_depths, - options, - } => ( - EntityQuery::Filter { filter }, - options, - SubgraphTraversalParams::ResolveDepths { - traversal_paths, - graph_resolve_depths, - }, - ), - } - } -} diff --git a/libs/@local/graph/api/src/rest/mod.rs b/libs/@local/graph/api/src/rest/mod.rs index eeb74c869dd..71a4ba16740 100644 --- a/libs/@local/graph/api/src/rest/mod.rs +++ b/libs/@local/graph/api/src/rest/mod.rs @@ -16,7 +16,6 @@ pub mod admin; pub mod http_tracing_layer; pub mod jwt; -mod entity_query_request; mod json; mod utoipa_typedef; use alloc::{borrow::Cow, sync::Arc}; diff --git a/libs/@local/hashql/compiletest/src/pipeline.rs b/libs/@local/hashql/compiletest/src/pipeline.rs index 44465cb52fa..1e0696810b7 100644 --- a/libs/@local/hashql/compiletest/src/pipeline.rs +++ b/libs/@local/hashql/compiletest/src/pipeline.rs @@ -125,6 +125,7 @@ impl<'heap> Pipeline<'heap> { parser .parse_expr(content.as_ref()) + .map_err(Diagnostic::generalize) .map_err(Diagnostic::boxed) } diff --git a/tests/graph/benches/manual_queries/entity_queries/mod.rs b/tests/graph/benches/manual_queries/entity_queries/mod.rs index ff0e4d76963..092d4cd9ad5 100644 --- a/tests/graph/benches/manual_queries/entity_queries/mod.rs +++ b/tests/graph/benches/manual_queries/entity_queries/mod.rs @@ -6,8 +6,8 @@ use criterion_macro::criterion; use either::Either; use error_stack::Report; use hash_graph_api::rest::{ - self, ApiConfig, - entity::{EntityQueryOptions, QueryEntitiesRequest, QueryEntitySubgraphRequest}, + ApiConfig, + entity::query::{QueryEntitiesRequest, QueryEntitySubgraphRequest}, }; use hash_graph_postgres_store::{ Environment, load_env, @@ -142,13 +142,11 @@ impl QueryEntitiesQuery<'_, '_, '_> { let modifies_limit = !self.settings.parameters.limit.is_empty(); let modifies_include_count = !self.settings.parameters.include_count.is_empty(); - let (query, options) = self.request.into_parts(); - let actor_id = iter::once(self.actor_id) .chain(mem::take(&mut self.settings.parameters.actor_id)) .sorted_by_key(|actor_id| Uuid::from(*actor_id)) .dedup(); - let limit = iter::once(options.limit) + let limit = iter::once(self.request.limit) .chain( mem::take(&mut self.settings.parameters.limit) .into_iter() @@ -156,7 +154,7 @@ impl QueryEntitiesQuery<'_, '_, '_> { ) .sorted() .dedup(); - let include_count = iter::once(options.include_count) + let include_count = iter::once(self.request.include_count) .chain(mem::take(&mut self.settings.parameters.include_count)) .sorted() .dedup(); @@ -175,14 +173,11 @@ impl QueryEntitiesQuery<'_, '_, '_> { ( Self { actor_id, - request: QueryEntitiesRequest::from_parts( - query.clone(), - EntityQueryOptions { - limit, - include_count, - ..options.clone() - }, - ), + request: QueryEntitiesRequest { + limit, + include_count, + ..self.request.clone() + }, settings: self.settings.clone(), }, parameters.join(","), @@ -252,13 +247,13 @@ impl QueryEntitySubgraphQuery<'_, '_, '_> { let modifies_include_count = !self.settings.parameters.include_count.is_empty(); let modifies_graph_resolve_depths = !self.settings.parameters.traversal_params.is_empty(); - let (query, options, traversal_params) = self.request.clone().into_parts(); + let (request, traversal_params) = self.request.clone().into_parts(); let actor_id = iter::once(self.actor_id) .chain(mem::take(&mut self.settings.parameters.actor_id)) .sorted_by_key(|actor_id| Uuid::from(*actor_id)) .dedup(); - let limit = iter::once(options.limit) + let limit = iter::once(request.limit) .chain( mem::take(&mut self.settings.parameters.limit) .into_iter() @@ -266,7 +261,7 @@ impl QueryEntitySubgraphQuery<'_, '_, '_> { ) .sorted() .dedup(); - let include_count = iter::once(options.include_count) + let include_count = iter::once(request.include_count) .chain(mem::take(&mut self.settings.parameters.include_count)) .sorted() .dedup(); @@ -292,11 +287,10 @@ impl QueryEntitySubgraphQuery<'_, '_, '_> { Self { actor_id, request: QueryEntitySubgraphRequest::from_parts( - query.clone(), - EntityQueryOptions { + QueryEntitiesRequest { limit, include_count, - ..options.clone() + ..request.clone() }, traversal_params, ), @@ -342,32 +336,24 @@ where match request { GraphQuery::QueryEntities(request) => { - let (query, options) = request.request.into_parts(); - let rest::entity::EntityQuery::Filter { filter } = query else { - panic!("unsupported query type") - }; - let _response = store .query_entities( request.actor_id, - options - .into_params(filter, config) + request + .request + .into_params(config) .expect("limit should not exceed configured maximum"), ) .await .expect("failed to read entities from store"); } GraphQuery::QueryEntitySubgraph(request) => { - let (query, options, traversal) = request.request.into_parts(); - let rest::entity::EntityQuery::Filter { filter } = query else { - panic!("unsupported query type") - }; - let _response = store .query_entity_subgraph( request.actor_id, - options - .into_traversal_params(filter, traversal, config) + request + .request + .into_traversal_params(config) .expect("limit should not exceed configured maximum"), ) .await From 94209654ef6da4c74e0af29746da108728c0c18e Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:41:50 +0200 Subject: [PATCH 19/25] feat: checkpoint --- apps/hash-graph/src/subcommand/server.rs | 101 +++++++++++++++++- .../graph/api/src/rest/hashql/compile.rs | 3 +- libs/@local/graph/api/src/rest/hashql/mod.rs | 79 ++++++++++++-- .../@local/graph/api/src/rest/hashql/value.rs | 3 +- libs/@local/graph/api/src/rest/mod.rs | 37 ++++++- 5 files changed, 205 insertions(+), 18 deletions(-) diff --git a/apps/hash-graph/src/subcommand/server.rs b/apps/hash-graph/src/subcommand/server.rs index 8c2dbeb27ab..c062b023f94 100644 --- a/apps/hash-graph/src/subcommand/server.rs +++ b/apps/hash-graph/src/subcommand/server.rs @@ -2,7 +2,7 @@ use alloc::sync::Arc; use core::{ fmt, net::{AddrParseError, SocketAddr}, - str::FromStr as _, + str::FromStr, time::Duration, }; use std::path::PathBuf; @@ -14,7 +14,10 @@ use harpc_codec::json::JsonCodec; use harpc_server::Server; use hash_codec::bytes::JsonLinesEncoder; use hash_graph_api::{ - rest::{ApiConfig, QueryLogger, RestApiStore, RestRouterDependencies, rest_api_router}, + rest::{ + ApiConfig, QueryLogger, RestApiStore, RestRouterDependencies, hashql::CompilerContext, + rest_api_router, + }, rpc::Dependencies, }; use hash_graph_authorization::policies::store::{PolicyStore, PrincipalStore}; @@ -103,6 +106,73 @@ pub struct TemporalConfig { pub address: TemporalAddress, } +/// A pool size that can be either a concrete count or unbounded. +/// +/// Parses positive integers as a bounded size and `-1` as unbounded. +#[derive(Debug, Copy, Clone)] +pub struct PoolSize(Option); + +impl PoolSize { + #[inline] + const fn get(self) -> Option { + self.0 + } +} + +impl fmt::Display for PoolSize { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Some(size) => write!(fmt, "{size}"), + None => write!(fmt, "-1"), + } + } +} + +impl FromStr for PoolSize { + type Err = ::Err; + + #[expect( + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + reason = "negative values produce None, and pool sizes never approach u32::MAX" + )] + fn from_str(s: &str) -> Result { + let value = s.parse::()?; + if value < 0 { + Ok(Self(None)) + } else { + Ok(Self(Some(value as usize))) + } + } +} + +/// Configuration for the HashQL compiler and execution pool. +#[derive(Debug, Clone, Parser)] +pub struct CompilerConfig { + /// Number of pre-allocated heap/scratch instances in the compiler memory pool. + /// + /// Set to -1 for an unbounded pool that grows without limit. + #[clap( + long, + default_value = "16", + env = "HASH_GRAPH_COMPILER_MEMORY_POOL_SIZE", + allow_hyphen_values = true + )] + pub compiler_memory_pool_size: PoolSize, + + /// Number of threads in the compiler execution pool. + /// + /// Each thread runs a `LocalSet` for `!Send` query execution. Set to -1 to use the number + /// of available CPU cores. + #[clap( + long, + default_value = "-1", + env = "HASH_GRAPH_COMPILER_EXEC_POOL_SIZE", + allow_hyphen_values = true + )] + pub compiler_exec_pool_size: PoolSize, +} + /// Configuration for the main graph API server. /// /// Groups HTTP address, RPC address, temporal client, store behavior, and @@ -167,6 +237,9 @@ pub struct ServerConfig { #[clap(flatten)] pub api_config: ApiConfig, + #[clap(flatten)] + pub compiler: CompilerConfig, + /// Outputs the queries made to the graph to the specified file. #[clap(long)] pub log_queries: Option, @@ -314,6 +387,8 @@ where /// Starts the main graph API server (REST + optional RPC). async fn start_server( pool: S, + postgres: PostgresStorePool, + compiler: Arc, config: ServerConfig, query_logger: Option, lifecycle: &ServerLifecycle, @@ -343,10 +418,12 @@ where let router = rest_api_router(RestRouterDependencies { store, - domain_regex: DomainValidator::new(config.allowed_url_domain), + postgres, temporal_client, + domain_regex: DomainValidator::new(config.allowed_url_domain), query_logger, api_config: config.api_config, + compiler, }); start_rest_server(router, config.http_address, lifecycle); @@ -405,6 +482,8 @@ pub async fn server(mut args: ServerArgs) -> Result<(), Report> { let lifecycle = ServerLifecycle::new(); + let postgres = pool.clone(); + if args.embed_admin { start_admin_server(pool.clone(), args.admin, &lifecycle); } @@ -441,7 +520,21 @@ pub async fn server(mut args: ServerArgs) -> Result<(), Report> { None }; - if let Err(error) = start_server(pool, args.config, query_logger, &lifecycle).await { + let compiler = Arc::new(CompilerContext::new( + args.config.compiler.compiler_memory_pool_size.get(), + args.config.compiler.compiler_exec_pool_size.get(), + )); + + if let Err(error) = start_server( + pool, + postgres, + compiler, + args.config, + query_logger, + &lifecycle, + ) + .await + { lifecycle.shutdown_and_wait().await; return Err(error); } diff --git a/libs/@local/graph/api/src/rest/hashql/compile.rs b/libs/@local/graph/api/src/rest/hashql/compile.rs index 9ba8a323794..0e12fbd2a47 100644 --- a/libs/@local/graph/api/src/rest/hashql/compile.rs +++ b/libs/@local/graph/api/src/rest/hashql/compile.rs @@ -41,7 +41,8 @@ pub(crate) struct Compilation<'heap> { } impl<'heap> Compilation<'heap> { - pub fn compile( + #[expect(clippy::too_many_lines, reason = "orchestration of sequential tasks")] + pub(crate) fn compile( heap: &'heap Heap, scratch: &mut Scratch, spans: &mut SpanTable, diff --git a/libs/@local/graph/api/src/rest/hashql/mod.rs b/libs/@local/graph/api/src/rest/hashql/mod.rs index 79968d00a32..90c5dc461fa 100644 --- a/libs/@local/graph/api/src/rest/hashql/mod.rs +++ b/libs/@local/graph/api/src/rest/hashql/mod.rs @@ -6,9 +6,9 @@ use alloc::sync::Arc; use core::num::NonZero; use std::thread::available_parallelism; -use axum::response::IntoResponse; +use axum::{Extension, Router, response::IntoResponse as _, routing::post}; use hash_graph_postgres_store::store::PostgresStorePool; -use hash_graph_store::pool::StorePool; +use hash_graph_store::pool::StorePool as _; use hash_temporal_client::TemporalClient; use hashql_core::{ heap::{HeapPool, ScratchPool}, @@ -23,13 +23,14 @@ use hashql_mir::interpret::Inputs; use hashql_syntax_jexpr::span::Span; use serde_json::value::RawValue; use tokio_util::task::LocalPoolHandle; +use utoipa::OpenApi; use self::{ compile::Compilation, error::{HashQlDiagnosticCategory, status_to_response}, value::OwnedValue, }; -use crate::rest::{json::Json, status::BoxedResponse}; +use crate::rest::{InteractiveHeader, JsonCompatHeader, json::Json, status::BoxedResponse}; pub struct CompilerContext { pub scratches: ScratchPool, @@ -39,13 +40,11 @@ pub struct CompilerContext { impl CompilerContext { pub fn new(memory_pool_size: Option, exec_pool_size: Option) -> Self { - let scratches = - memory_pool_size.map_or_else(|| ScratchPool::new(), |size| ScratchPool::bounded(size)); - let heaps = - memory_pool_size.map_or_else(|| HeapPool::new(), |size| HeapPool::bounded(size)); + let scratches = memory_pool_size.map_or_else(ScratchPool::new, ScratchPool::bounded); + let heaps = memory_pool_size.map_or_else(HeapPool::new, HeapPool::bounded); - let thread_count = exec_pool_size - .unwrap_or_else(|| available_parallelism().map(NonZero::get).unwrap_or(4)); + let thread_count = + exec_pool_size.unwrap_or_else(|| available_parallelism().map_or(4, NonZero::get)); let pool = LocalPoolHandle::new(thread_count); Self { @@ -136,7 +135,7 @@ async fn query_local( status_to_response(status, &sources, &spans, &options) } -async fn query( +async fn run_query( ctx: Arc, exec: ExecutionContext, query: Arc, @@ -153,3 +152,63 @@ async fn query( .into() }) } + +#[derive(serde::Deserialize, utoipa::ToSchema)] +pub(crate) struct HashQlRequest { + query: Arc, + #[expect( + dead_code, + reason = "inputs will be required once HashQL input support ships" + )] + inputs: Vec<()>, +} + +#[utoipa::path( + post, + path = "/hashql", + request_body = HashQlRequest, + tag = "HashQL", + params( + ("Interactive" = Option, Header, description = "When true, error responses are rendered as HTML instead of JSON"), + ("Json-Compat" = Option, Header, description = "When true, wraps the result value in a JSON-compatible envelope"), + ), + responses( + (status = 200, content_type = "application/json", description = "Query executed successfully"), + (status = 400, content_type = "application/json", description = "Query compilation or validation error"), + (status = 500, description = "Internal compiler or database error"), + ) +)] +pub(crate) async fn query_hashql( + Extension(compiler): Extension>, + Extension(postgres): Extension>, + Extension(temporal): Extension>>, + InteractiveHeader(interactive): InteractiveHeader, + JsonCompatHeader(json_compat): JsonCompatHeader, + Json(request): Json, +) -> BoxedResponse { + let exec = ExecutionContext { + postgres: (*postgres).clone(), + temporal, + }; + + let options = CompilationOutputOptions { + interactive, + json_compat, + }; + + run_query(compiler, exec, request.query, options).await +} + +#[derive(OpenApi)] +#[openapi( + paths(query_hashql), + components(schemas(HashQlRequest)), + tags((name = "HashQL", description = "HashQL query execution API")) +)] +pub(crate) struct HashQlResource; + +impl HashQlResource { + pub(crate) fn routes() -> Router { + Router::new().route("/hashql", post(query_hashql)) + } +} diff --git a/libs/@local/graph/api/src/rest/hashql/value.rs b/libs/@local/graph/api/src/rest/hashql/value.rs index 969fc32ede0..1c6c2c9bfa6 100644 --- a/libs/@local/graph/api/src/rest/hashql/value.rs +++ b/libs/@local/graph/api/src/rest/hashql/value.rs @@ -1,4 +1,5 @@ -use std::{alloc::Allocator, collections::BTreeMap, sync::Arc}; +use alloc::{collections::BTreeMap, sync::Arc}; +use core::alloc::Allocator; use hashql_core::id::Id as _; use hashql_mir::interpret::value::{Int, Num, Ptr, Value}; diff --git a/libs/@local/graph/api/src/rest/mod.rs b/libs/@local/graph/api/src/rest/mod.rs index 71a4ba16740..69ce17f3fcb 100644 --- a/libs/@local/graph/api/src/rest/mod.rs +++ b/libs/@local/graph/api/src/rest/mod.rs @@ -16,6 +16,7 @@ pub mod admin; pub mod http_tracing_layer; pub mod jwt; +pub mod hashql; mod json; mod utoipa_typedef; use alloc::{borrow::Cow, sync::Arc}; @@ -37,7 +38,7 @@ use error_stack::{Report, ResultExt as _}; use futures::{SinkExt as _, channel::mpsc::Sender}; use hash_codec::numeric::Real; use hash_graph_authorization::policies::store::{PolicyStore, PrincipalStore}; -use hash_graph_postgres_store::store::error::VersionedUrlAlreadyExists; +use hash_graph_postgres_store::store::{PostgresStorePool, error::VersionedUrlAlreadyExists}; use hash_graph_store::{ account::AccountStore, data_type::DataTypeStore, @@ -157,6 +158,32 @@ impl FromRequestParts for InteractiveHeader { } } +pub struct JsonCompatHeader(pub bool); + +impl FromRequestParts for JsonCompatHeader { + type Rejection = (StatusCode, Cow<'static, str>); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let Some(value) = parts.headers.get("Json-Compat") else { + return Ok(Self(false)); + }; + + let bytes = value.as_ref(); + if bytes.eq_ignore_ascii_case(b"true") || bytes.eq_ignore_ascii_case(b"1") { + return Ok(Self(true)); + } + + if bytes.eq_ignore_ascii_case(b"false") || bytes.eq_ignore_ascii_case(b"0") { + return Ok(Self(false)); + } + + Err(( + StatusCode::BAD_REQUEST, + Cow::Borrowed("`Json-Compat` header must be either `true` (`1`) or `false` (`0`)"), + )) + } +} + #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct PermissionResponse { pub has_permission: bool, @@ -238,6 +265,7 @@ fn api_documentation() -> Vec { entity::EntityResource::openapi(), permissions::PermissionResource::openapi(), principal::PrincipalResource::openapi(), + hashql::HashQlResource::openapi(), ] } @@ -383,10 +411,12 @@ where S: StorePool + Send + Sync + 'static, { pub store: Arc, + pub postgres: PostgresStorePool, pub temporal_client: Option>, pub domain_regex: DomainValidator, pub query_logger: Option, pub api_config: ApiConfig, + pub compiler: Arc, } /// A [`Router`] that only serves the `OpenAPI` specification (JSON, and necessary subschemas) for @@ -414,6 +444,7 @@ where let merged_routes = api_resources::() .into_iter() .fold(Router::new(), Router::merge) + .merge(hashql::HashQlResource::routes()) .fallback(|| { tracing::error!("404: Not found"); async { StatusCode::NOT_FOUND } @@ -431,9 +462,11 @@ where ) .layer(http_tracing_layer::HttpTracingLayer) .layer(Extension(dependencies.store)) + .layer(Extension(Arc::new(dependencies.postgres))) .layer(Extension(dependencies.temporal_client)) .layer(Extension(dependencies.domain_regex)) - .layer(Extension(dependencies.api_config)); + .layer(Extension(dependencies.api_config)) + .layer(Extension(dependencies.compiler)); if let Some(query_logger) = dependencies.query_logger { router = router.layer(Extension(query_logger)); From 7e3e503a957e714856c758b405a8db5d5b73fd8f Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:51:35 +0200 Subject: [PATCH 20/25] feat: checkpoint --- .../@local/graph/api/src/rest/hashql/error.rs | 2 +- libs/@local/graph/api/src/rest/hashql/mod.rs | 32 ++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/libs/@local/graph/api/src/rest/hashql/error.rs b/libs/@local/graph/api/src/rest/hashql/error.rs index fc60da91247..0fb9d7ed609 100644 --- a/libs/@local/graph/api/src/rest/hashql/error.rs +++ b/libs/@local/graph/api/src/rest/hashql/error.rs @@ -115,7 +115,7 @@ pub(crate) fn status_to_response( .into_response() .into() } else { - Json(value).into_response().into() + Json(Success { value, advisories }).into_response().into() } } Err(Failure { primary, secondary }) => { diff --git a/libs/@local/graph/api/src/rest/hashql/mod.rs b/libs/@local/graph/api/src/rest/hashql/mod.rs index 90c5dc461fa..30ae355a39f 100644 --- a/libs/@local/graph/api/src/rest/hashql/mod.rs +++ b/libs/@local/graph/api/src/rest/hashql/mod.rs @@ -1,3 +1,10 @@ +//! HashQL query endpoint. +//! +//! Accepts a HashQL query as raw JSON, compiles it through the full pipeline +//! (parse, type-check, optimize, codegen), executes the generated SQL, and returns +//! the result. Compilation errors are reported as structured diagnostics with +//! source spans. + mod compile; mod error; mod value; @@ -32,6 +39,7 @@ use self::{ }; use crate::rest::{InteractiveHeader, JsonCompatHeader, json::Json, status::BoxedResponse}; +/// Shared resources for HashQL query compilation and execution, created once at server startup. pub struct CompilerContext { pub scratches: ScratchPool, pub heaps: HeapPool, @@ -39,6 +47,10 @@ pub struct CompilerContext { } impl CompilerContext { + /// Creates a new compiler context. + /// + /// `memory_pool_size` bounds the heap and scratch pools; `None` leaves them unbounded. + /// `exec_pool_size` sets the thread count; `None` uses the number of available CPU cores. pub fn new(memory_pool_size: Option, exec_pool_size: Option) -> Self { let scratches = memory_pool_size.map_or_else(ScratchPool::new, ScratchPool::bounded); let heaps = memory_pool_size.map_or_else(HeapPool::new, HeapPool::bounded); @@ -55,16 +67,21 @@ impl CompilerContext { } } +/// Per-request database context. struct ExecutionContext { postgres: PostgresStorePool, temporal: Option>, } +/// Controls the response format for a HashQL query. pub(crate) struct CompilationOutputOptions { + /// Render errors as HTML with source annotations instead of structured JSON. pub interactive: bool, + /// Serialize the result as plain JSON values, stripping HashQL-specific type wrappers. pub json_compat: bool, } +/// Compiles and executes a HashQL query, returning the result as a [`Status`]. #[expect(clippy::future_not_send)] async fn query_local_impl( ctx: Arc, @@ -72,10 +89,9 @@ async fn query_local_impl( spans: &mut SpanTable, query: &[u8], ) -> Status { - // We're hitting a borrow checker limitation here, we cannot prove that scratch and heap are - // still alive by the time that we `spawn_local`, and due to the fact that it requires `'static` - // it means that we need to build the query inside here, and only then delegate, otherwise we'd - // be able to run it outside. + // Heap and scratch must be created inside this function because `spawn_pinned` requires + // `'static`. Moving them across the spawn boundary isn't possible since they borrow from + // the pool guards. let mut scratch = ctx.scratches.get(); let heap = ctx.heaps.get(); @@ -135,12 +151,16 @@ async fn query_local( status_to_response(status, &sources, &spans, &options) } +/// Spawns a query onto the local thread pool and awaits the response. async fn run_query( ctx: Arc, exec: ExecutionContext, query: Arc, options: CompilationOutputOptions, ) -> BoxedResponse { + // The compiler and interpreter hold references into bump-allocated heaps, making their + // futures `!Send`. `spawn_pinned` runs them on a dedicated thread; the returned handle + // is `Send` so the HTTP handler can await it normally. let pool = ctx.pool.clone(); let result = pool .spawn_pinned(|| query_local(ctx, exec, query, options)) @@ -153,9 +173,11 @@ async fn run_query( }) } +/// Request body for the `/hashql` endpoint. #[derive(serde::Deserialize, utoipa::ToSchema)] pub(crate) struct HashQlRequest { query: Arc, + /// Input values for the query. Must be an empty list until input support ships. #[expect( dead_code, reason = "inputs will be required once HashQL input support ships" @@ -170,7 +192,7 @@ pub(crate) struct HashQlRequest { tag = "HashQL", params( ("Interactive" = Option, Header, description = "When true, error responses are rendered as HTML instead of JSON"), - ("Json-Compat" = Option, Header, description = "When true, wraps the result value in a JSON-compatible envelope"), + ("Json-Compat" = Option, Header, description = "When true, serializes the result as plain JSON values, stripping HashQL-specific type wrappers"), ), responses( (status = 200, content_type = "application/json", description = "Query executed successfully"), From 6df9cdb7c28f62976ff03264204baedc08b7cde7 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:18:01 +0200 Subject: [PATCH 21/25] feat: checkpoint --- apps/hash-graph/src/subcommand/server.rs | 7 +- libs/@local/graph/api/src/rest/hashql/mod.rs | 2 +- tests/graph/http/test.sh | 2 + tests/graph/http/tests/hashql.http | 154 +++++++++++++++++++ 4 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 tests/graph/http/tests/hashql.http diff --git a/apps/hash-graph/src/subcommand/server.rs b/apps/hash-graph/src/subcommand/server.rs index c062b023f94..9d5a0506cc8 100644 --- a/apps/hash-graph/src/subcommand/server.rs +++ b/apps/hash-graph/src/subcommand/server.rs @@ -306,7 +306,12 @@ async fn run_rest_server( async fn create_temporal_client( config: &TemporalConfig, ) -> Result, Report> { - if let Some(host) = &config.address.temporal_host { + if let Some(host) = config + .address + .temporal_host + .as_deref() + .filter(|h| !h.is_empty()) + { TemporalClientConfig::new( Url::from_str(&format!("{host}:{}", config.address.temporal_port)) .change_context(GraphError)?, diff --git a/libs/@local/graph/api/src/rest/hashql/mod.rs b/libs/@local/graph/api/src/rest/hashql/mod.rs index 30ae355a39f..747b7dba904 100644 --- a/libs/@local/graph/api/src/rest/hashql/mod.rs +++ b/libs/@local/graph/api/src/rest/hashql/mod.rs @@ -100,7 +100,7 @@ async fn query_local_impl( let Success { value: compilation, advisories, - } = tokio::task::block_in_place(|| Compilation::compile(&heap, &mut scratch, spans, query))?; + } = Compilation::compile(&heap, &mut scratch, spans, query)?; let context = compilation.context(); diff --git a/tests/graph/http/test.sh b/tests/graph/http/test.sh index f97a1f651e5..d66e0992030 100755 --- a/tests/graph/http/test.sh +++ b/tests/graph/http/test.sh @@ -11,3 +11,5 @@ yarn httpyac send --all tests/ambiguous.http -o none yarn reset-database -o none yarn httpyac send --all tests/link-inheritance.http -o none yarn reset-database -o none +yarn httpyac send --all tests/hashql.http -o none +yarn reset-database -o none diff --git a/tests/graph/http/tests/hashql.http b/tests/graph/http/tests/hashql.http new file mode 100644 index 00000000000..64dc55a1d3e --- /dev/null +++ b/tests/graph/http/tests/hashql.http @@ -0,0 +1,154 @@ +# This file either runs with JetBrains' http requests or using httpYac (https://httpyac.github.io). + +### Seed default policies +GET http://127.0.0.1:4000/policies/seed +Content-Type: application/json + +> {% + client.test("status", function() { + client.assert(response.status === 204, "Response status is not 204"); + }); +%} + +### Get system user +GET http://127.0.0.1:4000/actors/machine/identifier/system/h +Content-Type: application/json + +> {% + client.test("status", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + client.global.set("system_machine_id", response.body); +%} + +### Create account +POST http://127.0.0.1:4000/actors/user +Content-Type: application/json +X-Authenticated-User-Actor-Id: {{system_machine_id}} + +{ + "shortname": "alice", + "registrationComplete": true +} + +> {% + client.test("status", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + client.global.set("user_id", response.body.userId); +%} + +### HashQL: filter-false returns empty list +POST http://127.0.0.1:4000/hashql +Content-Type: application/json + +{ + "query": ["::graph::tail::collect", + ["::graph::body::filter", + ["::graph::head::entities", ["input", "temporal_axes", "_"]], + ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", + {"#literal": false} + ] + ] + ], + "inputs": [] +} + +> {% + client.test("status", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + client.test("empty list", function() { + client.assert(Array.isArray(response.body), "Response body is not an array"); + client.assert(response.body.length === 0, "Expected empty list for filter-false"); + }); +%} + +### HashQL: parse error returns 400 with diagnostics +POST http://127.0.0.1:4000/hashql +Content-Type: application/json + +{ + "query": {"not": "a valid query"}, + "inputs": [] +} + +> {% + client.test("status", function() { + client.assert(response.status === 400, "Expected 400 for invalid query"); + }); + client.test("has diagnostic", function() { + client.assert(response.body.primary !== undefined, "Expected primary diagnostic"); + client.assert(response.body.primary.category !== undefined, "Expected diagnostic category"); + }); +%} + +### HashQL: missing inputs field returns 422 +POST http://127.0.0.1:4000/hashql +Content-Type: application/json + +{ + "query": ["::graph::tail::collect", + ["::graph::body::filter", + ["::graph::head::entities", ["input", "temporal_axes", "_"]], + ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", + {"#literal": false} + ] + ] + ] +} + +> {% + client.test("status", function() { + client.assert(response.status === 422, "Expected 422 for missing inputs field"); + }); +%} + +### HashQL: json-compat wraps result in envelope with advisories +POST http://127.0.0.1:4000/hashql +Content-Type: application/json +Json-Compat: true + +{ + "query": ["::graph::tail::collect", + ["::graph::body::filter", + ["::graph::head::entities", ["input", "temporal_axes", "_"]], + ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", + {"#literal": false} + ] + ] + ], + "inputs": [] +} + +> {% + client.test("status", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + client.test("envelope shape", function() { + client.assert(response.body.value !== undefined, "Expected 'value' field in envelope"); + client.assert(response.body.advisories !== undefined, "Expected 'advisories' field in envelope"); + client.assert(Array.isArray(response.body.value), "Expected value to be an array"); + client.assert(response.body.value.length === 0, "Expected empty list for filter-false"); + }); +%} + +### HashQL: interactive header returns HTML on error +POST http://127.0.0.1:4000/hashql +Content-Type: application/json +Interactive: true + +{ + "query": {"not": "a valid query"}, + "inputs": [] +} + +> {% + client.test("status", function() { + client.assert(response.status === 400, "Expected 400 for invalid query"); + }); + client.test("html response", function() { + var contentType = response.headers.valueOf("content-type"); + client.assert(contentType.includes("text/html"), "Expected HTML content type, got: " + contentType); + }); +%} From b0f9c3746f5be7d5a63788e125d5ef029881e5cc Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:09:17 +0200 Subject: [PATCH 22/25] fix: issue around cached thunking --- .../compiletest/src/suite/mir_interpret.rs | 66 +++ .../hashql/compiletest/src/suite/mod.rs | 4 +- libs/@local/hashql/core/src/span/table.rs | 8 +- libs/@local/hashql/mir/src/reify/rvalue.rs | 17 +- .../hashql/mir/tests/ui/interpret/.spec.toml | 1 + .../access-struct-through-opaque.aux.mir | 409 ++++++++++++++++++ .../access-struct-through-opaque.jsonc | 13 + .../access-struct-through-opaque.stdout | 37 ++ .../tests/ui/reify/ctor-cached-closure.jsonc | 15 + .../tests/ui/reify/ctor-cached-closure.stdout | 101 +++++ tests/graph/http/tests/hashql.http | 63 ++- 11 files changed, 703 insertions(+), 31 deletions(-) create mode 100644 libs/@local/hashql/compiletest/src/suite/mir_interpret.rs create mode 100644 libs/@local/hashql/mir/tests/ui/interpret/.spec.toml create mode 100644 libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.aux.mir create mode 100644 libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.stdout create mode 100644 libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.jsonc create mode 100644 libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.stdout diff --git a/libs/@local/hashql/compiletest/src/suite/mir_interpret.rs b/libs/@local/hashql/compiletest/src/suite/mir_interpret.rs new file mode 100644 index 00000000000..6d7bb4aed7b --- /dev/null +++ b/libs/@local/hashql/compiletest/src/suite/mir_interpret.rs @@ -0,0 +1,66 @@ +use hashql_core::r#type::environment::Environment; +use hashql_diagnostics::Diagnostic; +use hashql_mir::{ + intern::Interner, + interpret::{CallStack, Inputs, Runtime, RuntimeConfig}, +}; + +use super::{ + RunContext, Suite, SuiteDiagnostic, + mir_pass_transform_post_inline::mir_pass_transform_post_inline, + mir_pass_transform_pre_inline::TextRenderer, +}; + +pub(crate) struct MirInterpret; + +impl Suite for MirInterpret { + fn name(&self) -> &'static str { + "mir/interpret" + } + + fn description(&self) -> &'static str { + "Run the interpreter on the MIR" + } + + fn secondary_file_extensions(&self) -> &[&str] { + &["mir"] + } + + fn run<'heap>( + &self, + RunContext { + heap, + diagnostics, + secondary_outputs, + .. + }: RunContext<'_, 'heap>, + expr: hashql_ast::node::expr::Expr<'heap>, + ) -> Result { + let mut environment = Environment::new(heap); + let interner = Interner::new(heap); + + let mut buffer = Vec::new(); + + let (root, bodies, _) = mir_pass_transform_post_inline( + heap, + expr, + &interner, + TextRenderer::new(&mut buffer), + &mut environment, + diagnostics, + )?; + + secondary_outputs.insert("mir", String::from_utf8_lossy_owned(buffer)); + + let inputs = Inputs::new(); + let mut runtime = Runtime::new(RuntimeConfig::default(), &bodies, &inputs); + let callstack = CallStack::new(&runtime, root, []); + + let output = runtime + .run(callstack, |_| unimplemented!()) + .map_err(Diagnostic::generalize) + .map_err(Diagnostic::boxed)?; + + Ok(format!("{output:#?}")) + } +} diff --git a/libs/@local/hashql/compiletest/src/suite/mod.rs b/libs/@local/hashql/compiletest/src/suite/mod.rs index 1507ea45f0b..69edab286f1 100644 --- a/libs/@local/hashql/compiletest/src/suite/mod.rs +++ b/libs/@local/hashql/compiletest/src/suite/mod.rs @@ -20,6 +20,7 @@ mod hir_lower_normalization; mod hir_lower_specialization; mod hir_lower_thunking; mod hir_reify; +mod mir_interpret; mod mir_pass_analysis_data_dependency; mod mir_pass_transform_administrative_reduction; mod mir_pass_transform_cfg_simplify; @@ -57,7 +58,7 @@ use self::{ hir_lower_normalization::HirLowerNormalizationSuite, hir_lower_specialization::HirLowerSpecializationSuite, hir_lower_thunking::HirLowerThunkingSuite, hir_reify::HirReifySuite, - mir_pass_analysis_data_dependency::MirPassAnalysisDataDependency, + mir_interpret::MirInterpret, mir_pass_analysis_data_dependency::MirPassAnalysisDataDependency, mir_pass_transform_administrative_reduction::MirPassTransformAdministrativeReduction, mir_pass_transform_cfg_simplify::MirPassTransformCfgSimplify, mir_pass_transform_dse::MirPassTransformDse, @@ -160,6 +161,7 @@ const SUITES: &[&dyn Suite] = &[ &HirLowerTypeInferenceIntrinsicsSuite, &HirLowerTypeInferenceSuite, &HirReifySuite, + &MirInterpret, &MirPassAnalysisDataDependency, &MirPassTransformAdministrativeReduction, &MirPassTransformCfgSimplify, diff --git a/libs/@local/hashql/core/src/span/table.rs b/libs/@local/hashql/core/src/span/table.rs index 554f790ff78..a98f76ffad2 100644 --- a/libs/@local/hashql/core/src/span/table.rs +++ b/libs/@local/hashql/core/src/span/table.rs @@ -574,14 +574,14 @@ impl SpanTable { core::iter::from_fn(move || { loop { let span = stack.pop()?; - if !visited.insert(span.id()) { - continue; - } - let Some(entry) = self.get_entry(span) else { continue; }; + if !visited.insert(span.id()) { + continue; + } + let ancestors = &self.ancestors[entry.ancestors.clone()]; stack.extend(ancestors); diff --git a/libs/@local/hashql/mir/src/reify/rvalue.rs b/libs/@local/hashql/mir/src/reify/rvalue.rs index 4e2104e0a03..ec831fc1e9e 100644 --- a/libs/@local/hashql/mir/src/reify/rvalue.rs +++ b/libs/@local/hashql/mir/src/reify/rvalue.rs @@ -112,15 +112,16 @@ impl<'mir, 'heap, A: Allocator, S: Allocator> Reifier<'_, 'mir, '_, '_, 'heap, A RValue::Load(Operand::Constant(Constant::Unit)) } TypeOperation::Constructor(ctor @ TypeConstructor { name }) => { - if let Some(&ptr) = self.state.ctor.get(&name) { - return RValue::Load(Operand::Constant(Constant::FnPtr(ptr))); - } - - let compiler = Reifier::new(self.context, self.state); - let ptr = compiler.lower_ctor(hir, ctor); - self.state.ctor.insert(name, ptr); + let def = if let Some(&ptr) = self.state.ctor.get(&name) { + ptr + } else { + let compiler = Reifier::new(self.context, self.state); + let ptr = compiler.lower_ctor(hir, ctor); + self.state.ctor.insert(name, ptr); + ptr + }; - let ptr = Operand::Constant(Constant::FnPtr(ptr)); + let ptr = Operand::Constant(Constant::FnPtr(def)); let env = Operand::Constant(Constant::Unit); let mut operands = IdVec::with_capacity_in(2, self.context.mir.heap); operands.push(ptr); diff --git a/libs/@local/hashql/mir/tests/ui/interpret/.spec.toml b/libs/@local/hashql/mir/tests/ui/interpret/.spec.toml new file mode 100644 index 00000000000..acab5275af2 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/.spec.toml @@ -0,0 +1 @@ +suite = "mir/interpret" diff --git a/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.aux.mir b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.aux.mir new file mode 100644 index 00000000000..3b899d54d7f --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.aux.mir @@ -0,0 +1,409 @@ +════ Initial MIR ═══════════════════════════════════════════════════════════════ + +fn {ctor#::main::A:0}(%0: ()) -> ::main::A:0 { + let %1: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + + return %1 + } +} + +thunk {thunk#2}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#3}() -> ::main::A:0 { + let %0: () -> ::main::A:0 + let %1: ::main::A:0 + + bb0(): { + %0 = apply ({thunk#2} as FnPtr) + %1 = apply %0.0 %0.1 + + return %1 + } +} + +thunk {thunk#4}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#5}() -> ::main::A:0 { + let %0: () -> ::main::A:0 + let %1: ::main::A:0 + + bb0(): { + %0 = apply ({thunk#4} as FnPtr) + %1 = apply %0.0 %0.1 + + return %1 + } +} + +thunk {thunk#6}() -> (x: ::main::A:0, y: ::main::A:0) { + let %0: ::main::A:0 + let %1: ::main::A:0 + let %2: (x: ::main::A:0, y: ::main::A:0) + + bb0(): { + %0 = apply ({thunk#3} as FnPtr) + %1 = apply ({thunk#5} as FnPtr) + %2 = (x: %0, y: %1) + + return %2 + } +} + +fn {ctor#::main::Outer:0}(%0: (), %1: (x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %2: ::main::Outer:0 + + bb0(): { + %2 = opaque(::main::Outer:0, %1) + + return %2 + } +} + +thunk {thunk#7}() -> ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %0: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + + bb0(): { + %0 = closure(({ctor#::main::Outer:0} as FnPtr), ()) + + return %0 + } +} + +*thunk {thunk#8}() -> ::main::Outer:0 { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + let %2: ::main::Outer:0 + + bb0(): { + %0 = apply ({thunk#6} as FnPtr) + %1 = apply ({thunk#7} as FnPtr) + %2 = apply %1.0 %1.1 %0 + + return %2 + } +} + +════ Pre-inlining MIR ══════════════════════════════════════════════════════════ + +fn {ctor#::main::A:0}(%0: ()) -> ::main::A:0 { + let %1: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + + return %1 + } +} + +thunk {thunk#2}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#3}() -> ::main::A:0 { + let %0: ::main::A:0 + + bb0(): { + %0 = opaque(::main::A:0, ()) + + return %0 + } +} + +thunk {thunk#4}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#5}() -> ::main::A:0 { + let %0: ::main::A:0 + + bb0(): { + %0 = opaque(::main::A:0, ()) + + return %0 + } +} + +thunk {thunk#6}() -> (x: ::main::A:0, y: ::main::A:0) { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ::main::A:0 + let %2: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = opaque(::main::A:0, ()) + %0 = (x: %1, y: %2) + + return %0 + } +} + +fn {ctor#::main::Outer:0}(%0: (), %1: (x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %2: ::main::Outer:0 + + bb0(): { + %2 = opaque(::main::Outer:0, %1) + + return %2 + } +} + +thunk {thunk#7}() -> ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %0: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + + bb0(): { + %0 = closure(({ctor#::main::Outer:0} as FnPtr), ()) + + return %0 + } +} + +*thunk {thunk#8}() -> ::main::Outer:0 { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ::main::A:0 + let %2: ::main::A:0 + let %3: ::main::Outer:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = opaque(::main::A:0, ()) + %0 = (x: %1, y: %2) + %3 = opaque(::main::Outer:0, %0) + + return %3 + } +} + +════ Inlined MIR ═══════════════════════════════════════════════════════════════ + +fn {ctor#::main::A:0}(%0: ()) -> ::main::A:0 { + let %1: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + + return %1 + } +} + +thunk {thunk#2}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#3}() -> ::main::A:0 { + let %0: ::main::A:0 + + bb0(): { + %0 = opaque(::main::A:0, ()) + + return %0 + } +} + +thunk {thunk#4}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#5}() -> ::main::A:0 { + let %0: ::main::A:0 + + bb0(): { + %0 = opaque(::main::A:0, ()) + + return %0 + } +} + +thunk {thunk#6}() -> (x: ::main::A:0, y: ::main::A:0) { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ::main::A:0 + let %2: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = opaque(::main::A:0, ()) + %0 = (x: %1, y: %2) + + return %0 + } +} + +fn {ctor#::main::Outer:0}(%0: (), %1: (x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %2: ::main::Outer:0 + + bb0(): { + %2 = opaque(::main::Outer:0, %1) + + return %2 + } +} + +thunk {thunk#7}() -> ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %0: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + + bb0(): { + %0 = closure(({ctor#::main::Outer:0} as FnPtr), ()) + + return %0 + } +} + +*thunk {thunk#8}() -> ::main::Outer:0 { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ::main::A:0 + let %2: ::main::A:0 + let %3: ::main::Outer:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = opaque(::main::A:0, ()) + %0 = (x: %1, y: %2) + %3 = opaque(::main::Outer:0, %0) + + return %3 + } +} + +════ Post Inline MIR ═══════════════════════════════════════════════════════════ + +fn {ctor#::main::A:0}(%0: ()) -> ::main::A:0 { + let %1: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + + return %1 + } +} + +thunk {thunk#2}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#3}() -> ::main::A:0 { + let %0: ::main::A:0 + + bb0(): { + %0 = opaque(::main::A:0, ()) + + return %0 + } +} + +thunk {thunk#4}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#5}() -> ::main::A:0 { + let %0: ::main::A:0 + + bb0(): { + %0 = opaque(::main::A:0, ()) + + return %0 + } +} + +thunk {thunk#6}() -> (x: ::main::A:0, y: ::main::A:0) { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ::main::A:0 + let %2: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = opaque(::main::A:0, ()) + %0 = (x: %1, y: %2) + + return %0 + } +} + +fn {ctor#::main::Outer:0}(%0: (), %1: (x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %2: ::main::Outer:0 + + bb0(): { + %2 = opaque(::main::Outer:0, %1) + + return %2 + } +} + +thunk {thunk#7}() -> ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %0: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + + bb0(): { + %0 = closure(({ctor#::main::Outer:0} as FnPtr), ()) + + return %0 + } +} + +*thunk {thunk#8}() -> ::main::Outer:0 { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ::main::A:0 + let %2: ::main::A:0 + let %3: ::main::Outer:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + %2 = opaque(::main::A:0, ()) + %0 = (x: %1, y: %2) + %3 = opaque(::main::Outer:0, %0) + + return %3 + } +} \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.jsonc b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.jsonc new file mode 100644 index 00000000000..730d16d09c2 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.jsonc @@ -0,0 +1,13 @@ +//@ run: pass +//@ description: The interpreter should be able to simply delegate to the underlying struct. +[ + "newtype", + "A", + "Null", + [ + "newtype", + "Outer", + { "#struct": { "x": "A", "y": "A" } }, + ["Outer", { "#struct": { "x": ["A"], "y": ["A"] } }] + ] +] diff --git a/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.stdout b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.stdout new file mode 100644 index 00000000000..fa19ab2c819 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/interpret/access-struct-through-opaque.stdout @@ -0,0 +1,37 @@ +Opaque( + Opaque { + name: Symbol( + "::main::Outer:0", + ), + value: Struct( + Struct { + fields: [ + Symbol( + "x", + ), + Symbol( + "y", + ), + ], + values: [ + Opaque( + Opaque { + name: Symbol( + "::main::A:0", + ), + value: Unit, + }, + ), + Opaque( + Opaque { + name: Symbol( + "::main::A:0", + ), + value: Unit, + }, + ), + ], + }, + ), + }, +) \ No newline at end of file diff --git a/libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.jsonc b/libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.jsonc new file mode 100644 index 00000000000..68a06ccee6d --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.jsonc @@ -0,0 +1,15 @@ +//@ run: pass +//@ description: Cached constructor references must produce closure aggregates, not bare FnPtrs. +//@ description: Regression test: the second use of a constructor was previously emitted as a bare +//@ description: FnPtr (cache hit path), while the calling convention expects a fat pointer (closure). +[ + "newtype", + "A", + "Null", + [ + "newtype", + "Outer", + { "#struct": { "x": "A", "y": "A" } }, + ["Outer", { "#struct": { "x": ["A"], "y": ["A"] } }] + ] +] diff --git a/libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.stdout b/libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.stdout new file mode 100644 index 00000000000..17f3aed3dc1 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/reify/ctor-cached-closure.stdout @@ -0,0 +1,101 @@ +fn {ctor#::main::A:0}(%0: ()) -> ::main::A:0 { + let %1: ::main::A:0 + + bb0(): { + %1 = opaque(::main::A:0, ()) + + return %1 + } +} + +thunk {thunk#2}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#3}() -> ::main::A:0 { + let %0: () -> ::main::A:0 + let %1: ::main::A:0 + + bb0(): { + %0 = apply ({thunk#2} as FnPtr) + %1 = apply %0.0 %0.1 + + return %1 + } +} + +thunk {thunk#4}() -> () -> ::main::A:0 { + let %0: () -> ::main::A:0 + + bb0(): { + %0 = closure(({ctor#::main::A:0} as FnPtr), ()) + + return %0 + } +} + +thunk {thunk#5}() -> ::main::A:0 { + let %0: () -> ::main::A:0 + let %1: ::main::A:0 + + bb0(): { + %0 = apply ({thunk#4} as FnPtr) + %1 = apply %0.0 %0.1 + + return %1 + } +} + +thunk {thunk#6}() -> (x: ::main::A:0, y: ::main::A:0) { + let %0: ::main::A:0 + let %1: ::main::A:0 + let %2: (x: ::main::A:0, y: ::main::A:0) + + bb0(): { + %0 = apply ({thunk#3} as FnPtr) + %1 = apply ({thunk#5} as FnPtr) + %2 = (x: %0, y: %1) + + return %2 + } +} + +fn {ctor#::main::Outer:0}(%0: (), %1: (x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %2: ::main::Outer:0 + + bb0(): { + %2 = opaque(::main::Outer:0, %1) + + return %2 + } +} + +thunk {thunk#7}() -> ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 { + let %0: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + + bb0(): { + %0 = closure(({ctor#::main::Outer:0} as FnPtr), ()) + + return %0 + } +} + +*thunk {thunk#8}() -> ::main::Outer:0 { + let %0: (x: ::main::A:0, y: ::main::A:0) + let %1: ((x: ::main::A:0, y: ::main::A:0)) -> ::main::Outer:0 + let %2: ::main::Outer:0 + + bb0(): { + %0 = apply ({thunk#6} as FnPtr) + %1 = apply ({thunk#7} as FnPtr) + %2 = apply %1.0 %1.1 %0 + + return %2 + } +} \ No newline at end of file diff --git a/tests/graph/http/tests/hashql.http b/tests/graph/http/tests/hashql.http index 64dc55a1d3e..5779af8ea87 100644 --- a/tests/graph/http/tests/hashql.http +++ b/tests/graph/http/tests/hashql.http @@ -43,11 +43,28 @@ POST http://127.0.0.1:4000/hashql Content-Type: application/json { - "query": ["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["input", "temporal_axes", "_"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - {"#literal": false} + "query": ["let", "axes", + ["::graph::temporal::PinnedTransactionTimeTemporalAxes", + {"#struct": { + "pinned": ["::graph::temporal::TransactionTime", + ["::graph::temporal::Timestamp", {"#literal": 4102444800000}] + ], + "variable": ["::graph::temporal::DecisionTime", + ["::graph::temporal::Interval", {"#struct": { + "start": ["::graph::temporal::UnboundedTemporalBound", {"#literal": null}], + "end": ["::graph::temporal::ExclusiveTemporalBound", + ["::graph::temporal::Timestamp", {"#literal": 4102444800000}] + ] + }}] + ] + }} + ], + ["::graph::tail::collect", + ["::graph::body::filter", + ["::graph::head::entities", "axes"], + ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", + {"#literal": false} + ] ] ] ], @@ -88,14 +105,7 @@ POST http://127.0.0.1:4000/hashql Content-Type: application/json { - "query": ["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["input", "temporal_axes", "_"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - {"#literal": false} - ] - ] - ] + "query": {"#literal": 1} } > {% @@ -110,11 +120,28 @@ Content-Type: application/json Json-Compat: true { - "query": ["::graph::tail::collect", - ["::graph::body::filter", - ["::graph::head::entities", ["input", "temporal_axes", "_"]], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", - {"#literal": false} + "query": ["let", "axes", + ["::graph::temporal::PinnedTransactionTimeTemporalAxes", + {"#struct": { + "pinned": ["::graph::temporal::TransactionTime", + ["::graph::temporal::Timestamp", {"#literal": 4102444800000}] + ], + "variable": ["::graph::temporal::DecisionTime", + ["::graph::temporal::Interval", {"#struct": { + "start": ["::graph::temporal::UnboundedTemporalBound", {"#literal": null}], + "end": ["::graph::temporal::ExclusiveTemporalBound", + ["::graph::temporal::Timestamp", {"#literal": 4102444800000}] + ] + }}] + ] + }} + ], + ["::graph::tail::collect", + ["::graph::body::filter", + ["::graph::head::entities", "axes"], + ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", + {"#literal": false} + ] ] ] ], From 5fbfb1db9435805c5bcb99abbcb571ec368a1101 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:48:09 +0200 Subject: [PATCH 23/25] feat: covariance for opaque inners --- apps/hash-graph/src/subcommand/server.rs | 2 +- .../core/src/type/inference/solver/tests.rs | 49 ++++++++ .../hashql/core/src/type/kind/opaque.rs | 110 +++++++++++------- .../hashql/core/src/type/kind/tests/opaque.rs | 67 +++++++---- 4 files changed, 160 insertions(+), 68 deletions(-) diff --git a/apps/hash-graph/src/subcommand/server.rs b/apps/hash-graph/src/subcommand/server.rs index 9d5a0506cc8..c3133969198 100644 --- a/apps/hash-graph/src/subcommand/server.rs +++ b/apps/hash-graph/src/subcommand/server.rs @@ -310,7 +310,7 @@ async fn create_temporal_client( .address .temporal_host .as_deref() - .filter(|h| !h.is_empty()) + .filter(|host| !host.is_empty()) { TemporalClientConfig::new( Url::from_str(&format!("{host}:{}", config.address.temporal_port)) diff --git a/libs/@local/hashql/core/src/type/inference/solver/tests.rs b/libs/@local/hashql/core/src/type/inference/solver/tests.rs index ce441f82bf9..4f8380327ad 100644 --- a/libs/@local/hashql/core/src/type/inference/solver/tests.rs +++ b/libs/@local/hashql/core/src/type/inference/solver/tests.rs @@ -2364,6 +2364,55 @@ fn deferred_upper_constraint() { ); } +// Regression test: W(A()) as W where AB = A | B. +// The constraint system is: W <: W, with ?1 having lower bound A from the +// constructor call. The solver should decompose through the opaque covariantly and resolve +// ?1 = A, since A <: A | B. +#[test] +fn opaque_covariant_subtype_through_union() { + scaffold!(heap, env, builder); + + let hole = builder.fresh_hole(); + + let null = builder.null(); + let integer = builder.integer(); + + // newtype A = Null + let type_a = builder.opaque("A", null); + // newtype B = Integer + let type_b = builder.opaque("B", integer); + // type AB = A | B + let type_ab = builder.union([type_a, type_b]); + + // W (the value produced by the constructor call) + let w_hole = builder.opaque("W", builder.infer(hole)); + // W (the target type from the `as` annotation) + let w_ab = builder.opaque("W", type_ab); + + let mut inference = InferenceEnvironment::new(&env); + // ?1 has lower bound A (from the constructor argument A() flowing into W's parameter) + inference.add_constraint(Constraint::LowerBound { + variable: Variable::synthetic(VariableKind::Hole(hole)), + bound: type_a, + }); + // W <: W (from the `as` expression) + inference.collect_constraints(Variance::Covariant, w_hole, w_ab); + + let solver = inference.into_solver(); + let Success { + value: substitution, + advisories, + } = solver.solve().expect("should have solved"); + assert!(advisories.is_empty()); + + // ?1 should resolve to A + assert_equivalent( + &env, + substitution.infer(hole).expect("should be resolved"), + type_a, + ); +} + // The problem that we currently have is that when we have a nested inference constraint we do // **not** "freeze" a variable in place when we've already determined it's type. We require doing // so, so that we can propagate the type. diff --git a/libs/@local/hashql/core/src/type/kind/opaque.rs b/libs/@local/hashql/core/src/type/kind/opaque.rs index 15fc8cc9e11..173b73f8f28 100644 --- a/libs/@local/hashql/core/src/type/kind/opaque.rs +++ b/libs/@local/hashql/core/src/type/kind/opaque.rs @@ -24,19 +24,29 @@ use crate::{ /// even if their representations are identical. This contrasts with structural typing where /// compatibility is based on structure alone. /// -/// # Type System Design +/// # Semantic Model /// -/// This implementation uses a refined context-sensitive variance approach: -/// - For concrete types or when inference is disabled: Opaque types are **invariant** with respect -/// to their inner representation, enforcing strict nominal typing semantics. -/// - Only for non-concrete types during inference: Opaque types use a **covariant-like** approach -/// that allows them to act as "carriers" for inference variables. +/// For each opaque name `N`, the type constructor is interpreted as: /// -/// This precise, context-sensitive approach provides several benefits: -/// 1. Proper constraint propagation for types containing inference variables -/// 2. Strict nominal type safety for all concrete types -/// 3. Clear separation between inference and checking phases -/// 4. Consistent variance behavior across the type system +/// ```text +/// ⟦N⟧ = { wrap_N(v) | v ∈ ⟦T⟧ } +/// ``` +/// +/// with `wrap_N` injective and distinct names having disjoint images. This gives: +/// +/// - **Covariance**: `⟦A⟧ ⊆ ⟦B⟧ ⟹ ⟦N⟧ ⊆ ⟦N⟧`, so `N <: N`. +/// - **Nominal separation**: if `N ≠ M`, then `⟦N⟧ ∩ ⟦M⟧ = ∅`. +/// - **Meet**: `⟦N⟧ ∩ ⟦N⟧ = ⟦N⟧`. +/// - **Join**: `⟦N⟧ ∪ ⟦N⟧ = ⟦N⟧`, extensionally. +/// +/// Covariance is sound because HashQL values are immutable: the wrapper has no operation +/// that can exploit a `W` view to smuggle an arbitrary `B` back into a `W`. This +/// removes the classic mutation-based unsoundness argument (Pierce, TAPL Ch. 15). +/// +/// An opaque type is never the global top: `W(⊤)` is only the top of the `W` fiber +/// (all `W`-wrapped values), not the whole universe, because it does not contain +/// unwrapped values or values of other opaque families. However, `W(⊥)` IS the +/// global bottom: wrapping an empty set gives an empty set, and `∅ ⊆ S` for any `S`. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct OpaqueType<'heap> { pub name: Symbol<'heap>, @@ -134,21 +144,14 @@ impl<'heap> Lattice<'heap> for OpaqueType<'heap> { /// Computes the meet (greatest lower bound) of two opaque types. /// - /// The meet operation uses a refined context-sensitive approach: - /// - /// When inference is enabled AND at least one type contains inference variables: - /// - If types have different names: Return Never (empty set). - /// - If types have the same name: Meet their inner representations, allowing the opaque type to - /// act as a "carrier" for inference variables until they are resolved. + /// For same-name opaques, meets the inner representations covariantly: + /// `W & W` produces `W`, because `meet(A|B, A) = A`. /// - /// Otherwise (for concrete types or when inference is disabled): - /// - If types have different names: Return Never (empty set). - /// - If types have the same name but different representations: Return Never (empty set). - /// - If types have the same name and equivalent representations: Return the type itself. + /// During inference with non-concrete types, both representations are kept as a + /// "carrier" to defer evaluation until inference variables are resolved. /// - /// This approach prevents premature failure during inference while maintaining strict - /// nominal semantics once types are fully resolved. It allows constraint propagation while - /// preserving type safety. + /// For different-name opaques, returns Never (empty set): distinct nominal types have + /// no common subtype. fn meet( self: Type<'heap, Self>, other: Type<'heap, Self>, @@ -158,26 +161,38 @@ impl<'heap> Lattice<'heap> for OpaqueType<'heap> { return SmallVec::new(); } + if env.is_equivalent(self.kind.repr, other.kind.repr) { + return SmallVec::from_slice_copy(&[self.id]); + } + if env.is_inference_enabled() && (!env.is_concrete(self.kind.repr) || !env.is_concrete(other.kind.repr)) { + // During inference, keep both representations as carriers. The meet is deferred + // until inference variables are resolved and the result is simplified. let self_repr = env.r#type(self.kind.repr); let other_repr = env.r#type(other.kind.repr); - // We circumvent `env.meet` here, by directly meeting the representations. This is - // intentional, so that we can propagate the meet result instead of having a - // `Intersection`. let result = self_repr.meet(other_repr, env); - // If any of the types aren't concrete, we effectively convert ourselves into a - // "carrier" to defer evaluation of the term, once inference is completed we'll simplify - // and postprocess the result. - self.postprocess_lattice(result, env.environment) - } else if env.is_equivalent(self.kind.repr, other.kind.repr) { - SmallVec::from_slice_copy(&[self.id]) - } else { - SmallVec::new() + return self.postprocess_lattice(result, env.environment); } + + // For concrete same-name opaques, meet the inner representations through the full + // lattice meet path which handles simplification (e.g. `Number | Never` to `Number`). + let repr = env.meet(self.kind.repr, other.kind.repr); + + if env.is_bottom(repr) { + return SmallVec::new(); + } + + SmallVec::from_slice_copy(&[env.environment.intern_type(PartialType { + span: self.span, + kind: env.environment.intern_kind(TypeKind::Opaque(OpaqueType { + name: self.kind.name, + repr, + })), + })]) } fn projection( @@ -201,8 +216,11 @@ impl<'heap> Lattice<'heap> for OpaqueType<'heap> { env.is_bottom(self.kind.repr) } - fn is_top(self: Type<'heap, Self>, env: &mut AnalysisEnvironment<'_, 'heap>) -> bool { - env.is_top(self.kind.repr) + fn is_top(self: Type<'heap, Self>, _env: &mut AnalysisEnvironment<'_, 'heap>) -> bool { + // W(Unknown) is the top of the W fiber, not the global top. It does not contain + // unwrapped values or values of other opaque families. Reporting true here would + // cause `T | W(Unknown)` to collapse to `Unknown`, destroying nominal separation. + false } fn is_concrete(self: Type<'heap, Self>, env: &mut AnalysisEnvironment<'_, 'heap>) -> bool { @@ -231,10 +249,14 @@ impl<'heap> Lattice<'heap> for OpaqueType<'heap> { /// Determines if one opaque type is a subtype of another. /// - /// Implements invariant nominal typing semantics: + /// Implements covariant nominal typing semantics: /// 1. Types with different names are always unrelated (neither is a subtype of the other) - /// 2. Types with the same name follow invariant rules for their inner representation (enforced - /// via the `in_invariant` context) + /// 2. Types with the same name check their inner representations covariantly + /// + /// Covariance is sound because HashQL values are immutable: there is no operation that + /// could "put back" a value through the opaque boundary, so the classic mutation-based + /// unsoundness argument does not apply. The nominal boundary (distinct names) is preserved + /// regardless of variance. fn is_subtype_of( self: Type<'heap, Self>, supertype: Type<'heap, Self>, @@ -248,7 +270,7 @@ impl<'heap> Lattice<'heap> for OpaqueType<'heap> { return false; } - env.is_subtype_of(Variance::Invariant, self.kind.repr, supertype.kind.repr) + env.is_subtype_of(Variance::Covariant, self.kind.repr, supertype.kind.repr) } fn is_equivalent( @@ -297,8 +319,10 @@ impl<'heap> Inference<'heap> for OpaqueType<'heap> { return; } - // Opaque types are invariant in regards to their arguments - env.collect_constraints(Variance::Invariant, self.kind.repr, supertype.kind.repr); + // Opaque types are covariant: the inner representation of the subtype must be a subtype + // of the inner representation of the supertype. This is sound because HashQL values are + // immutable. + env.collect_constraints(Variance::Covariant, self.kind.repr, supertype.kind.repr); } fn instantiate(self: Type<'heap, Self>, env: &mut InstantiateEnvironment<'_, 'heap>) -> TypeId { diff --git a/libs/@local/hashql/core/src/type/kind/tests/opaque.rs b/libs/@local/hashql/core/src/type/kind/tests/opaque.rs index 6a4b4d63744..6d715cbf8fd 100644 --- a/libs/@local/hashql/core/src/type/kind/tests/opaque.rs +++ b/libs/@local/hashql/core/src/type/kind/tests/opaque.rs @@ -114,9 +114,9 @@ fn meet_same_name_different_repr() { let mut lattice_env = LatticeEnvironment::new(&env); - // Meeting should result in an opaque type with the same name but representation - // that is the meet of the two representations (just Number in this case) - assert_equiv!(env, a.meet(b, &mut lattice_env), []); + // Meeting covariantly: meet(Number, Number|String) = Number, + // so the result is MyType (which is `a`). + assert_equiv!(env, a.meet(b, &mut lattice_env), [a.id]); } #[test] @@ -180,8 +180,8 @@ fn is_subtype_of() { let mut analysis_env = AnalysisEnvironment::new(&env); - // a should not be a subtype of b (invariant) - assert!(!a.is_subtype_of(b, &mut analysis_env)); + // a should be a subtype of b (Number <: Number|String, covariant) + assert!(a.is_subtype_of(b, &mut analysis_env)); // b should not be a subtype of a (Number|String is not a subtype of Number) assert!(!b.is_subtype_of(a, &mut analysis_env)); @@ -306,6 +306,24 @@ fn lattice_laws() { assert_lattice_laws(&env, a, b, c); } +#[test] +fn is_not_top() { + let heap = Heap::new(); + let env = Environment::new(&heap); + let mut analysis_env = AnalysisEnvironment::new(&env); + + // An opaque wrapping Unknown is NOT the global top. It is only the top of + // its own fiber. Reporting true would cause `T | W` to collapse + // to `Unknown`, destroying nominal separation between opaque families. + let unknown = instantiate(&env, TypeKind::Unknown); + opaque!(env, opaque_unknown, "W", unknown); + assert!(!opaque_unknown.is_top(&mut analysis_env)); + + // Sanity: raw Unknown IS top + let raw_unknown = env.r#type(unknown); + assert!(raw_unknown.is_top(&mut analysis_env)); +} + #[test] fn is_concrete() { let heap = Heap::new(); @@ -350,14 +368,14 @@ fn collect_constraints_same_name() { // Collect constraints between the two opaque types number_opaque.collect_constraints(infer_opaque, &mut inference_env); - // Since opaque types are invariant, we should get an equality constraint - // rather than just an upper or lower bound + // Since opaque types are covariant, we get a lower bound constraint: + // the concrete type constrains the inference variable from below. let constraints = inference_env.take_constraints(); assert_eq!( constraints, - [Constraint::Equals { + [Constraint::LowerBound { variable: Variable::synthetic(VariableKind::Hole(hole)), - r#type: number + bound: number }] ); } @@ -408,13 +426,14 @@ fn collect_constraints_nested() { // Collect constraints between the two nested opaque types outer_a.collect_constraints(outer_b, &mut inference_env); - // Due to invariance through the chain, we should get an equality constraint + // Due to covariance through the chain, we get an upper bound constraint: + // the hole is in the subtype position, so the concrete type is an upper bound. let constraints = inference_env.take_constraints(); assert_eq!( constraints, - [Constraint::Equals { + [Constraint::UpperBound { variable: Variable::synthetic(VariableKind::Hole(hole)), - r#type: number + bound: number }] ); } @@ -483,17 +502,17 @@ fn collect_constraints_generic_params() { // Collect constraints between the two opaque types with generic parameters inference_env.collect_constraints(Variance::Covariant, opaque_a, opaque_b); - // Due to invariance, we should get an equality constraint between the generic parameters + // Due to covariance, we get an ordering constraint between the generic parameters let constraints = inference_env.take_constraints(); assert_eq!(constraints.len(), 1); assert_eq!( constraints[0], - Constraint::Unify { - lhs: Variable { + Constraint::Ordering { + lower: Variable { span: SpanId::SYNTHETIC, kind: VariableKind::Generic(arg1) }, - rhs: Variable { + upper: Variable { span: SpanId::SYNTHETIC, kind: VariableKind::Generic(arg2) } @@ -522,17 +541,17 @@ fn collect_constraints_multiple_infer_vars() { // Collect constraints between the two opaque types opaque_a.collect_constraints(opaque_b, &mut inference_env); - // Due to invariance, we should get an equality constraint between the inference variables + // Due to covariance, we get an ordering constraint between the inference variables let constraints = inference_env.take_constraints(); assert_eq!(constraints.len(), 1); assert_eq!( constraints[0], - Constraint::Unify { - lhs: Variable { + Constraint::Ordering { + lower: Variable { span: SpanId::SYNTHETIC, kind: VariableKind::Hole(hole_var1) }, - rhs: Variable { + upper: Variable { span: SpanId::SYNTHETIC, kind: VariableKind::Hole(hole_var2) } @@ -560,18 +579,18 @@ fn collect_constraints_infer_and_generic_var() { // Collect constraints between the two opaque types opaque_a.collect_constraints(opaque_b, &mut inference_env); - // Due to invariance, we should get an equality constraint between the inference variable + // Due to covariance, we get an ordering constraint between the inference variable // and the generic variable let constraints = inference_env.take_constraints(); assert_eq!(constraints.len(), 1); assert_eq!( constraints[0], - Constraint::Unify { - lhs: Variable { + Constraint::Ordering { + lower: Variable { span: SpanId::SYNTHETIC, kind: VariableKind::Hole(hole_var1) }, - rhs: Variable { + upper: Variable { span: SpanId::SYNTHETIC, kind: VariableKind::Generic(arg) } From 55c8eaa43ba727c3a71a202931b3805844f1a553 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:52:06 +0200 Subject: [PATCH 24/25] fix: cfgattr serde --- libs/@local/hashql/core/src/span/mod.rs | 9 +++++--- .../core/src/type/inference/solver/tests.rs | 1 + tests/graph/http/tests/hashql.http | 23 ++++++++++++------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/libs/@local/hashql/core/src/span/mod.rs b/libs/@local/hashql/core/src/span/mod.rs index 9d3da1b043f..3cd0a8b39f6 100644 --- a/libs/@local/hashql/core/src/span/mod.rs +++ b/libs/@local/hashql/core/src/span/mod.rs @@ -275,9 +275,12 @@ pub use self::table::SpanTable; /// Some(TextRange::new(0.into(), 10.into())) /// ); /// ``` -#[expect( - clippy::unsafe_derive_deserialize, - reason = "id() is safe to use with serde" +#[cfg_attr( + feature = "serde", + expect( + clippy::unsafe_derive_deserialize, + reason = "id() is safe to use with serde" + ) )] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/libs/@local/hashql/core/src/type/inference/solver/tests.rs b/libs/@local/hashql/core/src/type/inference/solver/tests.rs index 4f8380327ad..fb674711976 100644 --- a/libs/@local/hashql/core/src/type/inference/solver/tests.rs +++ b/libs/@local/hashql/core/src/type/inference/solver/tests.rs @@ -1,3 +1,4 @@ +#![expect(clippy::similar_names)] use hashql_diagnostics::Success; use super::{Constraint, InferenceSolver, VariableConstraint}; diff --git a/tests/graph/http/tests/hashql.http b/tests/graph/http/tests/hashql.http index 5779af8ea87..5dfe5041548 100644 --- a/tests/graph/http/tests/hashql.http +++ b/tests/graph/http/tests/hashql.http @@ -51,7 +51,7 @@ Content-Type: application/json ], "variable": ["::graph::temporal::DecisionTime", ["::graph::temporal::Interval", {"#struct": { - "start": ["::graph::temporal::UnboundedTemporalBound", {"#literal": null}], + "start": ["::graph::temporal::UnboundedTemporalBound"], "end": ["::graph::temporal::ExclusiveTemporalBound", ["::graph::temporal::Timestamp", {"#literal": 4102444800000}] ] @@ -62,7 +62,7 @@ Content-Type: application/json ["::graph::tail::collect", ["::graph::body::filter", ["::graph::head::entities", "axes"], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", + ["fn", {"#tuple": []}, {"#struct": {"vertex": "_"}}, "_", {"#literal": false} ] ] @@ -76,8 +76,10 @@ Content-Type: application/json client.assert(response.status === 200, "Response status is not 200"); }); client.test("empty list", function() { - client.assert(Array.isArray(response.body), "Response body is not an array"); - client.assert(response.body.length === 0, "Expected empty list for filter-false"); + client.assert(response.body.value !== undefined, "Expected 'value' field"); + client.assert(response.body.value.List !== undefined, "Expected 'List' in value"); + client.assert(response.body.value.List.length === 0, "Expected empty list for filter-false"); + client.assert(response.body.advisories !== undefined, "Expected 'advisories' field"); }); %} @@ -100,7 +102,7 @@ Content-Type: application/json }); %} -### HashQL: missing inputs field returns 422 +### HashQL: missing inputs field returns 400 POST http://127.0.0.1:4000/hashql Content-Type: application/json @@ -110,7 +112,12 @@ Content-Type: application/json > {% client.test("status", function() { - client.assert(response.status === 422, "Expected 422 for missing inputs field"); + client.assert(response.status === 400, "Expected 400 for missing inputs field"); + }); + client.test("error message mentions inputs", function() { + client.assert(response.body.message !== undefined, "Expected error message"); + client.assert(response.body.contents[0].Error.metadata.deserializationError.includes("inputs"), + "Expected error to mention missing 'inputs' field"); }); %} @@ -128,7 +135,7 @@ Json-Compat: true ], "variable": ["::graph::temporal::DecisionTime", ["::graph::temporal::Interval", {"#struct": { - "start": ["::graph::temporal::UnboundedTemporalBound", {"#literal": null}], + "start": ["::graph::temporal::UnboundedTemporalBound"], "end": ["::graph::temporal::ExclusiveTemporalBound", ["::graph::temporal::Timestamp", {"#literal": 4102444800000}] ] @@ -139,7 +146,7 @@ Json-Compat: true ["::graph::tail::collect", ["::graph::body::filter", ["::graph::head::entities", "axes"], - ["fn", { "#tuple": [] }, { "#struct": { "vertex": "_" } }, "_", + ["fn", {"#tuple": []}, {"#struct": {"vertex": "_"}}, "_", {"#literal": false} ] ] From cd47de2d082ea304a3c9f97f0ed1c335d2e29c0c Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:54:48 +0200 Subject: [PATCH 25/25] chore: remove graph module --- libs/@local/hashql/eval/src/graph/error.rs | 26 - libs/@local/hashql/eval/src/graph/mod.rs | 2 - .../hashql/eval/src/graph/read/convert.rs | 87 --- .../hashql/eval/src/graph/read/error.rs | 667 ---------------- .../hashql/eval/src/graph/read/filter.rs | 255 ------- .../hashql/eval/src/graph/read/filter_expr.rs | 715 ------------------ libs/@local/hashql/eval/src/graph/read/mod.rs | 259 ------- .../@local/hashql/eval/src/graph/read/path.rs | 583 -------------- .../@local/hashql/eval/src/graph/read/sink.rs | 72 -- 9 files changed, 2666 deletions(-) delete mode 100644 libs/@local/hashql/eval/src/graph/error.rs delete mode 100644 libs/@local/hashql/eval/src/graph/mod.rs delete mode 100644 libs/@local/hashql/eval/src/graph/read/convert.rs delete mode 100644 libs/@local/hashql/eval/src/graph/read/error.rs delete mode 100644 libs/@local/hashql/eval/src/graph/read/filter.rs delete mode 100644 libs/@local/hashql/eval/src/graph/read/filter_expr.rs delete mode 100644 libs/@local/hashql/eval/src/graph/read/mod.rs delete mode 100644 libs/@local/hashql/eval/src/graph/read/path.rs delete mode 100644 libs/@local/hashql/eval/src/graph/read/sink.rs diff --git a/libs/@local/hashql/eval/src/graph/error.rs b/libs/@local/hashql/eval/src/graph/error.rs deleted file mode 100644 index 2739f9ce016..00000000000 --- a/libs/@local/hashql/eval/src/graph/error.rs +++ /dev/null @@ -1,26 +0,0 @@ -use alloc::borrow::Cow; - -use hashql_diagnostics::category::DiagnosticCategory; - -use super::read::error::GraphReadCompilerDiagnosticCategory; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum GraphCompilerDiagnosticCategory { - Read(GraphReadCompilerDiagnosticCategory), -} - -impl DiagnosticCategory for GraphCompilerDiagnosticCategory { - fn id(&self) -> Cow<'_, str> { - Cow::Borrowed("graph") - } - - fn name(&self) -> Cow<'_, str> { - Cow::Borrowed("Graph") - } - - fn subcategory(&self) -> Option<&dyn DiagnosticCategory> { - match self { - Self::Read(read) => Some(read), - } - } -} diff --git a/libs/@local/hashql/eval/src/graph/mod.rs b/libs/@local/hashql/eval/src/graph/mod.rs deleted file mode 100644 index 6c733dc3f6c..00000000000 --- a/libs/@local/hashql/eval/src/graph/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod error; -pub mod read; diff --git a/libs/@local/hashql/eval/src/graph/read/convert.rs b/libs/@local/hashql/eval/src/graph/read/convert.rs deleted file mode 100644 index c86fd1642e4..00000000000 --- a/libs/@local/hashql/eval/src/graph/read/convert.rs +++ /dev/null @@ -1,87 +0,0 @@ -use alloc::borrow::Cow; - -use hash_graph_store::filter::Parameter; -use hashql_core::value::{Primitive, Value}; -use type_system::knowledge::PropertyValue; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, derive_more::Display)] -pub(crate) enum ConversionError { - #[display("dictionary keys must be strings")] - DictKeyNotString, -} - -fn value_to_string(value: &Value<'_>) -> Option { - match value { - Value::Primitive(Primitive::String(string)) => Some(string.as_str().to_owned()), - Value::Opaque(opaque) => value_to_string(opaque.value()), - Value::Primitive(_) - | Value::Struct(_) - | Value::Tuple(_) - | Value::List(_) - | Value::Dict(_) => None, - } -} - -fn value_to_property_value(value: &Value<'_>) -> Result { - match value { - Value::Primitive(Primitive::Null) => Ok(PropertyValue::Null), - &Value::Primitive(Primitive::Boolean(bool)) => Ok(PropertyValue::Bool(bool)), - Value::Primitive(Primitive::Integer(integer)) => { - Ok(PropertyValue::Number(integer.as_real())) - } - Value::Primitive(Primitive::Float(float)) => Ok(PropertyValue::Number(float.as_real())), - Value::Primitive(Primitive::String(string)) => { - Ok(PropertyValue::String(string.as_str().to_owned())) - } - Value::Struct(r#struct) => r#struct - .iter() - .map(|(key, value)| { - let key = key.as_str().to_owned(); - let value = value_to_property_value(value)?; - - Ok((key, value)) - }) - .try_collect() - .map(PropertyValue::Object), - Value::Tuple(tuple) => tuple - .iter() - .map(value_to_property_value) - .try_collect() - .map(PropertyValue::Array), - Value::List(list) => list - .iter() - .map(value_to_property_value) - .try_collect() - .map(PropertyValue::Array), - Value::Dict(dict) => dict - .iter() - .map(|(key, value)| { - let key = value_to_string(key).ok_or(ConversionError::DictKeyNotString)?; - let value = value_to_property_value(value)?; - - Ok((key, value)) - }) - .try_collect() - .map(PropertyValue::Object), - Value::Opaque(opaque) => value_to_property_value(opaque.value()), - } -} - -pub(super) fn convert_value_to_parameter<'heap>( - value: &Value<'heap>, -) -> Result, ConversionError> { - match value { - &Value::Primitive(Primitive::Boolean(bool)) => Ok(Parameter::Boolean(bool)), - Value::Primitive(Primitive::Integer(integer)) => Ok(Parameter::Decimal(integer.as_real())), - Value::Primitive(Primitive::Float(float)) => Ok(Parameter::Decimal(float.as_real())), - Value::Primitive(Primitive::String(string)) => { - Ok(Parameter::Text(Cow::Borrowed(string.as_symbol().unwrap()))) - } - Value::Primitive(Primitive::Null) - | Value::Struct(_) - | Value::Tuple(_) - | Value::List(_) - | Value::Dict(_) => value_to_property_value(value).map(Parameter::Any), - Value::Opaque(opaque) => convert_value_to_parameter(opaque.value()), - } -} diff --git a/libs/@local/hashql/eval/src/graph/read/error.rs b/libs/@local/hashql/eval/src/graph/read/error.rs deleted file mode 100644 index 49377a8be2c..00000000000 --- a/libs/@local/hashql/eval/src/graph/read/error.rs +++ /dev/null @@ -1,667 +0,0 @@ -use alloc::borrow::Cow; -use core::fmt::Debug; - -use hashql_core::{ - span::{SpanId, Spanned}, - symbol::Ident, - value::{FieldAccessError, IndexAccessError}, -}; -use hashql_diagnostics::{ - Diagnostic, DiagnosticIssues, Label, Status, - category::{DiagnosticCategory, TerminalDiagnosticCategory}, - diagnostic::Message, - severity::Severity, -}; -use hashql_hir::node::{branch::Branch, operation::BinOp, variable::QualifiedVariable}; - -use super::{FilterCompilerContext, convert::ConversionError}; - -pub type GraphReadCompilerDiagnostic = - Diagnostic; -pub type GraphReadCompilerIssues = - DiagnosticIssues; -pub type GraphReadCompilerStatus = Status; - -const VALUE_PARAMETER_CONVERSION: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "value-parameter-conversion", - name: "Cannot convert value to graph parameter", -}; - -const PATH_CONVERSION: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "path-conversion", - name: "Cannot query against complex object", -}; - -const QUALIFIED_VARIABLE_UNSUPPORTED: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "qualified-variable-unsupported", - name: "Qualified variables not supported", -}; - -const TYPE_CONSTRUCTOR_UNSUPPORTED: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "type-constructor-unsupported", - name: "Type constructors not supported as values", -}; - -const BINARY_OPERATION_UNSUPPORTED: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "binary-operation-unsupported", - name: "Binary operations not supported in this context", -}; - -const PATH_INDEXING_UNSUPPORTED: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "path-indexing-unsupported", - name: "Indexing through traversal paths not supported", -}; - -const FIELD_ACCESS_INTERNAL_ERROR: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "field-access-internal-error", - name: "Internal error during field access", -}; - -const INDEX_ACCESS_INTERNAL_ERROR: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "index-access-internal-error", - name: "Internal error during index access", -}; - -const PATH_TRAVERSAL_INTERNAL_ERROR: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "path-traversal-internal-error", - name: "Internal error during path traversal", -}; - -const CALL_UNSUPPORTED: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "call-unsupported", - name: "Function calls not supported", -}; - -const CLOSURE_UNSUPPORTED: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "closure-unsupported", - name: "Closures not supported", -}; - -const NESTED_GRAPH_READ_UNSUPPORTED: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "nested-graph-read-unsupported", - name: "Nested graph read operations not supported", -}; - -const BRANCH_UNSUPPORTED: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "branch-unsupported", - name: "Branch construct unsupported", -}; - -const PATH_IN_DATA_CONSTRUCT_UNSUPPORTED: TerminalDiagnosticCategory = TerminalDiagnosticCategory { - id: "path-in-data-construct-unsupported", - name: "Path in data construct unsupported", -}; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum GraphReadCompilerDiagnosticCategory { - ValueParameterConversion, - PathConversion, - QualifiedVariableUnsupported, - TypeConstructorUnsupported, - BinaryOperationUnsupported, - PathIndexingUnsupported, - FieldAccessInternalError, - IndexAccessInternalError, - PathTraversalInternalError, - CallUnsupported, - ClosureUnsupported, - NestedGraphReadUnsupported, - BranchUnsupported, - PathInDataConstructUnsupported, -} - -impl DiagnosticCategory for GraphReadCompilerDiagnosticCategory { - fn id(&self) -> Cow<'_, str> { - Cow::Borrowed("graph-read-compiler") - } - - fn name(&self) -> Cow<'_, str> { - Cow::Borrowed("Graph Read Compiler") - } - - fn subcategory(&self) -> Option<&dyn DiagnosticCategory> { - match self { - Self::ValueParameterConversion => Some(&VALUE_PARAMETER_CONVERSION), - Self::PathConversion => Some(&PATH_CONVERSION), - Self::QualifiedVariableUnsupported => Some(&QUALIFIED_VARIABLE_UNSUPPORTED), - Self::TypeConstructorUnsupported => Some(&TYPE_CONSTRUCTOR_UNSUPPORTED), - Self::BinaryOperationUnsupported => Some(&BINARY_OPERATION_UNSUPPORTED), - Self::PathIndexingUnsupported => Some(&PATH_INDEXING_UNSUPPORTED), - Self::FieldAccessInternalError => Some(&FIELD_ACCESS_INTERNAL_ERROR), - Self::IndexAccessInternalError => Some(&INDEX_ACCESS_INTERNAL_ERROR), - Self::PathTraversalInternalError => Some(&PATH_TRAVERSAL_INTERNAL_ERROR), - Self::CallUnsupported => Some(&CALL_UNSUPPORTED), - Self::ClosureUnsupported => Some(&CLOSURE_UNSUPPORTED), - Self::NestedGraphReadUnsupported => Some(&NESTED_GRAPH_READ_UNSUPPORTED), - Self::BranchUnsupported => Some(&BRANCH_UNSUPPORTED), - Self::PathInDataConstructUnsupported => Some(&PATH_IN_DATA_CONSTRUCT_UNSUPPORTED), - } - } -} - -// TODO: test once https://linear.app/hash/issue/H-4603/enable-dict-literal-construct lands -pub(super) fn value_parameter_conversion_error( - context: FilterCompilerContext, - value_span: SpanId, - error: ConversionError, -) -> GraphReadCompilerDiagnostic { - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::ValueParameterConversion, - Severity::Error, - ) - .primary(Label::new( - value_span, - format!("Cannot convert value to graph parameter: {error}"), - )); - - diagnostic.labels.push(Label::new( - context.span, - "... within this filter expression", - )); - - diagnostic.add_message(Message::help( - "Graph parameters require valid JSON-compatible values. Dictionary keys must be strings, \ - as non-string keys cannot be properly serialized. Ensure all object keys in your data \ - are strings.", - )); - - diagnostic.add_message(Message::note( - "This error may indicate a data modeling issue. Entities should be JSON-compliant with \ - string keys. If you're seeing this error, the data structure you're trying to convert is \ - not serializable into JSON.", - )); - - diagnostic -} - -pub(super) fn path_conversion_error( - context: FilterCompilerContext, - path_span: SpanId, -) -> GraphReadCompilerDiagnostic { - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::PathConversion, - Severity::Error, - ) - .primary(Label::new( - path_span, - "Cannot query against this complex object", - )); - - diagnostic.labels.push(Label::new( - context.span, - "... within this filter expression", - )); - - diagnostic.add_message(Message::help( - "Filter expressions can only query against simple scalar properties that map to database \ - columns, not complex objects. Use individual properties of the object instead (e.g., \ - `entity.metadata.record_id.entity_id.entity_uuid` instead of \ - `entity.metadata.record_id`).", - )); - - diagnostic.add_message(Message::note( - "This is a temporary limitation of the current query compiler. Support for querying \ - against complex objects in filter expressions is being tracked in \ - https://linear.app/hash/issue/H-4911/hashql-allow-for-querying-against-complex-objects.", - )); - - diagnostic -} - -// In *theory* there's no way to hit this, as any value that's a qualified variable has already been -// resolved to a type-constructor, or intrinsic. This is just here as a sanity check until the -// proper module system lands. -#[coverage(off)] -pub(super) fn qualified_variable_unsupported( - context: FilterCompilerContext, - variable: &QualifiedVariable, - span: SpanId, -) -> GraphReadCompilerDiagnostic { - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::QualifiedVariableUnsupported, - Severity::Error, - ) - .primary(Label::new( - span, - format!( - "Qualified variable `{}` not supported here", - variable.name() - ), - )); - - diagnostic.labels.push(Label::new( - context.span, - "... within this filter expression", - )); - - diagnostic.add_message(Message::help(format!( - "Qualified variables like `{}` are not currently supported in filter expressions. Use \ - local variables or direct parameter references instead.", - variable.name() - ))); - - diagnostic.add_message(Message::note( - "Qualified variables are not yet supported. Implementation of a proper module system is \ - being tracked in https://linear.app/hash/issue/H-4912/hashql-implement-modules.", - )); - - diagnostic -} - -pub(super) fn type_constructor_unsupported( - context: FilterCompilerContext, - span: SpanId, -) -> GraphReadCompilerDiagnostic { - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::TypeConstructorUnsupported, - Severity::Error, - ) - .primary(Label::new( - span, - Cow::Borrowed("Cannot use constructor as value here"), - )); - - diagnostic.labels.push(Label::new( - context.span, - "... within this filter expression", - )); - - diagnostic.add_message(Message::help( - "Constructor functions cannot currently be used as first-class values in filter \ - expressions. You can still call constructors to create values (e.g., `SomeType(x)`), but \ - you cannot use the constructor itself in comparisons or pass it as an argument within \ - filter contexts.", - )); - - diagnostic.add_message(Message::note( - "This is a current limitation of the filter expression compiler. Constructors work as \ - first-class values elsewhere in the language, and support for this in filter expressions \ - is being tracked in https://linear.app/hash/issue/H-4913/hashql-implement-vm.", - )); - - diagnostic -} - -pub(super) fn binary_operation_unsupported( - context: FilterCompilerContext, - op: Spanned, -) -> GraphReadCompilerDiagnostic { - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::BinaryOperationUnsupported, - Severity::Error, - ) - .primary(Label::new( - op.span, - format!("Operation `{}` not supported here", op.value.as_str()), - )); - - diagnostic.labels.push(Label::new( - context.span, - "... within this filter expression", - )); - - diagnostic.add_message(Message::help(format!( - "The `{0}` operation can only be used at the top level of filter conditions, not as an \ - operand in other operations. For example, `(a {0} b) == c` is not allowed, but `(a {0} \ - b) && (c == d)` is valid.", - op.value.as_str(), - ))); - - diagnostic.add_message(Message::note( - "This is an intentional current limitation to keep expressions simple, but there are \ - plans to remove this restriction in the future to allow more complex expressions. \ - Progress on this enhancement is tracked in \ - https://linear.app/hash/issue/H-4911/hashql-allow-for-querying-against-complex-objects.", - )); - - diagnostic -} - -// TODO: requires https://linear.app/hash/issue/H-4603/enable-dict-literal-construct or -// https://linear.app/hash/issue/H-4870/implement-properties-traversal-primitive -pub(super) fn path_indexing_unsupported( - context: FilterCompilerContext, - expr_span: SpanId, - index_span: SpanId, -) -> GraphReadCompilerDiagnostic { - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::PathIndexingUnsupported, - Severity::Error, - ) - .primary(Label::new(index_span, "Cannot use computed value as index")); - - diagnostic - .labels - .push(Label::new(expr_span, "... when indexing this value")); - - diagnostic.labels.push(Label::new( - context.span, - "... within this filter expression", - )); - - diagnostic.add_message(Message::help( - "Dynamic indexing using database values is not currently supported in filter expressions. \ - Use a literal value like `[\"key\"]` or `[0]` instead of computed values like \ - `[entity.id]`. This limitation exists because such operations are complex to translate \ - into database queries.", - )); - - diagnostic.add_message(Message::note( - "This is a temporary limitation of the database query compiler. Support for dynamic \ - indexing using computed values in filter expressions is being tracked in \ - https://linear.app/hash/issue/H-4914/hashql-support-indexing-into-collections-based-on-query-paths.", - )); - - diagnostic -} - -#[coverage(off)] -pub(crate) fn field_access_internal_error( - expr_span: SpanId, - field: &Ident, - error: &FieldAccessError, -) -> GraphReadCompilerDiagnostic { - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::FieldAccessInternalError, - Severity::Bug, - ) - .primary(Label::new( - field.span, - format!("Field access for `{field}` failed unexpectedly"), - )); - - diagnostic - .labels - .push(Label::new(expr_span, "... on this value")); - - diagnostic.add_message(Message::help( - "This is an internal compiler error. The field access should have been validated by the \ - type checker, but the operation failed during compilation. Please report this as a bug \ - with the code that triggered this error.", - )); - - diagnostic.add_message(Message::note( - "This error indicates a bug in the type checker or compiler. The field access was \ - expected to succeed based on type information, but failed during evaluation.", - )); - - diagnostic.add_message(Message::note(format!( - "Internal error that occurred: {error}" - ))); - - diagnostic -} - -#[coverage(off)] -pub(crate) fn index_access_internal_error( - expr_span: SpanId, - index_span: SpanId, - error: &IndexAccessError, -) -> GraphReadCompilerDiagnostic { - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::IndexAccessInternalError, - Severity::Bug, - ) - .primary(Label::new(index_span, "Index access failed unexpectedly")); - - diagnostic - .labels - .push(Label::new(expr_span, "... on this value")); - - diagnostic.add_message(Message::help( - "This is an internal compiler error. The index access should have been validated by the \ - type checker, but the operation failed during compilation. Please report this as a bug \ - with the code that triggered this error.", - )); - - diagnostic.add_message(Message::note( - "This error indicates a bug in the type checker or compiler. The index access was \ - expected to succeed based on type information, but failed during evaluation.", - )); - - diagnostic.add_message(Message::note(format!( - "Internal error that occurred: {error}" - ))); - - diagnostic -} - -#[coverage(off)] -pub(crate) fn path_traversal_internal_error

( - expr_span: SpanId, - access_span: SpanId, - path: Option<&P>, -) -> GraphReadCompilerDiagnostic -where - P: Debug, -{ - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::PathTraversalInternalError, - Severity::Bug, - ) - .primary(Label::new( - access_span, - "Path traversal failed unexpectedly", - )); - - diagnostic - .labels - .push(Label::new(expr_span, "... on this value")); - - diagnostic.add_message(Message::help( - "This is an internal compiler error. The path traversal should have been validated by the \ - type checker, but failed during compilation. This indicates a mismatch between the type \ - checker's expectations and the actual path structure. Please report this as a bug with \ - the code that triggered this error.", - )); - - diagnostic.add_message(Message::note( - "This error suggests that the partial query path code was not properly adjusted to match \ - the type checker's validation. This is a compiler implementation bug.", - )); - - diagnostic.add_message(Message::note(format!( - "The path you were trying to access: {path:?}" - ))); - - diagnostic -} - -pub(super) fn call_unsupported( - context: FilterCompilerContext, - call_span: SpanId, -) -> GraphReadCompilerDiagnostic { - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::CallUnsupported, - Severity::Error, - ) - .primary(Label::new(call_span, "Function call not supported here")); - - diagnostic.labels.push(Label::new( - context.span, - "... within this filter expression", - )); - - diagnostic.add_message(Message::help( - "Filter expressions do not currently support function calls. Move the function call \ - outside the filter expression, assign the result to a variable, and use that variable in \ - the filter instead.", - )); - - diagnostic.add_message(Message::note( - "Function calls in filter expressions are not yet implemented. This feature may be added \ - in future versions for specific categories of pure functions. Progress is tracked in \ - https://linear.app/hash/issue/H-4913/hashql-implement-vm.", - )); - - diagnostic -} - -pub(super) fn closure_unsupported( - context: FilterCompilerContext, - closure_span: SpanId, -) -> GraphReadCompilerDiagnostic { - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::ClosureUnsupported, - Severity::Error, - ) - .primary(Label::new( - closure_span, - "Closure definition not supported here", - )); - - diagnostic.labels.push(Label::new( - context.span, - "... within this filter expression", - )); - - diagnostic.add_message(Message::help( - "Filter expressions do not currently support closure definitions. Move the closure \ - outside the filter expression, assign the result to a variable, and use that variable in \ - the filter instead.", - )); - - diagnostic.add_message(Message::note( - "Closures in filter expressions are not yet implemented. This is a current limitation \ - that is being tracked in https://linear.app/hash/issue/H-4913/hashql-implement-vm.", - )); - - diagnostic -} - -pub(super) fn nested_graph_read_unsupported( - context: FilterCompilerContext, - graph_span: SpanId, -) -> GraphReadCompilerDiagnostic { - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::NestedGraphReadUnsupported, - Severity::Error, - ) - .primary(Label::new( - graph_span, - "Nested graph read operations not supported here", - )); - - diagnostic.labels.push(Label::new( - context.span, - "... within this filter expression", - )); - - diagnostic.add_message(Message::help( - "Filter expressions do not support nested graph operations. Use a separate query for the \ - nested graph read and pass the result to this filter expression.", - )); - - diagnostic.add_message(Message::note( - "Nested graph reads in filter expressions are not yet implemented. This is a current \ - limitation that is being tracked in \ - https://linear.app/hash/issue/H-4913/hashql-implement-vm.", - )); - - diagnostic -} - -/// Context type for branch unsupported diagnostics. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum BranchContext { - /// Branch in a filter boolean expression context. - Filter, - /// Branch in a filter expression context. - FilterExpression, -} - -/// Creates a diagnostic for unsupported branch constructs in filter contexts. -/// -/// This diagnostic is emitted when conditional statements (if/else) are encountered -/// in filter contexts where they are not supported. -/// -/// # Arguments -/// -/// * `context` - The filter compilation context containing the parent filter span -/// * `branch` - The branch construct that is not supported -/// * `branch_context` - Whether this is in a filter or filter expression context -/// -/// # Returns -/// -/// A diagnostic indicating that branch constructs are not supported in this context, -/// with guidance on how to restructure the code. -pub(super) fn branch_unsupported( - branch: &Branch, - span: SpanId, - branch_context: BranchContext, -) -> GraphReadCompilerDiagnostic { - // Create specific primary message based on branch type - let primary_message = match branch { - Branch::If(_) => "conditional expressions are not supported in filter contexts", - }; - - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::BranchUnsupported, - Severity::Error, - ) - .primary(Label::new(span, primary_message)); - - diagnostic.add_message(Message::help( - "rewrite the logic to avoid conditional statements", - )); - - diagnostic.add_message(Message::note( - "conditional statements (`if`/`else`) are not supported by the current query compiler. \ - This is a fundamental limitation that will be addressed in the next-generation compiler", - )); - - match branch_context { - BranchContext::Filter => { - diagnostic.add_message(Message::note( - "consider using boolean operators (`&&`, `||`, `!`) to combine conditions \ - directly instead of `if` statements", - )); - } - BranchContext::FilterExpression => { - diagnostic.add_message(Message::note( - "conditional logic must be handled outside the query or using other language \ - constructs", - )); - } - } - - diagnostic -} - -/// Creates a diagnostic for unsupported path expressions within data constructs. -/// -/// This diagnostic is emitted when path expressions are encountered within data -/// constructs like tuples, lists, dictionaries, or structs, where they are not -/// supported by the current query compiler. -/// -/// # Arguments -/// -/// * `path_span` - The span of the path expression within the data construct -/// * `construct_name` - The name of the data construct containing the path (e.g., "tuple", "list") -/// -/// # Returns -/// -/// A diagnostic indicating that path expressions are not supported within the -/// specified data construct, with guidance on alternative approaches. -pub(super) fn path_in_data_construct_unsupported(path_span: SpanId) -> GraphReadCompilerDiagnostic { - let mut diagnostic = Diagnostic::new( - GraphReadCompilerDiagnosticCategory::PathInDataConstructUnsupported, - Severity::Error, - ) - .primary(Label::new( - path_span, - "path expressions are not supported in data constructs", - )); - - diagnostic.add_message(Message::help( - "rewrite the logic to avoid path expressions in data constructs", - )); - - diagnostic.add_message(Message::note( - "path expressions within complex data constructs (tuples, lists, dictionaries, structs) \ - are not supported by the current query compiler. This is a fundamental limitation that \ - will be addressed in the next-generation compiler", - )); - - diagnostic -} diff --git a/libs/@local/hashql/eval/src/graph/read/filter.rs b/libs/@local/hashql/eval/src/graph/read/filter.rs deleted file mode 100644 index 9dd14148bb1..00000000000 --- a/libs/@local/hashql/eval/src/graph/read/filter.rs +++ /dev/null @@ -1,255 +0,0 @@ -use core::fmt::Debug; - -use hash_graph_store::filter::{Filter, FilterExpression, Parameter, QueryRecord}; -use hashql_core::value::Primitive; -use hashql_hir::node::{ - Node, - branch::{Branch, If}, - call::{Call, PointerKind}, - data::Data, - kind::NodeKind, - r#let::{Binding, Let}, - operation::{BinOp, BinaryOperation, Operation, TypeAssertion, TypeOperation}, - thunk::Thunk, - variable::{LocalVariable, Variable}, -}; - -use super::{ - CompilationError, FilterCompilerContext, GraphReadCompiler, - error::qualified_variable_unsupported, path::CompleteQueryPath, sink::FilterSink, -}; - -/// Checks if a [`Node`] represents a boolean literal with the specified value. -/// -/// This helper function is used to optimize conditional expressions when one -/// branch is a literal boolean value. -/// -/// # Examples -/// -/// ```ignore -/// // Returns true if the node represents `true` -/// assert!(is_bool_literal(true_node, true)); -/// // Returns false if the node represents `false` when checking for `true` -/// assert!(!is_bool_literal(false_node, true)); -/// ``` -fn is_bool_literal(node: Node<'_>, value: bool) -> bool { - matches!( - node.kind, - NodeKind::Data(Data::Primitive(Primitive::Boolean(constant))) if constant == value - ) -} - -impl<'env, 'heap: 'env> GraphReadCompiler<'env, 'heap> { - #[expect(clippy::too_many_lines, reason = "match statement")] - pub(super) fn compile_filter( - &mut self, - context: FilterCompilerContext, - node: Node<'heap>, - sink: &mut FilterSink<'_, 'heap, R>, - ) -> Result<(), CompilationError> - where - R: QueryRecord: CompleteQueryPath<'heap, PartialQueryPath: Debug> + Clone>, - { - match node.kind { - NodeKind::Variable(Variable::Local(LocalVariable { id, arguments: _ })) => { - debug_assert_ne!( - id.value, context.param_id, - "typecheck should have caught this, cannot just return the entity itself." - ); - - let value = self.locals[&id.value]; - self.compile_filter(context, value, sink) - } - NodeKind::Variable(Variable::Qualified(qualified)) => { - self.diagnostics.push(qualified_variable_unsupported( - context, &qualified, node.span, - )); - - Err(CompilationError) - } - NodeKind::Let(Let { bindings, body }) => { - for Binding { - span: _, - binder, - value, - } in bindings - { - self.locals.insert(binder.id, *value); - } - - let filter = self.compile_filter(context, body, sink); - - for Binding { - span: _, - binder, - value: _, - } in bindings - { - self.locals.remove(&binder.id); - } - - filter - } - NodeKind::Operation(Operation::Binary(BinaryOperation { op, left, right })) => { - let func = match op.value { - BinOp::And => { - let mut sink = sink.and(); - - let (left, right) = ( - self.compile_filter(context, left, &mut sink), - self.compile_filter(context, right, &mut sink), - ); - - // We defer the error handling to make sure we gather every diagnostic - left?; - right?; - - return Ok(()); - } - BinOp::Or => { - let mut sink = sink.or(); - - let (left, right) = ( - self.compile_filter(context, left, &mut sink), - self.compile_filter(context, right, &mut sink), - ); - - // We defer the error handling to make sure we gather every diagnostic - left?; - right?; - - return Ok(()); - } - BinOp::Eq => Filter::Equal, - BinOp::Ne => Filter::NotEqual, - BinOp::Lt => Filter::Less, - BinOp::Lte => Filter::LessOrEqual, - BinOp::Gt => Filter::Greater, - BinOp::Gte => Filter::GreaterOrEqual, - }; - - let (left, right) = ( - self.compile_filter_expr(context, left) - .and_then(|expr| expr.finish(context, &mut self.diagnostics)), - self.compile_filter_expr(context, right) - .and_then(|expr| expr.finish(context, &mut self.diagnostics)), - ); - - sink.push(func(left?, right?)); - Ok(()) - } - // Unary are currently not supported, so we can skip them - NodeKind::Operation(Operation::Type(TypeOperation::Assertion(TypeAssertion { - value, - r#type: _, - force: _, - }))) => self.compile_filter(context, value, sink), - NodeKind::Thunk(Thunk { body }) => self.compile_filter(context, body, sink), - NodeKind::Call(Call { - kind: PointerKind::Thin, - function, - arguments: _, - }) => { - // A thin call is a call to a thunk, so simply redirect to the thunks body - self.compile_filter(context, function, sink) - } - // If we came to this match arm using these nodes, then that means that the filter - // must have evaluated to a boolean expression. Therefore we can just check if the - // expression evaluates to true. - NodeKind::Operation( - Operation::Type(TypeOperation::Constructor(..)) | Operation::Input(..), - ) - | NodeKind::Data(_) - | NodeKind::Access(_) - | NodeKind::Call(_) - | NodeKind::Closure(_) - | NodeKind::Graph(_) => { - sink.push(Filter::Equal( - self.compile_filter_expr(context, node)? - .finish(context, &mut self.diagnostics)?, - FilterExpression::Parameter { - parameter: Parameter::Boolean(true), - convert: None, - }, - )); - - Ok(()) - } - // Conditional expressions (`if A then B else C`) are transformed into equivalent - // boolean logic expressions. We apply optimizations for common literal patterns: - // - // 1. `if A then true else C` → `A || C` - // 2. `if A then B else false` → `A && B` - // 3. General case: `if A then B else C` → `(A && B) || (!A && C)` - // - // The general transformation preserves semantics: - // - When A is true: `(true && B) || (false && C)` = `B || false` = `B` - // - When A is false: `(false && B) || (true && C)` = `false || C` = `C` - NodeKind::Branch(Branch::If(If { test, then, r#else })) => { - if is_bool_literal(then, true) { - // Optimization: `if A then true else C` → `A || C` - // When A is true, result is true; when A is false, result is C - let mut sink = sink.or(); - - let test = self.compile_filter(context, test, &mut sink); - let r#else = self.compile_filter(context, r#else, &mut sink); - - // Delayed to ensure that we get all diagnostics possible - test?; - r#else?; - } else if is_bool_literal(r#else, false) { - // Optimization: `if A then B else false` → `A && B` - // When A is true, result is B; when A is false, result is false - let mut sink = sink.and(); - - let test = self.compile_filter(context, test, &mut sink); - let then = self.compile_filter(context, then, &mut sink); - - // Delayed to ensure that we get all diagnostics possible - test?; - then?; - } else { - // General case: `if A then B else C` → `(A && B) || (!A && C)` - - // Compile each branch into separate filter vectors to enable reuse - // of the test condition in both the positive and negative forms - let mut test_filter = Vec::new(); - let mut test_sink = FilterSink::::And(&mut test_filter); - let test_result = self.compile_filter(context, test, &mut test_sink); - - let mut then_filter = Vec::new(); - let mut then_sink = FilterSink::::And(&mut then_filter); - let then_result = self.compile_filter(context, then, &mut then_sink); - - let mut else_filter = Vec::new(); - let mut else_sink = FilterSink::::And(&mut else_filter); - let else_result = self.compile_filter(context, r#else, &mut else_sink); - - // Build left side: `A && B` - let mut left = Vec::with_capacity(test_filter.len() + then_filter.len()); - left.extend_from_slice(&test_filter); - left.extend(then_filter); - let left = Filter::All(left); - - // Build right side: `!A && C` - let mut right = Vec::with_capacity(1 + else_filter.len()); - right.push(Filter::Not(Box::new(Filter::All(test_filter)))); - right.extend(else_filter); - let right = Filter::All(right); - - // Combine with OR: `(A && B) || (!A && C)` - let mut sink = sink.or(); - sink.push(left); - sink.push(right); - - // Defer error handling to collect all diagnostics before failing - test_result?; - then_result?; - else_result?; - } - - Ok(()) - } - } - } -} diff --git a/libs/@local/hashql/eval/src/graph/read/filter_expr.rs b/libs/@local/hashql/eval/src/graph/read/filter_expr.rs deleted file mode 100644 index 2bab9bc0d4b..00000000000 --- a/libs/@local/hashql/eval/src/graph/read/filter_expr.rs +++ /dev/null @@ -1,715 +0,0 @@ -use alloc::borrow::Cow; -use core::fmt::Debug; - -use hash_graph_store::filter::{FilterExpression, QueryRecord}; -use hashql_core::{ - span::SpanId, - value::{self, Opaque, Primitive, Value}, -}; -use hashql_hir::node::{ - Node, - access::{Access, FieldAccess, IndexAccess}, - call::{Call, PointerKind}, - data::{Data, DictField}, - graph::Graph, - kind::NodeKind, - r#let::{Binding, Let}, - operation::{ - BinOp, BinaryOperation, InputOp, InputOperation, Operation, TypeAssertion, TypeConstructor, - TypeOperation, - }, - thunk::Thunk, - variable::{LocalVariable, Variable}, -}; - -use super::{ - CompilationError, FilterCompilerContext, GraphReadCompiler, - convert::convert_value_to_parameter, - error::{ - BranchContext, GraphReadCompilerIssues, branch_unsupported, call_unsupported, - closure_unsupported, field_access_internal_error, nested_graph_read_unsupported, - path_conversion_error, path_in_data_construct_unsupported, path_indexing_unsupported, - path_traversal_internal_error, qualified_variable_unsupported, - value_parameter_conversion_error, - }, - path::{CompleteQueryPath, PartialQueryPath, traverse_into_field, traverse_into_index}, -}; -use crate::graph::read::error::{ - binary_operation_unsupported, index_access_internal_error, type_constructor_unsupported, -}; - -pub(crate) enum IntermediateExpression<'env, 'heap, P> { - Value { - value: Cow<'env, Value<'heap>>, - span: SpanId, - }, - Path { - path: Option

, - span: SpanId, - }, -} - -impl<'heap, P> IntermediateExpression<'_, 'heap, P> { - pub(crate) fn finish( - self, - context: FilterCompilerContext, - diagnostics: &mut GraphReadCompilerIssues, - ) -> Result, CompilationError> - where - R: QueryRecord: CompleteQueryPath<'heap, PartialQueryPath = P>>, - P: PartialQueryPath<'heap, QueryPath = R::QueryPath<'heap>>, - { - match self { - Self::Value { value, span } => { - let parameter = match convert_value_to_parameter(&value) { - Ok(value) => value, - Err(error) => { - diagnostics.push(value_parameter_conversion_error(context, span, error)); - return Err(CompilationError); - } - }; - - Ok(FilterExpression::Parameter { - parameter, - convert: None, - }) - } - Self::Path { path: None, span } => { - diagnostics.push(path_conversion_error(context, span)); - Err(CompilationError) - } - Self::Path { - path: Some(path), - span, - } => { - let Some(path) = path.finish() else { - diagnostics.push(path_conversion_error(context, span)); - return Err(CompilationError); - }; - - Ok(FilterExpression::Path { path }) - } - } - } -} - -impl<'env, 'heap: 'env> GraphReadCompiler<'env, 'heap> { - fn compile_filter_expr_data

( - &mut self, - context: FilterCompilerContext, - span: SpanId, - data: &'heap Data<'heap>, - ) -> Result, CompilationError> - where - P: PartialQueryPath<'heap> + Debug, - { - match data { - Data::Primitive(literal) => Ok(IntermediateExpression::Value { - value: Cow::Owned(Value::Primitive(*literal)), - span, - }), - Data::Tuple(tuple) => { - let mut values = Vec::with_capacity(tuple.fields.len()); - for (index, field) in tuple.fields.iter().enumerate() { - let Ok(IntermediateExpression::Value { value, span: _ }) = - self.compile_filter_expr::(context.with_current_span(span), *field) - else { - // `!` ensures that no `IntermediateExpression::Path` will be returned - continue; - }; - - if values.len() != index { - // Previous iteration failed, so pushing (and thereby potentially - // reallocating) is pointless. - continue; - } - - values.push(value.into_owned()); - } - - if values.len() != tuple.fields.len() { - return Err(CompilationError); - } - - Ok(IntermediateExpression::Value { - value: Cow::Owned(Value::Tuple(value::Tuple::from_values(values))), - span, - }) - } - Data::Struct(r#struct) => { - let mut fields = Vec::with_capacity(r#struct.fields.len()); - for (index, field) in r#struct.fields.iter().enumerate() { - let Ok(IntermediateExpression::Value { value, span: _ }) = - self.compile_filter_expr::(context.with_current_span(span), field.value) - else { - // `!` ensures that no `IntermediateExpression::Path` will be returned - continue; - }; - - if fields.len() != index { - // Previous iteration failed, so pushing (and thereby potentially - // reallocating) is pointless. - continue; - } - - fields.push((field.name.value, value.into_owned())); - } - - if fields.len() != r#struct.fields.len() { - return Err(CompilationError); - } - - Ok(IntermediateExpression::Value { - value: Cow::Owned(Value::Struct(value::Struct::from_fields(self.heap, fields))), - span, - }) - } - Data::List(list) => { - let mut values = Vec::with_capacity(list.elements.len()); - for (index, element) in list.elements.iter().enumerate() { - let Ok(IntermediateExpression::Value { value, span: _ }) = - self.compile_filter_expr::(context.with_current_span(span), *element) - else { - // `!` ensures that no `IntermediateExpression::Path` will be returned - continue; - }; - - if values.len() != index { - // Previous iteration failed, so pushing (and thereby potentially - // reallocating) is pointless. - continue; - } - - values.push(value.into_owned()); - } - - if values.len() != list.elements.len() { - return Err(CompilationError); - } - - Ok(IntermediateExpression::Value { - value: Cow::Owned(Value::List(value::List::from_values(values))), - span, - }) - } - Data::Dict(dict) => { - let mut entries = Vec::with_capacity(dict.fields.len()); - - for (index, DictField { key, value }) in dict.fields.iter().enumerate() { - let key = self.compile_filter_expr::(context.with_current_span(span), *key); - let value = - self.compile_filter_expr::(context.with_current_span(span), *value); - - // We delay destructing here, so that we can gather errors for both keys and - // values - let ( - Ok(IntermediateExpression::Value { - value: key, - span: _, - }), - Ok(IntermediateExpression::Value { value, span: _ }), - ) = (key, value) - else { - continue; - }; - - if entries.len() != index { - // Previous iteration failed, so pushing (and thereby potentially - // reallocating) is pointless. - continue; - } - - entries.push((key.into_owned(), value.into_owned())); - } - - if entries.len() != dict.fields.len() { - return Err(CompilationError); - } - - Ok(IntermediateExpression::Value { - value: Cow::Owned(Value::Dict(value::Dict::from_entries(entries))), - span, - }) - } - } - } - - fn compile_filter_expr_variable

( - &mut self, - context: FilterCompilerContext, - span: SpanId, - variable: &'heap Variable<'heap>, - ) -> Result, CompilationError> - where - P: PartialQueryPath<'heap> + Debug, - { - match variable { - &Variable::Local(LocalVariable { id, arguments: _ }) - if id.value == context.param_id => - { - if P::UNSUPPORTED { - self.diagnostics - .push(path_in_data_construct_unsupported(span)); - - return Err(CompilationError); - } - - Ok(IntermediateExpression::Path { path: None, span }) - } - Variable::Local(LocalVariable { id, arguments: _ }) => { - let value = self.locals[&id.value]; - self.compile_filter_expr(context.with_current_span(span), value) - } - Variable::Qualified(qualified_variable) => { - self.diagnostics.push(qualified_variable_unsupported( - context, - qualified_variable, - span, - )); - - Err(CompilationError) - } - } - } - - fn compile_filter_expr_let( - &mut self, - Let { bindings, body }: &'heap Let<'heap>, - recurse: impl FnOnce(&mut Self, Node<'heap>) -> T, - ) -> T { - for Binding { - span: _, - binder, - value, - } in bindings - { - self.locals.insert(binder.id, *value); - } - - let result = recurse(self, *body); - - for Binding { - span: _, - binder, - value: _, - } in bindings - { - self.locals.remove(&binder.id); - } - - result - } - - fn compile_filter_expr_operation_type

( - &mut self, - context: FilterCompilerContext, - span: SpanId, - operation: &'heap TypeOperation<'heap>, - ) -> Result, CompilationError> - where - P: PartialQueryPath<'heap> + Debug, - { - match operation { - TypeOperation::Assertion(TypeAssertion { - value, - r#type: _, - force: _, - }) => self.compile_filter_expr(context, *value), - &TypeOperation::Constructor(TypeConstructor { name: _ }) => { - self.diagnostics - .push(type_constructor_unsupported(context, span)); - - Err(CompilationError) - } - } - } - - fn compile_filter_expr_operation_binary

( - &mut self, - context: FilterCompilerContext, - &BinaryOperation { - op, - left: _, - right: _, - }: &'heap BinaryOperation<'heap>, - ) -> Result, CompilationError> - where - P: PartialQueryPath<'heap>, - { - match op.value { - BinOp::And - | BinOp::Or - | BinOp::Eq - | BinOp::Ne - | BinOp::Lt - | BinOp::Lte - | BinOp::Gt - | BinOp::Gte => { - self.diagnostics - .push(binary_operation_unsupported(context, op)); - - Err(CompilationError) - } - } - } - - fn compile_filter_expr_operation_input

( - &self, - span: SpanId, - InputOperation { op, name }: &'heap InputOperation<'heap>, - ) -> IntermediateExpression<'env, 'heap, P> - where - P: PartialQueryPath<'heap> + Debug, - { - match op.value { - InputOp::Exists => { - let exists = self.inputs.contains_key(&name.value); - - IntermediateExpression::Value { - value: Cow::Owned(value::Value::Primitive(value::Primitive::Boolean(exists))), - span, - } - } - InputOp::Load { .. } => { - let value = &self.inputs[&name.value]; - - IntermediateExpression::Value { - value: Cow::Borrowed(value), - span, - } - } - } - } - - fn compile_filter_expr_operation

( - &mut self, - context: FilterCompilerContext, - span: SpanId, - operation: &'heap Operation<'heap>, - ) -> Result, CompilationError> - where - P: PartialQueryPath<'heap> + Debug, - { - match operation { - Operation::Type(r#type) => { - self.compile_filter_expr_operation_type(context, span, r#type) - } - Operation::Binary(binary) => self.compile_filter_expr_operation_binary(context, binary), - Operation::Input(input) => Ok(self.compile_filter_expr_operation_input(span, input)), - } - } - - fn compile_filter_expr_access_field

( - &mut self, - context: FilterCompilerContext, - span: SpanId, - FieldAccess { - expr: expr_node, - field, - }: &'heap FieldAccess<'heap>, - ) -> Result, CompilationError> - where - P: PartialQueryPath<'heap> + Debug, - { - let expr = self.compile_filter_expr::

(context, *expr_node)?; - - let output = match expr { - IntermediateExpression::Value { value, span: _ } => { - let value = match value { - Cow::Borrowed(value) => value.access_by_field(field.value).map(Cow::Borrowed), - Cow::Owned(value) => { - value.access_by_field(field.value).cloned().map(Cow::Owned) - } - }; - - let value = match value { - Ok(value) => value, - Err(error) => { - self.diagnostics.push(field_access_internal_error( - expr_node.span, - field, - &error, - )); - return Err(CompilationError); - } - }; - - IntermediateExpression::Value { value, span } - } - - IntermediateExpression::Path { path, span: _ } => { - let path = match traverse_into_field(path, self.heap, field.value) { - Ok(path) => path, - Err(path) => { - if path.is_none() && P::UNSUPPORTED { - self.diagnostics - .push(path_in_data_construct_unsupported(span)); - - return Err(CompilationError); - } - - self.diagnostics.push(path_traversal_internal_error( - expr_node.span, - field.span, - path.as_ref(), - )); - - return Err(CompilationError); - } - }; - - IntermediateExpression::Path { - path: Some(path), - span, - } - } - }; - - Ok(output) - } - - fn compile_filter_expr_access_index

( - &mut self, - context: FilterCompilerContext, - span: SpanId, - IndexAccess { - expr: expr_node, - index: index_node, - }: &'heap IndexAccess<'heap>, - ) -> Result, CompilationError> - where - P: PartialQueryPath<'heap> + Debug, - { - let expr = self.compile_filter_expr::

(context, *expr_node); - let index = self.compile_filter_expr::

(context, *index_node); - - let (expr, index) = (expr?, index?); - - let index = match index { - IntermediateExpression::Value { value, span: _ } => value, - IntermediateExpression::Path { path: _, span: _ } => { - self.diagnostics.push(path_indexing_unsupported( - context, - expr_node.span, - index_node.span, - )); - - return Err(CompilationError); - } - }; - - let output = match expr { - IntermediateExpression::Value { value, span: _ } => { - let value = match value { - Cow::Borrowed(value) => value - .access_by_index(&index) - .map(|value| value.map(Cow::Borrowed)), - Cow::Owned(value) => value - .access_by_index(&index) - .map(|value| value.cloned().map(Cow::Owned)), - }; - - let value = match value { - Ok(value) => value, - Err(error) => { - self.diagnostics.push(index_access_internal_error( - expr_node.span, - index_node.span, - &error, - )); - return Err(CompilationError); - } - }; - - let value = value.unwrap_or_else(|| Cow::Owned(Value::Primitive(Primitive::Null))); - - IntermediateExpression::Value { value, span } - } - IntermediateExpression::Path { path, span: _ } => { - let path = match traverse_into_index(path, self.heap, index) { - Ok(path) => path, - Err(path) => { - if path.is_none() && P::UNSUPPORTED { - self.diagnostics - .push(path_in_data_construct_unsupported(span)); - - return Err(CompilationError); - } - - self.diagnostics.push(path_traversal_internal_error( - expr_node.span, - index_node.span, - path.as_ref(), - )); - - return Err(CompilationError); - } - }; - - IntermediateExpression::Path { - path: Some(path), - span, - } - } - }; - - Ok(output) - } - - fn compile_filter_expr_access

( - &mut self, - context: FilterCompilerContext, - span: SpanId, - access: &'heap Access<'heap>, - ) -> Result, CompilationError> - where - P: PartialQueryPath<'heap> + Debug, - { - match access { - Access::Field(field) => self.compile_filter_expr_access_field(context, span, field), - Access::Index(index) => self.compile_filter_expr_access_index(context, span, index), - } - } - - fn compile_filter_expr_call_ctor( - &mut self, - context: FilterCompilerContext, - span: SpanId, - node: Node<'heap>, - ) -> Result<&'heap TypeConstructor<'heap>, CompilationError> { - match &node.0.kind { - NodeKind::Operation(Operation::Type(TypeOperation::Constructor(ctor))) => Ok(ctor), - NodeKind::Variable(Variable::Local(local)) => { - self.compile_filter_expr_call_ctor(context, span, self.locals[&local.id.value]) - } - NodeKind::Call(Call { - kind: PointerKind::Thin, - function, - arguments: _, - }) => self.compile_filter_expr_call_ctor(context, span, *function), - NodeKind::Let(r#let) => self.compile_filter_expr_let(r#let, |this, body| { - this.compile_filter_expr_call_ctor(context, span, body) - }), - NodeKind::Thunk(Thunk { body }) => { - self.compile_filter_expr_call_ctor(context, span, *body) - } - NodeKind::Data(_) - | NodeKind::Variable(_) - | NodeKind::Operation(_) - | NodeKind::Access(_) - | NodeKind::Call(_) - | NodeKind::Branch(_) - | NodeKind::Closure(_) - | NodeKind::Graph(_) => { - self.diagnostics.push(call_unsupported(context, span)); - Err(CompilationError) - } - } - } - - // in theory they could be in the narrow context that they take a *single* argument, - // and that argument happens to be the entity being filtered, because then we can - // statically analyze them real easy. - fn compile_filter_expr_call

( - &mut self, - context: FilterCompilerContext, - span: SpanId, - Call { - kind, - function, - arguments, - }: &'heap Call<'heap>, - ) -> Result, CompilationError> - where - P: PartialQueryPath<'heap> + Debug, - { - if *kind == PointerKind::Thin { - // Thin pointer to a local variable = calling a thunk - return self.compile_filter_expr(context.with_current_span(span), *function); - } - - let ctor = self.compile_filter_expr_call_ctor(context, span, *function)?; - - match &**arguments { - [] => Ok(IntermediateExpression::Value { - value: Cow::Owned(Value::Opaque(Opaque::new( - ctor.name, - Value::Primitive(Primitive::Null), - ))), - span, - }), - [argument] => { - let argument = - self.compile_filter_expr(context.without_current_span(), argument.value)?; - - match argument { - IntermediateExpression::Value { value, span } => { - Ok(IntermediateExpression::Value { - value: Cow::Owned(Value::Opaque(Opaque::new( - ctor.name, - value.into_owned(), - ))), - span, - }) - } - // paths simply "pass through" - path @ IntermediateExpression::Path { .. } => Ok(path), - } - } - _ => unreachable!(), - } - } - - // We can't return a `FilterExpression` instead we require to return our own `Expression` that - // can then be converted to a `FilterExpression`. That allows us to carry the values using - // `&'heap` lifetimes. - pub(super) fn compile_filter_expr

( - &mut self, - context: FilterCompilerContext, - node: Node<'heap>, - ) -> Result, CompilationError> - where - P: PartialQueryPath<'heap> + Debug, - { - match &node.0.kind { - NodeKind::Data(data) => self.compile_filter_expr_data(context, node.span, data), - NodeKind::Variable(variable) => { - self.compile_filter_expr_variable(context, node.span, variable) - } - NodeKind::Let(r#let) => self.compile_filter_expr_let(r#let, |this, body| { - this.compile_filter_expr(context, body) - }), - NodeKind::Operation(operation) => { - self.compile_filter_expr_operation(context, node.span, operation) - } - NodeKind::Access(access) => self.compile_filter_expr_access(context, node.span, access), - NodeKind::Call(call) => self.compile_filter_expr_call(context, node.span, call), - NodeKind::Closure(_) => { - self.diagnostics.push(closure_unsupported( - context, - context.current_span.unwrap_or(node.span), - )); - Err(CompilationError) - } - NodeKind::Thunk(Thunk { body }) => self.compile_filter_expr(context, *body), - NodeKind::Graph(graph) => match graph { - Graph::Read(_) => { - self.diagnostics.push(nested_graph_read_unsupported( - context, - context.current_span.unwrap_or(node.span), - )); - Err(CompilationError) - } - }, - NodeKind::Branch(branch) => { - self.diagnostics.push(branch_unsupported( - branch, - node.span, - BranchContext::FilterExpression, - )); - - Err(CompilationError) - } - } - } -} diff --git a/libs/@local/hashql/eval/src/graph/read/mod.rs b/libs/@local/hashql/eval/src/graph/read/mod.rs deleted file mode 100644 index 823eaf9ff30..00000000000 --- a/libs/@local/hashql/eval/src/graph/read/mod.rs +++ /dev/null @@ -1,259 +0,0 @@ -mod convert; -pub mod error; -mod filter; -mod filter_expr; -mod path; -mod sink; - -use core::{fmt::Debug, ops::Range}; - -use hash_graph_store::filter::{Filter, QueryRecord}; -use hashql_core::{ - collections::FastHashMap, heap::Heap, span::SpanId, symbol::Symbol, value::Value, -}; -use hashql_diagnostics::DiagnosticIssues; -use hashql_hir::{ - node::{ - HirId, HirIdMap, Node, - graph::read::{GraphRead, GraphReadBody, GraphReadHead}, - kind::NodeKind, - r#let::{Binding, Let, VarId, VarIdMap}, - thunk::Thunk, - variable::LocalVariable, - }, - visit::{self, Visitor}, -}; -use type_system::knowledge::Entity; - -use self::{ - error::{GraphReadCompilerIssues, GraphReadCompilerStatus}, - path::CompleteQueryPath, - sink::FilterSink, -}; - -// The FilterSlice is an indirect approach to allow us to easily copy a filter between different -// nodes. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum FilterSlice { - Entity { range: Range }, -} - -#[derive(Debug, Clone, PartialEq, Default)] -pub struct Filters<'heap> { - entity: Vec>, -} - -impl<'heap> Filters<'heap> { - #[must_use] - pub fn entity(&self, range: Range) -> &[Filter<'heap, Entity>] { - &self.entity[range] - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -struct FilterCompilerContext { - span: SpanId, - current_span: Option, - param_id: VarId, -} - -impl FilterCompilerContext { - const fn without_current_span(self) -> Self { - Self { - span: self.span, - current_span: None, - param_id: self.param_id, - } - } - - const fn with_current_span(self, span: SpanId) -> Self { - Self { - span: self.span, - current_span: match self.current_span { - None => Some(span), - Some(_) => self.current_span, - }, - param_id: self.param_id, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct GraphReadCompilerResidual<'heap> { - pub filters: Filters<'heap>, - pub output: HirIdMap, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -struct CompilationError; - -pub struct GraphReadCompiler<'env, 'heap> { - current: HirId, - heap: &'heap Heap, - filters: Filters<'heap>, - - diagnostics: GraphReadCompilerIssues, - - locals: VarIdMap>, - inputs: &'env FastHashMap, Value<'heap>>, - output: HirIdMap, - variables: VarIdMap, -} - -impl<'env, 'heap: 'env> GraphReadCompiler<'env, 'heap> { - pub fn new(heap: &'heap Heap, inputs: &'env FastHashMap, Value<'heap>>) -> Self { - Self { - current: HirId::PLACEHOLDER, - heap, - filters: Filters::default(), - diagnostics: DiagnosticIssues::new(), - locals: FastHashMap::default(), - inputs, - output: FastHashMap::default(), - variables: FastHashMap::default(), - } - } - - /// Finish the compilation process. - /// - /// # Errors - /// - /// Returns an error if any diagnostics were collected during compilation. - pub fn finish(self) -> GraphReadCompilerStatus> { - self.diagnostics.into_status(GraphReadCompilerResidual { - filters: self.filters, - output: self.output, - }) - } - - fn compile_graph_body( - &mut self, - body: &'heap [GraphReadBody<'heap>], - ) -> Result>, CompilationError> - where - R: QueryRecord: CompleteQueryPath<'heap, PartialQueryPath: Debug> + Clone>, - { - let mut filters = Ok(Vec::new()); - - for body in body { - match body { - GraphReadBody::Filter(node) => { - let NodeKind::Closure(closure) = node.kind else { - unreachable!() - }; - - let mut sink = FilterSink::from_result(&mut filters); - - let filter = self.compile_filter::( - FilterCompilerContext { - span: closure.body.span, - current_span: None, - param_id: closure.signature.params[0].name.id, - }, - closure.body, - &mut sink, - ); - - if let Err(error) = filter { - filters = Err(error); - } - } - } - } - - filters - } - - fn compile_graph_read( - &mut self, - read: &'heap GraphRead<'heap>, - ) -> Result { - match read.head { - GraphReadHead::Entity { axis: _ } => { - let filters = self.compile_graph_body(&read.body)?; - - let start = self.filters.entity.len(); - self.filters.entity.extend(filters); - let end = self.filters.entity.len(); - - Ok(FilterSlice::Entity { range: start..end }) - } - } - } -} - -impl<'heap> Visitor<'heap> for GraphReadCompiler<'_, 'heap> { - fn visit_node(&mut self, node: Node<'heap>) { - if self.output.contains_key(&node.id) { - return; // We've already processed this node, so skip it. - } - - let previous = self.current; - self.current = node.id; - - visit::walk_node(self, node); - - self.current = previous; - } - - fn visit_binding(&mut self, binding: &'heap Binding<'heap>) { - visit::walk_binding(self, binding); - - // Check if the binder has been assigned to an output - if let Some(output) = self.output.get(&binding.value.id) { - self.variables.insert(binding.binder.id, output.clone()); - } - } - - fn visit_local_variable(&mut self, variable: &'heap LocalVariable<'heap>) { - visit::walk_local_variable(self, variable); - - if let Some(output) = self.variables.get(&variable.id.value) { - self.output.insert(self.current, output.clone()); - } - } - - fn visit_thunk(&mut self, thunk: &'heap Thunk<'heap>) { - visit::walk_thunk(self, thunk); - - if let Some(output) = self.output.get(&thunk.body.id) { - self.output.insert(self.current, output.clone()); - } - } - - fn visit_let(&mut self, r#let: &'heap Let<'heap>) { - for Binding { - span: _, - binder, - value, - } in &r#let.bindings - { - self.locals.insert(binder.id, *value); - } - - visit::walk_let(self, r#let); - - for Binding { - span: _, - binder, - value: _, - } in &r#let.bindings - { - self.locals.remove(&binder.id); - } - - if let Some(value) = self.output.get(&r#let.body.id) { - self.output.insert(self.current, value.clone()); - } - } - - fn visit_graph_read(&mut self, graph_read: &'heap GraphRead<'heap>) { - visit::walk_graph_read(self, graph_read); - - if let Ok(filter) = self.compile_graph_read(graph_read) { - self.output - .try_insert(self.current, filter) - .expect("Same node shouldn't be processed multiple times"); - } - } -} diff --git a/libs/@local/hashql/eval/src/graph/read/path.rs b/libs/@local/hashql/eval/src/graph/read/path.rs deleted file mode 100644 index 67438f24c3e..00000000000 --- a/libs/@local/hashql/eval/src/graph/read/path.rs +++ /dev/null @@ -1,583 +0,0 @@ -use alloc::borrow::Cow; - -use hash_graph_store::{ - entity::EntityQueryPath, - filter::{JsonPath, PathToken, QueryPath}, - subgraph::edges::{EdgeDirection, KnowledgeGraphEdgeKind}, -}; -use hashql_core::{ - heap::Heap, - symbol::{Symbol, sym}, - value::{Primitive, Value}, -}; - -pub(crate) trait CompleteQueryPath<'heap>: QueryPath { - type PartialQueryPath: PartialQueryPath<'heap, QueryPath = Self>; -} - -pub(crate) trait PartialQueryPath<'heap>: Sized { - type QueryPath; - - const UNSUPPORTED: bool = false; - - fn from_field(heap: &'heap Heap, field: Symbol<'heap>) -> Option; - fn access_field(self, heap: &'heap Heap, field: Symbol<'heap>) -> Result; - fn from_index(heap: &'heap Heap, index: Cow<'_, Value<'heap>>) -> Option; - fn access_index(self, heap: &'heap Heap, index: Cow<'_, Value<'heap>>) -> Result; - fn finish(self) -> Option; -} - -impl<'heap> PartialQueryPath<'heap> for ! { - type QueryPath = !; - - const UNSUPPORTED: bool = true; - - fn from_field(_: &'heap Heap, _: Symbol<'heap>) -> Option { - None - } - - fn access_field(self, _: &'heap Heap, _: Symbol<'heap>) -> Result { - self - } - - fn from_index(_: &'heap Heap, _: Cow<'_, Value<'heap>>) -> Option { - None - } - - fn access_index(self, _: &'heap Heap, _: Cow<'_, Value<'heap>>) -> Result { - self - } - - fn finish(self) -> Option { - None - } -} - -impl<'heap> CompleteQueryPath<'heap> for EntityQueryPath<'heap> { - type PartialQueryPath = PartialEntityQueryPath<'heap>; -} - -pub(crate) fn traverse_into_field<'heap, P>( - path: Option

, - heap: &'heap Heap, - field: Symbol<'heap>, -) -> Result> -where - P: PartialQueryPath<'heap>, -{ - #[expect(clippy::option_if_let_else, reason = "readability")] - match path { - Some(path) => path.access_field(heap, field).map_err(Some), - None => P::from_field(heap, field).ok_or(None), - } -} - -pub(crate) fn traverse_into_index<'heap, P>( - path: Option

, - heap: &'heap Heap, - index: Cow<'_, Value<'heap>>, -) -> Result> -where - P: PartialQueryPath<'heap>, -{ - match path { - Some(path) => path.access_index(heap, index).map_err(Some), - None => P::from_index(heap, index).ok_or(None), - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub(crate) enum PartialEntityIdQueryPath { - WebId, - EntityUuid, - DraftId, -} - -impl<'heap> PartialQueryPath<'heap> for PartialEntityIdQueryPath { - type QueryPath = EntityQueryPath<'heap>; - - fn from_field(_: &'heap Heap, field: Symbol<'heap>) -> Option { - match field.as_constant()? { - sym::web_id::CONST => Some(Self::WebId), - sym::entity_uuid::CONST => Some(Self::EntityUuid), - sym::draft_id::CONST => Some(Self::DraftId), - _ => None, - } - } - - fn access_field(self, _: &'heap Heap, _: Symbol<'heap>) -> Result { - Err(self) - } - - fn from_index(_: &'heap Heap, _: Cow<'_, Value<'heap>>) -> Option { - None - } - - fn access_index(self, _: &'heap Heap, _: Cow<'_, Value<'heap>>) -> Result { - Err(self) - } - - fn finish(self) -> Option { - match self { - Self::WebId => Some(EntityQueryPath::WebId), - Self::EntityUuid => Some(EntityQueryPath::Uuid), - Self::DraftId => Some(EntityQueryPath::DraftId), - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub(crate) enum PartialEntityRecordIdPath { - EntityId(Option), - EditionId, -} - -impl<'heap> PartialQueryPath<'heap> for PartialEntityRecordIdPath { - type QueryPath = EntityQueryPath<'heap>; - - fn from_field(_: &'heap Heap, field: Symbol<'heap>) -> Option { - match field.as_constant()? { - sym::entity_id::CONST => Some(Self::EntityId(None)), - sym::edition_id::CONST => Some(Self::EditionId), - _ => None, - } - } - - fn access_field(self, heap: &'heap Heap, field: Symbol<'heap>) -> Result { - match self { - Self::EntityId(path) => traverse_into_field(path, heap, field) - .map(Some) - .map(Self::EntityId) - .map_err(Self::EntityId), - Self::EditionId => Err(self), - } - } - - fn from_index(_: &'heap Heap, _: Cow<'_, Value<'heap>>) -> Option { - None - } - - fn access_index(self, heap: &'heap Heap, index: Cow<'_, Value<'heap>>) -> Result { - match self { - Self::EntityId(path) => traverse_into_index(path, heap, index) - .map(Some) - .map(Self::EntityId) - .map_err(Self::EntityId), - Self::EditionId => Err(self), - } - } - - fn finish(self) -> Option { - match self { - Self::EntityId(Some(partial)) => partial.finish(), - Self::EntityId(None) => None, - Self::EditionId => Some(EntityQueryPath::EditionId), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(crate) enum PartialLinkDataPath { - LeftEntityId(Option), - RightEntityId(Option), - LeftEntityConfidence, - LeftEntityProvenance, - RightEntityConfidence, - RightEntityProvenance, -} - -impl<'heap> PartialQueryPath<'heap> for PartialLinkDataPath { - type QueryPath = EntityQueryPath<'heap>; - - fn from_field(_: &'heap Heap, field: Symbol<'heap>) -> Option { - match field.as_constant()? { - sym::left_entity_id::CONST => Some(Self::LeftEntityId(None)), - sym::right_entity_id::CONST => Some(Self::RightEntityId(None)), - sym::left_entity_confidence::CONST => Some(Self::LeftEntityConfidence), - sym::left_entity_provenance::CONST => Some(Self::LeftEntityProvenance), - sym::right_entity_confidence::CONST => Some(Self::RightEntityConfidence), - sym::right_entity_provenance::CONST => Some(Self::RightEntityProvenance), - _ => None, - } - } - - fn access_field(self, heap: &'heap Heap, field: Symbol<'heap>) -> Result { - match self { - Self::LeftEntityId(path) => traverse_into_field(path, heap, field) - .map(Some) - .map(Self::LeftEntityId) - .map_err(Self::LeftEntityId), - Self::RightEntityId(path) => traverse_into_field(path, heap, field) - .map(Some) - .map(Self::RightEntityId) - .map_err(Self::RightEntityId), - Self::LeftEntityConfidence - | Self::LeftEntityProvenance - | Self::RightEntityConfidence - | Self::RightEntityProvenance => Err(self), - } - } - - fn from_index(_: &'heap Heap, _: Cow<'_, Value<'heap>>) -> Option { - None - } - - fn access_index(self, heap: &'heap Heap, index: Cow<'_, Value<'heap>>) -> Result { - match self { - Self::LeftEntityId(path) => traverse_into_index(path, heap, index) - .map(Some) - .map(Self::LeftEntityId) - .map_err(Self::LeftEntityId), - Self::RightEntityId(path) => traverse_into_index(path, heap, index) - .map(Some) - .map(Self::RightEntityId) - .map_err(Self::RightEntityId), - Self::LeftEntityConfidence - | Self::LeftEntityProvenance - | Self::RightEntityConfidence - | Self::RightEntityProvenance => Err(self), - } - } - - #[expect(clippy::match_same_arms, reason = "readability")] - fn finish(self) -> Option { - match self { - Self::LeftEntityId(Some(partial)) => Some(EntityQueryPath::EntityEdge { - edge_kind: KnowledgeGraphEdgeKind::HasLeftEntity, - path: Box::new(partial.finish()?), - direction: EdgeDirection::Outgoing, - }), - Self::LeftEntityId(None) => None, - - Self::RightEntityId(Some(partial)) => Some(EntityQueryPath::EntityEdge { - edge_kind: KnowledgeGraphEdgeKind::HasRightEntity, - path: Box::new(partial.finish()?), - direction: EdgeDirection::Outgoing, - }), - Self::RightEntityId(None) => None, - - Self::LeftEntityConfidence => Some(EntityQueryPath::LeftEntityConfidence), - Self::LeftEntityProvenance => Some(EntityQueryPath::LeftEntityProvenance), - Self::RightEntityConfidence => Some(EntityQueryPath::RightEntityConfidence), - Self::RightEntityProvenance => Some(EntityQueryPath::RightEntityProvenance), - } - } -} - -fn value_as_usize(value: &Value<'_>) -> Option { - match value { - Value::Primitive(Primitive::Integer(integer)) => integer.as_usize(), - Value::Primitive(Primitive::Float(float)) if let Some(integer) = float.as_integer() => { - integer.as_usize() - } - Value::Primitive(_) - | Value::Struct(_) - | Value::Tuple(_) - | Value::List(_) - | Value::Dict(_) - | Value::Opaque(_) => None, - } -} - -impl<'heap> PartialQueryPath<'heap> for JsonPath<'heap> { - type QueryPath = Self; - - fn from_field(_: &'heap Heap, field: Symbol<'heap>) -> Option { - Some(JsonPath::from_path_tokens(vec![PathToken::Field( - Cow::Borrowed(field.unwrap()), - )])) - } - - fn access_field(mut self, _: &'heap Heap, field: Symbol<'heap>) -> Result { - self.push(PathToken::Field(Cow::Borrowed(field.unwrap()))); - Ok(self) - } - - fn from_index(_: &'heap Heap, index: Cow>) -> Option { - let index = value_as_usize(&index)?; - - Some(JsonPath::from_path_tokens(vec![PathToken::Index(index)])) - } - - fn access_index(mut self, _: &'heap Heap, index: Cow>) -> Result { - let Some(index) = value_as_usize(&index) else { - return Err(self); - }; - - self.push(PathToken::Index(index)); - Ok(self) - } - - fn finish(self) -> Option { - Some(self) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(crate) enum PartialEntityMetadataPath<'heap> { - RecordId(Option), - TemporalVersioning(Option), - Archived, - Confidence, - EntityTypeIds, - Provenance(Option>), - Properties(Option>), -} - -impl<'heap> PartialQueryPath<'heap> for PartialEntityMetadataPath<'heap> { - type QueryPath = EntityQueryPath<'heap>; - - fn from_field(_: &'heap Heap, field: Symbol<'heap>) -> Option { - match field.as_constant()? { - sym::record_id::CONST => Some(Self::RecordId(None)), - sym::temporal_versioning::CONST => Some(Self::TemporalVersioning(None)), - sym::archived::CONST => Some(Self::Archived), - sym::confidence::CONST => Some(Self::Confidence), - sym::entity_type_ids::CONST => Some(Self::EntityTypeIds), - sym::provenance::CONST => Some(Self::Provenance(None)), - sym::properties::CONST => Some(Self::Properties(None)), - _ => None, - } - } - - fn access_field(self, heap: &'heap Heap, field: Symbol<'heap>) -> Result { - match self { - Self::RecordId(path) => traverse_into_field(path, heap, field) - .map(Some) - .map(Self::RecordId) - .map_err(Self::RecordId), - Self::TemporalVersioning(path) => traverse_into_field(path, heap, field) - .map(Some) - .map(Self::TemporalVersioning) - .map_err(Self::TemporalVersioning), - Self::Provenance(path) => traverse_into_field(path, heap, field) - .map(Some) - .map(Self::Provenance) - .map_err(Self::Provenance), - Self::Properties(path) => traverse_into_field(path, heap, field) - .map(Some) - .map(Self::Properties) - .map_err(Self::Properties), - Self::Archived | Self::Confidence | Self::EntityTypeIds => Err(self), - } - } - - fn from_index(_: &'heap Heap, _: Cow<'_, Value<'heap>>) -> Option { - None - } - - fn access_index(self, heap: &'heap Heap, index: Cow<'_, Value<'heap>>) -> Result { - match self { - Self::RecordId(path) => traverse_into_index(path, heap, index) - .map(Some) - .map(Self::RecordId) - .map_err(Self::RecordId), - Self::Properties(path) => traverse_into_index(path, heap, index) - .map(Some) - .map(Self::Properties) - .map_err(Self::Properties), - Self::TemporalVersioning(_) - | Self::Archived - | Self::Confidence - | Self::EntityTypeIds - | Self::Provenance(_) => Err(self), - } - } - - #[expect(clippy::match_same_arms, reason = "readability")] - fn finish(self) -> Option { - match self { - Self::RecordId(Some(partial)) => partial.finish(), - Self::RecordId(None) => None, - - Self::TemporalVersioning(Some(partial)) => partial.finish(), - Self::TemporalVersioning(None) => None, - - Self::Archived => Some(EntityQueryPath::Archived), - Self::Confidence => Some(EntityQueryPath::EntityConfidence), - Self::EntityTypeIds => Some(EntityQueryPath::EntityTypeEdge { - edge_kind: hash_graph_store::subgraph::edges::SharedEdgeKind::IsOfType, - path: hash_graph_store::entity_type::EntityTypeQueryPath::VersionedUrl, - inheritance_depth: Some(0), - }), - - Self::Provenance(Some(partial)) => partial.finish(), - Self::Provenance(None) => None, - - Self::Properties(Some(partial)) => { - Some(EntityQueryPath::PropertyMetadata(Some(partial.finish()?))) - } - Self::Properties(None) => Some(EntityQueryPath::PropertyMetadata(None)), - } - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub(crate) enum PartialTemporalVersioningPath { - DecisionTime, - TransactionTime, -} - -impl<'heap> PartialQueryPath<'heap> for PartialTemporalVersioningPath { - type QueryPath = EntityQueryPath<'heap>; - - fn from_field(_: &'heap Heap, field: Symbol<'heap>) -> Option { - match field.as_constant()? { - sym::decision_time::CONST => Some(Self::DecisionTime), - sym::transaction_time::CONST => Some(Self::TransactionTime), - _ => None, - } - } - - fn access_field(self, _: &'heap Heap, _: Symbol<'heap>) -> Result { - Err(self) - } - - fn from_index(_: &'heap Heap, _: Cow<'_, Value<'heap>>) -> Option { - None - } - - fn access_index(self, _: &'heap Heap, _: Cow<'_, Value<'heap>>) -> Result { - Err(self) - } - - fn finish(self) -> Option { - match self { - Self::DecisionTime => Some(EntityQueryPath::DecisionTime), - Self::TransactionTime => Some(EntityQueryPath::TransactionTime), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(crate) enum PartialEntityProvenancePath<'heap> { - Inferred(Option>), - Edition(Option>), -} - -impl<'heap> PartialQueryPath<'heap> for PartialEntityProvenancePath<'heap> { - type QueryPath = EntityQueryPath<'heap>; - - fn from_field(_: &'heap Heap, field: Symbol<'heap>) -> Option { - match field.as_constant()? { - sym::inferred::CONST => Some(Self::Inferred(None)), - sym::edition::CONST => Some(Self::Edition(None)), - _ => None, - } - } - - fn access_field(self, heap: &'heap Heap, field: Symbol<'heap>) -> Result { - match self { - Self::Inferred(path) => traverse_into_field(path, heap, field) - .map(Some) - .map(Self::Inferred) - .map_err(Self::Inferred), - Self::Edition(path) => traverse_into_field(path, heap, field) - .map(Some) - .map(Self::Edition) - .map_err(Self::Edition), - } - } - - fn from_index(_: &'heap Heap, _: Cow<'_, Value<'heap>>) -> Option { - None - } - - fn access_index(self, heap: &'heap Heap, index: Cow<'_, Value<'heap>>) -> Result { - match self { - Self::Inferred(path) => traverse_into_index(path, heap, index) - .map(Some) - .map(Self::Inferred) - .map_err(Self::Inferred), - Self::Edition(path) => traverse_into_index(path, heap, index) - .map(Some) - .map(Self::Edition) - .map_err(Self::Edition), - } - } - - fn finish(self) -> Option { - match self { - Self::Inferred(path) => { - Some(EntityQueryPath::Provenance(path.and_then(JsonPath::finish))) - } - Self::Edition(path) => Some(EntityQueryPath::EditionProvenance( - path.and_then(JsonPath::finish), - )), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(crate) enum PartialEntityQueryPath<'heap> { - Properties(Option>), - LinkData(Option), - Metadata(Option>), -} - -impl<'heap> PartialQueryPath<'heap> for PartialEntityQueryPath<'heap> { - type QueryPath = EntityQueryPath<'heap>; - - fn from_field(_: &'heap Heap, field: Symbol<'heap>) -> Option { - match field.as_constant()? { - sym::properties::CONST => Some(Self::Properties(None)), - sym::link_data::CONST => Some(Self::LinkData(None)), - sym::metadata::CONST => Some(Self::Metadata(None)), - _ => None, - } - } - - fn access_field(self, heap: &'heap Heap, field: Symbol<'heap>) -> Result { - match self { - Self::Properties(path) => traverse_into_field(path, heap, field) - .map(Some) - .map(Self::Properties) - .map_err(Self::Properties), - Self::LinkData(path) => traverse_into_field(path, heap, field) - .map(Some) - .map(Self::LinkData) - .map_err(Self::LinkData), - Self::Metadata(path) => traverse_into_field(path, heap, field) - .map(Some) - .map(Self::Metadata) - .map_err(Self::Metadata), - } - } - - fn from_index(_: &'heap Heap, _: Cow>) -> Option { - None - } - - fn access_index(self, heap: &'heap Heap, index: Cow>) -> Result { - match self { - Self::Properties(path) => traverse_into_index(path, heap, index) - .map(Some) - .map(Self::Properties) - .map_err(Self::Properties), - Self::LinkData(path) => traverse_into_index(path, heap, index) - .map(Some) - .map(Self::LinkData) - .map_err(Self::LinkData), - Self::Metadata(path) => traverse_into_index(path, heap, index) - .map(Some) - .map(Self::Metadata) - .map_err(Self::Metadata), - } - } - - #[expect(clippy::match_same_arms, reason = "readability")] - fn finish(self) -> Option { - match self { - Self::Properties(Some(partial)) => { - Some(EntityQueryPath::Properties(Some(partial.finish()?))) - } - Self::Properties(None) => Some(EntityQueryPath::Properties(None)), - - Self::LinkData(Some(partial)) => partial.finish(), - Self::LinkData(None) => None, - - Self::Metadata(Some(partial)) => partial.finish(), - Self::Metadata(None) => None, - } - } -} diff --git a/libs/@local/hashql/eval/src/graph/read/sink.rs b/libs/@local/hashql/eval/src/graph/read/sink.rs deleted file mode 100644 index e95520bb5c8..00000000000 --- a/libs/@local/hashql/eval/src/graph/read/sink.rs +++ /dev/null @@ -1,72 +0,0 @@ -use hash_graph_store::filter::{Filter, QueryRecord}; - -/// A mutable sink for building filter expressions during query evaluation. -pub(crate) enum FilterSink<'a, 'b, R: QueryRecord> { - /// Collects filters that will be combined with AND logic. - And(&'a mut Vec>), - /// Collects filters that will be combined with OR logic. - Or(&'a mut Vec>), - /// Represents a failed state where filters are discarded. - Fail, -} - -impl<'a, 'b, R: QueryRecord> FilterSink<'a, 'b, R> { - /// Create a sink from a result. - /// - /// Converts `Ok` to `And` and `Err` to `Fail`. - pub(crate) const fn from_result(result: &'a mut Result>, E>) -> Self { - match result { - Ok(filters) => FilterSink::And(filters), - Err(_) => FilterSink::Fail, - } - } - - /// Creates an AND filter sink. - /// - /// In the case that the existing sink is already an AND sink, the same vector is returned, - /// otherwise a new vector is created as a child. - /// - /// If the sink persists the `Fail` state. - pub(crate) fn and(&mut self) -> FilterSink<'_, 'b, R> { - match self { - FilterSink::And(inner) => FilterSink::And(inner), - FilterSink::Or(inner) => { - let Filter::All(inner) = inner.push_mut(Filter::All(Vec::new())) else { - unreachable!() - }; - - FilterSink::And(inner) - } - FilterSink::Fail => FilterSink::Fail, - } - } - - /// Creates an OR filter sink. - /// - /// In the case that the existing sink is already an OR sink, the same vector is returned, - /// otherwise a new vector is created as a child. - pub(crate) fn or(&mut self) -> FilterSink<'_, 'b, R> { - match self { - FilterSink::And(inner) => { - let Filter::Any(inner) = inner.push_mut(Filter::Any(Vec::new())) else { - unreachable!() - }; - - FilterSink::Or(inner) - } - FilterSink::Or(inner) => FilterSink::Or(inner), - FilterSink::Fail => FilterSink::Fail, - } - } - - /// Pushes a filter into the sink. - /// - /// Pushes to the current vector, corresponding to either AND or OR. In the case of a failed - /// state, the filter is discarded. - pub(crate) fn push(&mut self, filter: Filter<'b, R>) { - match self { - FilterSink::And(inner) | FilterSink::Or(inner) => inner.push(filter), - FilterSink::Fail => {} - } - } -}