Struct TokenTree

Source
pub struct TokenTree { /* private fields */ }
Expand description

A tree of token streams generated from a GLSL source string.

The tree represents all conditional compilation branches. Call the root(), evaluate() or with_key() method to parse an abstract syntax tree with the selected conditional branches into a ParseResult.

§Examples

For a fully detailed example on how to use this struct to create an AST, see the documentation for the parse_from_str() function.

§Why is this necessary?

Conditional compilation is implemented through the preprocessor, which sets no rules as to where conditional branching can take place, (apart from the fact that a preprocessor directive must exist on its own line). This means that a conditional branch could, for example, completely change the signature of a program:

 1│ void foo() {
 2│
 3│     int i = 5;
 4│
 5│     #ifdef TOGGLE
 6│     }
 7│     void bar() {
 8│     #endif
 9│
10│     int p = 0;
11│ }

In the example above, if TOGGLE is not defined, we have a function foo who’s scope ends on line 11 and includes two variable definitions i and p. However, if TOGGLE is defined, the function foo ends on line 6 instead and only contains the variable i, and we have a completely new function bar which has the variable p.

This technically can be representable in the AST, it’s just that it would look something like this:

Root(
    Either(
        (
            Function(
                name="foo"
                start=1
                end=11
                contents(
                    Variable(name="i" value=5)
                    Variable(name="p" value=0)
                )
            )
        ),
        (
            Function(
                name="foo"
                start=1
                end=6
                contents(
                    Variable(name="i" value=5)
                )
            ),
            Function(
                name="bar"
                start=7
                end=11
                contents(
                    Variable(name="p" value=0)
                )
            ),
        )
    )
)

Notice how this AST is effectively Either(AST_with_condition_false, AST_with_condition_true). This is because the function foo could potentially be split in the middle, but an AST node cannot have multiple end points, which means that we can’t include both permutations within the function node; we need separate function nodes instead. And since we have two separate possibilities for foo, we need to branch in the node above foo, which in this example is effectively the root node.

It is arguable whether such a representation would be better than the current solution. On one hand all possibilities are within the single AST, but on the other hand such an AST would quickly become confusing to work with, manipulate, and analyse in the scenario of complex conditional branching.

The main reason this option wasn’t chosen is because it would immensely complicate the parsing logic, and in turn the maintainability of this project. As with all recursive-descent parsers, the individual parsing functions hold onto any temporary state. In this case, the function for parsing functions holds information such as the name, the starting position, the parameters, etc. If we would encounter the conditional branching within this parsing function, we would somehow need to know ahead-of-time whether this conditional branch will affect the function node, and if so, be able to return up the call stack to split the parser whilst also somehow not losing the temporary state. This would require abandoning the recursive-descent approach, which would greatly complicate the parser and make writing & maintaining the parsing logic itself a convoluted mess, and that is not a trade-off I’m willing to take.

This complication occurs because the preprocessor is a separate pass ran before the main compiler and does not follow the GLSL grammar rules, which means that preprocessor directives and macros can be included literally anywhere and the file may still be valid after expansion. In comparison, some newer languages include conditional compilation as part of the language grammar itself. In Rust for example, conditional compilation is applied via attributes to entire expressions/statements, which means that you can’t run into this mess where a conditional branch could split a function mid-way through parsing. GLSL unfortunately uses the C preprocessor, which results in the approach taken by this crate being necessary to achieve 100% specification-compliant behaviour.

Note that macros can actually be correctly expanded within the same pass as the main parser without introducing too much complexity, it’s just that conditional compilation can’t.

Implementations§

Source§

impl TokenTree

Source

pub fn root(&self, syntax_highlight_entire_source: bool) -> ParseResult

Parses the root token stream; no conditional branches are included.

Whilst this is guaranteed to succeed, if the entire source string is wrapped within a conditional block this will return an empty AST.

§Syntax highlighting

The syntax_highlight_entire_source parameter controls whether to produce syntax tokens for the entire source string, rather than just for the root tokens. This involves parsing all conditional branches in order to produce all the syntax highlighting information. Whilst the implementation of this functionality uses the smallest possible number of permutations that cover the entire source string, if there are a lot of conditional branches that can result in the token tree being parsed many times which may have performance implications.

