<p>Correct me if I'm wrong, but as I understand it, a compiler infrastructure such as Roslyn and maybe the JavaParser's symbol solver defines a semantic model with an unified semantic data type often denominated <code>Symbol</code>.</p>
<p>The <code>Symbol</code> data type is one of several variants, and each variant may be a subvariant in different depths. For example, there may be:</p>
<ul>
<li>Aliases</li>
<li>Classes</li>
<li>Types after substitution</li>
<li>Variable slots
<ul>
<li>Variable slot after type substitution</li>
</ul>
</li>
<li>Method slots
<ul>
<li>Method slot after type substitution</li>
</ul>
</li>
<li>Values
<ul>
<li>Constant values</li>
<li>Reference values</li>
</ul>
</li>
</ul>
<p>To facilitate accessing the compound data of the several variants of a <code>Symbol</code>, the <code>Symbol</code> data type is responsible for defining several methods that work across more than one variant; for example:</p>
<ul>
<li><code>name()</code> could return a text that represents the non qualified name of either a variable slot, a class, an alias, or method slot</li>
</ul>
<p>If the language where the semantic model is written in does not support class inheritance, then several <code>is_</code> prefixed methods would be provided; for example:</p>
<ul>
<li><code>is_variable_slot()</code></li>
<li><code>is_class()</code></li>
<li><code>is_variable_slot_after_type_substitution()</code></li>
</ul>
<p>In Rust I've explored implementing the semantic model for a language using an arena of strong references, and a <code>Symbol</code> tuple structure consisting of a weak reference to data in that arena, initialized from a factory from a <code>CompilerHost</code>.</p>
<p>I've found it too much work to structure all this kind of semantic model in Rust without using class inheritance, so I'm wondering whether I'm doing it in a fine way?</p>
<p>I say so because I've to provide a <code>match</code> expression in Rust repeatedly for destructuring the various variants of symbols in a lot of different methods, and the code got a chaos.</p>
<p>In the end, I got happy with the result, but I'd like to know if there's a macro maybe for using inheritance?</p>
<h1>Dynamic dispatch</h1>
<p>Dynamic dispatch, that is, implementations of methods with patterns such as:</p>
<p>(Example from my old Jet code.)</p>
<blockquote>
<p>Note: Yes, I know <code>dyn</code> trait objects do actual dynamic dispatch.</p>
</blockquote>
<blockquote>
<p>Note: in the following code, <code>data</code> has different types above, thus the lack of match <code>|</code> separators for code reuse.</p>
</blockquote>
<pre class="lang-rust prettyprint-override"><code>...
    pub fn is_virtual_property(&amp;self) -&gt; bool {
        matches!(
            self.0.upgrade().unwrap().as_ref(),
            SymbolKind::VirtualProperty(_) | SymbolKind::VirtualPropertyAfterIndirectTypeSubstitution(_)
        )
    }

    pub fn parent(&amp;self) -&gt; Option&lt;Symbol&gt; {
        let symbol = self.0.upgrade().unwrap();
        match symbol.as_ref() {
            SymbolKind::Type(TypeKind::ClassType(data)) =&gt; data.parent.borrow().clone(),
            SymbolKind::Type(TypeKind::EnumType(data)) =&gt; data.parent.borrow().clone(),
            SymbolKind::Type(TypeKind::InterfaceType(data)) =&gt; data.parent.borrow().clone(),
            SymbolKind::Type(TypeKind::TypeAfterExplicitTypeSubstitution(data)) =&gt; data.origin.parent(),
            SymbolKind::Alias(data) =&gt; data.parent.borrow().clone(),
            SymbolKind::Package(data) =&gt; data.parent.borrow().clone(),
            SymbolKind::PackageSet(data) =&gt; data.parent.borrow().clone(),
            SymbolKind::VariableProperty(data) =&gt; data.parent.borrow().clone(),
            SymbolKind::VariablePropertyAfterIndirectTypeSubstitution(data) =&gt; data.origin.parent(),
            SymbolKind::VirtualProperty(data) =&gt; data.parent.borrow().clone(),
            SymbolKind::VirtualPropertyAfterIndirectTypeSubstitution(data) =&gt; data.origin.parent(),
            SymbolKind::Function(data) =&gt; data.parent.borrow().clone(),
            SymbolKind::MethodAfterIndirectTypeSubstitution(data) =&gt; data.origin.parent(),
            SymbolKind::Scope(data, _) =&gt; data.parent.borrow().clone(),
            _ =&gt; None,
        }
    }

    pub fn extends_class(&amp;self, host: &amp;SymbolHost) -&gt; Option&lt;Symbol&gt; {
        let symbol = self.0.upgrade().unwrap();
        match symbol.as_ref() {
            SymbolKind::Type(TypeKind::ClassType(data)) =&gt; data.extends_class.borrow().clone(),
            SymbolKind::Type(TypeKind::EnumType(_)) =&gt; Some(host.object_type()),
            SymbolKind::Type(TypeKind::TypeAfterExplicitTypeSubstitution(data)) =&gt; {
                if let Some(r) = data.extends_class.borrow().as_ref() {
                    return Some(r.clone());
                }
                let r = data.origin.extends_class(host);
                if r.is_none() {
                    return None;
                }
                let r = r.unwrap();
                if r.is_unresolved() {
                    return Some(r.clone());
                }
                let r = TypeSubstitution(host).execute(&amp;r, &amp;data.origin.type_parameters().unwrap(), &amp;data.substitute_types);
                data.extends_class.replace(Some(r.clone()));
                Some(r)
            },
            _ =&gt; panic!(),
        }
    }

