-
Notifications
You must be signed in to change notification settings - Fork 64
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Public API for traversing the design hierarchy / AST #219
Comments
Some preliminary thoughts from myself without getting too much into details:
Are any of these out of scope? Did I miss anything? What do you think? |
Regarding scope I am a believer in adding only what is currently needed. We can add more stuff in the future when there is a use case that can drive the design. I find one rarely gets the API right if you add everything up front. For now I think the goal should be to have a nice API without duplication of Search/Visitor that supports completions, lsp and document generation as the initial scope. |
For navigation I want to add, that getting parents declarations from AST elements is important as well. Which may already be possible with Even if it is not immediately required, it would probably be good to ensure that AST elements are traversed in the order they appear in the source code, as that would probably be rather hard to rework after the fact. The current implementation does seem to do that as well. I may encounter further things when I come around to play with the current implementation. |
AnyEnt has a parent field. So this is already supported.
Yes this has always been a goal so that sorting on srcpos is not necessary. I add it as an explicit requirement above. |
There are two main things I have in mind for the API:
With these two things in mind I think we could create a type like this: enum NodeRef<'a> {
Declaration(&'a ast::Declaration),
Sequential(&'a ast::Sequential),,
Concurrent(&'a ast::Concurrent),
}
trait<'a> NodeVisitor<'a> {
// Only iterates over immediate children, not recursively
fn for_each_child(&self, fun: FnMut(NodeRef<'a>));
// An alternative is to use impl Iterator<Item = NodeRef<'a>> but I think the type inferred by impl Iterator would be complex to create without Box<dyn Iterator> however maybe the heap allocation is not so bad as @Schottkyc137 benchmarked.
} The API on the top level would be the impl <'a> Root<'a> {
fn libraries(&self) -> impl Iterator<item = Library<'a>> {}
fn get_library(&self, sym: &Symbol) -> Option<Library<'a>> {}
}
impl <'a> Library<'a> {
fn primary_design_units(&self) -> impl Iterator<item = DesignUnit<'a>> {}
fn get_primary_unit(&self, sym: &Symbol) -> Option<DesignUnit<'a>> {}
}
impl <'a> DesignUnit<'a> {
fn secondary_design_units(&self) -> impl Iterator<item = DesignUnit<'a>> {}
fn get_secondary_unit(&self, sym: &Symbol) -> Option<DesignUnit<'a>> {}
fn context_clause(&self) -> ContextClause<&'a> {}
}
impl<'a> NodeVisitor<'a> for DesignUnit {}
|
What I really like about the current visitor implementation is that simple use cases require simple code. The use case Get all procedure declarations can be achieved by only implementing the Although I do not know how easy it is to use custom iterators, they would allow the user a great amount of control, if needed. I could imagine uses like these: let design_root = ...;
// Get all primary units from the design
let primary_units_iter = design_root.get_primary_units(); // impl Iterator<item = DesignUnit>
// Get all declarations from the design
let declarations_iter = design_root.get_declarations(); // impl Iterator<item = Declaration> By using iterators where possible the user would have access to iterator functions like The relevant traits could look like this: trait PrimaryUnitVisitor {
fn get_primary_units(&self) -> impl Iterator<item = PrimaryUnit>;
}
trait DeclarationVisitor {
fn get_declarations(&self) -> impl Iterator<item = Declaration>;
} By implementing them for the right types ( // Get all declarations from all primary units (even .filter(...) might be possible between get_primary_units and get_declarations)
let declarations_iter = design_root.get_primary_units().get_declarations(); // impl Iterator<item = Declaration> Please bear in mind that there may be some roadblocks I did not consider (because of lack of experience), but I think an iterator based API (if possible) would be quite versatile and powerful. |
@skaupper A context free vistor API could always be built as an utility on top of the API I proposed. I do not think we need a visitor function for every litte thing from day one though. I would rather have the use cases drive the design when they occur. Day one is enough with an API for the current use cases. Regarding methods returning |
That's fine for me. I wasn't proposing a visitor API as it is now, just wanted to emphasize its simplicity in cases where context is not needed. For the initial design it should be sufficient to iterate declarations and statements.
Too bad that is not possible as of right now. I do like your API suggestion, though. Equivalent to impl <'a> DesignUnit<'a> {
fn children(&self) -> impl Iterator<item = NodeRef<'a>> {}
} If possible, I would prefer it to write |
Maybe the right trade-off in this case is to sacrifice a little bit of performance and use a boxed dyn Iterator: fn children(&self) -> Box<dyn Iterator<Item=NodeRef<'a>>> It is at least better than using a |
As it seems the overall design is ok I will create an implementation of my proposal and make an PR so we can discuss more details. |
I made a first draft here: #228 |
I added a benchmark that can be run with |
I am currently also working on a bottom-up refactor to bring the Searcher and the Visitor closer together. I already fixed that the Searcher does not need &mut on the AST. I use AtomicUsize to clear references instead. |
@Schottkyc137 I did some benchmarking on Search trait vs Visitor. You can se what I did on the benchmark branch.
As you can see the Search trait is almost 8x faster. I will try to rewrite the Visitor to use an iterator instead of a vec and see if it helps. |
Alright then it's time to ditch the visitor (at least in its current implementation) :D |
Not at the moment. I will try to modify the |
@Schottkyc137 I added the stuff needed by completion.rs to the Search trait and used it instead there. Then I removed the Visitor trait so we only have one implementation. |
@kraigher Regarding feedback: I hope that I find some time this week to throw together an example to get a feel for the API. From what I've already tried, it seems a bit problematic if a whole |
@skaupper My idea would be to have a facade for the design hierarchy named |
So, I played around with it and looked mostly at two things:
For 1. I tried to be as concise as possible (If I have a single VHDL file, how do I get the AST for it?), which resulted in the following code: let config = Config::read_file_path(Path::new("../rust_hdl/vhdl_libraries/vhdl_ls.toml")).unwrap();
let mut project = Project::from_config(config, &mut NullMessages);
project.update_source(&Source::from_latin1_file(Path::new("./vhdl/package.vhd")).unwrap());
project.analyse();
let root = project.to_api_root(); What I do not like is, that analysing the project fails with a panic if For 2. I played around with the iterator API. let lib = root
.libraries()
.find(|lib| lib.name() == "work")
.expect("Did not find library work.");
println!("Library: {}", lib.name());
for primary_unit in lib.primary_units() {
println!(
" Primary: {}",
match primary_unit.get() {
AnyPrimaryUnit::Package(p) => p.ident.to_string(),
AnyPrimaryUnit::Entity(e) => e.ident.to_string(),
AnyPrimaryUnit::Context(c) => c.ident.to_string(),
_ => "<not implemented>".to_string(),
}
);
for secondary_unit in primary_unit.secondary_units() {
println!(
" Secondary: {}",
match secondary_unit.get() {
AnySecondaryUnit::Architecture(a) => a.ident.to_string(),
AnySecondaryUnit::PackageBody(p) => p.ident.to_string(),
}
);
}
} The simple for-loops work just fine, except that convenience functions like What does not work is chaining iterator functions in cases like this: let packages_in_work: Vec<_> = root
.libraries()
.filter(|lib| lib.name() == "work")
.flat_map(Library::primary_units) // This does not work!
.filter(|pu| matches!(pu.get(), AnyPrimaryUnit::Package(_)))
.collect();
// Do something with the filtered units
packages_in_work.iter().for_each(|p| println!("{}", p.name())); Since The last example of 2. can be fixed with some rather small changes, like providing consuming variants of For 1. I did not try to find a workaround/fix but it seems like some fields of |
On a different note: It seems to be a common pattern to expect What is your preferred way to obtain |
Doing analysis without the standard library is not feasible. Without analysis there would be no semantic information such as symbol references from use to declaration. What you want is just doing the parsing, that is still possible to do and you would get an AST without any references set. When only doing parsing there is not much use for the
Maybe the most user friendly is to just have public API functions take a |
You are right about that. Adding the parser to the API is definitely something I would want. Also I think it would not hurt to have a wrapper type (just like That way we could have the API you proposed for the analyzed AST, while for use cases which do not require the analyse step a simpler and less involved API for the parsed AST would be available. Maybe the analyzed API could event build on the parsed API. If you want to keep analyzed and parsed APIs, I would be happy to give the parsed API a go (but probably not before christmas), assuming that you mostly care about the analyzed API for the language server etc.
I agree that it's probably best if the users do not need to care about internals like |
I have been quiet for some time now as I generally agree with the points that @skaupper raises. However, I want to raise one more point concerning scope that I have alluded to earlier. While I agree with @kraigher that features should only be implemented if the desire arises, implementations should not forbid certain classes of features or make it unreasonable difficult to implement these features. I'm saying all of this to still be careful not to discourage future applications, even if there is no immediate use-case. |
You are right about that. Which makes me think that it would probably a good idea to make that enrichment more explicit. Currently the
Splitting these functionalities in three structs, there could be
In the future, this structure could be further extended by an For reference, a small example of how this struct could interact with each other, and how a user would have to use them: // Prepare sources
let config = Config::read_file_path(Path::new("../rust_hdl/vhdl_libraries/vhdl_ls.toml")).unwrap();
let source_file = Path::new("test.vhd");
// Create SourceProject
let mut source_project = SourceProject::from_config(config); // or SourceProject::new() for an empty project
source_project.add_file(&source_file);
// Create ParsedProject
let parsed_project = source_project.parse();
{
let parsed_api = parsed_project.api();
// Do something with the raw AST
}
// Create AnalysedProject
let analysed_project = parsed_project.analyse();
{
let analysed_api = analysed_project.api();
// Do something with the analysed AST
} To conclude, I see multiple benefits by such a restructuring:
Getting rid of some |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
We need to create a stable public API to traverse the design hierarchy and the AST. I am hoping @Schottkyc137 and @skaupper can contribute their ideas and feedback here. My goal is to replace the
Search
trait and theVisitor
trait with this implementation. First we need to gather some requirements than we can discuss the implementation.Performance requirements
Public requirements
Library
Design units
Declarations & Statements
Navigation
AnyEnt
) from source locationAnyEnt
)AnyEnt
)Private requirements
The text was updated successfully, but these errors were encountered: