From 37932fbd901fa3366fc816bcd5c92be61af886ac Mon Sep 17 00:00:00 2001 From: Ilya Lakhin Date: Tue, 3 Sep 2024 23:29:38 +0600 Subject: [PATCH] Analyzer-Wide Common Semantics API (#22) --- work/book/src/SUMMARY.md | 1 + .../book/src/semantics/multi-file-analysis.md | 173 +++++ work/crates/derive/src/feature/output.rs | 5 + work/crates/derive/src/lib.rs | 10 + work/crates/derive/src/node/inheritance.rs | 18 + work/crates/derive/src/node/input.rs | 11 + work/crates/derive/src/node/output.rs | 18 + work/crates/examples/readme.md | 16 +- work/crates/examples/src/lib.rs | 1 + .../examples/src/shared_semantics/lexis.rs | 66 ++ .../examples/src/shared_semantics/mod.rs | 173 +++++ .../src/shared_semantics/semantics.rs | 263 ++++++++ .../examples/src/shared_semantics/syntax.rs | 115 ++++ work/crates/main/src/analysis/analyzer.rs | 62 +- work/crates/main/src/analysis/attribute.rs | 32 +- work/crates/main/src/analysis/compute.rs | 246 ++++++- work/crates/main/src/analysis/database.rs | 348 ++++------ work/crates/main/src/analysis/entry.rs | 19 +- work/crates/main/src/analysis/error.rs | 15 + work/crates/main/src/analysis/grammar.rs | 116 +++- work/crates/main/src/analysis/lock.rs | 231 +++++++ work/crates/main/src/analysis/mod.rs | 5 +- work/crates/main/src/analysis/scope.rs | 10 +- work/crates/main/src/analysis/slot.rs | 606 ++++++++++++++++++ work/crates/main/src/analysis/tasks.rs | 14 + 25 files changed, 2291 insertions(+), 283 deletions(-) create mode 100644 work/book/src/semantics/multi-file-analysis.md create mode 100644 work/crates/examples/src/shared_semantics/lexis.rs create mode 100644 work/crates/examples/src/shared_semantics/mod.rs create mode 100644 work/crates/examples/src/shared_semantics/semantics.rs create mode 100644 work/crates/examples/src/shared_semantics/syntax.rs create mode 100644 work/crates/main/src/analysis/lock.rs create mode 100644 work/crates/main/src/analysis/slot.rs diff --git a/work/book/src/SUMMARY.md b/work/book/src/SUMMARY.md index 3e51b66..58257c5 100644 --- a/work/book/src/SUMMARY.md +++ b/work/book/src/SUMMARY.md @@ -68,6 +68,7 @@ - [Granularity](semantics/granularity.md) - [The Analyzer](semantics/the-analyzer.md) - [Tasks Management](semantics/tasks-management.md) + - [Multi-File Analysis](semantics/multi-file-analysis.md) - [Language Server Design](semantics/language-server-design.md) - [Configuration Issues](semantics/configuration-issues.md) - [Code Diagnostics](semantics/code-diagnostics.md) diff --git a/work/book/src/semantics/multi-file-analysis.md b/work/book/src/semantics/multi-file-analysis.md new file mode 100644 index 0000000..f531c74 --- /dev/null +++ b/work/book/src/semantics/multi-file-analysis.md @@ -0,0 +1,173 @@ + + +# Multi-File Analysis + +A compilation project usually consists of multiple compilation units that are +semantically connected to each other. + +For example, a Java file may declare a class with signatures that reference +classes declared in other files within the same Java package. + +To establish semantic relationships between these compilation units, you can +define a special analyzer-wide feature object. + +From the [Shared Semantics](todo) example: + +```rust,noplayground +#[derive(Node)] + +// Defines a semantic feature that is shared across all documents in the Analyzer. +#[semantics(CommonSemantics)] + +pub enum SharedSemanticsNode { + // ... +} + +#[derive(Feature)] +#[node(SharedSemanticsNode)] +pub struct CommonSemantics { + pub modules: Slot>, +} +``` + +## Common Semantics + +The common semantics feature is a typical feature object, except that it is not +bound to any specific node within a compilation unit and is instantiated during +the creation of the Analyzer. + +This feature is not tied to any syntax tree scope. Therefore, its members will +not be directly invalidated during the editing of the Analyzer's documents. + +However, the members of this feature are part of the semantic graph and are +subject to the normal rules of the semantic graph, such as the prohibition of +cycles between computable functions. + +Common semantic features typically include: + +- Analyzer-wide reducing attributes, such as an attribute that collects all + syntax and semantic issues detected across all managed documents. +- External configuration metadata specified via the system of + [Slots](https://docs.rs/lady-deirdre/2.0.1/lady_deirdre/analysis/struct.Slot.html). + For instance, a map between file names and their document IDs within the + Analyzer (as in the example above). + +You can access common semantics both inside and outside of computable +functions. Inside a computable function, you can access common semantics +using the [AttrContext::common](https://docs.rs/lady-deirdre/2.0.1/lady_deirdre/analysis/struct.AttrContext.html#method.common) +method. To access the semantics outside, you would use the +[AbstractTask::common](https://docs.rs/lady-deirdre/2.0.1/lady_deirdre/analysis/trait.AbstractTask.html#method.common) +method. + +```rust,noplayground +#[derive(Clone, PartialEq, Eq)] +pub enum KeyResolution { + Unresolved, + Recusrive, + Number(usize), +} + +impl Computable for KeyResolution { + type Node = SharedSemanticsNode; + + fn compute( + context: &mut AttrContext, + ) -> AnalysisResult { + // ... + + // Reading the common semantics inside the computable function. + let modules = context.common().modules.read(context).unwrap_abnormal()?; + + // ... + } +} + +let handle = TriggerHandle::new(); +let mut task = analyzer.mutate(&handle, 1).unwrap(); + +let doc_id = task.add_mutable_doc("x = 10; y = module_2::b; z = module_2::c;"); +doc_id.set_name("module_1"); + +// Modifying the Slot value of the common semantics outside. +task.common() + .modules + .mutate(&task, |modules| { + let _ = modules.insert(String::from("module_1"), doc_id); + + true + }) + .unwrap(); +``` + +## Slots + +The primary purpose of a [Slot](https://docs.rs/lady-deirdre/2.0.1/lady_deirdre/analysis/struct.Slot.html) +is to provide a convenient mechanism for injecting configuration metadata +external to the Analyzer into the semantic graph. For instance, mapping between +file system names and the Analyzer's document IDs can be injected through a +common semantics Slot. + +Slot is a special feature of the semantic graph that is quite similar to +attributes, except that a Slot does not have an associated computable function. +Instead, Slots have associated values of a specified type (the second generic +argument of the `Slot` signature). + +You can [snapshot](https://docs.rs/lady-deirdre/2.0.1/lady_deirdre/analysis/struct.Slot.html#method.snapshot) +the current Slot value outside of computable functions, and you can +[read](https://docs.rs/lady-deirdre/2.0.1/lady_deirdre/analysis/struct.Slot.html#method.read) +Slot values within the computable functions of attributes, thereby subscribing +those attributes to changes in the Slot, much like with normal attributes. + +By default, Slot values are set to the `Default` of the value type. You can +modify the content of the Slot value using the +[Slot::mutate](https://docs.rs/lady-deirdre/2.0.1/lady_deirdre/analysis/struct.Slot.html#method.mutate) +method with a mutable (or exclusive) task. + +```rust,noplayground +task.common() + .modules + // The `task` is a MutationTask or an ExclusiveTask. + // + // The provided callback accepts a mutable reference to the current + // value of the Slot, and returns a boolean flag indicating whether the + // value has changed. + .mutate(&task, |modules| { + let _ = modules.insert(String::from("module_1"), doc_id); + + // Indicates that the `modules` content has been changed. + true + }) + .unwrap(); +``` diff --git a/work/crates/derive/src/feature/output.rs b/work/crates/derive/src/feature/output.rs index 01761a9..b4e749b 100644 --- a/work/crates/derive/src/feature/output.rs +++ b/work/crates/derive/src/feature/output.rs @@ -165,6 +165,11 @@ impl ToTokens for FeatureInput { &#core::analysis::NIL_ATTR_REF } + #[inline(always)] + fn slot_ref(&self) -> &#core::analysis::SlotRef { + &#core::analysis::NIL_SLOT_REF + } + fn feature(&self, key: #core::syntax::Key) -> #core::analysis::AnalysisResult<&dyn #core::analysis::AbstractFeature> { diff --git a/work/crates/derive/src/lib.rs b/work/crates/derive/src/lib.rs index 52312fa..ebfe92c 100644 --- a/work/crates/derive/src/lib.rs +++ b/work/crates/derive/src/lib.rs @@ -390,6 +390,16 @@ pub fn token(input: proc_macro::TokenStream) -> proc_macro::TokenStream { /// /// // Optional. /// // +/// // Specifies the semantic entry-point for the common semantics shared across +/// // all documents in the Analyzer. +/// // +/// // The type of this field must implement the `Feature` trait. +/// // +/// // If omitted, the default common semantics will be `VoidFeature` +/// #[semantics()] +/// +/// // Optional. +/// // /// // Defines the expression that will be automatically parsed zero or more /// // times between every consumed token in the node's parse rules. /// // diff --git a/work/crates/derive/src/node/inheritance.rs b/work/crates/derive/src/node/inheritance.rs index 8879f4d..cc360ba 100644 --- a/work/crates/derive/src/node/inheritance.rs +++ b/work/crates/derive/src/node/inheritance.rs @@ -391,6 +391,24 @@ impl Inheritance { Some(quote_spanned!(span=> Self::#ident { #field_ident: _0, .. } => #body,)) } + pub(super) fn compile_slot_ref(&self) -> Option { + let (field_ident, field_ty) = self.semantics.as_ref()?; + + let body = { + let span = field_ty.span(); + let core = span.face_core(); + + quote_spanned!(span=> + <#field_ty as #core::analysis::AbstractFeature>::slot_ref(_0) + ) + }; + + let ident = &self.ident; + let span = ident.span(); + + Some(quote_spanned!(span=> Self::#ident { #field_ident: _0, .. } => #body,)) + } + pub(super) fn compile_feature_getter(&self) -> Option { let (field_ident, field_ty) = self.semantics.as_ref()?; diff --git a/work/crates/derive/src/node/input.rs b/work/crates/derive/src/node/input.rs index 3212701..988e671 100644 --- a/work/crates/derive/src/node/input.rs +++ b/work/crates/derive/src/node/input.rs @@ -73,6 +73,7 @@ pub struct NodeInput { pub(super) generics: ParserGenerics, pub(super) token: Type, pub(super) classifier: Option, + pub(super) common: Option, pub(super) trivia: Option, pub(super) recovery: Option, pub(crate) dump: Dump, @@ -131,6 +132,7 @@ impl TryFrom for NodeInput { let mut token = None; let mut classifier = None; + let mut common = None; let mut trivia = None; let mut recovery = None; let mut dump = Dump::None; @@ -165,6 +167,14 @@ impl TryFrom for NodeInput { classifier = Some(attr.parse_args::()?); } + "semantics" => { + if common.is_some() { + return Err(error!(span, "Duplicate Semantics attribute.",)); + } + + common = Some(attr.parse_args::()?); + } + "trivia" => { if trivia.is_some() { return Err(error!(span, "Duplicate Trivia attribute.",)); @@ -562,6 +572,7 @@ impl TryFrom for NodeInput { generics, token, classifier, + common, trivia, recovery, dump, diff --git a/work/crates/derive/src/node/output.rs b/work/crates/derive/src/node/output.rs index beea6de..2eff3ba 100644 --- a/work/crates/derive/src/node/output.rs +++ b/work/crates/derive/src/node/output.rs @@ -166,6 +166,7 @@ impl NodeInput { let capacity = self.variants.len(); let mut attr_ref = Vec::with_capacity(capacity); + let mut slot_ref = Vec::with_capacity(capacity); let mut feature_getter = Vec::with_capacity(capacity); let mut feature_keys = Vec::with_capacity(capacity); @@ -175,6 +176,7 @@ impl NodeInput { } attr_ref.push(variant.inheritance.compile_attr_ref()); + slot_ref.push(variant.inheritance.compile_slot_ref()); feature_getter.push(variant.inheritance.compile_feature_getter()); feature_keys.push(variant.inheritance.compile_feature_keys()); } @@ -192,6 +194,15 @@ impl NodeInput { } } + fn slot_ref(&self) -> &#core::analysis::SlotRef { + match self { + #( #slot_ref )* + + #[allow(unreachable_patterns)] + _ => &#core::analysis::NIL_SLOT_REF, + } + } + #[allow(unused_variables)] fn feature(&self, key: #core::syntax::Key) -> #core::analysis::AnalysisResult<&dyn #core::analysis::AbstractFeature> @@ -228,6 +239,11 @@ impl NodeInput { None => quote_spanned!(span=> #core::analysis::VoidClassifier::), }; + let common = match &self.common { + Some(ty) => ty.to_token_stream(), + None => quote_spanned!(span=> #core::analysis::VoidFeature::), + }; + let (impl_generics, type_generics, where_clause) = self.generics.ty.split_for_impl(); let capacity = self.variants.len(); @@ -264,6 +280,8 @@ impl NodeInput { { type Classifier = #classifier; + type CommonSemantics = #common; + #[allow(unused_variables)] fn init< H: #core::analysis::TaskHandle, diff --git a/work/crates/examples/readme.md b/work/crates/examples/readme.md index b4936a3..d9ee037 100644 --- a/work/crates/examples/readme.md +++ b/work/crates/examples/readme.md @@ -37,8 +37,8 @@ This crate contains examples showcasing the core features of Lady Deirdre. The source code of each example is accompanied by detailed explanations and -comments in the [User Guide](https://lady-deirdre.lakhin.com/). Therefore, it is recommended to explore them -alongside the corresponding chapters of the guide. +comments in the [User Guide](https://lady-deirdre.lakhin.com/). Therefore, it is +recommended to explore them alongside the corresponding chapters of the guide. Each example is located in its own crate module within the "src" directory. The root "mod.rs" file of each module includes runnable tests that demonstrate @@ -76,6 +76,18 @@ specific features of the example. Relevant User Guide chapter: [Semantics](https://lady-deirdre.lakhin.com/semantics/semantics.html). +- [Shared Semantics](https://github.com/Eliah-Lakhin/lady-deirdre/tree/master/work/crates/examples/src/shared_semantics). + + Illustrates how to organize cross-file semantic connections. + + The source code of the files contains a set of key-value pairs, where the key + is any identifier, and the value is either a numeric value or a reference to + another identifier within the same file or a different one. This example + demonstrates resolving key values through a system of references down to their + numeric values. + + Relevant User Guide chapter: [Multi-File Analysis](https://lady-deirdre.lakhin.com/semantics/multi-file-analysis.html). + - [JSON Formatter](https://github.com/Eliah-Lakhin/lady-deirdre/tree/master/work/crates/examples/src/json_formatter). Shows how to use the source code formatter tools of Lady Deirdre to implement diff --git a/work/crates/examples/src/lib.rs b/work/crates/examples/src/lib.rs index 5a87dbb..fc13194 100644 --- a/work/crates/examples/src/lib.rs +++ b/work/crates/examples/src/lib.rs @@ -40,3 +40,4 @@ pub mod expr_parser; pub mod json_formatter; pub mod json_grammar; pub mod json_highlight; +pub mod shared_semantics; diff --git a/work/crates/examples/src/shared_semantics/lexis.rs b/work/crates/examples/src/shared_semantics/lexis.rs new file mode 100644 index 0000000..d82dcc7 --- /dev/null +++ b/work/crates/examples/src/shared_semantics/lexis.rs @@ -0,0 +1,66 @@ +//////////////////////////////////////////////////////////////////////////////// +// This file is a part of the "Lady Deirdre" work, // +// a compiler front-end foundation technology. // +// // +// This work is proprietary software with source-available code. // +// // +// To copy, use, distribute, and contribute to this work, you must agree to // +// the terms of the General License Agreement: // +// // +// https://github.com/Eliah-Lakhin/lady-deirdre/blob/master/EULA.md. // +// // +// The agreement grants you a Commercial-Limited License that gives you // +// the right to use my work in non-commercial and limited commercial products // +// with a total gross revenue cap. To remove this commercial limit for one of // +// your products, you must acquire an Unrestricted Commercial License. // +// // +// If you contribute to the source code, documentation, or related materials // +// of this work, you must assign these changes to me. Contributions are // +// governed by the "Derivative Work" section of the General License // +// Agreement. // +// // +// Copying the work in parts is strictly forbidden, except as permitted under // +// the terms of the General License Agreement. // +// // +// If you do not or cannot agree to the terms of this Agreement, // +// do not use this work. // +// // +// This work is provided "as is" without any warranties, express or implied, // +// except to the extent that such disclaimers are held to be legally invalid. // +// // +// Copyright (c) 2024 Ilya Lakhin (Илья Александрович Лахин). // +// All rights reserved. // +//////////////////////////////////////////////////////////////////////////////// + +use lady_deirdre::lexis::Token; + +#[derive(Token, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum SharedSemanticsToken { + EOI = 0, + + Mismatch = 1, + + #[rule(['a'..'z'] ['a'..'z', '0'..'9', '_']*)] + #[describe("ident")] + Ident, + + #[rule(['0'..'9']+)] + #[describe("number")] + Num, + + #[rule("=")] + #[describe("=")] + Assign, + + #[rule("::")] + #[describe("::")] + DoubleColon, + + #[rule(";")] + #[describe(";")] + Semicolon, + + #[rule([' ', '\t', '\n', '\x0c', '\r']+)] + Whitespace, +} diff --git a/work/crates/examples/src/shared_semantics/mod.rs b/work/crates/examples/src/shared_semantics/mod.rs new file mode 100644 index 0000000..a25d94f --- /dev/null +++ b/work/crates/examples/src/shared_semantics/mod.rs @@ -0,0 +1,173 @@ +//////////////////////////////////////////////////////////////////////////////// +// This file is a part of the "Lady Deirdre" work, // +// a compiler front-end foundation technology. // +// // +// This work is proprietary software with source-available code. // +// // +// To copy, use, distribute, and contribute to this work, you must agree to // +// the terms of the General License Agreement: // +// // +// https://github.com/Eliah-Lakhin/lady-deirdre/blob/master/EULA.md. // +// // +// The agreement grants you a Commercial-Limited License that gives you // +// the right to use my work in non-commercial and limited commercial products // +// with a total gross revenue cap. To remove this commercial limit for one of // +// your products, you must acquire an Unrestricted Commercial License. // +// // +// If you contribute to the source code, documentation, or related materials // +// of this work, you must assign these changes to me. Contributions are // +// governed by the "Derivative Work" section of the General License // +// Agreement. // +// // +// Copying the work in parts is strictly forbidden, except as permitted under // +// the terms of the General License Agreement. // +// // +// If you do not or cannot agree to the terms of this Agreement, // +// do not use this work. // +// // +// This work is provided "as is" without any warranties, express or implied, // +// except to the extent that such disclaimers are held to be legally invalid. // +// // +// Copyright (c) 2024 Ilya Lakhin (Илья Александрович Лахин). // +// All rights reserved. // +//////////////////////////////////////////////////////////////////////////////// + +pub mod lexis; +pub mod semantics; +pub mod syntax; + +#[cfg(test)] +mod tests { + use std::{ + fmt::{Display, Formatter}, + ops::Deref, + }; + + use lady_deirdre::{ + analysis::{ + AbstractTask, + AnalysisTask, + Analyzer, + AnalyzerConfig, + MutationAccess, + TriggerHandle, + }, + arena::Identifiable, + format::{AnnotationPriority, SnippetFormatter}, + syntax::{PolyRef, SyntaxTree}, + }; + + use crate::shared_semantics::{semantics::KeyResolution, syntax::SharedSemanticsNode}; + + #[test] + fn test_multi_modules() { + let analyzer = Analyzer::::new(AnalyzerConfig::default()); + + { + let handle = TriggerHandle::new(); + + let mut task = analyzer.mutate(&handle, 1).unwrap(); + + let doc_id = task.add_mutable_doc("x = 10; y = module_2::b; z = module_2::c;"); + + doc_id.set_name("module_1"); + + task.common() + .modules + .mutate(&task, |modules| { + let _ = modules.insert(String::from("module_1"), doc_id); + + true + }) + .unwrap(); + } + + { + let handle = TriggerHandle::new(); + + let mut task = analyzer.mutate(&handle, 1).unwrap(); + + let doc_id = task.add_mutable_doc("a = module_1::x; b = module_2::c; c = 20;"); + + doc_id.set_name("module_2"); + + task.common() + .modules + .mutate(&task, |modules| { + let _ = modules.insert(String::from("module_2"), doc_id); + + true + }) + .unwrap(); + } + + { + let handle = TriggerHandle::new(); + let task = analyzer.analyze(&handle, 1).unwrap(); + println!("{:#}", DisplayModules(&task)); + } + } + + struct DisplayModules<'a>(&'a AnalysisTask<'a, SharedSemanticsNode>); + + impl<'a> Display for DisplayModules<'a> { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + let (_, modules) = self.0.common().modules.snapshot(self.0).unwrap(); + + for doc_id in modules.values() { + let doc_read = self.0.read_doc(*doc_id).unwrap(); + let doc = doc_read.deref(); + + let mut snippet = formatter.snippet(doc); + + snippet.set_caption(doc.id().name()); + + let SharedSemanticsNode::Root { defs, .. } = doc.root() else { + unreachable!("Malformed root"); + }; + + for def_ref in defs { + let Some(SharedSemanticsNode::Def { key, .. }) = def_ref.deref(doc) else { + continue; + }; + + let Some(SharedSemanticsNode::Key { + token, semantics, .. + }) = key.deref(doc) + else { + continue; + }; + + let Some(span) = token.span(doc) else { + continue; + }; + + let (_, resolution) = semantics + .get() + .unwrap() + .resolution + .snapshot(self.0) + .unwrap(); + + match resolution { + KeyResolution::Unresolved => { + snippet.annotate(span, AnnotationPriority::Default, "unresolved"); + } + KeyResolution::Recusrive => { + snippet.annotate(span, AnnotationPriority::Default, "recusrive"); + } + KeyResolution::Number(value) => { + snippet.annotate(span, AnnotationPriority::Default, format!("{value}")); + } + }; + } + + snippet.finish()?; + drop(snippet); + formatter.write_str("\n")?; + } + + Ok(()) + } + } +} diff --git a/work/crates/examples/src/shared_semantics/semantics.rs b/work/crates/examples/src/shared_semantics/semantics.rs new file mode 100644 index 0000000..00e44fe --- /dev/null +++ b/work/crates/examples/src/shared_semantics/semantics.rs @@ -0,0 +1,263 @@ +//////////////////////////////////////////////////////////////////////////////// +// This file is a part of the "Lady Deirdre" work, // +// a compiler front-end foundation technology. // +// // +// This work is proprietary software with source-available code. // +// // +// To copy, use, distribute, and contribute to this work, you must agree to // +// the terms of the General License Agreement: // +// // +// https://github.com/Eliah-Lakhin/lady-deirdre/blob/master/EULA.md. // +// // +// The agreement grants you a Commercial-Limited License that gives you // +// the right to use my work in non-commercial and limited commercial products // +// with a total gross revenue cap. To remove this commercial limit for one of // +// your products, you must acquire an Unrestricted Commercial License. // +// // +// If you contribute to the source code, documentation, or related materials // +// of this work, you must assign these changes to me. Contributions are // +// governed by the "Derivative Work" section of the General License // +// Agreement. // +// // +// Copying the work in parts is strictly forbidden, except as permitted under // +// the terms of the General License Agreement. // +// // +// If you do not or cannot agree to the terms of this Agreement, // +// do not use this work. // +// // +// This work is provided "as is" without any warranties, express or implied, // +// except to the extent that such disclaimers are held to be legally invalid. // +// // +// Copyright (c) 2024 Ilya Lakhin (Илья Александрович Лахин). // +// All rights reserved. // +//////////////////////////////////////////////////////////////////////////////// + +use std::{ + any::{type_name, Any}, + collections::{HashMap, HashSet}, + ops::Deref, +}; + +use lady_deirdre::{ + analysis::{ + AnalysisResult, + AnalysisResultEx, + Attr, + AttrContext, + Computable, + Feature, + Slot, + TaskHandle, + }, + arena::{Id, Identifiable}, + sync::SyncBuildHasher, + syntax::{NodeRef, PolyRef, SyntaxTree}, +}; +use log::debug; + +use crate::shared_semantics::syntax::SharedSemanticsNode; + +#[derive(Feature)] +#[node(SharedSemanticsNode)] +pub struct CommonSemantics { + pub modules: Slot>, +} + +#[derive(Feature)] +#[node(SharedSemanticsNode)] +pub struct ModuleSemantics { + #[scoped] + pub tables: Attr, +} + +#[derive(Default, Clone, PartialEq, Eq)] +pub struct ModuleTables { + pub key_table: HashMap, + pub def_table: HashMap, +} + +impl Computable for ModuleTables { + type Node = SharedSemanticsNode; + + fn compute( + context: &mut AttrContext, + ) -> AnalysisResult { + log_attr::(context)?; + + let root_ref = context.node_ref(); + let doc_read = context.read_doc(root_ref.id).unwrap_abnormal()?; + let doc = doc_read.deref(); + + let Some(SharedSemanticsNode::Root { defs, .. }) = root_ref.deref(doc) else { + return Ok(Self::default()); + }; + + let mut key_table = HashMap::new(); + let mut def_table = HashMap::new(); + + for def_ref in defs { + let Some(SharedSemanticsNode::Def { key, value, .. }) = def_ref.deref(doc) else { + continue; + }; + + let Some(SharedSemanticsNode::Key { token, .. }) = key.deref(doc) else { + continue; + }; + + let Some(key_string) = token.string(doc) else { + continue; + }; + + let _ = key_table.insert(*key, key_string.to_string()); + + match value.deref(doc) { + Some(SharedSemanticsNode::Num { token, .. }) => { + let Some(num_string) = token.string(doc) else { + continue; + }; + + let Ok(value) = num_string.parse::() else { + continue; + }; + + let _ = def_table.insert(key_string.to_string(), Definition::Num { value }); + } + + Some(SharedSemanticsNode::Ref { module, ident, .. }) => { + let Some(module_string) = module.string(doc) else { + continue; + }; + + let Some(ident_string) = ident.string(doc) else { + continue; + }; + + let _ = def_table.insert( + key_string.to_string(), + Definition::Ref { + module: module_string.to_string(), + key: ident_string.to_string(), + }, + ); + } + + _ => continue, + } + } + + Ok(Self { + key_table, + def_table, + }) + } +} + +#[derive(Clone, PartialEq, Eq)] +pub enum Definition { + Num { value: usize }, + Ref { module: String, key: String }, +} + +#[derive(Feature)] +#[node(SharedSemanticsNode)] +pub struct KeySemantics { + pub resolution: Attr, +} + +#[derive(Clone, PartialEq, Eq)] +pub enum KeyResolution { + Unresolved, + Recusrive, + Number(usize), +} + +impl Computable for KeyResolution { + type Node = SharedSemanticsNode; + + fn compute( + context: &mut AttrContext, + ) -> AnalysisResult { + log_attr::(context)?; + + let key_ref = context.node_ref(); + let doc_read = context.read_doc(key_ref.id).unwrap_abnormal()?; + let module_doc = doc_read.deref(); + let root_ref = module_doc.root_node_ref(); + + let Some(SharedSemanticsNode::Root { semantics, .. }) = root_ref.deref(module_doc) else { + return Ok(Self::Unresolved); + }; + + let module_semantics = semantics.get().unwrap_abnormal()?; + let module_tables = module_semantics.tables.read(context).unwrap_abnormal()?; + + let Some(key) = module_tables.key_table.get(key_ref) else { + return Ok(Self::Unresolved); + }; + + let (ref_module_name, mut ref_key) = match module_tables.def_table.get(key) { + Some(Definition::Num { value }) => return Ok(Self::Number(*value)), + Some(Definition::Ref { module, key }) => (module, key.clone()), + None => return Ok(Self::Unresolved), + }; + + let mut trace = HashSet::new(); + + let _ = trace.insert((module_doc.id(), key.clone())); + + let modules = context.common().modules.read(context).unwrap_abnormal()?; + + let Some(mut ref_module_id) = modules.get(ref_module_name).copied() else { + return Ok(Self::Unresolved); + }; + + loop { + if !trace.insert((ref_module_id, ref_key.clone())) { + return Ok(Self::Recusrive); + } + + let doc_read = context.read_doc(ref_module_id).unwrap_abnormal()?; + let module_doc = doc_read.deref(); + let root_ref = module_doc.root_node_ref(); + + let Some(SharedSemanticsNode::Root { semantics, .. }) = root_ref.deref(module_doc) + else { + return Ok(Self::Unresolved); + }; + + let module_semantics = semantics.get().unwrap_abnormal()?; + let module_tables = module_semantics.tables.read(context).unwrap_abnormal()?; + + let ref_module_name = match module_tables.def_table.get(&ref_key) { + Some(Definition::Num { value }) => return Ok(Self::Number(*value)), + Some(Definition::Ref { module, key }) => { + ref_key = key.clone(); + + module + } + None => return Ok(Self::Unresolved), + }; + + let modules = context.common().modules.read(context).unwrap_abnormal()?; + + match modules.get(ref_module_name) { + Some(id) => ref_module_id = *id, + None => return Ok(Self::Unresolved), + }; + } + } +} + +fn log_attr( + context: &mut AttrContext, +) -> AnalysisResult<()> { + let node_ref = context.node_ref(); + let doc_read = context.read_doc(node_ref.id).unwrap_abnormal()?; + + let name = type_name::(); + let display = context.node_ref().display(doc_read.deref()); + + debug!("Computing {name}.\n{display:#}"); + + Ok(()) +} diff --git a/work/crates/examples/src/shared_semantics/syntax.rs b/work/crates/examples/src/shared_semantics/syntax.rs new file mode 100644 index 0000000..d038afe --- /dev/null +++ b/work/crates/examples/src/shared_semantics/syntax.rs @@ -0,0 +1,115 @@ +//////////////////////////////////////////////////////////////////////////////// +// This file is a part of the "Lady Deirdre" work, // +// a compiler front-end foundation technology. // +// // +// This work is proprietary software with source-available code. // +// // +// To copy, use, distribute, and contribute to this work, you must agree to // +// the terms of the General License Agreement: // +// // +// https://github.com/Eliah-Lakhin/lady-deirdre/blob/master/EULA.md. // +// // +// The agreement grants you a Commercial-Limited License that gives you // +// the right to use my work in non-commercial and limited commercial products // +// with a total gross revenue cap. To remove this commercial limit for one of // +// your products, you must acquire an Unrestricted Commercial License. // +// // +// If you contribute to the source code, documentation, or related materials // +// of this work, you must assign these changes to me. Contributions are // +// governed by the "Derivative Work" section of the General License // +// Agreement. // +// // +// Copying the work in parts is strictly forbidden, except as permitted under // +// the terms of the General License Agreement. // +// // +// If you do not or cannot agree to the terms of this Agreement, // +// do not use this work. // +// // +// This work is provided "as is" without any warranties, express or implied, // +// except to the extent that such disclaimers are held to be legally invalid. // +// // +// Copyright (c) 2024 Ilya Lakhin (Илья Александрович Лахин). // +// All rights reserved. // +//////////////////////////////////////////////////////////////////////////////// + +use lady_deirdre::{ + analysis::{Semantics, VoidFeature}, + lexis::TokenRef, + syntax::{Node, NodeRef}, +}; + +use crate::shared_semantics::{ + lexis::SharedSemanticsToken, + semantics::{CommonSemantics, KeySemantics, ModuleSemantics}, +}; + +#[derive(Node)] +#[token(SharedSemanticsToken)] +#[trivia($Whitespace)] +#[semantics(CommonSemantics)] +pub enum SharedSemanticsNode { + #[root] + #[rule(defs: Def*)] + Root { + #[node] + node: NodeRef, + #[parent] + parent: NodeRef, + #[child] + defs: Vec, + #[semantics] + semantics: Semantics, + }, + + #[rule(key: Key $Assign value: (Ref | Num) $Semicolon)] + Def { + #[node] + node: NodeRef, + #[parent] + parent: NodeRef, + #[child] + key: NodeRef, + #[child] + value: NodeRef, + #[semantics] + semantics: Semantics>, + }, + + #[rule(token: $Ident)] + Key { + #[node] + node: NodeRef, + #[parent] + parent: NodeRef, + #[child] + token: TokenRef, + #[semantics] + semantics: Semantics, + }, + + #[rule(module: $Ident $DoubleColon ident: $Ident)] + Ref { + #[node] + node: NodeRef, + #[parent] + parent: NodeRef, + #[child] + module: TokenRef, + #[child] + ident: TokenRef, + #[semantics] + semantics: Semantics>, + }, + + #[rule(token: $Num)] + Num { + #[node] + node: NodeRef, + #[parent] + parent: NodeRef, + #[child] + token: TokenRef, + #[semantics] + semantics: Semantics>, + }, +} diff --git a/work/crates/main/src/analysis/analyzer.rs b/work/crates/main/src/analysis/analyzer.rs index 0606de9..7de5451 100644 --- a/work/crates/main/src/analysis/analyzer.rs +++ b/work/crates/main/src/analysis/analyzer.rs @@ -32,18 +32,25 @@ // All rights reserved. // //////////////////////////////////////////////////////////////////////////////// -use std::{collections::HashMap, hash::RandomState, sync::Arc, time::Duration}; +use std::{ + collections::HashMap, + hash::RandomState, + sync::{Arc, Weak}, + time::Duration, +}; use crate::{ analysis::{ - database::Database, + database::{Database, DocRecords}, entry::DocEntry, manager::{TaskKind, TaskManager}, AnalysisResult, AnalysisTask, Event, ExclusiveTask, + Feature, Grammar, + Initializer, MutationTask, Revision, TaskHandle, @@ -52,6 +59,7 @@ use crate::{ }, arena::Id, sync::{SyncBuildHasher, Table}, + syntax::NodeRef, }; /// An initial configuration of the [Analyzer]. @@ -639,6 +647,7 @@ impl AnalyzerConfig { /// if the specified access cannot be granted instantly. pub struct Analyzer { pub(super) docs: Table, S>, + pub(super) common: N::CommonSemantics, pub(super) events: Table, S>, pub(super) db: Arc>, pub(super) tasks: TaskManager, @@ -656,17 +665,46 @@ impl Analyzer { /// /// Initially, the Analyzer does not hold any document. pub fn new(config: AnalyzerConfig) -> Self { + let docs = match config.single_document { + true => Table::with_capacity_and_hasher_and_shards(1, S::default(), 1), + false => Table::new(), + }; + + let events = match config.single_document { + true => Table::with_capacity_and_hasher_and_shards(1, S::default(), 1), + false => Table::with_capacity_and_hasher_and_shards(1, S::default(), 1), + }; + + let db = Arc::new(Database::new(&config)); + + let mut common = ::new(NodeRef::nil()); + + { + let mut records = DocRecords::new(); + + let mut initializer: Initializer<'_, N, H, S> = Initializer { + id: Id::nil(), + database: Arc::downgrade(&db) as Weak<_>, + records: &mut records, + inserts: false, + }; + + common.init(&mut initializer); + + if initializer.inserts { + let _ = db.commit_revision(); + db.records.insert(Id::nil(), records); + } + } + + let tasks = TaskManager::new(); + Self { - docs: match config.single_document { - true => Table::with_capacity_and_hasher_and_shards(1, S::default(), 1), - false => Table::new(), - }, - events: match config.single_document { - true => Table::with_capacity_and_hasher_and_shards(1, S::default(), 1), - false => Table::with_capacity_and_hasher_and_shards(1, S::default(), 1), - }, - db: Arc::new(Database::new(&config)), - tasks: TaskManager::new(), + docs, + common, + events, + db, + tasks, } } diff --git a/work/crates/main/src/analysis/attribute.rs b/work/crates/main/src/analysis/attribute.rs index e76818d..202f359 100644 --- a/work/crates/main/src/analysis/attribute.rs +++ b/work/crates/main/src/analysis/attribute.rs @@ -58,7 +58,9 @@ use crate::{ MutationAccess, Revision, SemanticAccess, + SlotRef, TaskHandle, + NIL_SLOT_REF, }, arena::{Entry, Id, Identifiable}, sync::SyncBuildHasher, @@ -102,6 +104,9 @@ pub static NIL_ATTR_REF: AttrRef = AttrRef::nil(); /// /// The [AttrRef] can be obtained using the [AsRef] and the [Feature] /// implementations of the Attr. +/// +/// See also [Slot](crate::analysis::Slot), a specialized version of an +/// attribute that enables manual control over the attribute's value. #[repr(transparent)] pub struct Attr { inner: AttrInner, @@ -192,6 +197,11 @@ impl AbstractFeature for Attr { self.as_ref() } + #[inline(always)] + fn slot_ref(&self) -> &SlotRef { + &NIL_SLOT_REF + } + #[inline(always)] fn feature(&self, _key: Key) -> AnalysisResult<&dyn AbstractFeature> { Err(AnalysisError::MissingFeature) @@ -261,7 +271,7 @@ impl Attr { /// /// Returns a pair of two elements: /// 1. The [revision](Revision) under which the attribute's value has been - /// computed. + /// computed. /// 2. A copy of the attribute's value. /// /// This function is supposed to be called **outside** of @@ -379,6 +389,9 @@ impl Attr { pub struct AttrRef { /// An identifier of the document managed by the Analyzer to which /// the attribute belongs. + /// + /// If the attribute belongs to the + /// [common semantics](Grammar::CommonSemantics), this value is [Id::nil]. pub id: Id, /// A versioned index of the attribute instance within the Analyzer's inner @@ -389,12 +402,15 @@ pub struct AttrRef { impl Debug for AttrRef { #[inline] fn fmt(&self, formatter: &mut Formatter) -> std::fmt::Result { - match self.is_nil() { - false => formatter.write_fmt(format_args!( - "AttrRef(id: {:?}, entry: {:?})", + match (self.id.is_nil(), self.entry.is_nil()) { + (false, _) => formatter.write_fmt(format_args!( + "Attr(id: {:?}, entry: {:?})", self.id, self.entry, )), - true => formatter.write_str("AttrRef(Nil)"), + + (true, false) => formatter.write_fmt(format_args!("Attr(entry: {:?})", self.entry)), + + (true, true) => formatter.write_str("Attr(Nil)"), } } } @@ -431,7 +447,7 @@ impl AttrRef { /// attribute within any Analyzer. #[inline(always)] pub const fn is_nil(&self) -> bool { - self.id.is_nil() || self.entry.is_nil() + self.id.is_nil() && self.entry.is_nil() } /// Requests a copy of the attribute's value. @@ -524,7 +540,7 @@ impl AttrRef { } }; - let Some(record) = records.get(&self.entry) else { + let Some(record) = records.attrs.get(&self.entry) else { return; }; @@ -549,7 +565,7 @@ impl AttrRef { return false; }; - records.contains(&self.entry) + records.attrs.contains(&self.entry) } } diff --git a/work/crates/main/src/analysis/compute.rs b/work/crates/main/src/analysis/compute.rs index 89864c9..6292bb2 100644 --- a/work/crates/main/src/analysis/compute.rs +++ b/work/crates/main/src/analysis/compute.rs @@ -42,7 +42,8 @@ use std::{ use crate::{ analysis::{ - database::{CacheDeps, Record, RecordCache, RecordReadGuard}, + database::{AttrRecordCache, AttrRecordData, CacheDeps, DocRecords, SlotRecordData}, + lock::TimeoutRwLockReadGuard, AnalysisError, AnalysisResult, Analyzer, @@ -51,13 +52,15 @@ use crate::{ DocumentReadGuard, Event, Grammar, + MutationAccess, Revision, + SlotRef, TaskHandle, TriggerHandle, DOC_REMOVED_EVENT, DOC_UPDATED_EVENT, }, - arena::{Id, Repo}, + arena::Id, report::ld_unreachable, sync::{Shared, SyncBuildHasher, TableReadGuard}, syntax::{NodeRef, NIL_NODE_REF}, @@ -318,6 +321,20 @@ impl<'a, N: Grammar, H: TaskHandle, S: SyncBuildHasher> AttrContext<'a, N, H, S> let _ = self.deps.events.insert((id, event)); } + /// Provides access to the Analyzer's + /// [common semantics](Grammar::CommonSemantics), a special semantic + /// feature that is instantiated during the Analyzer's creation. It does + /// not belong to any specific document and is common across the entire + /// Analyzer. + /// + /// If the Analyzer's grammar does not specify common semantics, this + /// function returns a reference to the + /// [VoidFeature](crate::analysis::VoidFeature). + #[inline(always)] + pub fn common(&self) -> &'a N::CommonSemantics { + &self.analyzer.common + } + /// Returns Ok if the underlying task has not been /// [signaled](TaskHandle::is_triggered) for graceful shutdown yet; /// otherwise returns an [Interrupted](AnalysisError::Interrupted) error. @@ -353,10 +370,15 @@ impl<'a, N: Grammar, H: TaskHandle, S: SyncBuildHasher> AttrContext<'a, N, H, S> } #[inline(always)] - pub(super) fn track(&mut self, dep: &AttrRef) { + pub(super) fn track_attr(&mut self, dep: &AttrRef) { let _ = self.deps.attrs.insert(*dep); } + #[inline(always)] + pub(super) fn track_slot(&mut self, dep: &SlotRef) { + let _ = self.deps.slots.insert(*dep); + } + #[inline(always)] pub(super) fn into_deps(self) -> Shared> { Shared::new(self.deps) @@ -381,8 +403,9 @@ pub struct AttrReadGuard< S: SyncBuildHasher = RandomState, > { pub(super) data: &'a C, - pub(super) cell_guard: RecordReadGuard<'a, ::Node, H, S>, - pub(super) records_guard: TableReadGuard<'a, Id, Repo>, S>, + pub(super) cell_guard: + TimeoutRwLockReadGuard<'a, AttrRecordData<::Node, H, S>>, + pub(super) records_guard: TableReadGuard<'a, Id, DocRecords, S>, } impl<'a, C: Computable + Debug, H: TaskHandle, S: SyncBuildHasher> Debug @@ -440,7 +463,7 @@ impl AttrRef { return Err(AnalysisError::MissingDocument); }; - let Some(record) = records_guard.get(&self.entry) else { + let Some(record) = records_guard.attrs.get(&self.entry) else { return Err(AnalysisError::MissingAttribute); }; @@ -455,7 +478,7 @@ impl AttrRef { false => unsafe { cache.downcast_unchecked::() }, }; - context.track(self); + context.track_attr(self); // Safety: Prolongs lifetime to Analyzer's lifetime. // The reference will ve valid for as long as the parent guard is held. @@ -465,8 +488,11 @@ impl AttrRef { // The guard will ve valid for as long as the parent guard is held. let cell_guard = unsafe { transmute::< - RecordReadGuard<::Node, H, S>, - RecordReadGuard<'a, ::Node, H, S>, + TimeoutRwLockReadGuard::Node, H, S>>, + TimeoutRwLockReadGuard< + 'a, + AttrRecordData<::Node, H, S>, + >, >(record_read_guard) }; @@ -474,8 +500,8 @@ impl AttrRef { // The reference will ve valid for as long as the Analyzer is held. let records_guard = unsafe { transmute::< - TableReadGuard::Node, H, S>>, S>, - TableReadGuard<'a, Id, Repo::Node, H, S>>, S>, + TableReadGuard::Node, H, S>, S>, + TableReadGuard<'a, Id, DocRecords<::Node, H, S>, S>, >(records_guard) }; @@ -503,7 +529,7 @@ impl AttrRef { return Err(AnalysisError::MissingDocument); }; - let Some(record) = records.get(&self.entry) else { + let Some(record) = records.attrs.get(&self.entry) else { return Err(AnalysisError::MissingAttribute); }; @@ -524,7 +550,7 @@ impl AttrRef { let memo = record_data.function.invoke(&mut forked)?; let deps = forked.into_deps(); - record_data.cache = Some(RecordCache { + record_data.cache = Some(AttrRecordCache { dirty: false, updated_at: context.revision, memo, @@ -574,6 +600,27 @@ impl AttrRef { } } + if !cache.dirty && !cache.deps.as_ref().slots.is_empty() { + for slot_ref in &cache.deps.as_ref().slots { + let Some(dep_records) = context.analyzer.db.records.get(&slot_ref.id) else { + cache.dirty = true; + break; + }; + + let Some(dep_record) = dep_records.slots.get(&slot_ref.entry) else { + cache.dirty = true; + break; + }; + + let dep_record_read_guard = dep_record.read(&context.analyzer.db.timeout)?; + + if dep_record_read_guard.revision > record_data.verified_at { + cache.dirty = true; + break; + } + } + } + if !cache.dirty && !cache.deps.as_ref().attrs.is_empty() { let mut deps_verified = true; @@ -583,7 +630,7 @@ impl AttrRef { break; }; - let Some(dep_record) = dep_records.get(&attr_ref.entry) else { + let Some(dep_record) = dep_records.attrs.get(&attr_ref.entry) else { cache.dirty = true; break; }; @@ -639,7 +686,7 @@ impl AttrRef { let new_deps = forked.into_deps(); // Safety: New and previous values produced by the same Cell function. - let same = unsafe { cache.memo.memo_eq(new_memo.as_ref()) }; + let same = unsafe { cache.memo.attr_memo_eq(new_memo.as_ref()) }; cache.dirty = false; cache.memo = new_memo; @@ -655,3 +702,172 @@ impl AttrRef { } } } + +/// A RAII guard that provides read-only access to +/// the [slot](crate::analysis::Slot)'s value. +/// +/// The underlying value can be accessed through the [Deref] implementation of +/// this object. +/// +/// This object is created by the [Slot::read](crate::analysis::Slot::read) and +/// the [SlotRef::read] functions and can practically be obtain from +/// the attribute computation context only. +// Safety: Entries order reflects guards drop semantics. +#[allow(dead_code)] +pub struct SlotReadGuard< + 'a, + T: Default + Send + Sync + 'static, + N: Grammar, + H: TaskHandle = TriggerHandle, + S: SyncBuildHasher = RandomState, +> { + pub(super) revision: Revision, + pub(super) data: &'a T, + pub(super) cell_guard: TimeoutRwLockReadGuard<'a, SlotRecordData>, + pub(super) records_guard: TableReadGuard<'a, Id, DocRecords, S>, +} + +impl<'a, T, N, H, S> Debug for SlotReadGuard<'a, T, N, H, S> +where + T: Debug + Default + Send + Sync + 'static, + N: Grammar, + H: TaskHandle, + S: SyncBuildHasher, +{ + #[inline(always)] + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self.data, formatter) + } +} + +impl<'a, T, N, H, S> Display for SlotReadGuard<'a, T, N, H, S> +where + T: Display + Default + Send + Sync + 'static, + N: Grammar, + H: TaskHandle, + S: SyncBuildHasher, +{ + #[inline(always)] + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(self.data, formatter) + } +} + +impl<'a, T, N, H, S> Deref for SlotReadGuard<'a, T, N, H, S> +where + T: Default + Send + Sync + 'static, + N: Grammar, + H: TaskHandle, + S: SyncBuildHasher, +{ + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + self.data + } +} + +impl SlotRef { + // Safety: If `CHECK == false` then `T` properly describes underlying slot's computable data. + pub(super) unsafe fn fetch< + 'a, + const CHECK: bool, + T: Default + Send + Sync + 'static, + N: Grammar, + H: TaskHandle, + S: SyncBuildHasher, + >( + &self, + context: &mut AttrContext<'a, N, H, S>, + ) -> AnalysisResult> { + let Some(records_guard) = context.analyzer.db.records.get(&self.id) else { + return Err(AnalysisError::MissingDocument); + }; + + let Some(record) = records_guard.slots.get(&self.entry) else { + return Err(AnalysisError::MissingSlot); + }; + + let record_read_guard = record.read(&context.analyzer.db.timeout)?; + + let data = match CHECK { + true => record_read_guard.downcast::()?, + + // Safety: Upheld by the caller. + false => unsafe { record_read_guard.downcast_unchecked::() }, + }; + + context.track_slot(self); + + let revision = record_read_guard.revision; + + // Safety: Prolongs lifetime to Analyzer's lifetime. + // The reference will ve valid for as long as the parent guard is held. + let data = unsafe { transmute::<&T, &'a T>(data) }; + + // Safety: Prolongs lifetime to Analyzer's lifetime. + // The guard will ve valid for as long as the parent guard is held. + let cell_guard = unsafe { + transmute::< + TimeoutRwLockReadGuard, + TimeoutRwLockReadGuard<'a, SlotRecordData>, + >(record_read_guard) + }; + + // Safety: Prolongs lifetime to Analyzer's lifetime. + // The reference will ve valid for as long as the Analyzer is held. + let records_guard = unsafe { + transmute::< + TableReadGuard, S>, + TableReadGuard<'a, Id, DocRecords, S>, + >(records_guard) + }; + + Ok(SlotReadGuard { + revision, + data, + cell_guard, + records_guard, + }) + } + + // Safety: If `CHECK == false` then `T` properly describes underlying slot's computable data. + pub(super) unsafe fn change< + 'a, + const CHECK: bool, + T: Default + Send + Sync + 'static, + N: Grammar, + H: TaskHandle, + S: SyncBuildHasher, + >( + &self, + task: &impl MutationAccess, + map: impl FnOnce(&mut T) -> bool, + ) -> AnalysisResult<()> { + let Some(records_guard) = task.analyzer().db.records.get(&self.id) else { + return Err(AnalysisError::MissingDocument); + }; + + let Some(record) = records_guard.slots.get(&self.entry) else { + return Err(AnalysisError::MissingSlot); + }; + + let mut record_write_guard = record.write(&task.analyzer().db.timeout)?; + + let data = match CHECK { + true => record_write_guard.downcast_mut::()?, + + // Safety: Upheld by the caller. + false => unsafe { record_write_guard.downcast_unchecked_mut::() }, + }; + + let mutated = map(data); + + if mutated { + record_write_guard.revision = task.analyzer().db.commit_revision(); + } + + Ok(()) + } +} diff --git a/work/crates/main/src/analysis/database.rs b/work/crates/main/src/analysis/database.rs index 3e2bb2d..b1dda9d 100644 --- a/work/crates/main/src/analysis/database.rs +++ b/work/crates/main/src/analysis/database.rs @@ -34,19 +34,15 @@ use std::{ any::TypeId, - cell::UnsafeCell, collections::HashSet, ops::{Deref, DerefMut}, - sync::{ - atomic::{AtomicU64, Ordering}, - Condvar, - Mutex, - }, - time::{Duration, Instant}, + sync::atomic::{AtomicU64, Ordering}, + time::Duration, }; use crate::{ analysis::{ + lock::TimeoutRwLock, AnalysisError, AnalysisResult, AnalyzerConfig, @@ -56,10 +52,11 @@ use crate::{ Computable, Event, Grammar, + SlotRef, TaskHandle, }, arena::{Entry, Id, Repo}, - report::{ld_assert, ld_unreachable}, + report::ld_unreachable, sync::{Shared, SyncBuildHasher, Table}, syntax::NodeRef, }; @@ -71,7 +68,7 @@ use crate::{ pub type Revision = u64; pub(super) struct Database { - pub(super) records: Table>, S>, + pub(super) records: Table, S>, pub(super) timeout: Duration, pub(super) revision: AtomicU64, } @@ -110,35 +107,36 @@ impl AbstractDatabase for Databas return; }; - records_guard.remove(entry); + records_guard.attrs.remove(entry); } } -const UNLOCK_MASK: usize = 0; -const READ_MASK: usize = !0 ^ 1; -const READ_BIT: usize = 1 << 1; -const WRITE_MASK: usize = 1; - -pub(super) struct Record { - state: Mutex, - state_changed: Condvar, - data: UnsafeCell>, +pub(super) struct DocRecords { + pub(super) attrs: Repo>, + pub(super) slots: Repo, } -unsafe impl Send for Record {} - -unsafe impl Sync for Record {} +impl DocRecords { + #[inline(always)] + pub(super) fn new() -> Self { + Self { + attrs: Repo::new(), + slots: Repo::new(), + } + } -impl Record { #[inline(always)] - pub(super) fn new + Eq>(node_ref: NodeRef) -> Self { + pub(super) fn with_capacity(attrs_capacity: usize) -> Self { Self { - state: Mutex::new(UNLOCK_MASK), - state_changed: Condvar::new(), - data: UnsafeCell::new(RecordData::new::(node_ref)), + attrs: Repo::with_capacity(attrs_capacity), + slots: Repo::new(), } } +} + +pub(super) type AttrRecord = TimeoutRwLock>; +impl AttrRecord { #[inline(always)] pub(super) fn invalidate(&self) { let Ok(mut guard) = self.write(&Duration::ZERO) else { @@ -151,269 +149,201 @@ impl Record { cache.dirty = true; } +} - #[cfg(not(target_family = "wasm"))] - pub(super) fn read(&self, timeout: &Duration) -> AnalysisResult> { - let mut state_guard = self - .state - .lock() - .unwrap_or_else(|poison| poison.into_inner()); - - let mut cooldown = 1; - let time = Instant::now(); - - loop { - if *state_guard & WRITE_MASK == 0 { - *state_guard += READ_BIT; - return Ok(RecordReadGuard { record: self }); - } - - (state_guard, _) = self - .state_changed - .wait_timeout(state_guard, Duration::from_millis(cooldown)) - .unwrap_or_else(|poison| poison.into_inner()); - - if &time.elapsed() > timeout { - return Err(AnalysisError::Timeout); - } +pub(super) struct AttrRecordData { + pub(super) verified_at: Revision, + pub(super) cache: Option>, + pub(super) node_ref: NodeRef, + pub(super) function: &'static dyn Function, +} - cooldown <<= 1; +impl AttrRecordData { + #[inline(always)] + pub(super) fn new + Eq>(node_ref: NodeRef) -> Self { + Self { + verified_at: 0, + cache: None, + node_ref, + function: &(C::compute as fn(&mut AttrContext) -> AnalysisResult), } } +} + +pub(super) struct AttrRecordCache { + pub(super) dirty: bool, + pub(super) updated_at: Revision, + pub(super) memo: Box, + pub(super) deps: Shared>, +} - #[cfg(target_family = "wasm")] - pub(super) fn read(&self, timeout: &Duration) -> AnalysisResult> { - let mut state_guard = self - .state - .lock() - .unwrap_or_else(|poison| poison.into_inner()); +impl AttrRecordCache { + #[inline(always)] + pub(super) fn downcast(&self) -> AnalysisResult<&T> { + let memo = self.memo.deref(); - if *state_guard & WRITE_MASK == 0 { - *state_guard += READ_BIT; - return Ok(RecordReadGuard { record: self }); + if memo.attr_memo_type_id() != TypeId::of::() { + return Err(AnalysisError::TypeMismatch); } - Err(AnalysisError::Timeout) + // Safety: Type checked above. + Ok(unsafe { &*(memo as *const dyn AttrMemo as *const T) }) } - #[cfg(not(target_family = "wasm"))] - pub(super) fn write(&self, timeout: &Duration) -> AnalysisResult> { - let mut state_guard = self - .state - .lock() - .unwrap_or_else(|poison| poison.into_inner()); - - let mut culldown = 1; - let time = Instant::now(); - - loop { - if *state_guard == UNLOCK_MASK { - *state_guard = WRITE_MASK; - return Ok(RecordWriteGuard { record: self }); - } - - (state_guard, _) = self - .state_changed - .wait_timeout(state_guard, Duration::from_millis(culldown)) - .unwrap_or_else(|poison| poison.into_inner()); - - if &time.elapsed() > timeout { - return Err(AnalysisError::Timeout); - } + // Safety: `T` properly describes `memo` type. + #[inline(always)] + pub(super) unsafe fn downcast_unchecked(&self) -> &T { + let memo = self.memo.deref(); - culldown <<= 1; + #[cfg(debug_assertions)] + if memo.attr_memo_type_id() != TypeId::of::() { + // Safety: Upheld by the caller. + unsafe { ld_unreachable!("Incorrect memo type.") } } + + // Safety: Upheld by the caller. + unsafe { &*(memo as *const dyn AttrMemo as *const T) } } - #[cfg(target_family = "wasm")] - pub(super) fn write(&self, timeout: &Duration) -> AnalysisResult> { - let mut state_guard = self - .state - .lock() - .unwrap_or_else(|poison| poison.into_inner()); + // Safety: `T` properly describes `memo` type. + #[inline(always)] + pub(super) unsafe fn downcast_unchecked_mut(&mut self) -> &mut T { + let memo = self.memo.deref_mut(); - if *state_guard == UNLOCK_MASK { - *state_guard = WRITE_MASK; - return Ok(RecordWriteGuard { record: self }); + #[cfg(debug_assertions)] + if memo.attr_memo_type_id() != TypeId::of::() { + // Safety: Upheld by the caller. + unsafe { ld_unreachable!("Incorrect memo type.") } } - Err(AnalysisError::Timeout) + // Safety: Upheld by the caller. + unsafe { &mut *(memo as *mut dyn AttrMemo as *mut T) } } } -pub(super) struct RecordReadGuard<'a, N: Grammar, H: TaskHandle, S: SyncBuildHasher> { - record: &'a Record, -} - -impl<'a, N: Grammar, H: TaskHandle, S: SyncBuildHasher> Drop for RecordReadGuard<'a, N, H, S> { - fn drop(&mut self) { - let mut state_guard = self - .record - .state - .lock() - .unwrap_or_else(|poison| poison.into_inner()); - - ld_assert!(*state_guard & WRITE_MASK == 0, "Invalid lock state."); - ld_assert!(*state_guard & READ_MASK > 0, "Invalid lock state."); - - *state_guard -= READ_BIT; - - if *state_guard == UNLOCK_MASK { - drop(state_guard); - self.record.state_changed.notify_one(); - } - } +pub(super) struct CacheDeps { + pub(super) attrs: HashSet, + pub(super) slots: HashSet, + pub(super) events: HashSet<(Id, Event), S>, + pub(super) classes: HashSet<(Id, ::Class), S>, } -impl<'a, N: Grammar, H: TaskHandle, S: SyncBuildHasher> Deref for RecordReadGuard<'a, N, H, S> { - type Target = RecordData; - +impl Default for CacheDeps { #[inline(always)] - fn deref(&self) -> &Self::Target { - unsafe { &*self.record.data.get() } + fn default() -> Self { + Self { + attrs: HashSet::default(), + slots: HashSet::default(), + events: HashSet::default(), + classes: HashSet::default(), + } } } -pub(super) struct RecordWriteGuard<'a, N: Grammar, H: TaskHandle, S: SyncBuildHasher> { - record: &'a Record, -} - -impl<'a, N: Grammar, H: TaskHandle, S: SyncBuildHasher> Drop for RecordWriteGuard<'a, N, H, S> { - fn drop(&mut self) { - let mut state_guard = self - .record - .state - .lock() - .unwrap_or_else(|poison| poison.into_inner()); +pub(super) type SlotRecord = TimeoutRwLock; - ld_assert!(*state_guard & WRITE_MASK > 0, "Invalid lock state."); - ld_assert!(*state_guard & READ_MASK == 0, "Invalid lock state."); - - *state_guard = UNLOCK_MASK; - - drop(state_guard); - - self.record.state_changed.notify_all(); - } +pub(super) struct SlotRecordData { + pub(super) revision: Revision, + pub(super) memo: Box, } -impl<'a, N: Grammar, H: TaskHandle, S: SyncBuildHasher> Deref for RecordWriteGuard<'a, N, H, S> { - type Target = RecordData; - +impl SlotRecordData { #[inline(always)] - fn deref(&self) -> &Self::Target { - unsafe { &*self.record.data.get() } + pub(super) fn new() -> Self { + Self { + revision: 0, + memo: Box::new(T::default()), + } } -} -impl<'a, N: Grammar, H: TaskHandle, S: SyncBuildHasher> DerefMut for RecordWriteGuard<'a, N, H, S> { #[inline(always)] - fn deref_mut(&mut self) -> &mut Self::Target { - unsafe { &mut *self.record.data.get() } - } -} - -pub(super) struct RecordData { - pub(super) verified_at: Revision, - pub(super) cache: Option>, - pub(super) node_ref: NodeRef, - pub(super) function: &'static dyn Function, -} + pub(super) fn downcast(&self) -> AnalysisResult<&T> { + let memo = self.memo.deref(); -impl RecordData { - #[inline(always)] - fn new + Eq>(node_ref: NodeRef) -> Self { - Self { - verified_at: 0, - cache: None, - node_ref, - function: &(C::compute as fn(&mut AttrContext) -> AnalysisResult), + if memo.slot_memo_type_id() != TypeId::of::() { + return Err(AnalysisError::TypeMismatch); } - } -} -pub(super) struct RecordCache { - pub(super) dirty: bool, - pub(super) updated_at: Revision, - pub(super) memo: Box, - pub(super) deps: Shared>, -} + // Safety: Type checked above. + Ok(unsafe { &*(memo as *const dyn SlotMemo as *const T) }) + } -impl RecordCache { #[inline(always)] - pub(super) fn downcast(&self) -> AnalysisResult<&T> { - if self.memo.memo_type_id() != TypeId::of::() { + pub(super) fn downcast_mut(&mut self) -> AnalysisResult<&mut T> { + let memo = self.memo.deref_mut(); + + if memo.slot_memo_type_id() != TypeId::of::() { return Err(AnalysisError::TypeMismatch); } // Safety: Type checked above. - Ok(unsafe { &*(self.memo.deref() as *const dyn Memo as *const T) }) + Ok(unsafe { &mut *(memo as *mut dyn SlotMemo as *mut T) }) } - // Safety: `T` properly describes `memo` type. + // Safety: `T` properly describes underlying `memo` type. #[inline(always)] pub(super) unsafe fn downcast_unchecked(&self) -> &T { - if self.memo.memo_type_id() != TypeId::of::() { + let memo = self.memo.deref(); + + #[cfg(debug_assertions)] + if memo.slot_memo_type_id() != TypeId::of::() { // Safety: Upheld by the caller. unsafe { ld_unreachable!("Incorrect memo type.") } } // Safety: Upheld by the caller. - unsafe { &*(self.memo.deref() as *const dyn Memo as *const T) } + unsafe { &*(memo as *const dyn SlotMemo as *const T) } } - // Safety: `T` properly describes `memo` type. + // Safety: `T` properly describes underlying `memo` type. #[inline(always)] pub(super) unsafe fn downcast_unchecked_mut(&mut self) -> &mut T { - if self.memo.memo_type_id() != TypeId::of::() { + let memo = Box::deref_mut(&mut self.memo); + + #[cfg(debug_assertions)] + if memo.slot_memo_type_id() != TypeId::of::() { // Safety: Upheld by the caller. unsafe { ld_unreachable!("Incorrect memo type.") } } // Safety: Upheld by the caller. - unsafe { &mut *(self.memo.deref_mut() as *mut dyn Memo as *mut T) } + unsafe { &mut *(memo as *mut dyn SlotMemo as *mut T) } } } -pub(super) struct CacheDeps { - pub(super) attrs: HashSet, - pub(super) events: HashSet<(Id, Event), S>, - pub(super) classes: HashSet<(Id, ::Class), S>, +pub(super) trait SlotMemo: Send + Sync + 'static { + fn slot_memo_type_id(&self) -> TypeId; } -impl Default for CacheDeps { +impl SlotMemo for T { #[inline(always)] - fn default() -> Self { - Self { - attrs: HashSet::default(), - events: HashSet::default(), - classes: HashSet::default(), - } + fn slot_memo_type_id(&self) -> TypeId { + TypeId::of::() } } -pub(super) trait Memo: Send + Sync + 'static { - fn memo_type_id(&self) -> TypeId; +pub(super) trait AttrMemo: Send + Sync + 'static { + fn attr_memo_type_id(&self) -> TypeId; // Safety: `self` and `other` represent the same type. - unsafe fn memo_eq(&self, other: &dyn Memo) -> bool; + unsafe fn attr_memo_eq(&self, other: &dyn AttrMemo) -> bool; } -impl Memo for T { +impl AttrMemo for T { #[inline(always)] - fn memo_type_id(&self) -> TypeId { + fn attr_memo_type_id(&self) -> TypeId { TypeId::of::() } #[inline(always)] - unsafe fn memo_eq(&self, other: &dyn Memo) -> bool { - if self.memo_type_id() != other.memo_type_id() { + unsafe fn attr_memo_eq(&self, other: &dyn AttrMemo) -> bool { + if self.attr_memo_type_id() != other.attr_memo_type_id() { // Safety: Upheld by the caller. unsafe { ld_unreachable!("Incorrect memo type.") } } // Safety: Upheld by the caller. - let other = unsafe { &*(other as *const dyn Memo as *const T) }; + let other = unsafe { &*(other as *const dyn AttrMemo as *const T) }; self.eq(other) } @@ -422,7 +352,7 @@ impl Memo for T { pub(super) trait Function: Send + Sync + 'static { - fn invoke(&self, task: &mut AttrContext) -> AnalysisResult>; + fn invoke(&self, task: &mut AttrContext) -> AnalysisResult>; } impl Function for fn(&mut AttrContext) -> AnalysisResult @@ -432,7 +362,7 @@ where H: TaskHandle, S: SyncBuildHasher, { - fn invoke(&self, context: &mut AttrContext) -> AnalysisResult> { + fn invoke(&self, context: &mut AttrContext) -> AnalysisResult> { Ok(Box::new(self(context)?)) } } diff --git a/work/crates/main/src/analysis/entry.rs b/work/crates/main/src/analysis/entry.rs index bd60398..a047250 100644 --- a/work/crates/main/src/analysis/entry.rs +++ b/work/crates/main/src/analysis/entry.rs @@ -41,6 +41,7 @@ use std::{ use crate::{ analysis::{ + database::DocRecords, AnalysisError, AnalysisResult, Analyzer, @@ -53,7 +54,7 @@ use crate::{ ScopeAttr, TaskHandle, }, - arena::{Entry, Id, Identifiable, Repo}, + arena::{Entry, Id, Identifiable}, lexis::ToSpan, report::ld_unreachable, sync::{Shared, SyncBuildHasher, TableReadGuard}, @@ -145,7 +146,7 @@ impl Analyzer { let id = doc.id(); let node_refs = doc.node_refs().collect::>(); - let mut records = Repo::with_capacity(node_refs.len()); + let mut records = DocRecords::with_capacity(node_refs.len()); let mut classes_to_nodes = HashMap::<::Class, ClassToNodes, S>::default(); let mut nodes_to_classes = HashMap::, S>::default(); @@ -157,6 +158,7 @@ impl Analyzer { id, database: Arc::downgrade(&self.db) as Weak<_>, records: &mut records, + inserts: false, }; for node_ref in &node_refs { @@ -289,6 +291,7 @@ impl Analyzer { id, database: Arc::downgrade(&self.db) as Weak<_>, records: records.deref_mut(), + inserts: false, }; for node_ref in &report.node_refs { @@ -329,7 +332,7 @@ impl Analyzer { let mut invalidator = Invalidator { id, - records: records.deref_mut(), + records: &mut records.attrs, }; for node_ref in &report.node_refs { @@ -413,13 +416,7 @@ impl Analyzer { // Safety: `scope_attr_ref` belongs to `scope_attr`. let scope_ref = unsafe { - ScopeAttr::snapshot_manually( - scope_attr_ref, - handle, - doc, - records.deref(), - revision, - )? + ScopeAttr::snapshot_manually(scope_attr_ref, handle, doc, &records.attrs, revision)? }; if !scope_ref.is_nil() { @@ -430,7 +427,7 @@ impl Analyzer { if !scope_accumulator.is_empty() { let mut invalidator = Invalidator { id, - records: records.deref_mut(), + records: &mut records.attrs, }; for scope_ref in scope_accumulator { diff --git a/work/crates/main/src/analysis/error.rs b/work/crates/main/src/analysis/error.rs index ab518fd..d73ad25 100644 --- a/work/crates/main/src/analysis/error.rs +++ b/work/crates/main/src/analysis/error.rs @@ -115,11 +115,24 @@ pub enum AnalysisError { /// This error is an **abnormal** error. UninitAttribute, + /// An attempt to access an [Slot](crate::analysis::Slot) object which is + /// not fully initialized. + /// + /// See [Feature Lifetime](crate::analysis::Feature#feature-lifetime) for details. + /// + /// This error is an **abnormal** error. + UninitSlot, + /// The referred attribute does not exist in the Analyzer's semantic graph. /// /// This error is an **abnormal** error. MissingAttribute, + /// The referred slot does not exist in the Analyzer's semantic graph. + /// + /// This error is an **abnormal** error. + MissingSlot, + /// An attempt to access a [Semantics](crate::analysis::Semantics) object /// which is not fully initialized. /// @@ -170,7 +183,9 @@ impl Display for AnalysisError { Self::ImmutableDocument => "An attempt to write into immutable document.", Self::InvalidSpan => "Provided span is not valid for the specified document.", Self::UninitAttribute => "An attempt to access uninitialized attribute object.", + Self::UninitSlot => "An attempt to access uninitialized slot object.", Self::MissingAttribute => "Referred attribute does not exist in the analyzer.", + Self::MissingSlot => "Referred slot does not exist in the analyzer.", Self::UninitSemantics => "An attempt to access uninitialized semantics object.", Self::MissingSemantics => "Node variant does not have semantics.", Self::TypeMismatch => "Incorrect attribute type.", diff --git a/work/crates/main/src/analysis/grammar.rs b/work/crates/main/src/analysis/grammar.rs index a02e8d4..e820c91 100644 --- a/work/crates/main/src/analysis/grammar.rs +++ b/work/crates/main/src/analysis/grammar.rs @@ -46,15 +46,24 @@ pub use lady_deirdre_derive::Feature; use crate::{ analysis::{ - database::{AbstractDatabase, Record}, + database::{ + AbstractDatabase, + AttrRecord, + AttrRecordData, + DocRecords, + SlotRecord, + SlotRecordData, + }, AnalysisError, AnalysisResult, AttrRef, Computable, ScopeAttr, + SlotRef, TaskHandle, TriggerHandle, NIL_ATTR_REF, + NIL_SLOT_REF, }, arena::{Entry, Id, Identifiable, Repo}, sync::SyncBuildHasher, @@ -90,6 +99,31 @@ pub trait Grammar: Node + AbstractFeature { /// ``` type Classifier: Classifier; + /// A special semantic feature that describes the semantics of the entire + /// Analyzer and is instantiated during the Analyzer's creation. + /// This feature does not belong to any specific document and is shared + /// across the entire Analyzer. + /// + /// The common semantics can be accessed **outside** of the computation + /// context using the + /// [AbstractTask::common](crate::analysis::AbstractTask::common) method of + /// any task type, and **inside** of the + /// [computation context](Computable::compute) using the + /// [AttrContext::common](crate::analysis::AttrContext::common) method. + /// + /// When using the [Node](lady_deirdre_derive::Node) macro, this value is + /// set to [VoidFeature] by default, or could be overridden using + /// the `#[semantics(...)]` attribute: + /// + /// ```ignore + /// #[derive(Node)] + /// #[semantics(MyCommonSemantics)] + /// enum MyNode { + /// // ... + /// } + /// ``` + type CommonSemantics: Feature; + /// Initializes a new node semantics. /// /// This function should only be called once the node is created @@ -282,16 +316,16 @@ impl Classifier for VoidClassifier { /// The root of the composition is a [Semantics] object owned by the syntax tree /// node. This object is an entry point to the syntax tree node's semantics. /// -/// The [Attr](crate::analysis::Attr) object (attribute) is a final building -/// block of the semantics composition that infers a particular fact about -/// the compilation project semantics model related to the specified syntax -/// tree node. +/// The [Attr](crate::analysis::Attr) objects (attributes) and +/// [Slots](crate::analysis::Slot) are final building blocks of the semantics +/// composition that infers a particular fact about the compilation project +/// semantics model related to the specified syntax tree node. /// /// Any other composition part is any arbitrary user-defined type that owns /// attributes and other composition objects. Usually, these are struct /// types with attributes and other similar struct types. /// -/// These three kinds of composition objects implement the Feature trait, which +/// These four kinds of composition objects implement the Feature trait, which /// provides abstract access to their content and functions to control /// their lifetime. /// @@ -329,14 +363,17 @@ impl Classifier for VoidClassifier { /// the [Grammar::invalidate] function) to indicate that the scoped content /// has been changed. In this case, it is up to the feature tree /// implementation to decide which feature inner [Attr](crate::analysis::Attr) -/// objects require invalidation. In particular, -/// the [Feature](lady_deirdre_derive::Feature) derive macro propagates -/// the invalidation event to the struct fields annotated with the `#[scoped]` -/// macro attribute. -/// -/// Note that the feature creation, initialization, invalidation, and -/// destruction are controlled by the Analyzer. The end-user code usually -/// doesn’t need to call related functions manually. +/// or [Slot](crate::analysis::Slot) objects require invalidation. +/// In particular, the [Feature](lady_deirdre_derive::Feature) derive macro +/// propagates the invalidation event to the struct fields annotated with the +/// `#[scoped]` macro attribute. +/// +/// The feature creation, initialization, invalidation, and destruction are +/// controlled by the Analyzer. The end-user code usually doesn't need to call +/// related functions manually. +/// +/// **NOTE**: This trait API is not stabilized yet. New trait members may be +/// added in future minor versions of Lady Deirdre. pub trait Feature: AbstractFeature { /// A type of the [Grammar] to which this semantic feature belongs. type Node: Grammar; @@ -380,16 +417,22 @@ pub trait Feature: AbstractFeature { } /// An object-safe part of the [Feature] API. +/// +/// **NOTE**: This trait API is not stabilized yet. New trait members may be +/// added in future minor versions of Lady Deirdre. pub trait AbstractFeature { /// Returns an [AttrRef] reference object of the attribute if this feature /// represents an [attribute](crate::analysis::Attr). /// /// Otherwise, the function returns [nil](AttrRef::nil). - /// - /// This function is particularly useful to determine if the Feature is - /// an attribute or a composite object. fn attr_ref(&self) -> &AttrRef; + /// Returns a [SlotRef] reference object of the slot if this feature + /// represents a [slot](crate::analysis::Slot). + /// + /// Otherwise, the function returns [nil](SlotRef::nil). + fn slot_ref(&self) -> &SlotRef; + /// Returns a sub-feature of this feature by `key`. /// /// If there is no corresponding sub-feature the function returns @@ -463,7 +506,8 @@ pub struct Initializer< > { pub(super) id: Id, pub(super) database: Weak, - pub(super) records: &'a mut Repo>, + pub(super) records: &'a mut DocRecords, + pub(super) inserts: bool, } /// A marker-[feature](Feature) of the syntax tree nodes with empty @@ -497,6 +541,11 @@ impl AbstractFeature for VoidFeature { &NIL_ATTR_REF } + #[inline(always)] + fn slot_ref(&self) -> &SlotRef { + &NIL_SLOT_REF + } + #[inline(always)] fn feature(&self, _key: Key) -> AnalysisResult<&dyn AbstractFeature> { Err(AnalysisError::MissingFeature) @@ -544,9 +593,27 @@ impl<'a, N: Grammar, H: TaskHandle, S: SyncBuildHasher> Initializer<'a, N, H, S> &mut self, node_ref: NodeRef, ) -> (Weak, Entry) { + self.inserts = true; + + ( + self.database.clone(), + self.records + .attrs + .insert(AttrRecord::new(AttrRecordData::new::(node_ref))), + ) + } + + #[inline(always)] + pub(super) fn register_slot( + &mut self, + ) -> (Weak, Entry) { + self.inserts = true; + ( self.database.clone(), - self.records.insert(Record::new::(node_ref)), + self.records + .slots + .insert(SlotRecord::new(SlotRecordData::new::())), ) } } @@ -576,7 +643,7 @@ pub struct Invalidator< S: SyncBuildHasher = RandomState, > { pub(super) id: Id, - pub(super) records: &'a mut Repo>, + pub(super) records: &'a mut Repo>, } impl<'a, N: Grammar, H: TaskHandle, S: SyncBuildHasher> Identifiable for Invalidator<'a, N, H, S> { @@ -674,6 +741,15 @@ impl AbstractFeature for Semantics { inner.attr_ref() } + #[inline(always)] + fn slot_ref(&self) -> &SlotRef { + let Ok(inner) = self.get() else { + return &NIL_SLOT_REF; + }; + + inner.slot_ref() + } + #[inline(always)] fn feature(&self, key: Key) -> AnalysisResult<&dyn AbstractFeature> { self.get()?.feature(key) diff --git a/work/crates/main/src/analysis/lock.rs b/work/crates/main/src/analysis/lock.rs new file mode 100644 index 0000000..f9a07c8 --- /dev/null +++ b/work/crates/main/src/analysis/lock.rs @@ -0,0 +1,231 @@ +//////////////////////////////////////////////////////////////////////////////// +// This file is a part of the "Lady Deirdre" work, // +// a compiler front-end foundation technology. // +// // +// This work is proprietary software with source-available code. // +// // +// To copy, use, distribute, and contribute to this work, you must agree to // +// the terms of the General License Agreement: // +// // +// https://github.com/Eliah-Lakhin/lady-deirdre/blob/master/EULA.md. // +// // +// The agreement grants you a Commercial-Limited License that gives you // +// the right to use my work in non-commercial and limited commercial products // +// with a total gross revenue cap. To remove this commercial limit for one of // +// your products, you must acquire an Unrestricted Commercial License. // +// // +// If you contribute to the source code, documentation, or related materials // +// of this work, you must assign these changes to me. Contributions are // +// governed by the "Derivative Work" section of the General License // +// Agreement. // +// // +// Copying the work in parts is strictly forbidden, except as permitted under // +// the terms of the General License Agreement. // +// // +// If you do not or cannot agree to the terms of this Agreement, // +// do not use this work. // +// // +// This work is provided "as is" without any warranties, express or implied, // +// except to the extent that such disclaimers are held to be legally invalid. // +// // +// Copyright (c) 2024 Ilya Lakhin (Илья Александрович Лахин). // +// All rights reserved. // +//////////////////////////////////////////////////////////////////////////////// + +use std::{ + cell::UnsafeCell, + ops::{Deref, DerefMut}, + sync::{Condvar, Mutex}, + time::{Duration, Instant}, +}; + +use crate::{ + analysis::{AnalysisError, AnalysisResult}, + report::ld_assert, +}; + +const UNLOCK_MASK: usize = 0; +const READ_MASK: usize = !0 ^ 1; +const READ_BIT: usize = 1 << 1; +const WRITE_MASK: usize = 1; + +pub(super) struct TimeoutRwLock { + state: Mutex, + state_changed: Condvar, + data: UnsafeCell, +} + +unsafe impl Send for TimeoutRwLock {} + +unsafe impl Sync for TimeoutRwLock {} + +impl TimeoutRwLock { + #[inline(always)] + pub(super) fn new(data: T) -> Self { + Self { + state: Mutex::new(UNLOCK_MASK), + state_changed: Condvar::new(), + data: UnsafeCell::new(data), + } + } + + #[cfg(not(target_family = "wasm"))] + pub(super) fn read(&self, timeout: &Duration) -> AnalysisResult> { + let mut state_guard = self + .state + .lock() + .unwrap_or_else(|poison| poison.into_inner()); + + let mut cooldown = 1; + let time = Instant::now(); + + loop { + if *state_guard & WRITE_MASK == 0 { + *state_guard += READ_BIT; + return Ok(TimeoutRwLockReadGuard { record: self }); + } + + (state_guard, _) = self + .state_changed + .wait_timeout(state_guard, Duration::from_millis(cooldown)) + .unwrap_or_else(|poison| poison.into_inner()); + + if &time.elapsed() > timeout { + return Err(AnalysisError::Timeout); + } + + cooldown <<= 1; + } + } + + #[cfg(target_family = "wasm")] + pub(super) fn read(&self, timeout: &Duration) -> AnalysisResult> { + let mut state_guard = self + .state + .lock() + .unwrap_or_else(|poison| poison.into_inner()); + + if *state_guard & WRITE_MASK == 0 { + *state_guard += READ_BIT; + return Ok(TimeoutRwLockReadGuard { record: self }); + } + + Err(AnalysisError::Timeout) + } + + #[cfg(not(target_family = "wasm"))] + pub(super) fn write(&self, timeout: &Duration) -> AnalysisResult> { + let mut state_guard = self + .state + .lock() + .unwrap_or_else(|poison| poison.into_inner()); + + let mut culldown = 1; + let time = Instant::now(); + + loop { + if *state_guard == UNLOCK_MASK { + *state_guard = WRITE_MASK; + return Ok(TimeoutRwLockWriteGuard { record: self }); + } + + (state_guard, _) = self + .state_changed + .wait_timeout(state_guard, Duration::from_millis(culldown)) + .unwrap_or_else(|poison| poison.into_inner()); + + if &time.elapsed() > timeout { + return Err(AnalysisError::Timeout); + } + + culldown <<= 1; + } + } + + #[cfg(target_family = "wasm")] + pub(super) fn write(&self, timeout: &Duration) -> AnalysisResult> { + let mut state_guard = self + .state + .lock() + .unwrap_or_else(|poison| poison.into_inner()); + + if *state_guard == UNLOCK_MASK { + *state_guard = WRITE_MASK; + return Ok(TimeoutRwLockWriteGuard { record: self }); + } + + Err(AnalysisError::Timeout) + } +} + +pub(super) struct TimeoutRwLockReadGuard<'a, T: 'static> { + record: &'a TimeoutRwLock, +} + +impl<'a, T: 'static> Drop for TimeoutRwLockReadGuard<'a, T> { + fn drop(&mut self) { + let mut state_guard = self + .record + .state + .lock() + .unwrap_or_else(|poison| poison.into_inner()); + + ld_assert!(*state_guard & WRITE_MASK == 0, "Invalid lock state."); + ld_assert!(*state_guard & READ_MASK > 0, "Invalid lock state."); + + *state_guard -= READ_BIT; + + if *state_guard == UNLOCK_MASK { + drop(state_guard); + self.record.state_changed.notify_one(); + } + } +} + +impl<'a, T: 'static> Deref for TimeoutRwLockReadGuard<'a, T> { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + unsafe { &*self.record.data.get() } + } +} + +pub(super) struct TimeoutRwLockWriteGuard<'a, T: 'static> { + record: &'a TimeoutRwLock, +} + +impl<'a, T: 'static> Drop for TimeoutRwLockWriteGuard<'a, T> { + fn drop(&mut self) { + let mut state_guard = self + .record + .state + .lock() + .unwrap_or_else(|poison| poison.into_inner()); + + ld_assert!(*state_guard & WRITE_MASK > 0, "Invalid lock state."); + ld_assert!(*state_guard & READ_MASK == 0, "Invalid lock state."); + + *state_guard = UNLOCK_MASK; + + drop(state_guard); + + self.record.state_changed.notify_all(); + } +} + +impl<'a, T: 'static> Deref for TimeoutRwLockWriteGuard<'a, T> { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + unsafe { &*self.record.data.get() } + } +} + +impl<'a, T: 'static> DerefMut for TimeoutRwLockWriteGuard<'a, T> { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { &mut *self.record.data.get() } + } +} diff --git a/work/crates/main/src/analysis/mod.rs b/work/crates/main/src/analysis/mod.rs index 0a4d9a2..e5f967f 100644 --- a/work/crates/main/src/analysis/mod.rs +++ b/work/crates/main/src/analysis/mod.rs @@ -39,14 +39,16 @@ mod database; mod entry; mod error; mod grammar; +mod lock; mod manager; mod scope; +mod slot; mod tasks; pub use crate::analysis::{ analyzer::{Analyzer, AnalyzerConfig}, attribute::{Attr, AttrRef, NIL_ATTR_REF}, - compute::{AttrContext, AttrReadGuard, Computable, SharedComputable}, + compute::{AttrContext, AttrReadGuard, Computable, SharedComputable, SlotReadGuard}, database::Revision, entry::{ DocumentReadGuard, @@ -71,6 +73,7 @@ pub use crate::analysis::{ }, manager::{TaskHandle, TaskPriority, TriggerHandle}, scope::{Scope, ScopeAttr}, + slot::{Slot, SlotRef, NIL_SLOT_REF}, tasks::{ AbstractTask, AnalysisTask, diff --git a/work/crates/main/src/analysis/scope.rs b/work/crates/main/src/analysis/scope.rs index e72a3d1..6924f69 100644 --- a/work/crates/main/src/analysis/scope.rs +++ b/work/crates/main/src/analysis/scope.rs @@ -41,7 +41,7 @@ use std::{ use crate::{ analysis::{ - database::{CacheDeps, Memo, Record, RecordCache}, + database::{AttrMemo, AttrRecord, AttrRecordCache, CacheDeps}, AnalysisError, AnalysisResult, Attr, @@ -168,7 +168,7 @@ impl ScopeAttr { attr_ref: &AttrRef, handle: &H, doc: &Document, - records: &Repo>, + records: &Repo>, revision: Revision, ) -> AnalysisResult { let Some(record) = records.get(&attr_ref.entry) else { @@ -206,13 +206,13 @@ impl ScopeAttr { let _ = deps.attrs.insert(dep); } - record_data.cache = Some(RecordCache { + record_data.cache = Some(AttrRecordCache { dirty: false, updated_at: revision, memo: Box::new(Scope { scope_ref, _grammar: PhantomData::, - }) as Box, + }) as Box, deps: Shared::new(deps), }); @@ -324,7 +324,7 @@ impl ScopeAttr { node_ref: &NodeRef, handle: &H, doc: &Document, - records: &Repo>, + records: &Repo>, revision: Revision, ) -> AnalysisResult<(Option, NodeRef)> { let Some(node) = node_ref.deref(doc) else { diff --git a/work/crates/main/src/analysis/slot.rs b/work/crates/main/src/analysis/slot.rs new file mode 100644 index 0000000..2a67cb7 --- /dev/null +++ b/work/crates/main/src/analysis/slot.rs @@ -0,0 +1,606 @@ +//////////////////////////////////////////////////////////////////////////////// +// This file is a part of the "Lady Deirdre" work, // +// a compiler front-end foundation technology. // +// // +// This work is proprietary software with source-available code. // +// // +// To copy, use, distribute, and contribute to this work, you must agree to // +// the terms of the General License Agreement: // +// // +// https://github.com/Eliah-Lakhin/lady-deirdre/blob/master/EULA.md. // +// // +// The agreement grants you a Commercial-Limited License that gives you // +// the right to use my work in non-commercial and limited commercial products // +// with a total gross revenue cap. To remove this commercial limit for one of // +// your products, you must acquire an Unrestricted Commercial License. // +// // +// If you contribute to the source code, documentation, or related materials // +// of this work, you must assign these changes to me. Contributions are // +// governed by the "Derivative Work" section of the General License // +// Agreement. // +// // +// Copying the work in parts is strictly forbidden, except as permitted under // +// the terms of the General License Agreement. // +// // +// If you do not or cannot agree to the terms of this Agreement, // +// do not use this work. // +// // +// This work is provided "as is" without any warranties, express or implied, // +// except to the extent that such disclaimers are held to be legally invalid. // +// // +// Copyright (c) 2024 Ilya Lakhin (Илья Александрович Лахин). // +// All rights reserved. // +//////////////////////////////////////////////////////////////////////////////// + +use std::{ + cmp::Ordering, + fmt::{Debug, Formatter}, + hash::{Hash, Hasher}, + marker::PhantomData, + ops::Deref, + sync::Weak, +}; + +use crate::{ + analysis::{ + database::AbstractDatabase, + AbstractFeature, + AbstractTask, + AnalysisError, + AnalysisResult, + AttrContext, + AttrRef, + Feature, + Grammar, + Initializer, + Invalidator, + MutationAccess, + Revision, + SemanticAccess, + SlotReadGuard, + TaskHandle, + NIL_ATTR_REF, + }, + arena::{Entry, Id, Identifiable}, + sync::SyncBuildHasher, + syntax::{Key, NodeRef}, +}; + +/// An [SlotRef] reference that does not point to any [slot](Slot). +/// +/// The value of this static equals to the [SlotRef::nil] value. +pub static NIL_SLOT_REF: SlotRef = SlotRef::nil(); + +/// A specialized version of an attribute that enables manual control over the +/// underlying value. +/// +/// The purpose of a Slot is to provide a conventional mechanism for injecting +/// metadata from the environment, which is external to the Analyzer, into the +/// Analyzer's semantic model. Slots are particularly useful in the Analyzer's +/// [common semantics](Grammar::CommonSemantics) for storing common +/// configurations, such as the mapping between file names and their +/// corresponding document [IDs](Id) within the Analyzer. +/// +/// The value of a Slot is fully integrated into the Analyzer's semantic graph +/// and semantic model, except that the content of the value is managed manually +/// by the API user. +/// +/// Unlike a typical [attribute](crate::analysis::Attr), a Slot does not have an +/// associated function that computes its value. Instead, the Slot's value is +/// initialized using the [Default] implementation of type `T`, and the API user +/// manually modifies the value's content using the [Slot::mutate] function. +/// +/// This interface is similar to the [Attr](crate::analysis::Attr) object, in +/// that attributes can [read](Slot::read) (and thus subscribe to changes in) +/// the Slot's content within the attribute's +/// [Computable](crate::analysis::Computable) implementation. The API user can +/// also obtain a [snapshot](Slot::snapshot) outside of the computation +/// procedure. +/// +/// Note that, unlike Attr objects, Slot values will not be automatically +/// invalidated even if the Slot is part of a scoped node. However, they may be +/// recreated (and reset to their defaults) by the Analyzer when a document is +/// [edited](MutationAccess::write_to_doc). Therefore, Slots within syntax tree +/// nodes need to be maintained with extra care. +/// +/// An associated [SlotRef] referential interface can be obtained using the +/// [AsRef] and the [Feature] implementations of the Slot. +#[repr(transparent)] +pub struct Slot { + inner: SlotInner, + _node: PhantomData, + _data: PhantomData, +} + +impl Debug for Slot +where + N: Grammar, + T: Default + Send + Sync + 'static, +{ + #[inline] + fn fmt(&self, formatter: &mut Formatter) -> std::fmt::Result { + let slot_ref = self.as_ref(); + + match (slot_ref.id.is_nil(), slot_ref.entry.is_nil()) { + (false, _) => formatter.write_fmt(format_args!( + "Slot(id: {:?}, entry: {:?})", + slot_ref.id, slot_ref.entry, + )), + + (true, false) => formatter.write_fmt(format_args!("Slot(entry: {:?})", slot_ref.entry)), + + (true, true) => formatter.write_str("Slot(Nil)"), + } + } +} + +impl Identifiable for Slot { + #[inline(always)] + fn id(&self) -> Id { + self.as_ref().id + } +} + +impl PartialEq> for Slot +where + N: Grammar, + T: Default + Send + Sync + 'static, + U: Default + Send + Sync + 'static, +{ + #[inline(always)] + fn eq(&self, other: &Slot) -> bool { + self.as_ref().eq(other.as_ref()) + } +} + +impl Eq for Slot +where + N: Grammar, + T: Default + Send + Sync + 'static, +{ +} + +impl PartialOrd> for Slot +where + N: Grammar, + T: Default + Send + Sync + 'static, + U: Default + Default + Send + Sync + 'static, +{ + #[inline(always)] + fn partial_cmp(&self, other: &Slot) -> Option { + self.as_ref().partial_cmp(other.as_ref()) + } +} + +impl Ord for Slot +where + N: Grammar, + T: Default + Send + Sync + 'static, +{ + #[inline(always)] + fn cmp(&self, other: &Slot) -> Ordering { + self.as_ref().cmp(other.as_ref()) + } +} + +impl Hash for Slot +where + N: Grammar, + T: Default + Send + Sync + 'static, +{ + #[inline(always)] + fn hash(&self, state: &mut H) { + self.as_ref().hash(state) + } +} + +impl AsRef for Slot +where + N: Grammar, + T: Default + Send + Sync + 'static, +{ + #[inline(always)] + fn as_ref(&self) -> &SlotRef { + let SlotInner::Init { attr_ref, .. } = &self.inner else { + return &NIL_SLOT_REF; + }; + + attr_ref + } +} + +impl Drop for Slot +where + N: Grammar, + T: Default + Send + Sync + 'static, +{ + fn drop(&mut self) { + let SlotInner::Init { attr_ref, database } = &self.inner else { + return; + }; + + let Some(database) = database.upgrade() else { + return; + }; + + database.deregister_attribute(attr_ref.id, &attr_ref.entry); + } +} + +impl AbstractFeature for Slot +where + N: Grammar, + T: Default + Send + Sync + 'static, +{ + #[inline(always)] + fn attr_ref(&self) -> &AttrRef { + &NIL_ATTR_REF + } + + #[inline(always)] + fn slot_ref(&self) -> &SlotRef { + self.as_ref() + } + + #[inline(always)] + fn feature(&self, _key: Key) -> AnalysisResult<&dyn AbstractFeature> { + Err(AnalysisError::MissingFeature) + } + + #[inline(always)] + fn feature_keys(&self) -> &'static [&'static Key] { + &[] + } +} + +impl Feature for Slot +where + N: Grammar, + T: Default + Send + Sync + 'static, +{ + type Node = N; + + #[inline(always)] + fn new(node_ref: NodeRef) -> Self { + Self { + inner: SlotInner::Uninit(node_ref), + _node: PhantomData, + _data: PhantomData, + } + } + + fn init( + &mut self, + initializer: &mut Initializer, + ) { + let SlotInner::Uninit(node_ref) = &self.inner else { + return; + }; + + let id = node_ref.id; + + #[cfg(debug_assertions)] + if initializer.id() != id { + panic!("Slot and Compilation Unit mismatch."); + } + + let (database, entry) = initializer.register_slot::(); + + self.inner = SlotInner::Init { + attr_ref: SlotRef { id, entry }, + database, + }; + } + + #[inline(always)] + fn invalidate( + &self, + _invalidator: &mut Invalidator, + ) { + } +} + +impl Slot +where + N: Grammar, + T: Default + Send + Sync + 'static, +{ + /// Requests a copy of the slot's value. + /// + /// Returns a pair of two elements: + /// 1. The [revision](Revision) under which the slot's value was last + /// modified. + /// 2. A copy of the slot's value. + /// + /// This function is supposed to be called **outside** of + /// the [computation context](crate::analysis::Computable::compute). + /// + /// As a general rule, if the returning revision number equals the revision + /// number of the previous call to the snapshot function, you can treat + /// both copies of the attribute value as equal. Otherwise, the equality is + /// not guaranteed. + /// + /// The `task` parameter grants access to the Analyzer's semantics and + /// could be either an [AnalysisTask](crate::analysis::AnalysisTask) or + /// an [ExclusiveTask](crate::analysis::ExclusiveTask). + /// + /// If the Analyzer unable to fetch the value within the current time + /// limits, the function returns a [Timeout](AnalysisError::Timeout) error. + #[inline(always)] + pub fn snapshot( + &self, + task: &impl SemanticAccess, + ) -> AnalysisResult<(Revision, T)> + where + T: Clone, + { + let mut reader = AttrContext::new(task.analyzer(), task.revision(), task.handle()); + + let result = self.read::(&mut reader)?; + let revision = result.revision; + let data = result.deref().clone(); + + Ok((revision, data)) + } + + /// Mutates the content of the slot's value. + /// + /// This function is supposed be called **outside** of + /// the [computation context](crate::analysis::Computable::compute). + /// + /// The `task` parameter grants write access to the Analyzer's semantic + /// model and can be either a + /// [MutationTask](crate::analysis::MutationTask) or an + /// [ExclusiveTask](crate::analysis::ExclusiveTask). + /// + /// The `map` parameter is a callback that receives mutable references to + /// the slot's value. This function can mutate the underlying content or + /// leave it unchanged, and it must return a boolean flag indicating whether + /// the content has been modified (`true` means that the value has been + /// modified). + /// + /// Failure to adhere to the `map` function's flag requirement does not + /// result in undefined behavior, but it could lead to inconsistencies in + /// the semantic model. + /// + /// If the Analyzer is unable to acquire mutation access to the slot's value + /// within the current time limits, the function returns a + /// [Timeout](AnalysisError::Timeout) error. + #[inline(always)] + pub fn mutate( + &self, + task: &impl MutationAccess, + map: impl FnOnce(&mut T) -> bool, + ) -> AnalysisResult<()> { + let slot_ref = self.as_ref(); + + if slot_ref.is_nil() { + return Err(AnalysisError::UninitSlot); + } + + // Safety: Slot initialized with `T` default value. + unsafe { slot_ref.change::(task, map) } + } + + /// Provides read-only access to the slot's value. + /// + /// This function is supposed be called **inside** of + /// the computation context. + /// + /// The `context` parameter is the "context" argument of the current + /// [computable function](Computable::compute). + /// + /// By calling this function, the computable attribute **subscribes** to + /// changes in this slot, establishing a relationship between the attribute + /// and the slot. + /// + /// If the Analyzer is unable to fetch the value within the current time + /// limits, the function returns a [Timeout](AnalysisError::Timeout) error. + #[inline(always)] + pub fn read<'a, H: TaskHandle, S: SyncBuildHasher>( + &self, + reader: &mut AttrContext<'a, N, H, S>, + ) -> AnalysisResult> { + let slot_ref = self.as_ref(); + + if slot_ref.is_nil() { + return Err(AnalysisError::UninitSlot); + } + + // Safety: Slot initialized with `T` default value. + unsafe { slot_ref.fetch::(reader) } + } +} + +/// A reference of the [slot](Slot) in the Analyzer's semantics graph. +/// +/// Essentially, SlotRef is a composite index within the Analyzer’s inner +/// database. Both components of this index form a unique pair within +/// the lifetime of the Analyzer. +/// +/// If the slot instance has been removed from the Analyzer's semantics +/// graph over time, new slots within this Analyzer will never occupy +/// the same SlotRef object. However, the SlotRef referred to the removed +/// slot would become _invalid_. +/// +/// You can obtain a copy of the SlotRef using the [AsRef] and +/// the [Feature] implementations of the [Slot] object. +/// +/// In general, it is recommended to access slot values directly using +/// the [Slot::snapshot], [Slot::read], and [Slot::mutate] functions to avoid +/// extra checks. However, you can use similar SlotRef functions that require +/// specifying the type of the slot's value explicitly and involve extra checks +/// of the type (even though these checks are relatively cheap to perform). +/// +/// The [nil](SlotRef::nil) SlotRefs are special references that are considered +/// to be always invalid. They intentionally don't refer any slot within +/// any Analyzer. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SlotRef { + /// An identifier of the document managed by the Analyzer to which + /// the slot belongs. + /// + /// If the slot belongs to the [common semantics](Grammar::CommonSemantics), + /// this value is [Id::nil]. + pub id: Id, + + /// A versioned index of the slot instance within the Analyzer's inner + /// database. + pub entry: Entry, +} + +impl Debug for SlotRef { + #[inline] + fn fmt(&self, formatter: &mut Formatter) -> std::fmt::Result { + match (self.id.is_nil(), self.entry.is_nil()) { + (false, _) => formatter.write_fmt(format_args!( + "SlotRef(id: {:?}, entry: {:?})", + self.id, self.entry, + )), + + (true, false) => formatter.write_fmt(format_args!("SlotRef(entry: {:?})", self.entry)), + + (true, true) => formatter.write_str("SlotRef(Nil)"), + } + } +} + +impl Identifiable for SlotRef { + #[inline(always)] + fn id(&self) -> Id { + self.id + } +} + +impl Default for SlotRef { + #[inline(always)] + fn default() -> Self { + Self::nil() + } +} + +impl SlotRef { + /// Returns a SlotRef that intentionally does not refer to any slot + /// within any Analyzer. + /// + /// If you need just a static reference to the nil SlotRef, use + /// the predefined [NIL_SLOT_REF] static. + #[inline(always)] + pub const fn nil() -> Self { + Self { + id: Id::nil(), + entry: Entry::nil(), + } + } + + /// Returns true, if the SlotRef intentionally does not refer to any + /// slot within any Analyzer. + #[inline(always)] + pub const fn is_nil(&self) -> bool { + self.id.is_nil() && self.entry.is_nil() + } + + /// Requests a copy of the slot's value. + /// + /// This function is similar to the [Slot::snapshot] function, but requires + /// an additional generic parameter `T` that specifies the type of + /// the [Slot]'s value. + /// + /// If the `T` parameter does not match the Slot's value type, the function + /// returns a [TypeMismatch](AnalysisError::TypeMismatch) error. + #[inline(always)] + pub fn snapshot( + &self, + task: &impl SemanticAccess, + ) -> AnalysisResult<(Revision, T)> + where + T: Clone + Default + Send + Sync + 'static, + N: Grammar, + H: TaskHandle, + S: SyncBuildHasher, + { + let mut reader = AttrContext::new(task.analyzer(), task.revision(), task.handle()); + + let result = self.read::(&mut reader)?; + let revision = result.revision; + let data = result.deref().clone(); + + Ok((revision, data)) + } + + /// Provides mutation access to the slot's value. + /// + /// This function is similar to the [Slot::mutate] function, but requires + /// an additional generic parameter `T` that specifies the type of + /// the [Slot]'s value. + /// + /// If the `T` parameter does not match the Slot's value type, the function + /// returns a [TypeMismatch](AnalysisError::TypeMismatch) error. + #[inline(always)] + pub fn mutate( + &self, + task: &impl MutationAccess, + map: impl FnOnce(&mut T) -> bool, + ) -> AnalysisResult<()> + where + T: Default + Send + Sync + 'static, + N: Grammar, + H: TaskHandle, + S: SyncBuildHasher, + { + // Safety: `CHECK` set to true + unsafe { self.change::(task, map) } + } + + /// Provides read-only access to the slot's value. + /// + /// This function is similar to the [Slot::read] function, but requires + /// an additional generic parameter `T` that specifies the type of + /// the [Slot]'s value. + /// + /// If the `T` parameter does not match the Slot's value type, the function + /// returns a [TypeMismatch](AnalysisError::TypeMismatch) error. + #[inline(always)] + pub fn read<'a, T, N, H, S>( + &self, + reader: &mut AttrContext<'a, N, H, S>, + ) -> AnalysisResult> + where + T: Default + Send + Sync + 'static, + N: Grammar, + H: TaskHandle, + S: SyncBuildHasher, + { + // Safety: `CHECK` set to true + unsafe { self.fetch::(reader) } + } + + /// Returns true if the slot referred to by this SlotRef exists in + /// the Analyzer's database. + /// + /// The `task` parameter could be a task that grants any kind of access to + /// the Analyzer: + /// [AnalysisTask](crate::analysis::AnalysisTask), + /// [MutationTask](crate::analysis::MutationTask), + /// or [ExclusiveTask](crate::analysis::ExclusiveTask). + #[inline(always)] + pub fn is_valid_ref( + &self, + task: &mut impl AbstractTask, + ) -> bool { + let Some(records) = task.analyzer().db.records.get(&self.id) else { + return false; + }; + + records.slots.contains(&self.entry) + } +} + +enum SlotInner { + Uninit(NodeRef), + + Init { + attr_ref: SlotRef, + database: Weak, + }, +} diff --git a/work/crates/main/src/analysis/tasks.rs b/work/crates/main/src/analysis/tasks.rs index f7dd3c9..7c6ed30 100644 --- a/work/crates/main/src/analysis/tasks.rs +++ b/work/crates/main/src/analysis/tasks.rs @@ -510,6 +510,20 @@ pub trait AbstractTask: TaskSeale Ok(class_to_nodes.nodes.clone()) } + + /// Provides access to the Analyzer's + /// [common semantics](Grammar::CommonSemantics), a special semantic + /// feature that is instantiated during the Analyzer's creation. It does + /// not belong to any specific document and is common across the entire + /// Analyzer. + /// + /// If the Analyzer's grammar does not specify common semantics, this + /// function returns a reference to the + /// [VoidFeature](crate::analysis::VoidFeature). + #[inline(always)] + fn common(&self) -> &N::CommonSemantics { + &self.analyzer().common + } } pub trait TaskSealed {