feat: function scope alias (#426)

* feat: function scope alias

* fix types
This commit is contained in:
stefan-gorules
2026-01-19 10:44:16 +01:00
committed by GitHub
parent 61465bd6d4
commit 73d5d5c6cb
10 changed files with 154 additions and 30 deletions
+36 -9
View File
@@ -38,11 +38,28 @@ impl Compiler {
struct CompilerInner<'arena, 'bytecode_ref> {
root: &'arena Node<'arena>,
bytecode: &'bytecode_ref mut Vec<Opcode>,
closure_aliases: Vec<Option<&'arena str>>,
}
impl<'arena, 'bytecode_ref> CompilerInner<'arena, 'bytecode_ref> {
pub fn new(bytecode: &'bytecode_ref mut Vec<Opcode>, root: &'arena Node<'arena>) -> Self {
Self { root, bytecode }
Self {
root,
bytecode,
closure_aliases: Vec::new(),
}
}
fn lookup_alias(&self, name: &str) -> Option<u32> {
self.closure_aliases
.iter()
.enumerate()
.rev()
.find_map(|(index, &alias)| {
alias.and_then(|a| {
(a == name).then_some((self.closure_aliases.len() - 1 - index) as u32)
})
})
}
pub fn compile(&mut self) -> CompilerResult<()> {
@@ -116,10 +133,12 @@ impl<'arena, 'bytecode_ref> CompilerInner<'arena, 'bytecode_ref> {
fn compile_member_fast(&mut self, node: &'arena Node<'arena>) -> Option<Vec<FetchFastTarget>> {
match node {
Node::Root => Some(vec![FetchFastTarget::Root]),
Node::Identifier(v) => Some(vec![
FetchFastTarget::Begin,
FetchFastTarget::String(Arc::from(*v)),
]),
Node::Identifier(v) => self.lookup_alias(v).is_none().then(|| {
vec![
FetchFastTarget::Begin,
FetchFastTarget::String(Arc::from(*v)),
]
}),
Node::Member { node, property } => {
let mut path = self.compile_member_fast(node)?;
match property {
@@ -149,7 +168,7 @@ impl<'arena, 'bytecode_ref> CompilerInner<'arena, 'bytecode_ref> {
Node::Bool(v) => Ok(self.emit(Opcode::PushBool(*v))),
Node::Number(v) => Ok(self.emit(Opcode::PushNumber(*v))),
Node::String(v) => Ok(self.emit(Opcode::PushString(Arc::from(*v)))),
Node::Pointer => Ok(self.emit(Opcode::Pointer)),
Node::Pointer => Ok(self.emit(Opcode::Pointer(0))),
Node::Root => Ok(self.emit(Opcode::FetchRootEnv)),
Node::Array(v) => {
v.iter()
@@ -189,8 +208,16 @@ impl<'arena, 'bytecode_ref> CompilerInner<'arena, 'bytecode_ref> {
with_return: output.is_some(),
}))
}
Node::Identifier(v) => Ok(self.emit(Opcode::FetchEnv(Arc::from(*v)))),
Node::Closure(v) => self.compile_node(v),
Node::Identifier(v) => Ok(self.emit(
self.lookup_alias(v)
.map_or_else(|| Opcode::FetchEnv(Arc::from(*v)), Opcode::Pointer),
)),
Node::Closure { body, alias } => {
self.closure_aliases.push(*alias);
let result = self.compile_node(body);
self.closure_aliases.pop();
result
}
Node::Parenthesized(v) => self.compile_node(v),
Node::Member {
node: n,
@@ -496,7 +523,7 @@ impl<'arena, 'bytecode_ref> CompilerInner<'arena, 'bytecode_ref> {
c.compile_argument(kind, arguments, 1)?;
c.emit_cond(|c| {
c.emit(Opcode::IncrementCount);
c.emit(Opcode::Pointer);
c.emit(Opcode::Pointer(0));
});
Ok(())
})?;
+2 -1
View File
@@ -51,7 +51,8 @@ pub enum Opcode {
IncrementCount,
GetCount,
GetLen,
Pointer,
/// The u32 is the depth from innermost scope (0 = innermost, 1 = parent, etc.)
Pointer(u32),
Begin,
End,
CallFunction {
+2
View File
@@ -52,6 +52,7 @@ impl<'arena> IntelliSense<'arena> {
pointer_data: data.shallow_clone(),
root_data: data.shallow_clone(),
current_data: data.shallow_clone(),
..Default::default()
},
);
@@ -98,6 +99,7 @@ impl<'arena> IntelliSense<'arena> {
pointer_data: data.shallow_clone(),
root_data: data.shallow_clone(),
current_data: data.shallow_clone(),
..Default::default()
},
);
+10 -1
View File
@@ -1,8 +1,17 @@
use crate::variable::VariableType;
use ahash::HashMap;
use std::rc::Rc;
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct IntelliSenseScope {
pub root_data: VariableType,
pub current_data: VariableType,
pub pointer_data: VariableType,
pub aliases: HashMap<Rc<str>, VariableType>,
}
impl IntelliSenseScope {
pub fn get_alias(&self, name: &str) -> Option<&VariableType> {
self.aliases.get(name)
}
}
@@ -165,7 +165,10 @@ impl TypesProvider {
}
}
Node::Identifier(i) => TypeInfo::from(scope.root_data.get(i)),
Node::Identifier(i) => scope
.get_alias(i)
.map(|t| TypeInfo::from(t.clone()))
.unwrap_or_else(|| TypeInfo::from(scope.root_data.get(i))),
Node::Member { node, property } => {
let node_type = self.determine(node, scope.clone());
let property_type = self.determine(property, scope.clone());
@@ -389,15 +392,27 @@ impl TypesProvider {
if let FunctionKind::Closure(_) = kind {
let ptr_type = type_list[0].iterator().unwrap_or_default();
let new_type = self.determine(
arguments[1],
IntelliSenseScope {
pointer_data: ptr_type.deref().clone(),
current_data: scope.current_data,
root_data: scope.root_data,
},
);
let ptr_type_inner = ptr_type.deref().clone();
let alias = match arguments[1] {
Node::Closure { alias, .. } => *alias,
_ => None,
};
let mut closure_scope = IntelliSenseScope {
pointer_data: ptr_type_inner.clone(),
current_data: scope.current_data.clone(),
root_data: scope.root_data.clone(),
aliases: scope.aliases.clone(),
};
if let Some(alias_name) = alias {
closure_scope
.aliases
.insert(Rc::from(alias_name), ptr_type_inner);
}
let new_type = self.determine(arguments[1], closure_scope);
type_list[1] = new_type.kind;
}
@@ -493,7 +508,7 @@ impl TypesProvider {
error: typecheck.general,
}
}
Node::Closure(c) => self.determine(c, scope.clone()),
Node::Closure { body, .. } => self.determine(body, scope.clone()),
Node::Parenthesized(c) => self.determine(c, scope.clone()),
Node::Error { node, error } => match node {
None => TypeInfo {
+5 -2
View File
@@ -21,7 +21,10 @@ pub enum Node<'a> {
output: Option<&'a Node<'a>>,
},
Identifier(&'a str),
Closure(&'a Node<'a>),
Closure {
body: &'a Node<'a>,
alias: Option<&'a str>,
},
Parenthesized(&'a Node<'a>),
Root,
Member {
@@ -106,7 +109,7 @@ impl<'a> Node<'a> {
output.walk(func.clone());
}
}
Node::Closure(closure) => closure.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());
+38 -3
View File
@@ -638,7 +638,11 @@ impl<'arena, 'token_ref, Flavor> Parser<'arena, 'token_ref, Flavor> {
}
/// Closure
pub(crate) fn closure<F>(&self, expression_parser: &F) -> &'arena Node<'arena>
pub(crate) fn closure<F>(
&self,
expression_parser: &F,
alias: Option<&'arena str>,
) -> &'arena Node<'arena>
where
F: Fn(ParserContext) -> &'arena Node<'arena>,
{
@@ -648,7 +652,7 @@ impl<'arena, 'token_ref, Flavor> Parser<'arena, 'token_ref, Flavor> {
let node = expression_parser(ParserContext::Closure);
self.depth.set(self.depth.get() - 1);
self.node(Node::Closure(node), |_| NodeMetadata {
self.node(Node::Closure { body: node, alias }, |_| NodeMetadata {
span: (start, self.prev_token_end()),
})
}
@@ -734,11 +738,42 @@ impl<'arena, 'token_ref, Flavor> Parser<'arena, 'token_ref, Flavor> {
let mut arguments = BumpVec::new_in(&self.bump);
arguments.push(expression_parser(ParserContext::Global));
let alias: Option<&'arena str> =
if self
.current()
.is_some_and(|t| t.kind == TokenKind::Literal && t.value == "as")
{
self.next();
let alias_token = self.current();
match alias_token {
Some(t) if t.kind == TokenKind::Literal => {
let alias_str = self.bump.alloc_str(t.value);
self.next();
Some(alias_str)
}
_ => {
arguments.push(self.error(AstNodeError::Custom {
message: afmt!(self, "Expected identifier after 'as'"),
span:
alias_token.map(|t| t.span).unwrap_or((
self.prev_token_end(),
self.prev_token_end(),
)),
}));
None
}
}
} else {
None
};
if let Some(error) = self.expect(TokenKind::Operator(Operator::Comma)) {
arguments.push(error);
};
arguments.push(self.closure(&expression_parser));
arguments.push(self.closure(&expression_parser, alias));
if let Some(error) = self.expect(TokenKind::Bracket(Bracket::RightParenthesis)) {
arguments.push(error);
}
+1 -1
View File
@@ -299,7 +299,7 @@ impl From<&Node<'_>> for UnaryNodeBehaviour {
Node::Pointer => AsBoolean,
Node::Array(_) => CompareWithReference(In),
Node::Identifier(_) => CompareWithReference(Equal),
Node::Closure(_) => AsBoolean,
Node::Closure { .. } => AsBoolean,
Node::Member { .. } => CompareWithReference(Equal),
Node::Slice { .. } => CompareWithReference(In),
Node::Interval { .. } => CompareWithReference(In),
+11 -2
View File
@@ -845,8 +845,17 @@ impl<'arena, 'parent_ref, 'bytecode_ref> VMInner<'parent_ref, 'bytecode_ref> {
self.push(Number(scope.len.into()));
}
Opcode::Pointer => {
let scope = self.scopes.last().ok_or_else(|| OpcodeErr {
Opcode::Pointer(depth) => {
let scope_index = self
.scopes
.len()
.checked_sub(1 + *depth as usize)
.ok_or_else(|| OpcodeErr {
opcode: "Pointer".into(),
message: format!("Scope depth {} out of bounds", depth),
})?;
let scope = self.scopes.get(scope_index).ok_or_else(|| OpcodeErr {
opcode: "Pointer".into(),
message: "Empty scope".into(),
})?;
+24 -1
View File
@@ -613,4 +613,27 @@ round(1.234e2);;123
"user.name = 'Eve'; user.name";{};'Eve'
"config.debug = true; config.env = 'dev'; config";{};{"debug": true, "env": "dev"}
"config.debug = true; config.env = 'dev'; $root";{};{"config": {"debug": true, "env": "dev"}}
"a = 5; b = 10; a + b";{};15
"a = 5; b = 10; a + b";{};15
# Closure 'as' alias syntax
map(products as p, p.price);{"products": [{"name": "A", "price": 10}, {"name": "B", "price": 20}]};[10, 20]
filter(products as p, p.price > 15);{"products": [{"name": "A", "price": 10}, {"name": "B", "price": 20}]};[{"name": "B", "price": 20}]
some(items as i, i.active);{"items": [{"active": false}, {"active": true}]};true
all(items as i, i.value > 0);{"items": [{"value": 1}, {"value": 2}]};true
none(items as i, i.value < 0);{"items": [{"value": 1}, {"value": 2}]};true
count(items as i, i.value > 1);{"items": [{"value": 1}, {"value": 2}, {"value": 3}]};2
one(items as i, i.id == 2);{"items": [{"id": 1}, {"id": 2}, {"id": 3}]};true
flatMap(groups as g, g.items);{"groups": [{"items": [1, 2]}, {"items": [3, 4]}]};[1, 2, 3, 4]
# Nested closures with 'as' aliases
map(orders as o, map(o.items as i, i.qty));{"orders": [{"items": [{"qty": 1}, {"qty": 2}]}, {"items": [{"qty": 3}]}]};[[1, 2], [3]]
map(orders as o, map(o.items as i, o.id + i.qty));{"orders": [{"id": 10, "items": [{"qty": 1}, {"qty": 2}]}]};[[11, 12]]
filter(orders as o, some(o.items as i, i.qty > 2));{"orders": [{"id": 1, "items": [{"qty": 1}]}, {"id": 2, "items": [{"qty": 3}]}]};[{"id": 2, "items": [{"qty": 3}]}]
# Mixed # and 'as' alias
map(orders as o, map(o.items, #.qty));{"orders": [{"items": [{"qty": 5}, {"qty": 6}]}]};[[5, 6]]
map(items, #.value * 2);{"items": [{"value": 1}, {"value": 2}]};[2, 4]
# Backwards compatibility - # still works
map([1, 2, 3], # * 2);;[2, 4, 6]
filter([1, 2, 3, 4], # > 2);;[3, 4]
Can't render this file because it contains an unexpected character in line 14 and column 13.