Files
wasmtime/cranelift/docs/testing.md
Nick Fitzgerald c0b587ac5f Remove heaps from core Cranelift, push them into cranelift-wasm (#5386)
* cranelift-wasm: translate Wasm loads into lower-level CLIF operations

Rather than using `heap_{load,store,addr}`.

* cranelift: Remove the `heap_{addr,load,store}` instructions

These are now legalized in the `cranelift-wasm` frontend.

* cranelift: Remove the `ir::Heap` entity from CLIF

* Port basic memory operation tests to .wat filetests

* Remove test for verifying CLIF heaps

* Remove `heap_addr` from replace_branching_instructions_and_cfg_predecessors.clif test

* Remove `heap_addr` from readonly.clif test

* Remove `heap_addr` from `table_addr.clif` test

* Remove `heap_addr` from the simd-fvpromote_low.clif test

* Remove `heap_addr` from simd-fvdemote.clif test

* Remove `heap_addr` from the load-op-store.clif test

* Remove the CLIF heap runtest

* Remove `heap_addr` from the global_value.clif test

* Remove `heap_addr` from fpromote.clif runtests

* Remove `heap_addr` from fdemote.clif runtests

* Remove `heap_addr` from memory.clif parser test

* Remove `heap_addr` from reject_load_readonly.clif test

* Remove `heap_addr` from reject_load_notrap.clif test

* Remove `heap_addr` from load_readonly_notrap.clif test

* Remove `static-heap-without-guard-pages.clif` test

Will be subsumed when we port `make-heap-load-store-tests.sh` to generating
`.wat` tests.

* Remove `static-heap-with-guard-pages.clif` test

Will be subsumed when we port `make-heap-load-store-tests.sh` over to `.wat`
tests.

* Remove more heap tests

These will be subsumed by porting `make-heap-load-store-tests.sh` over to `.wat`
tests.

* Remove `heap_addr` from `simple-alias.clif` test

* Remove `heap_addr` from partial-redundancy.clif test

* Remove `heap_addr` from multiple-blocks.clif test

* Remove `heap_addr` from fence.clif test

* Remove `heap_addr` from extends.clif test

* Remove runtests that rely on heaps

Heaps are not a thing in CLIF or the interpreter anymore

* Add generated load/store `.wat` tests

* Enable memory-related wasm features in `.wat` tests

* Remove CLIF heap from fcmp-mem-bug.clif test

* Add a mode for compiling `.wat` all the way to assembly in filetests

* Also generate WAT to assembly tests in `make-load-store-tests.sh`

* cargo fmt

* Reinstate `f{de,pro}mote.clif` tests without the heap bits

* Remove undefined doc link

* Remove outdated SVG and dot file from docs

* Add docs about `None` returns for base address computation helpers

* Factor out `env.heap_access_spectre_mitigation()` to a local

* Expand docs for `FuncEnvironment::heaps` trait method

* Restore f{de,pro}mote+load clif runtests with stack memory
2022-12-15 00:26:45 +00:00

12 KiB

Testing Cranelift

Cranelift is tested at multiple levels of abstraction and integration. When possible, Rust unit tests are used to verify single functions and types. When testing the interaction between compiler passes, file-level tests are appropriate.

Rust tests

Rust and Cargo have good support for testing. Cranelift uses unit tests, doc tests, and integration tests where appropriate. The Rust By Example page on Testing is a great illustration on how to write each of these forms of test.

File tests

Compilers work with large data structures representing programs, and it quickly gets unwieldy to generate test data programmatically. File-level tests make it easier to provide substantial input functions for the compiler tests.

File tests are *.clif files in the filetests/ directory hierarchy. Each file has a header describing what to test followed by a number of input functions in the :doc:Cranelift textual intermediate representation <ir>:

.. productionlist:: test_file : test_header function_list test_header : test_commands (isa_specs | settings) test_commands : test_command { test_command } test_command : "test" test_name { option } "\n"

The available test commands are described below.

Many test commands only make sense in the context of a target instruction set architecture. These tests require one or more ISA specifications in the test header:

.. productionlist:: isa_specs : { [settings] isa_spec } isa_spec : "isa" isa_name { option } "\n"

The options given on the isa line modify the ISA-specific settings defined in cranelift-codegen/meta-python/isa/*/settings.py.

All types of tests allow shared Cranelift settings to be modified:

.. productionlist:: settings : { setting } setting : "set" { option } "\n" option : flag | setting "=" value

The shared settings available for all target ISAs are defined in cranelift-codegen/meta-python/base/settings.py.

The set lines apply settings cumulatively:

    test legalizer
    set opt_level=best
    set is_pic=1
    isa riscv64
    set is_pic=0
    isa riscv32 supports_m=false

    function %foo() {}

This example will run the legalizer test twice. Both runs will have opt_level=best, but they will have different is_pic settings. The 32-bit run will also have the RISC-V specific flag supports_m disabled.

The filetests are run automatically as part of cargo test, and they can also be run manually with the clif-util test command.

By default, the test runner will spawn a thread pool with as many threads as there are logical CPUs. You can explicitly control how many threads are spawned via the CRANELIFT_FILETESTS_THREADS environment variable. For example, to limit the test runner to a single thread, use:

$ CRANELIFT_FILETESTS_THREADS=1 clif-util test path/to/file.clif

Filecheck

Many of the test commands described below use filecheck to verify their output. Filecheck is a Rust implementation of the LLVM tool of the same name. See the documentation <https://docs.rs/filecheck/>_ for details of its syntax.

Comments in .clif files are associated with the entity they follow. This typically means an instruction or the whole function. Those tests that use filecheck will extract comments associated with each function (or its entities) and scan them for filecheck directives. The test output for each function is then matched against the filecheck directives for that function.

Comments appearing before the first function in a file apply to every function. This is useful for defining common regular expression variables with the regex: directive, for example.

Note that LLVM's file tests don't separate filecheck directives by their associated function. It verifies the concatenated output against all filecheck directives in the test file. LLVM's :command:FileCheck command has a CHECK-LABEL: directive to help separate the output from different functions. Cranelift's tests don't need this.

test cat

This is one of the simplest file tests, used for testing the conversion to and from textual IR. The test cat command simply parses each function and converts it back to text again. The text of each function is then matched against the associated filecheck directives.

Example:

    function %r1() -> i32, f32 {
    ebb1:
        v10 = iconst.i32 3
        v20 = f32const 0.0
        return v10, v20
    }
    ; sameln: function %r1() -> i32, f32 {
    ; nextln: ebb0:
    ; nextln:     v10 = iconst.i32 3
    ; nextln:     v20 = f32const 0.0
    ; nextln:     return v10, v20
    ; nextln: }

test verifier

Run each function through the IR verifier and check that it produces the expected error messages.

Expected error messages are indicated with an error: directive on the instruction that produces the verifier error. Both the error message and reported location of the error is verified:

    test verifier

    function %test(i32) {
        ebb0(v0: i32):
            jump ebb1       ; error: terminator
            return
    }

This example test passes if the verifier fails with an error message containing the sub-string "terminator" and the error is reported for the jump instruction.

If a function contains no error: annotations, the test passes if the function verifies correctly.

test print-cfg

Print the control flow graph of each function as a Graphviz graph, and run filecheck over the result. See also the :command:clif-util print-cfg command:

    ; For testing cfg generation. This code is nonsense.
    test print-cfg
    test verifier

    function %nonsense(i32, i32) -> f32 {
    ; check: digraph %nonsense {
    ; regex: I=\binst\d+\b
    ; check: label="{ebb0 | <$(BRZ=$I)>brz ebb2 | <$(JUMP=$I)>jump ebb1}"]

    ebb0(v0: i32, v1: i32):
        brz v1, ebb2            ; unordered: ebb0:$BRZ -> ebb2
        v2 = iconst.i32 0
        jump ebb1(v2)           ; unordered: ebb0:$JUMP -> ebb1

    ebb1(v5: i32):
        return v0

    ebb2:
        v100 = f32const 0.0
        return v100
    }

test domtree

Compute the dominator tree of each function and validate it against the dominates: annotations::

    test domtree

    function %test(i32) {
        ebb0(v0: i32):
            jump ebb1     ; dominates: ebb1
        ebb1:
            brz v0, ebb3  ; dominates: ebb3
            jump ebb2     ; dominates: ebb2
        ebb2:
            jump ebb3
        ebb3:
            return
    }

Every reachable extended basic block except for the entry block has an immediate dominator which is a jump or branch instruction. This test passes if the dominates: annotations on the immediate dominator instructions are both correct and complete.

This test also sends the computed CFG post-order through filecheck.

test legalizer

Legalize each function for the specified target ISA and run the resulting function through filecheck. This test command can be used to validate the encodings selected for legal instructions as well as the instruction transformations performed by the legalizer.

test regalloc

Test the register allocator.

First, each function is legalized for the specified target ISA. This is required for register allocation since the instruction encodings provide register class constraints to the register allocator.

Second, the register allocator is run on the function, inserting spill code and assigning registers and stack slots to all values.

The resulting function is then run through filecheck.

test binemit

Test the emission of binary machine code.

The functions must contains instructions that are annotated with both encodings and value locations (registers or stack slots). For instructions that are annotated with a bin: directive, the emitted hexadecimal machine code for that instruction is compared to the directive:

    test binemit
    isa riscv

    function %int32() {
    ebb0:
        [-,%x5]             v0 = iconst.i32 1
        [-,%x6]             v1 = iconst.i32 2
        [R#0c,%x7]          v10 = iadd v0, v1       ; bin: 006283b3
        [R#200c,%x8]        v11 = isub v0, v1       ; bin: 40628433
        return
    }

If any instructions are unencoded (indicated with a [-] encoding field), they will be encoded using the same mechanism as the legalizer uses. However, illegal instructions for the ISA won't be expanded into other instruction sequences. Instead the test will fail.

Value locations must be present if they are required to compute the binary bits. Missing value locations will cause the test to crash.

test simple-gvn

Test the simple GVN pass.

The simple GVN pass is run on each function, and then results are run through filecheck.

test licm

Test the LICM pass.

The LICM pass is run on each function, and then results are run through filecheck.

test dce

Test the DCE pass.

The DCE pass is run on each function, and then results are run through filecheck.

test shrink

Test the instruction shrinking pass.

The shrink pass is run on each function, and then results are run through filecheck.

test preopt

Test the preopt pass.

The preopt pass is run on each function, and then results are run through filecheck.

test compile

Test the whole code generation pipeline.

Each function is passed through the full Context::compile() function which is normally used to compile code. This type of test often depends on assertions or verifier errors, but it is also possible to use filecheck directives which will be matched against the final form of the Cranelift IR right before binary machine code emission.

test run

Compile and execute a function.

This test command allows several directives:

  • to print the result of running a function to stdout, add a print directive and call the preceding function with arguments (see %foo in the example below); remember to enable --nocapture if running these tests through Cargo
  • to check the result of a function, add a run directive and call the preceding function with a comparison (== or !=) (see %bar below)
  • for backwards compatibility, to check the result of a function with a () -> b* signature, only the run directive is required, with no invocation or comparison (see %baz below); a true value is interpreted as a successful test execution, whereas a false value is interpreted as a failed test.

Currently a target is required but is only used to indicate whether the host platform can run the test and currently only the architecture is filtered. The host platform's native target will be used to actually compile the test.

Example:

    test run
    target x86_64

    ; how to print the results of a function
    function %foo() -> i32 {
    block0:
        v0 = iconst.i32 42
        return v0
    }
    ; print: %foo()

    ; how to check the results of a function
    function %bar(i32) -> i32 {
    block0(v0:i32):
        v1 = iadd_imm v0, 1
        return v1
    }
    ; run: %bar(1) == 2

    ; legacy method of checking the results of a function
    function %baz() -> b1 {
    block0:
        v0 = bconst.b1 true
        return v0
    }
    ; run

Environment directives

Some tests need additional resources to be provided by the filetest infrastructure.

When any of the following directives is present the first argument of the function is required to be a i64 vmctx. The filetest infrastructure will then pass a pointer to the environment struct via this argument.

The environment struct is essentially a list of pointers with info about the resources requested by the directives. These pointers are always 8 bytes, and laid out sequentially in memory. Even for 32 bit machines, where we only fill the first 4 bytes of the pointer slot.

Currently, we only support requesting heaps, however this is a generic mechanism that should be able to introduce any sort of environment support that we may need later. (e.g. tables, global values, external functions)