WIP resolve stage

This commit is contained in:
2026-05-06 14:41:48 -06:00
parent 20c1e2d084
commit 0af9989323
4 changed files with 250 additions and 1 deletions

1
Cargo.lock generated
View File

@@ -730,6 +730,7 @@ version = "0.1.0"
dependencies = [
"ariadne",
"datatest-stable",
"indexmap",
"kerolox-ir",
"lsp-types",
"ropey",

View File

@@ -16,6 +16,7 @@ ariadne = { version = "0.5", features = ["auto-color"] }
datatest-stable = "0.3"
derive-where = "1.6"
futures-util = { version = "0.3", default-features = false }
indexmap = "2"
lsp-types = "0.94"
ordered-float = { version = "4", features = ["serde"] }
ropey = "1.6"

View File

@@ -9,6 +9,7 @@ rust-version.workspace = true
[dependencies]
ariadne.workspace = true
indexmap.workspace = true
kerolox-ir = { workspace = true, features = ["salsa"] }
lsp-types.workspace = true
ropey.workspace = true

View File

@@ -19,8 +19,9 @@
//! The "resolve" stage of the frontend: match up names of objects with the
//! semantic objects they refer to.
use std::collections::BTreeMap;
use std::{collections::BTreeMap, convert::Infallible, marker::PhantomData, sync::Arc};
use indexmap::IndexSet;
use salsa::Update;
use crate::prelude::*;
@@ -64,3 +65,248 @@ impl<'db> NamespaceItem<'db> {
}
}
}
/// Resolves all references in an [ast::Rule].
#[salsa::tracked]
pub fn rule<'db>(db: &'db dyn Database, ast: ast::Rule<'db>) -> Rule<'db> {
// resolve the body
let body = match ast.body(db) {
// if a body is present, resolve separately
Some(body) => rule_body(db, body),
// if a body is absent, create an explicit rule body by hand
None => RuleBody {
meta: ast.ast(db),
vars: Default::default(),
loaded: Default::default(),
body: Expr {
meta: ast.ast(db),
kind: ir::ExprKind::Extra(Arc::new(ExprExtra::Implicit)),
},
},
};
// the head is empty for now because it is untyped
let head = Default::default();
// resolve the name of the target relation
let relation = relation_name(db, ast.relation(db));
// assemble the finished rule
Rule {
meta: ast,
relation,
head,
body,
}
}
/// Resolves all references in an [ast::Assumption].
#[salsa::tracked]
pub fn assumption<'db>(db: &'db dyn Database, ast: ast::Assumption<'db>) -> Assumption<'db> {
todo!()
}
/// Resolves all references in an [ast::RuleBody].
#[salsa::tracked]
pub fn rule_body<'db>(db: &'db dyn Database, ast: ast::RuleBody<'db>) -> RuleBody<'db> {
// initialize conversion tables
let mut vars = IndexSet::new();
let mut loaded = IndexSet::new();
// convert AST clauses to resolved clauses
let clauses = ast
.clauses(db)
.into_iter()
.map(|ast| expr(db, &mut vars, &mut loaded, &ast));
// reduce each clause to a single "and" operation
let body = clauses.reduce(|lhs, rhs| Expr {
meta: ast.ast(db),
kind: ir::ExprKind::BinaryOp {
op: ir::BinaryOpKind::And.into(),
lhs: Arc::new(lhs),
rhs: Arc::new(rhs),
},
});
// if the body has no clauses at all, mark it specially
let body = body.unwrap_or(Expr {
meta: ast.ast(db),
kind: ir::ExprKind::Extra(Arc::new(ExprExtra::Implicit)),
});
// assemble completed rule body
RuleBody {
loaded: loaded.into_iter().collect(),
vars: vars.into_iter().collect(),
meta: ast.ast(db),
body,
}
}
/// Internal conversion tool to resolved expressions, with side-effects.
pub fn expr<'db>(
db: &'db dyn Database,
vars: &mut IndexSet<VariableMeta>,
loaded: &mut IndexSet<RelationLabel<'db>>,
expr: &ast::Expr,
) -> Expr<'db> {
// alias recursive call with curried side-effects
let mut resolve = |ast| crate::resolve::expr(db, vars, loaded, ast);
// convert inner expression variant
use ir::ExprKind::*;
let kind = match &expr.kind {
// directly translate existing value definitions
// TODO: symbols are unresolved, handle unreachability better?
Value(value) => Value(value.clone()),
// recursively descend into unary operations
UnaryOp { op, term } => UnaryOp {
op: *op,
term: Arc::new(resolve(term.as_ref())),
},
// recursively descend into binary operations
BinaryOp { op, lhs, rhs } => BinaryOp {
op: *op,
lhs: Arc::new(resolve(lhs.as_ref())),
rhs: Arc::new(resolve(rhs.as_ref())),
},
// do deeper conversion for AST-specific expression variants
Extra(extra) => match extra.as_ref() {
// insert a fresh variable identified by this expression's AST node
ast::ExprExtra::Hole => Variable(vars.insert_full(VariableMeta::Hole(expr.meta)).0),
// recursively convert elements of tuples
ast::ExprExtra::Tuple(exprs) => Extra(Arc::new(ExprExtra::Tuple(
exprs.iter().map(resolve).collect(),
))),
ast::ExprExtra::SymbolName(name) => todo!(),
// resolve common variable name to index
ast::ExprExtra::VariableName(name) => {
Variable(vars.insert_full(VariableMeta::Named(name.clone())).0)
}
ast::ExprExtra::Apply(apply) => todo!(),
ast::ExprExtra::Aggregate(aggregate) => todo!(),
},
// TODO: handle unreachability better?
Variable(idx) => Variable(*idx),
// TODO: handle unreachability better?
Load { relation, query } => Load {
relation: *relation,
query: query.clone(),
},
};
// construct converted expression
Expr {
meta: expr.meta,
kind,
}
}
/// Resolve a reference to a relation by name.
// TODO: track incrementally?
pub fn relation_name<'db>(db: &'db dyn Database, name: ast::Name) -> RelationLabel<'db> {
todo!()
}
/// A type alias for a resolve stage [ir::Rule].
pub type Rule<'db> = ir::Rule<ProgramInfo<'db>>;
/// A type alias for a resolve stage [ir::Assumption].
pub type Assumption<'db> = ir::Assumption<ProgramInfo<'db>>;
/// A type alias for a resolve stage [ir::RuleBody].
pub type RuleBody<'db> = ir::RuleBody<ProgramInfo<'db>>;
/// A type alias for a resolve stage [ir::Expr].
pub type Expr<'db> = ir::Expr<ProgramInfo<'db>>;
/// Resolve stage-specific extensions to [ir::ProgramInfo].
pub struct ProgramInfo<'db>(PhantomData<&'db ()>);
impl<'db> ir::ProgramInfo for ProgramInfo<'db> {
type SymbolMeta = Infallible;
/// [RelationLabel] fallibly identifies relations after the resolution stage.
type RelationLabel = RelationLabel<'db>;
type AssumptionMeta = Infallible;
type RuleMeta = ast::Rule<'db>;
/// Resolved rule bodies point only to their AST origins.
///
/// Rule bodies may be implicit, so there is not always an associated
/// [ast::RuleBody]. In this case, the AST node is the rule, not its body.
type RuleBodyMeta = AstNode;
type VariableMeta = VariableMeta;
/// Information about the source location is preserved from the AST stage.
type ExprMeta = AstNode;
type BinaryOp = ast::BinaryOpKind;
type UnaryOp = ir::UnaryOpKind;
type ExprExtra = Arc<ExprExtra<'db>>;
}
/// Extra [ExprKind] variants in the resolve stage.
#[derive(Clone, PartialEq, Eq, Hash)]
pub enum ExprExtra<'db> {
/// If a rule body has no clauses, this is its body's expression variant.
Implicit,
/// A structured tuple of sub-expressions.
Tuple(Vec<Expr<'db>>),
/// A resolved symbol.
///
/// Not yet aggregated globally, so no index as [ir::Value::Symbol] yet.
Symbol(WithAst<ast::Name>),
/// A resolved application expression.
Apply(Apply<'db>),
}
/// Identifiers for relations in the resolve stage.
#[derive(Clone, PartialEq, Eq, Hash)]
pub enum RelationLabel<'db> {
/// The relation has been resolved to a concrete relation object.
Relation(ast::Relation<'db>),
/// The relation could not be resolved.
Unknown(ast::Name),
}
/// Resolved identification of a variable within a rule body.
#[derive(Clone, PartialEq, Eq, Hash)]
pub enum VariableMeta {
/// The variable has a name.
Named(String),
/// The variable is unnamed (a hole) but is unique to its AST node.
Hole(AstNode),
}
/// A high-level application operation.
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Apply<'db> {
/// The source relation's index in the parent [RuleBody].
pub relation: usize,
/// The parameters to the function or query.
pub body: Expr<'db>,
}