...
</code></pre>
<p>is very common.</p>
<p>The intuition of all these methods is to facilitate developing the compiler and its semantic analyser.</p>
<p>The problem with all of this is that the structures are decentralized and I've to go from one point of the code to another point of the code whenever modifying the semantic model, including even the occasion of introducing enumerations for new categories of variants.</p>

<semantics><symbol-solving>
Semantic modeling in Rust
CC BY-SA 4.0
2024-04-28T21:44:42.943Z
In Rust you generally use enums, and handle every variant in the same function (note, they are not technically methods despite the syntax for calling them), instead of writing separate implementations of each method for each variant. Typically you should use a `match` or `if let` statement instead of `is_XXXXX()` functions, because usually you don't just want to know which variant you have, you also want to do something with the data.
kaya3
CC BY-SA 4.0
2024-04-28T21:47:32.673Z
For the "after type substitution" variants, if type substitution is a specific stage of compilation then it probably makes sense to have separate enums for before and after this stage, since most code should know whether it belongs before or after type substitution has been performed.
kaya3
CC BY-SA 4.0
2024-04-28T21:50:21.723Z
I've written all of this in comments because I'm not sure if it really answers your question. The question you actually wrote is *"I'd like to know if there's a macro maybe for using inheritance?"*, which seems unrelated to everything else, and it's not clear what such a macro would be supposed to do given that Rust's type system simply doesn't support subtyping for structs or enums.
kaya3
CC BY-SA 4.0
2024-04-28T21:53:30.133Z
@kaya3 Yes, I've used enumerations for the various variations in different depths and more abstract layers over them. I use `is_x()` instead of `match!(..., PATTERN)` because the match pattern is very large to write. To be real, these `is_x()` methods are used to avoid pattern matching and using the same symbol reference to access data using lots of dynamic dispatches behind the scenes. I'm actually even implementing a solutional macro for this, but I don't want to share much right now until I finish it.
Hydroper
CC BY-SA 4.0
2024-04-28T22:10:59.570Z
It's not clear from your question why dynamic dispatch is needed for anything. If you intend this question to be self-answered, you still need to make sure that the question clearly describes the problem you are attempting to solve.
kaya3
CC BY-SA 4.0
2024-04-28T23:35:30.667Z
Post-edit, it's still not clear why you need dynamic dispatch; the problem seems to be that you want to design your compiler as if it were written in C++ or some other object-oriented language, but you also don't want to use that other language for some reason. I'll note that your `match` expressions have a lot of duplicated code which, assuming `data` is the same type in many of those branches, could be simplified using patterns like `SymbolKind::Alias(data) | SymbolKind::Package(data) | ...`. The arms for `SymbolKind::Type` would be simplified if `TypeKind` had its own `parent` function.
kaya3
CC BY-SA 4.0
2024-04-28T23:38:31.263Z
Also none of the code in your example actually seems to use dynamic dispatch. In Rust, dynamic dispatch is done using `dyn` trait types.
kaya3
CC BY-SA 4.0
2024-04-28T23:56:01.630Z
@kaya3 I used this terminology for illustration purposes; but yes, it's a form of dynamic dispatch manually done.
Hydroper
CC BY-SA 4.0
2024-04-28T23:57:07.360Z
@kaya3 These repeated matches have different `data` types.
Hydroper
CC BY-SA 4.0
2024-05-01T17:48:46.740Z
Fun story, we had a HUGE argument that lasted for months about "symbol" in Roslyn, an argument which I lost. My argument was that given the things we were using it for, it ought to be called "thingy". Symbols are not necessarily things with names, source locations, metadata locations, and so on; there is no concept in the business domain of a compiler that "symbol" specifically represents.
Eric Lippert
CC BY-SA 4.0
2024-05-01T17:50:54.050Z
And of course, the world did not end as a result of what was to my mind a poor design compromise. But I was vindicated a couple years later when I went to work for Coverity and one of my coworkers called me over to his desk and said Eric, I'm reading the Roslyn API documentation and I cannot for the life of me figure out what a symbol is for, you designed this, explain it to me. Of course my response was I KNOW, RIGHT?
Eric Lippert
CC BY-SA 4.0
2024-05-01T17:53:59.430Z
All this is to say: I feel your pain, this is a tricky point in designing a modern compiler-as-a-service, and experienced experts disagree on how to do it effectively.
Eric Lippert
CC BY-SA 4.0
2024-05-01T19:37:08.927Z
@EricLippert Yeah, so we go the covarianter way ;) In my initial examples of SModel for Rust I was thinking of "meaning" rather than "symbol", but I ended up finding "symbol" just more readable. "Thingy" doesn't look that bad either.
Hydroper
<p>I have just implemented the <a href="https://github.com/hydroper/rust.smodel" rel="nofollow noreferrer">SModel</a> crate for Rust, which solves this in a fine way.</p>
<p>It requires that you define symbol data types in ascending order, and it may not be entirely flexible, but it does support dynamic dispatching by performing two passes on your defined methods and finishing dynamic dispatch code at that second phase.</p>
<p>The <code>#[inheritdoc]</code> attribute works across methods, which inherits the RustDoc comment of the <em>nearest</em> base method.</p>
<p>Just like in the question, SModel will use an arena of <code>std::rc::Rc</code> that allows for circular references. Constructors (<code>M::new</code>) will take an auto prepended <code>arena</code> borrow.</p>
<p>The procedural macro generates enumerations at varying depths, and generates <code>.is::&lt;T&gt;()</code> methods for you, as well as <code>.to::&lt;T&gt;()</code>, that rely on <code>TryFrom</code>.</p>
<p>The auto generated code from the <code>smodel!</code> macro is huge to post here, so I am leaving a <a href="https://github.com/hydroper/rust.smodel/wiki/Auto-generated-code" rel="nofollow noreferrer">link</a> here.</p>

