Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions crates/oak_core/src/syntax_ext.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use aether_syntax::AnyRExpression;
use aether_syntax::AnyRSelector;
use aether_syntax::RCall;
use aether_syntax::RIdentifier;
use aether_syntax::RStringValue;
use biome_rowan::AstNode;
use biome_rowan::AstSeparatedList;

// Candidates for upstreaming into `aether_syntax`.

Expand Down Expand Up @@ -61,6 +64,24 @@ impl AnyRSelectorExt for AnyRSelector {
}
}

pub trait RCallExt {
/// The value expression of the argument at `position`, counting in call
/// order. `None` when there's no argument there or it has no value.
fn argument_value(&self, position: usize) -> Option<AnyRExpression>;
}

impl RCallExt for RCall {
fn argument_value(&self, position: usize) -> Option<AnyRExpression> {
self.arguments()
.ok()?
.items()
.iter()
.nth(position)?
.ok()?
.value()
}
}

#[cfg(test)]
mod tests {
use aether_parser::RParserOptions;
Expand Down
71 changes: 71 additions & 0 deletions crates/oak_semantic/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ use rustc_hash::FxHashSet;

use crate::effects::AssignBinding;
use crate::effects::ResolvedArgumentEffects;
use crate::effects::ScopeBindings;
use crate::effects::TargetAccess;
use crate::resolver::ImportsResolver;
use crate::resolver::SourceResolution;
Expand Down Expand Up @@ -491,6 +492,40 @@ impl<R: ImportsResolver> SemanticIndexBuilder<R> {
self.walked_binding(scope, name).is_some() || self.bound_names[scope].binds(name)
}

/// Whether the current evaluation frame binds `name` (see [`scan_scope`]).
/// For a scope, delegates to [`scope_binds_anywhere`]. For a `local()`
/// descent body, the names collected into it so far.
///
/// [`scan_scope`]: Self::scan_scope
/// [`scope_binds_anywhere`]: Self::scope_binds_anywhere
fn scan_scope_binds(&self, name: &str) -> bool {
match self.scan_scope() {
Some(ScanScope::Descent(bound)) => bound.binds(name),
Some(ScanScope::Scope(scope)) => self.scope_binds_anywhere(scope, name),
None => false,
}
}

fn scan_scope_is_global(&self) -> bool {
match self.scan_scope() {
Some(ScanScope::Scope(scope)) => matches!(self.scopes[scope].kind, ScopeKind::File),
Some(ScanScope::Descent(_)) => false,
None => true,
}
}

fn scan_scope(&self) -> Option<ScanScope<'_>> {
if let Some(bound) = self.eager_descent.open.last() {
return Some(ScanScope::Descent(bound));
}

let scope = match self.scopes[self.current_scope].kind {
ScopeKind::Nse(EvalEnv::Current, EvalTiming::Lazy) => self.definition_owner()?,
_ => self.current_scope,
};
Some(ScanScope::Scope(scope))
}

/// The site where `scope` binds `name`, matching what
/// [`scope_binds_anywhere`](Self::scope_binds_anywhere) counts as a binding
/// (so it returns `Some` on exactly the same names). Prefers the
Expand Down Expand Up @@ -1497,6 +1532,31 @@ struct SourcedFile {
resolution: Option<SourceResolution>,
}

/// Backs a [`CallContext`]'s [`ScopeBindings`] with the builder's live scope
/// state, so an effect handler (`substitute`) can query bindings during the
/// scan without reaching into the builder directly.
///
/// [`CallContext`]: crate::effects::CallContext
struct ScanBindings<'a, R: ImportsResolver> {
builder: &'a SemanticIndexBuilder<R>,
}

impl<R: ImportsResolver> ScopeBindings for ScanBindings<'_, R> {
fn is_bound(&self, name: &str, inherits: bool) -> bool {
if inherits {
// The scan's `flow_state` carries the current scope's bindings plus
// the inherited eager environment seeded at `begin_scan`, so it's
// the lexical answer.
return self.builder.flow_state.is_bound(name);
}
self.builder.scan_scope_binds(name)
}

