3. Stratego API

The Stratego API to NaBL2 allows the customization of certain parts of the analysis process, and access to analysis result during after analysis.

The full definition of the API can be found in the nabl2/api module.

3.1. Setup

Using the Stratego API requires a dependency on the NaBL2 runtime, and an import of nabl2/api.

Example. A Stratego module importing the NaBL2 API.

module example

imports

  nabl2/api

3.2. Customizing analysis

Several aspects of the analysis process can be customized by implementing hooks in Stratego.

3.2.1. Custom pretty-printing

By default the object language terms that are mentioned in error messages are printed as generic terms. This may lead to error messages like Expected number, but got FunT(IntT(), Int()). By implementing the nabl2-prettyprint-hook, the term might be pretty-printed, resulting in a message like Expected number, but got 'int -> int'. Care needs to be taken though, because the terms might contain constraint variables, that are not part of the object language and would break pretty-printing. This can be fixed by injecting the nabl2-prettyprint-term strategy into the object language pretty-printing rule.

Example. A module that implements the pretty-printing hooks to format types from the object language. This assumes that types are defined in an SDF3 file using the sort Type.

module example

imports

  nabl2/api

rules

  nabl2-prettyprint-hook    = prettyprint-YOURLANG-Type
  prettyprint-YOURLANG-Type = nabl2-prettyprint-term

3.2.2. Custom analysis

Analysis in NaBL2 proceeds in three phases. An initial phase is used to create initial scopes and parameters. A second phase is used to collected, and partially solve, constraints per compilation unit. A final phase solves constraints. The constraints in the final phase are those of all compilation units combined, if multi-file analysis is enabled.

It is possible to run custom code after each of these phases, by implementing nabl2-custom-analysis-init-hook, nabl2-custom-analysis-unit-hook, and nabl2-custom-analysis-final-hook(|a).

The initial hook receives a tuple of a resource string and AST node as its argument. The result type of the initial hook is free.

The unit hook receives a tuple of a resource string, and AST node, and the initial result if the initial hook was implemented. If the initial hook is not implemented, an empty tuple () is passed as the initial result. The result type of the unit hook is free.

The final hook receives a tuple of a resource string, the result if the initial hook, and a list of results of the unit hook. The initial result will again be () if the initial hook is not implemented. The unit results might be an empty list if the unit hook is not implemented. The final hook also receives an term argument for the analysis result, that can be passed to the strategies to access the analysis result. The final hook should return a tuple of errors, warnings, notes, and a custom result. Messages should be tuples (origin-term, message) of the origin term from the AST, and the message to report.

Custom analysis hooks are advised to use the strategies nabl2-custom-analysis-info-msg(|msg) and nabl2-custom-analysis-info(|msg) to report to the user. The first version just logs the message term, while the second version also logs the current term. If formatting the info messages is expensive, the strategy nabl2-is-custom-analysis-info-enabled to check if logging is actually enabled. The advantage of using these logging strategies is that they are influenced by the logging settings in the project configuration.

Example. A Stratego module that shows how to set up custom analysis strategies.

module example

imports

  nabl2/api

rules

  nabl2-custom-analysis-init-hook:
      (resource, ast) -> custom-initial-result
    with nabl2-custom-analysis-info-msg(|"Custom initial analysis step");
         custom-initial-result := ...

  nabl2-custom-analysis-unit-hook:
      (resource, ast, custom-initial-result) -> custom-unit-result
    with <nabl2-custom-analysis-info(|"Custom unit analysis step")> resource;
         custom-unit-result := ...

  nabl2-custom-analysis-final-hook(|a):
      (resource, custom-initial-result, custom-unit-results) -> (errors, warnings, notes, custom-final-result)
    with nabl2-custom-analysis-info-msg(|"Custom final analysis step");
         custom-final-result := ... ;
         errors   := ... ;
         warnings := ... ;
         notes    := ...

3.3. Querying analysis

The analysis API gives access to the result of analysis. The analysis result is available during the final custom analysis step, or in post-analysis transformations.