CC BY-SA 4.0
2024-04-28T22:33:27.663Z
2024-05-04T13:40:57.787Z
2024-05-04T13:40:57.787Z
Hydroper
# Semantic modeling in Rust
Posted by: username_0 on 2024-04-28
Correct me if I'm wrong, but as I understand it, a compiler infrastructure such as Roslyn and maybe the JavaParser's symbol solver defines a semantic model with an unified semantic data type often denominated Symbol.
The Symbol data type is one of several variants, and each variant may be a subvariant in different depths. For example, there may be:

Aliases
Classes
Types after substitution
Variable slots

Variable slot after type substitution


Method slots

Method slot after type substitution


Values

Constant values
Reference values



To facilitate accessing the compound data of the several variants of a Symbol, the Symbol data type is responsible for defining several methods that work across more than one variant; for example:

name() could return a text that represents the non qualified name of either a variable slot, a class, an alias, or method slot

If the language where the semantic model is written in does not support class inheritance, then several is_ prefixed methods would be provided; for example:

is_variable_slot()
is_class()
is_variable_slot_after_type_substitution()

In Rust I've explored implementing the semantic model for a language using an arena of strong references, and a Symbol tuple structure consisting of a weak reference to data in that arena, initialized from a factory from a CompilerHost.
I've found it too much work to structure all this kind of semantic model in Rust without using class inheritance, so I'm wondering whether I'm doing it in a fine way?
I say so because I've to provide a match expression in Rust repeatedly for destructuring the various variants of symbols in a lot of different methods, and the code got a chaos.
In the end, I got happy with the result, but I'd like to know if there's a macro maybe for using inheritance?
Dynamic dispatch
Dynamic dispatch, that is, implementations of methods with patterns such as:
(Example from my old Jet code.)