fn is_global_scope(&self) -> bool {
self.builder.scan_scope_is_global()
}
}

/// The scan's flow-precise binding state: which names are bound at the current
/// point of the current scan unit, in flow order.
///
Expand Down Expand Up @@ -1604,6 +1664,17 @@ impl BoundNames {
}
}

/// A scope as the scan sees it. A `local()` body scanned inline has no arena
/// scope yet and its bindings are stored in the staging [`EagerNestedDescent`].
/// Every other scope is materialized in the arena. [`scan_scope`] resolves
/// which one is the current evaluation frame.
///
/// [`scan_scope`]: SemanticIndexBuilder::scan_scope
enum ScanScope<'a> {
Descent(&'a BoundNames),
Scope(ScopeId),
}

fn is_assignment(bin: &RBinaryExpression) -> bool {
let Ok(op) = bin.operator() else {
return false;
Expand Down
11 changes: 9 additions & 2 deletions crates/oak_semantic/src/builder/builder_nse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use super::is_assignment;
use super::is_right_assignment;
use super::is_super_assignment;
use super::BoundNames;
use super::ScanBindings;
use super::SemanticIndexBuilder;
use super::SourcedFile;
use crate::effects;
Expand Down Expand Up @@ -211,7 +212,9 @@ impl<R: ImportsResolver> SemanticIndexBuilder<R> {
}

let handlers = self.resolve_symbol_effects(op_text, bin.syntax().text_trimmed_range())?;
let ctx = CallContext::new();

let bindings = ScanBindings { builder: &*self };
let ctx = CallContext::with_bindings(&bindings);
handlers.assign?.resolve(EffectSite::Operator(bin), &ctx)
}

Expand Down Expand Up @@ -314,7 +317,11 @@ impl<R: ImportsResolver> SemanticIndexBuilder<R> {
/// Resolve a call's effects.
fn resolve_effects(&mut self, call: &RCall) -> Option<Effects> {
let handlers = self.resolve_effects_handlers(call)?;
let ctx = CallContext::new();

// `resolve_effects_handlers()` returns owned handlers, so its `&mut
// self` borrow is finished. Reborrow immutably.
let bindings = ScanBindings { builder: &*self };
let ctx = CallContext::with_bindings(&bindings);

let arguments = handlers
.arguments
Expand Down
68 changes: 55 additions & 13 deletions crates/oak_semantic/src/effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ pub trait EffectHandler: std::fmt::Debug + Sync {
///
/// `ctx` provides semantic resolution, e.g. resolve an argument to a
/// statically known string or boolean.
fn resolve(&self, call: &RCall, ctx: &CallContext) -> Option<Self::Output>;
fn resolve(&self, call: &RCall, ctx: &CallContext<'_>) -> Option<Self::Output>;
}

/// Where an effect is invoked. Most effects are only ever calls but an Assign
Expand All @@ -113,7 +113,26 @@ pub enum EffectSite<'a> {
/// Contributed statically like [`EffectHandler`], so it's `Sync` for the
/// registry `static`s.
pub trait AssignHandler: std::fmt::Debug + Sync {
fn resolve(&self, site: EffectSite, ctx: &CallContext) -> Option<Vec<AssignBinding>>;
fn resolve(&self, site: EffectSite, ctx: &CallContext<'_>) -> Option<Vec<AssignBinding>>;
}

/// Scope state a handler needs that the call syntax alone can't answer, backed
/// by the builder's flow-precise binding tables.
///
/// `substitute` uses this to tell which symbols in its argument name a binding
/// in the current scope (so they resolve here, against substitute's env) from
/// those that stay quoted (so they resolve wherever the result is later
/// evaluated).
pub trait ScopeBindings {
/// Whether `name` is bound in the current scope. With `inherits`, also
/// counts bindings inherited from enclosing scopes, mirroring R's
/// `get(..., inherits=)`.
fn is_bound(&self, name: &str, inherits: bool) -> bool;

/// Whether the current scope is the global (file) scope. R's `substitute`
/// substitutes nothing in the global environment, so a handler falls back to
/// a plain quote there.
fn is_global_scope(&self) -> bool;
}

/// Whether an assign effect reads its target before writing it.
Expand All @@ -128,15 +147,38 @@ pub enum TargetAccess {

/// Context for effect handlers.
///
/// Allows querying the properties or static values of arguments. Stateless
/// today, an extension point for information a call's syntax doesn't carry (e.g.
/// resolving a `character.only` variable to its string value) once that lands.
/// Allows querying the properties or static values of arguments, and the
/// binding state of the surrounding scope.
#[derive(Default)]
pub struct CallContext;
pub struct CallContext<'a> {
bindings: Option<&'a dyn ScopeBindings>,
}

impl<'a> CallContext<'a> {
/// A context backed by the builder's scope state, for handlers that query
/// bindings (`substitute`).
pub fn with_bindings(bindings: &'a dyn ScopeBindings) -> Self {
Self {
bindings: Some(bindings),
}
}

/// Whether `name` is bound in the current scope (see
/// [`ScopeBindings::is_bound`]). Without a bindings backing (a [`Default`]
/// context) we can't tell, so we answer "unbound", the choice that leaves a
/// symbol quoted rather than treating it as a use.
pub fn is_bound(&self, name: &str, inherits: bool) -> bool {
self.bindings
.is_some_and(|bindings| bindings.is_bound(name, inherits))
}

impl CallContext {
pub fn new() -> Self {
Self
/// Whether the current scope is the global (file) scope (see
/// [`ScopeBindings::is_global_scope`]). Without a bindings backing (a
/// [`Default`] context) we assume global, so `substitute` degrades to a
/// plain quote (its no-substitution behaviour).
pub fn current_scope_is_global(&self) -> bool {
self.bindings
.is_none_or(|bindings| bindings.is_global_scope())
}

/// Match `call`'s arguments to `formals`, returning for each call argument
Expand Down Expand Up @@ -287,7 +329,7 @@ impl ArgumentEffect {
impl EffectHandler for ArgumentsAnnotation {
type Output = ResolvedArgumentEffects;

fn resolve(&self, call: &RCall, ctx: &CallContext) -> Option<ResolvedArgumentEffects> {
fn resolve(&self, call: &RCall, ctx: &CallContext<'_>) -> Option<ResolvedArgumentEffects> {
let arguments = self.arguments;
let formals: Vec<Formal> = arguments
.iter()
Expand Down Expand Up @@ -322,7 +364,7 @@ pub struct SourceAnnotation {
impl EffectHandler for SourceAnnotation {
type Output = Vec<String>;

fn resolve(&self, call: &RCall, ctx: &CallContext) -> Option<Vec<String>> {
fn resolve(&self, call: &RCall, ctx: &CallContext<'_>) -> Option<Vec<String>> {
let args = call.arguments().ok()?;

// The path is matched positionally among unnamed arguments rather than
Expand Down Expand Up @@ -387,7 +429,7 @@ pub struct AssignAnnotation {
}

impl AssignHandler for AssignAnnotation {
fn resolve(&self, site: EffectSite, ctx: &CallContext) -> Option<Vec<AssignBinding>> {
fn resolve(&self, site: EffectSite, ctx: &CallContext<'_>) -> Option<Vec<AssignBinding>> {
let EffectSite::Call(call) = site else {
return None;
};
Expand Down Expand Up @@ -454,7 +496,7 @@ pub struct BindingOperatorHandler {
}

impl AssignHandler for BindingOperatorHandler {
fn resolve(&self, site: EffectSite, ctx: &CallContext) -> Option<Vec<AssignBinding>> {
fn resolve(&self, site: EffectSite, ctx: &CallContext<'_>) -> Option<Vec<AssignBinding>> {
let EffectSite::Operator(bin) = site else {
return None;
};
Expand Down
Loading
Loading