The API defines several strategies to get an analysis term by resource name or from an AST node. This analysis term can then be passed to the querying strategies that give access to the scope graph, name resolution, etc.

3.3.1. Getting the analysis result

/**
 * Get analysis for the given AST node
 *
 * @type node:Term -> Analysis
 */
nabl2-get-ast-analysis

/**
 * Get analysis for the given resource
 *
 * @type filename:String -> Analysis
 */
nabl2-get-resource-analysis

/**
 * Test if analysis has errors
 *
 * Fails if there are no errors, succeeds otherwise.
 *
 * @type Analysis -> _
 */
nabl2-analysis-has-errors

There are two ways to get the result of analysis. The first is calling nabl2-get-ast-analysis on a node if the analyzed AST. The second is to call nabl2-get-resource-analysis with a resource name. The resulting term can be passed as a term argument to the different query strategies.

To check if analysis was successful, the strategy nabl2-analysis-has-errors can be used. This strategy will succeed if any errors were encountered, and fail otherwise.

Example. Builder that only runs if analysis has no errors.

module example

imports

  nabl2/api

rules

  example-builder:
      (_, _, ast, path, project-path) -> (output-file, result)
    where analysis := <nabl2-get-resource-analysis> $[[project-path]/[path]];
          <not(nabl2-analysis-has-errors)> analysis
    with output-file := ... ;
         result      := ...

3.3.2. AST properties

/**
 * @param a : Analysis
 * @type node:Term -> Term
 */
nabl2-get-ast-params(|a)

/**
 * @param a : Analysis
 * @type node:Term -> Type
 */
nabl2-get-ast-type(|a)

AST nodes are associated with the parameters and (optionally) the type mentioned in the rule that was applied to the node. For example, if a rule like [[ e ^ (s) : ty ]] was applied to an expression in the AST, it is possible to query the analysis for the scope s and the type ty. The strategy nabl2-get-ast-params(|a) expects an AST node, and returns a tuple with the parameters. Similary nabl2-get-ast-type(|a) expects an AST node and returns the type. If no type was specified, for example in a rule such as [[ e ^ (s1, s2) ]], the call will fail. The term argument a should be an analysis result.

Nodes in the AST are indexed to make the connection between the AST and the analysis result. The following strategies can be used to preserve or manipulate AST indices. Note that this has no effect on the result of analysis, so whether such manipulation is sound is up to the user.

/**
 * Get AST index. Fails if term has no index.
 *
 * @type Term -> TermIndex
 */
nabl2-get-ast-index

/**
 * Set AST index on a term. Throws an exception of the index argument
 * is not a valid index.
 *
 * @param index : Termindex
 * @type Term -> Term
 */
nabl2-set-ast-index(|index)

/**
 * Copy AST index from one term to another. Fails if the source has no
 * index.
 *
 * @param from : Termindex
 * @type Term -> Term
 */
nabl2-copy-ast-index(|from)

/**
 * Execute a strategy and copy the index of the input term to the output
 * term. If the original term has no index, the result of applying s is
 * returned unchanged. Thus, failure behaviour of s is preserved.
 *
 * @type Term -> Term
 */
nabl2-preserve-ast-index(s)

/**
 * Erase AST indices from a term, preserving other annotations and
 * attachments.
 *
 * @type Term -> Term
 */
nabl2-erase-ast-indices

3.3.3. Scope graph & name resolution

The strategies concerning scope graphs and name resolution are organized in three groups. The first group are strategies to create and query occurrences in the scope graph. The second group gives access to the structure of the scope graph. The third group exposes the result of name resolution, as well as types and properties that are set on declarations.

Working with occurrences

/**
 * Make an occurrence in the default namespace
 *
 * NaBL2 equivalent: {node}
 *
 * @type node:Term -> Occurrence
 */
nabl2-mk-occurrence

/**
 * Make an occurrence in the specified namespace
 *
 * NaBL2 equivalent: ns{node}
 *
 * @param ns : String
 * @type node:Term -> Occurrence
 */
nabl2-mk-occurrence(|ns)