Note: Yes, I know dyn trait objects do actual dynamic dispatch.


Note: in the following code, data has different types above, thus the lack of match | separators for code reuse.

...
    pub fn is_virtual_property(&self) -> bool {
        matches!(
            self.0.upgrade().unwrap().as_ref(),
            SymbolKind::VirtualProperty(_) | SymbolKind::VirtualPropertyAfterIndirectTypeSubstitution(_)
        )
    }

    pub fn parent(&self) -> Option {
        let symbol = self.0.upgrade().unwrap();
        match symbol.as_ref() {
            SymbolKind::Type(TypeKind::ClassType(data)) => data.parent.borrow().clone(),
            SymbolKind::Type(TypeKind::EnumType(data)) => data.parent.borrow().clone(),
            SymbolKind::Type(TypeKind::InterfaceType(data)) => data.parent.borrow().clone(),
            SymbolKind::Type(TypeKind::TypeAfterExplicitTypeSubstitution(data)) => data.origin.parent(),
            SymbolKind::Alias(data) => data.parent.borrow().clone(),
            SymbolKind::Package(data) => data.parent.borrow().clone(),
            SymbolKind::PackageSet(data) => data.parent.borrow().clone(),
            SymbolKind::VariableProperty(data) => data.parent.borrow().clone(),
            SymbolKind::VariablePropertyAfterIndirectTypeSubstitution(data) => data.origin.parent(),
            SymbolKind::VirtualProperty(data) => data.parent.borrow().clone(),
            SymbolKind::VirtualPropertyAfterIndirectTypeSubstitution(data) => data.origin.parent(),
            SymbolKind::Function(data) => data.parent.borrow().clone(),
            SymbolKind::MethodAfterIndirectTypeSubstitution(data) => data.origin.parent(),
            SymbolKind::Scope(data, _) => data.parent.borrow().clone(),
            _ => None,
        }
    }

    pub fn extends_class(&self, host: &SymbolHost) -> Option {
        let symbol = self.0.upgrade().unwrap();
        match symbol.as_ref() {
            SymbolKind::Type(TypeKind::ClassType(data)) => data.extends_class.borrow().clone(),
            SymbolKind::Type(TypeKind::EnumType(_)) => Some(host.object_type()),
            SymbolKind::Type(TypeKind::TypeAfterExplicitTypeSubstitution(data)) => {
                if let Some(r) = data.extends_class.borrow().as_ref() {
                    return Some(r.clone());
                }
                let r = data.origin.extends_class(host);
                if r.is_none() {
                    return None;
                }
                let r = r.unwrap();
                if r.is_unresolved() {
                    return Some(r.clone());
                }
                let r = TypeSubstitution(host).execute(&r, &data.origin.type_parameters().unwrap(), &data.substitute_types);
                data.extends_class.replace(Some(r.clone()));
                Some(r)
            },
            _ => panic!(),
        }
    }

...