The actual syntax highlighting results are based off the chosen permutations which cannot be controlled. If you require more control, you must manually parse the relevant permutations and collect the tokens yourself.

If there are no conditional branches, this parameter does nothing.

§Examples

For a fully detailed example on how to use this method to create an abstract syntax tree, see the documentation for the parse_from_str() function.

Source

pub fn evaluate( &self, syntax_highlight_entire_source: bool, ) -> (ParseResult, Vec<usize>)

Parses the token tree by including conditional branches if they evaluate to true.

Whilst this is guaranteed to succeed, if the entire source string is wrapped within a conditional branch that fails evaluation this will return an empty AST. This method also returns the evaluated key.

§Syntax highlighting

The syntax_highlight_entire_source parameter controls whether to produce syntax tokens for the entire source string, rather than just for the included conditional branches. This involves parsing all conditional branches in order to produce all the syntax highlighting information. Whilst the implementation of this functionality uses the smallest possible number of permutations that cover the entire source string, if there are a lot of conditional branches that can result in the token tree being parsed many times which may have performance implications.

The actual syntax highlighting results are based off the chosen permutations which cannot be controlled. If you require more control, you must manually parse the relevant permutations and collect the tokens yourself.

If there are no conditional branches, or the only conditional branches that exist are also evaluated as true and included in the running of the parser, this parameter does nothing.

§Examples

For a fully detailed example on how to use this method to create an abstract syntax tree, see the documentation for the parse_from_str() function.

Source

pub fn with_key( &self, key: impl AsRef<[usize]>, syntax_highlight_entire_source: bool, ) -> Result<ParseResult, ParseErr>

Parses a token tree by including conditional branches if they are part of the provided key.

This method can return an Err in the following cases:

  • The key has a number which doesn’t map to a controlling conditional directive.
  • The key has a number which depends on another number that is missing.
§Syntax highlighting

The syntax_highlight_entire_source parameter controls whether to produce syntax tokens for the entire source string, rather than just for the selected conditional branches. This involves parsing all conditional branches in order to produce all the syntax highlighting information. Whilst the implementation of this functionality uses the smallest possible number of permutations that cover the entire source string, if there are a lot of conditional branches that can result in the token tree being parsed many times which may have performance implications.

The actual syntax highlighting results are based off the chosen permutations which cannot be controlled. If you require more control, you must manually parse the relevant permutations and collect the tokens yourself.

If there are no conditional branches, this parameter does nothing.

§Examples

For a fully detailed example on how to use this method to create an abstract syntax tree, see the documentation for the parse_from_str() function.

Source

pub fn get_all_controlling_conditional_directives( &self, ) -> Vec<(Conditional, Span)>

Returns a vector of all controlling conditional directives in the tree.

The return value consists of:

  • 0 - The conditional directive type. This cannot be Conditional::End.
  • 1 - Span of the directive.

Note that the first controlling conditional directive (index of 1) is at the beginning of this vector (index 0), so an offset must be performed.

Source

pub fn create_key(&self, chosen_conditional_directive: usize) -> Vec<usize>

Creates a new key to access the specified controlling conditional directive.

This method takes the index (of the chronological appearance) of the controlling conditional directive (#if/#ifdef/#ifndef/#elif/#else), and returns a key that reaches that conditional branch. The new key contains the minimal number of prerequisite branches necessary to reach the chosen directive.

Source

pub fn add_selection_to_key( &self, existing_key: &Vec<usize>, chosen_conditional_directive: usize, ) -> Vec<usize>

Modifies an existing key to access the specified controlling conditional directive.

This method keeps all existing conditional branches as long as they don’t conflict with the newly chosen branch.

Source

pub fn remove_selection_from_key( &self, existing_key: &Vec<usize>, removed_conditional_directive: usize, ) -> Vec<usize>

Modifies an existing key to remove access to the specified controlling conditional directive.

This method keeps all existing conditional branches as long as they don’t depend on the specified to-remove branch.

Source

pub fn contains_conditional_directives(&self) -> bool

Returns whether the source string contains any conditional directives.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.