docs/rosette: Reword

Acknowledge the minimal functional backend, highlighting what's new/different for our SExpr targets.
Add and use the reference `minimal backend`.
Use `note` directives to point out missing code sections (highlighting that the included diffs are not complete).
Racket *is* still strongly-typed, it's just dynamic instead of static.  Adjust phrasing to reflect that.
Adjust some of the literal includes, adding a new section from the `Functional::AbstractVisitor`, splitting the `Module::write_eval()` in twain and adding a `smtlib.cc` literal include for the node iteration, as well as for the `FunctionalSmtBackend` to compare against the minimal backend.
Move `Backend` description up to minimal functional backend section.
This commit is contained in:
Krystine Sherwin 2025-02-28 16:33:57 +13:00
parent e01a413722
commit 3c493d2bef
No known key found for this signature in database
1 changed files with 102 additions and 64 deletions

View File

@ -144,6 +144,8 @@ S-expressions can be constructed with ``SExpr::list``, for example ``SExpr expr
parentheses. parentheses.
- The destructor calls ``flush`` but also closes all unclosed parentheses. - The destructor calls ``flush`` but also closes all unclosed parentheses.
.. _minimal backend:
Example: A minimal functional backend Example: A minimal functional backend
------------------------------------- -------------------------------------
@ -158,6 +160,13 @@ Example: A minimal functional backend
+ handle outputs and next state + handle outputs and next state
- backend pass boiler plate gives us ``write_functional_dummy`` command - backend pass boiler plate gives us ``write_functional_dummy`` command
The final part is the ``Backend`` itself, which registers the command in Yosys.
The ``execute`` method is the part that runs when the user calls the command,
handling any options, preparing the output file for writing, and iterating over
selected modules in the design. For more on adding new commands to Yosys and
how they work, refer to :doc:`/yosys_internals/extending_yosys/extensions`.
- pointer ``f`` is a ``std::ostream`` we can write to, being either a file or - pointer ``f`` is a ``std::ostream`` we can write to, being either a file or
stdout stdout
- FunctionalIR conversion done by ``Functional::IR::from_module()`` - FunctionalIR conversion done by ``Functional::IR::from_module()``
@ -233,18 +242,23 @@ The major changes from the SMT-LIB backend are as follows:
Scope Scope
~~~~~ ~~~~~
As described above, the ``Functional::Scope`` class is derived in order to avoid Our first addition to the `minimal backend`_ above is that for both SMT-LIB and
collisions between identifiers in the generated output. In the SMT-LIB version Rosette backends, we are now targetting real languages which bring with them
the ``SmtScope`` class implements ``Scope<int>``; provides a constructor that their own sets of constraints with what we can use as identifiers. This is
iterates over a list of reserved keywords, calling ``reserve`` on each; and where the ``Functional::Scope`` class described above comes in; by using this
defines the ``is_character_legal`` method to reject any characters which are not class we can safely rename our identifiers in the generated output without
allowed in SMT-LIB variable names to then be replaced with underscores in the worrying about collisions or illegal names/characters.
output.
In the Rosette version we switch out ``Smt`` in the class name for ``Smtr`` to In the SMT-LIB version, the ``SmtScope`` class implements ``Scope<int>``;
mean ``smtlib_rosette``; this will happen through the rest of the code too. We provides a constructor that iterates over a list of reserved keywords, calling
also update list of legal ascii characters in the ``is_character_legal`` method ``reserve`` on each; and defines the ``is_character_legal`` method to reject any
to only those allowed in Racket variable names. characters which are not allowed in SMT-LIB variable names to then be replaced
with underscores in the output. To use this scope we create an instance of it,
and call the ``Scope::unique_name()`` method to generate a unique and legal name
for each of our identifiers.
In the Rosette version we update the list of legal ascii characters in the
``is_character_legal`` method to only those allowed in Racket variable names.
.. literalinclude:: /generated/functional/rosette.diff .. literalinclude:: /generated/functional/rosette.diff
:language: diff :language: diff
@ -276,8 +290,8 @@ provides this functionality natively with ``cons``, which we will see later.
nullptr nullptr
}; };
Note that we skip over the actual list of reserved keywords from both the smtlib .. note:: We skip over the actual list of reserved keywords from both the smtlib
and racket specifications to save on space in this document. and racket specifications to save on space in this document.
Sort Sort
~~~~ ~~~~
@ -297,16 +311,17 @@ signals represented as ``bitvector``\ s, and memories as ``list``\ s of signals.
Struct Struct
~~~~~~ ~~~~~~
The ``Functional::IR`` class tracks the set of inputs, the set of outputs, and As we saw in the `minimal backend`_ above, the ``Functional::IR`` class tracks
the set of "state" variables. The SMT-LIB backend maps each of these sets into the set of inputs, the set of outputs, and the set of "state" variables. The
its own ``SmtStruct``, with each variable getting a corresponding field in the SMT-LIB backend maps each of these sets into its own ``SmtStruct``, with each
struct and a specified `Sort`_. `write_functional_smt2` then defines each of variable getting a corresponding field in the struct and a specified `Sort`_.
these structs as a new ``datatype``, with each element being strongly-typed. `write_functional_smt2` then defines each of these structs as a new
``datatype``, with each element being strongly-typed.
In Rosette, rather than defining new datatypes for our structs, we use the In Rosette, rather than defining new datatypes for our structs, we use the
native ``struct``. We also only declare each field by name because Racket is native ``struct``. We also only declare each field by name because Racket
not as strongly-typed. For ease of use, we provide the expected type for each provides less static typing. For ease of use, we provide the expected type for
field as comments. each field as comments.
.. literalinclude:: /generated/functional/rosette.diff .. literalinclude:: /generated/functional/rosette.diff
:language: diff :language: diff
@ -355,18 +370,19 @@ Rosette ``struct``\ s.
PrintVisitor PrintVisitor
~~~~~~~~~~~~ ~~~~~~~~~~~~
The ``PrintVisitor`` implements the abstract ``Functional::AbstractVisitor`` Remember in the `minimal backend`_ we converted nodes into strings for writing
class, described above in `What is FunctionalIR`_, with a return type of using the ``node.to_string()`` method, which wrapped ``node.visit()`` with a
``SExpr``. This class converts FunctionalIR functions into s-expressions, private visitor. We now want a custom visitor which can convert nodes into
including reading inputs/current state with the ``access`` method from the s-expressions. This is where the ``PrintVisitor`` comes in, implementing the
`Struct`_. For most functions, the Rosette output is very similar to the abstract ``Functional::AbstractVisitor`` class with a return type of ``SExpr``.
corresponding SMT-LIB function with minor adjustments for syntax. For most functions, the Rosette output is very similar to the corresponding
SMT-LIB function with minor adjustments for syntax.
.. literalinclude:: /generated/functional/rosette.diff .. literalinclude:: /generated/functional/rosette.diff
:language: diff :language: diff
:caption: portion of ``Functional::AbstractVisitor`` implementation diff showing similarities :caption: portion of ``Functional::AbstractVisitor`` implementation diff showing similarities
:start-at: SExpr logical_shift_left :start-at: SExpr logical_shift_left
:end-before: SExpr input :end-at: "list-set-bv"
However there are some differences in the two formats with regards to how However there are some differences in the two formats with regards to how
booleans are handled, with Rosette providing built-in functions for conversion. booleans are handled, with Rosette providing built-in functions for conversion.
@ -377,13 +393,30 @@ booleans are handled, with Rosette providing built-in functions for conversion.
:start-at: SExpr from_bool :start-at: SExpr from_bool
:end-before: SExpr extract :end-before: SExpr extract
Of note here is the rare instance of the Rosette implementation *gaining* static
typing rather than losing it. Where SMT_LIB calls zero/sign extension with the
number of extra bits needed (given by ``out_width - a.width()``), Rosette
instead specifies the type of the output (given by ``list("bitvector",
out_width)``).
.. literalinclude:: /generated/functional/rosette.diff
:language: diff
:caption: zero/sign extension implementation diff
:start-after: SExpr buf(
:end-before: SExpr concat(
:lines: 2-3, 5-6
.. note:: Be sure to check the source code for the full list of differences here.
Module Module
~~~~~~ ~~~~~~
The ``Functional::IR`` is wrapped in the ``SmtModule`` class, with the mapping With most of the supporting classes out of the way, we now reach our three main
from RTLIL module to FunctionalIR happening in the constructor. Each of the steps from the `minimal backend`_. These are all handled by the ``SmtModule``
three ``SmtStruct``\ s; inputs, outputs, and state; are created, with each value class, with the mapping from RTLIL module to FunctionalIR happening in the
in the corresponding lists in the IR being ``insert``\ ed. constructor. Each of the three ``SmtStruct``\ s; inputs, outputs, and state;
are also created in the constructor, with each value in the corresponding lists
in the IR being ``insert``\ ed.
.. literalinclude:: /generated/functional/smtlib.cc .. literalinclude:: /generated/functional/smtlib.cc
:language: c++ :language: c++
@ -403,7 +436,7 @@ uses an underscore for the name of the initial state.
The ``write`` method is then responsible for writing the FunctionalIR to the The ``write`` method is then responsible for writing the FunctionalIR to the
output file, formatted for the corresponding backend. ``SmtModule::write()`` output file, formatted for the corresponding backend. ``SmtModule::write()``
breaks the output file down into four parts: defining the three structs, breaks the output file down into four parts: defining the three structs,
declaring the ``pair`` datatype, defining the mapping function ``(inputs, declaring the ``pair`` datatype, defining the transfer function ``(inputs,
current_state) -> (outputs, next_state)`` with ``write_eval``, and declaring the current_state) -> (outputs, next_state)`` with ``write_eval``, and declaring the
initial state with ``write_initial``. The only change for the ``SmtrModule`` is initial state with ``write_initial``. The only change for the ``SmtrModule`` is
that the ``pair`` declaration isn't needed. that the ``pair`` declaration isn't needed.
@ -414,33 +447,35 @@ that the ``pair`` declaration isn't needed.
:start-at: void write(std::ostream &out) :start-at: void write(std::ostream &out)
:end-at: } :end-at: }
For the ``write_eval`` method, the main differences are syntactical. First we The ``write_eval`` method is where the FunctionalIR nodes, outputs, and next
change the function declaration line for the Rosette style which drops the state are handled. Just as with the `minimal backend`_, we iterate over the
explicit output typing and uses the ``define`` keyword instead of nodes with ``for(auto n : ir)``, and then use the ``Struct::write_value()``
``define-fun``. And then we change the final result from a ``pair`` to the method for the ``output_struct`` and ``state_struct`` to iterate over the
native ``cons`` which acts in much the same way, returning both the ``outputs`` outputs and next state respectively.
and the ``next_state`` in a single variable. Iteration over all of the
``Functional::Node``\ s in the IR is the same in both.
.. inlined diff for showing the whole function while skipping the middle part .. literalinclude:: /generated/functional/smtlib.cc
.. code-block:: diff :language: c++
:caption: diff of ``Module::write_eval()`` method :caption: iterating over FunctionalIR nodes in ``SmtModule::write_eval()``
:start-at: for(auto n : ir)
:end-at: }
void write_eval(SExprWriter &w) The main differences between our two backends here are syntactical. First we
{ change the ``define-fun`` for the Racket style ``define`` which drops the
w.push(); explicitly typed inputs/outputs. And then we change the final result from a
- w.open(list("define-fun", name, ``pair`` to the native ``cons`` which acts in much the same way, returning both
- list(list("inputs", input_struct.name), the ``outputs`` and the ``next_state`` in a single variable.
- list("state", state_struct.name)),
- list("Pair", output_struct.name, state_struct.name))); .. literalinclude:: /generated/functional/rosette.diff
+ w.open(list("define", list(name, "inputs", "state"))); :language: diff
... :caption: diff of ``Module::write_eval()`` transfer function declaration
- w.open(list("pair")); :start-at: w.open(list("define-fun"
+ w.open(list("cons")); :end-at: w.open(list("define"
output_struct.write_value(w, [&](IdString name) { return node_to_sexpr(ir.output(name).value()); });
state_struct.write_value(w, [&](IdString name) { return node_to_sexpr(ir.state(name).next_value()); }); .. literalinclude:: /generated/functional/rosette.diff
w.pop(); :language: diff
} :caption: diff of output/next state handling ``Module::write_eval()``
:start-at: w.open(list("pair"
:end-at: w.pop();
For the ``write_initial`` method, the SMT-LIB backend uses ``declare-const`` and For the ``write_initial`` method, the SMT-LIB backend uses ``declare-const`` and
``assert``\ s which must always hold true. For Rosette we instead define the ``assert``\ s which must always hold true. For Rosette we instead define the
@ -458,12 +493,15 @@ whereas the SMT-LIB code can only verify that a given ``next_state`` is correct.
Backend Backend
~~~~~~~ ~~~~~~~
The final part is the ``Backend`` itself, which registers the command in Yosys. The final part is the ``Backend`` itself, with much of the same boiler plate as
The ``execute`` method is the part that runs when the user calls the `minimal backend`_. The main difference is that we use the `Module`_ to
`write_functional_rosette`, handling any options, preparing the output file for perform the actual processing.
writing, and iterating over selected modules in the design. For more on adding
new commands to Yosys and how they work, refer to .. literalinclude:: /generated/functional/smtlib.cc
:doc:`/yosys_internals/extending_yosys/extensions`. :language: c++
:caption: The ``FunctionalSmtBackend``
:start-at: struct FunctionalSmtBackend
:end-at: } FunctionalSmtBackend;
There are two additions here for Rosette. The first is that the output file There are two additions here for Rosette. The first is that the output file
needs to start with the ``#lang`` definition which tells the needs to start with the ``#lang`` definition which tells the