/**
 * Make an occurrence in the specified namespace, using an origin term
 *
 * NaBL2 equivalent: ns{node @t}
 *
 * @param ns : String
 * @param t : Term
 * @type node:Term -> Occurrence
 */
nabl2-mk-occurrence(|ns,t)

/**
 * Get namespace of an occurrence
 *
 * @type Occurrence -> ns:String
 */
nabl2-get-occurrence-ns

/**
 * Get name of an occurrence
 *
 * @type Occurrence -> Term
 */
nabl2-get-occurrence-name

Querying the scope graph

/**
 * Get all declarations in the scope graph
 *
 * @param a : Analysis
 * @type _ -> List(Occurrences)
 */
nabl2-get-all-decls(|a)

/**
 * Get all references in the scope graph
 *
 * @param a : Analysis
 * @type _ -> List(Occurrences)
 */
nabl2-get-all-refs(|a)

/**
 * Get all scopes in the scope graph
 *
 * @param a : Analysis
 * @type _ -> List(Scope)
 */
nabl2-get-all-scopes(|a)

/**
 * Get the scope of a reference
 *
 * @param a : Analysis
 * @type ref:Occurrence -> Scope
 */
nabl2-get-ref-scope(|a)

/**
 * Get the scope of a declaration
 *
 * @param a : Analysis
 * @type decl:Occurrence -> Scope
 */
nabl2-get-decl-scope(|a)

/**
 * Get declarations in a scope
 *
 * @param a : Analysis
 * @type Scope -> List(Occurrence)
 */
nabl2-get-scope-decls(|a)

/**
 * Get references in a scope
 *
 * @param a : Analysis
 * @type Scope -> List(ref:Occurrence)
 */
nabl2-get-scope-refs(|a)

/**
 * Get direct edges from a scope
 *
 * @param a : Analysis
 * @type Scope -> List((Label,Scope))
 * @type (Scope,Label) -> List(Scope)
 */
nabl2-get-direct-edges(|a)

/**
 * Get inverse direct edges from a scope
 *
 * @param a : Analysis
 * @type Scope -> List((Label,Scope))
 * @type (Scope,Label) -> List(Scope)
 */
nabl2-get-direct-edges-inv(|a)

/**
 * Get import edges from a scope
 *
 * @param a : Analysis
 * @type Scope -> List((Label,ref:Occurrence))
 * @type (Scope,Label) -> List(ref:Occurrence)
 */
nabl2-get-import-edges(|a)

/**
 * Get inverse import edges from a reference
 *
 * @param a : Analysis
 * @type ref:Occurrence -> List((Label,Scope))
 * @type (ref:Occurrence,Label) -> List(Scope)
 */
nabl2-get-import-edges-inv(|a)

/**
 * Get associated scopes of a declaration
 *
 * @param a : Analysis
 * @type decl:Occurrence -> List((Label,Scope))
 * @type (decl:Occurrence,Label) -> List(Scope)
 */
nabl2-get-assoc-edges(|a)

/**
 * Get associated declarations of a scope
 *
 * @param a : Analysis
 * @type Scope -> List((Label,decl:Occurrence))
 * @type (Scope,Label) -> List(decl:Occurrence)
 */
nabl2-get-assoc-edges-inv(|a)

Querying name resolution

/**
 * @param a : Analysis
 * @type decl:Occurrence -> Type
 */
nabl2-get-type(|a)

/**
 * @param a : Analysis
 * @param prop : String
 * @type decl:Occurrence -> Term
 */
nabl2-get-property(|a,prop)

/**
 * @param a : Analysis
 * @type ref:Occurrence -> (decl:Occurrence, Path)
 */
nabl2-get-resolved-name(|a)

/**
 * @param a : Analysis
 * @type ref:Occurrence -> List((decl:Occurrence, Path))
 */
nabl2-get-resolved-names(|a)

/**
 * Get visible declarations in scope
 *
 * @param a : Analysis
 * @type Scope -> List(Occurrence)
 */
nabl2-get-visible-decls(|a)

/**
 * Get reachable declarations in scope
 *
 * @param a : Analysis
 * @type Scope -> List(Occurrence)
 */
nabl2-get-reachable-decls(|a)