Files
zen/core/expression/src/parser/ast.rs
T
stefan-gorules ffdfd807d1 feat: implement slots instead of natural language (#527)
* feat: implement slots instead of natural language

Replace the natural-language projection (`nl`) with engine-owned slot
completion: for a cursor position the engine classifies the slot being
edited (operand, operator, literal, member access, ...) and returns typed
options, literal facts and the expected type, for policies and graphs.

- zen-expression: new `slot` module (classification, literals, operators)
  and a lenient lexer mode for incomplete input; `nl` removed
- Intellisense: typed diagnostic codes with arguments, richer inspect for
  hover, dedicated parser error variants (messages unchanged)
- Engine: `Workspace::cursor_scope`, `slot`, `facts`; `rename_from` and
  `references_from`; editor spans in UTF-16 units; renames reach imported
  policies, `$nodes` reads, child graphs and output column heads; no `$root`
  completion in policies
- Node bindings: `slot`, `facts`, `cursorScope`, `slotBatch` replace
  `nl`, `nlTokenize`, `nlEncodeString`, `nlTokenizeBatch`

No change to expression evaluation: the VM, compiler, functions and the
default lexer path are untouched, and parser error messages keep their text.

* fix: harden slot classification, scopes and graph expectations

- restore master execution order; slot scopes hide writes of the edited
  block and its dependents instead of relying on block ranking
- token-start editing gated by slot state; operator, list and operand
  spans no longer splice into neighbouring tokens
- interval-aware bracket pairing for half-open and reversed intervals
- lenient lexer recovers from unknown characters (slots only)
- graph expected type follows switch and pass-through nodes and merges
  compatible output schemas
- sibling inference caps distinct values and analyses lazily
- deterministic overload return type display

* fix: order policy blocks by entity reads through closures and relationships

- record reads for `#.field` in closures so entity fields read through
  lists become dependencies
- link reads through derived lists (filter/map results) to the entity
  fields they carry and order the list after those fields
- evaluate entity blocks on single relationships, and demand entity
  paths for plain reads through relationships
- slot scopes keep the top-level variable when hiding written fields
2026-09-28 21:47:40 +02:00

270 lines
8.3 KiB
Rust

use crate::functions::{FunctionKind, MethodKind};
use crate::lexer::{Bracket, Operator};
use bumpalo::collections::Vec as BumpVec;
use rust_decimal::Decimal;
use std::cell::Cell;
use strum_macros::IntoStaticStr;
use thiserror::Error;
#[derive(Debug, PartialEq, Clone, IntoStaticStr)]
pub enum Node<'a> {
Null,
Bool(bool),
Number(Decimal),
String(&'a str),
TemplateString(&'a [&'a Node<'a>]),
Pointer,
Array(&'a [&'a Node<'a>]),
Object(&'a [(&'a Node<'a>, &'a Node<'a>)]),
Assignments {
list: &'a [(&'a Node<'a>, &'a Node<'a>)],
output: Option<&'a Node<'a>>,
},
Identifier(&'a str),
Closure {
body: &'a Node<'a>,
alias: Option<&'a str>,
},
Parenthesized(&'a Node<'a>),
Root,
Member {
node: &'a Node<'a>,
property: &'a Node<'a>,
},
Slice {
node: &'a Node<'a>,
from: Option<&'a Node<'a>>,
to: Option<&'a Node<'a>>,
},
Interval {
left: &'a Node<'a>,
right: &'a Node<'a>,
left_bracket: Bracket,
right_bracket: Bracket,
},
Conditional {
condition: &'a Node<'a>,
on_true: &'a Node<'a>,
on_false: &'a Node<'a>,
},
Unary {
node: &'a Node<'a>,
operator: Operator,
},
Binary {
left: &'a Node<'a>,
operator: Operator,
right: &'a Node<'a>,
},
FunctionCall {
kind: FunctionKind,
arguments: &'a [&'a Node<'a>],
},
MethodCall {
kind: MethodKind,
this: &'a Node<'a>,
arguments: &'a [&'a Node<'a>],
},
Error {
node: Option<&'a Node<'a>>,
error: AstNodeError<'a>,
},
}
impl<'a> Node<'a> {
pub fn walk<F>(&self, mut func: F)
where
F: FnMut(&Self) + Clone,
{
{
func(self);
};
match self {
Node::Null => {}
Node::Bool(_) => {}
Node::Number(_) => {}
Node::String(_) => {}
Node::Pointer => {}
Node::Identifier(_) => {}
Node::Root => {}
Node::Error { node, .. } => {
if let Some(n) = node {
n.walk(func.clone())
}
}
Node::TemplateString(parts) => parts.iter().for_each(|n| n.walk(func.clone())),
Node::Array(parts) => parts.iter().for_each(|n| n.walk(func.clone())),
Node::Object(obj) => obj.iter().for_each(|(k, v)| {
k.walk(func.clone());
v.walk(func.clone());
}),
Node::Assignments { list, output } => {
list.iter().for_each(|(k, v)| {
k.walk(func.clone());
v.walk(func.clone());
});
if let Some(output) = output {
output.walk(func.clone());
}
}
Node::Closure { body, .. } => body.walk(func.clone()),
Node::Parenthesized(c) => c.walk(func.clone()),
Node::Member { node, property } => {
node.walk(func.clone());
property.walk(func.clone());
}
Node::Slice { node, to, from } => {
node.walk(func.clone());
if let Some(to) = to {
to.walk(func.clone());
}
if let Some(from) = from {
from.walk(func.clone());
}
}
Node::Interval { left, right, .. } => {
left.walk(func.clone());
right.walk(func.clone());
}
Node::Unary { node, .. } => {
node.walk(func);
}
Node::Binary { left, right, .. } => {
left.walk(func.clone());
right.walk(func.clone());
}
Node::FunctionCall { arguments, .. } => {
arguments.iter().for_each(|n| n.walk(func.clone()));
}
Node::MethodCall {
this, arguments, ..
} => {
this.walk(func.clone());
arguments.iter().for_each(|n| n.walk(func.clone()));
}
Node::Conditional {
on_true,
condition,
on_false,
} => {
condition.walk(func.clone());
on_true.walk(func.clone());
on_false.walk(func.clone());
}
};
}
pub fn first_error(&'a self) -> Option<AstNodeError<'a>> {
let error_cell = Cell::new(None);
self.walk(|n| {
if let Node::Error { error, .. } = n {
error_cell.set(Some(error.clone()))
}
});
error_cell.into_inner()
}
pub fn has_error(&self) -> bool {
self.first_error().is_some()
}
pub(crate) fn span(&self) -> Option<(u32, u32)> {
match self {
Node::Error { error, .. } => match error {
AstNodeError::UnknownBuiltIn { span, .. } => Some(span.clone()),
AstNodeError::UnknownMethod { span, .. } => Some(span.clone()),
AstNodeError::UnexpectedIdentifier { span, .. } => Some(span.clone()),
AstNodeError::UnexpectedToken { span, .. } => Some(span.clone()),
AstNodeError::InvalidNumber { span, .. } => Some(span.clone()),
AstNodeError::InvalidBoolean { span, .. } => Some(span.clone()),
AstNodeError::InvalidProperty { span, .. } => Some(span.clone()),
AstNodeError::MissingToken { position, .. } => {
Some((*position as u32, *position as u32))
}
AstNodeError::ExpectedProperty { span }
| AstNodeError::ExpectedLiteral { span }
| AstNodeError::UnexpectedEnd { span }
| AstNodeError::Custom { span, .. } => Some(span.clone()),
},
_ => None,
}
}
pub(crate) fn member_key(&self, vec: &mut BumpVec<&'a str>) -> Option<()> {
match self {
Node::Member { node, property } => {
node.member_key(vec)?;
let property_key = match property {
Node::String(key) => Some(*key),
Node::Root => Some("$root"),
_ => None,
}?;
vec.push(property_key);
Some(())
}
Node::Identifier(name) => {
vec.push(name);
Some(())
}
Node::Root => {
vec.push("$root");
Some(())
}
_ => None,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Error)]
pub enum AstNodeError<'a> {
#[error("Unknown function `{name}` at ({}, {})", span.0, span.1)]
UnknownBuiltIn { name: &'a str, span: (u32, u32) },
#[error("Unknown method `{name}` at ({}, {})", span.0, span.1)]
UnknownMethod { name: &'a str, span: (u32, u32) },
#[error("Unexpected identifier: {received} at ({}, {}); Expected {expected}.", span.0, span.1)]
UnexpectedIdentifier {
received: &'a str,
expected: &'a str,
span: (u32, u32),
},
#[error("Unexpected token: {received} at ({}, {}); Expected {expected}.", span.0, span.1)]
UnexpectedToken {
received: &'a str,
expected: &'a str,
span: (u32, u32),
},
#[error("Invalid number: {number} at ({}, {})", span.0, span.1)]
InvalidNumber { number: &'a str, span: (u32, u32) },
#[error("Invalid boolean: {boolean} at ({}, {})", span.0, span.1)]
InvalidBoolean { boolean: &'a str, span: (u32, u32) },
#[error("Invalid property: {property} at ({}, {})", span.0, span.1)]
InvalidProperty { property: &'a str, span: (u32, u32) },
#[error("Missing expected token: {expected} at {position}")]
MissingToken { expected: &'a str, position: usize },
#[error("Expected a property at ({}, {})", span.0, span.1)]
ExpectedProperty { span: (u32, u32) },
#[error("Expected a literal at ({}, {})", span.0, span.1)]
ExpectedLiteral { span: (u32, u32) },
#[error("Unexpected end of unary expression at ({}, {})", span.0, span.1)]
UnexpectedEnd { span: (u32, u32) },
#[error("{message} at ({}, {})", span.0, span.1)]
Custom { message: &'a str, span: (u32, u32) },
}