is very common.
The intuition of all these methods is to facilitate developing the compiler and its semantic analyser.
The problem with all of this is that the structures are decentralized and I've to go from one point of the code to another point of the code whenever modifying the semantic model, including even the occasion of introducing enumerations for new categories of variants.
**Tags:** semantics symbol-solving
> **Comments:**
> **username_1**: In Rust you generally use enums, and handle every variant in the same function (note, they are not technically methods despite the syntax for calling them), instead of writing separate implementations of each method for each variant. Typically you should use a `match` or `if let` statement instead of `is_XXXXX()` functions, because usually you don't just want to know which variant you have, you also want to do something with the data.  
> **username_1**: For the "after type substitution" variants, if type substitution is a specific stage of compilation then it probably makes sense to have separate enums for before and after this stage, since most code should know whether it belongs before or after type substitution has been performed.  
> **username_1**: I've written all of this in comments because I'm not sure if it really answers your question. The question you actually wrote is *"I'd like to know if there's a macro maybe for using inheritance?"*, which seems unrelated to everything else, and it's not clear what such a macro would be supposed to do given that Rust's type system simply doesn't support subtyping for structs or enums.  
> **username_0**: @kaya3 Yes, I've used enumerations for the various variations in different depths and more abstract layers over them. I use `is_x()` instead of `match!(..., PATTERN)` because the match pattern is very large to write. To be real, these `is_x()` methods are used to avoid pattern matching and using the same symbol reference to access data using lots of dynamic dispatches behind the scenes. I'm actually even implementing a solutional macro for this, but I don't want to share much right now until I finish it.  
> **username_1**: It's not clear from your question why dynamic dispatch is needed for anything. If you intend this question to be self-answered, you still need to make sure that the question clearly describes the problem you are attempting to solve.  
> **username_1**: Post-edit, it's still not clear why you need dynamic dispatch; the problem seems to be that you want to design your compiler as if it were written in C++ or some other object-oriented language, but you also don't want to use that other language for some reason. I'll note that your `match` expressions have a lot of duplicated code which, assuming `data` is the same type in many of those branches, could be simplified using patterns like `SymbolKind::Alias(data) | SymbolKind::Package(data) | ...`. The arms for `SymbolKind::Type` would be simplified if `TypeKind` had its own `parent` function.  
> **username_1**: Also none of the code in your example actually seems to use dynamic dispatch. In Rust, dynamic dispatch is done using `dyn` trait types.  
> **username_0**: @kaya3 I used this terminology for illustration purposes; but yes, it's a form of dynamic dispatch manually done.  
> **username_0**: @kaya3 These repeated matches have different `data` types.  
> **username_2**: Fun story, we had a HUGE argument that lasted for months about "symbol" in Roslyn, an argument which I lost. My argument was that given the things we were using it for, it ought to be called "thingy". Symbols are not necessarily things with names, source locations, metadata locations, and so on; there is no concept in the business domain of a compiler that "symbol" specifically represents.  
> **username_2**: And of course, the world did not end as a result of what was to my mind a poor design compromise. But I was vindicated a couple years later when I went to work for Coverity and one of my coworkers called me over to his desk and said Eric, I'm reading the Roslyn API documentation and I cannot for the life of me figure out what a symbol is for, you designed this, explain it to me. Of course my response was I KNOW, RIGHT?  
> **username_2**: All this is to say: I feel your pain, this is a tricky point in designing a modern compiler-as-a-service, and experienced experts disagree on how to do it effectively.  
> **username_0**: @EricLippert Yeah, so we go the covarianter way ;) In my initial examples of SModel for Rust I was thinking of "meaning" rather than "symbol", but I ended up finding "symbol" just more readable. "Thingy" doesn't look that bad either.  
---
## 1 answers
### Answer by username_0 on 2024-04-28. Score: -1
I have just implemented the [SModel](https://github.com/hydroper/rust.smodel) crate for Rust, which solves this in a fine way.

It requires that you define symbol data types in ascending order, and it may not be entirely flexible, but it does support dynamic dispatching by performing two passes on your defined methods and finishing dynamic dispatch code at that second phase.

The `#[inheritdoc]` attribute works across methods, which inherits the RustDoc comment of the *nearest* base method.

Just like in the question, SModel will use an arena of `std::rc::Rc` that allows for circular references. Constructors (`M::new`) will take an auto prepended `arena` borrow.

The procedural macro generates enumerations at varying depths, and generates `.is::()` methods for you, as well as `.to::()`, that rely on `TryFrom`.

The auto generated code from the `smodel!` macro is huge to post here, so I am leaving a [link](https://github.com/hydroper/rust.smodel/wiki/Auto-generated-code) here.
---
