diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 16bb9b02..00000000 --- a/.appveyor.yml +++ /dev/null @@ -1,31 +0,0 @@ -# Documentation: https://github.com/JuliaCI/Appveyor.jl -environment: - matrix: - - julia_version: 1.3 - - julia_version: nightly -platform: - - x86 - - x64 -matrix: - allow_failures: - - julia_version: nightly -branches: - only: - - master - - /release-.*/ -notifications: - - provider: Email - on_build_success: false - on_build_failure: false - on_build_status_changed: false -install: - - ps: iex ((new-object net.webclient).DownloadString("https://raw.githubusercontent.com/JuliaCI/Appveyor.jl/version-1/bin/install.ps1")) -build_script: - - echo "%JL_BUILD_SCRIPT%" - - C:\julia\bin\julia -e "%JL_BUILD_SCRIPT%" -test_script: - - echo "%JL_TEST_SCRIPT%" - - C:\julia\bin\julia -e "%JL_TEST_SCRIPT%" -on_success: - - echo "%JL_CODECOV_SCRIPT%" - - C:\julia\bin\julia -e "%JL_CODECOV_SCRIPT%" diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index 7c17290f..486adf2b 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -1,27 +1,16 @@ name: CompatHelper - on: schedule: - - cron: '0 1 * * *' - issues: - types: [opened, reopened] - + - cron: 0 0 * * 0 + workflow_dispatch: jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - julia-version: [1.3.1] - julia-arch: [x64] - os: [ubuntu-latest] + CompatHelper: + runs-on: ubuntu-latest steps: - - uses: julia-actions/setup-julia@latest - with: - version: ${{ matrix.julia-version }} - - name: Install dependencies - run: julia -e 'using Pkg; Pkg.add(Pkg.PackageSpec(name = "CompatHelper", url = "https://github.com/bcbi/CompatHelper.jl.git"))' - - name: CompatHelper.main + - name: Pkg.add("CompatHelper") + run: julia -e 'using Pkg; Pkg.add("CompatHelper")' + - name: CompatHelper.main() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - JULIA_DEBUG: CompatHelper + COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index d77d3a0c..f49313b6 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -1,11 +1,15 @@ name: TagBot on: - schedule: - - cron: 0 * * * * + issue_comment: + types: + - created + workflow_dispatch: jobs: TagBot: + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' runs-on: ubuntu-latest steps: - uses: JuliaRegistries/TagBot@v1 with: token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/ci-integration-nightly.yml b/.github/workflows/ci-integration-nightly.yml new file mode 100644 index 00000000..00074acc --- /dev/null +++ b/.github/workflows/ci-integration-nightly.yml @@ -0,0 +1,38 @@ +name: CI (Integration nightly) +on: + push: + branches: + - '**' + paths-ignore: + - 'README.md' + pull_request: + branches: + - master + paths-ignore: + - 'README.md' +jobs: + test-integration-nightly: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - 'nightly' + os: + - ubuntu-latest + - macOS-latest + arch: + - x64 + group: + - Integration + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@latest + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: julia-actions/julia-buildpkg@latest + - uses: julia-actions/julia-runtest@latest + env: + GROUP: ${{ matrix.group }} diff --git a/.github/workflows/ci-integration.yml b/.github/workflows/ci-integration.yml new file mode 100644 index 00000000..544b0a43 --- /dev/null +++ b/.github/workflows/ci-integration.yml @@ -0,0 +1,45 @@ +name: CI (Integration) +on: + push: + branches: + - '**' + paths-ignore: + - 'README.md' + pull_request: + branches: + - master + paths-ignore: + - 'README.md' +jobs: + test-integration: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - '1.8' + - '1.9' + - '1.10.0-rc1' + os: + - ubuntu-latest + - macOS-latest + - windows-latest + arch: + - x64 + group: + - Integration + steps: + - uses: actions/checkout@v2 + - uses: KyleMayes/install-llvm-action@v2 + with: + version: "17" + if: matrix.os == 'windows-latest' + - uses: julia-actions/setup-julia@latest + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: julia-actions/julia-buildpkg@latest + - uses: julia-actions/julia-runtest@latest + env: + GROUP: ${{ matrix.group }} diff --git a/.github/workflows/ci-julia-nightly.yml b/.github/workflows/ci-julia-nightly.yml new file mode 100644 index 00000000..4cdbcfa8 --- /dev/null +++ b/.github/workflows/ci-julia-nightly.yml @@ -0,0 +1,37 @@ +name: CI (Julia nightly) +on: + push: + branches: + - '**' + paths-ignore: + - 'README.md' + pull_request: + branches: + - master + paths-ignore: + - 'README.md' +jobs: + test-julia-nightly: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - 'nightly' + os: + - ubuntu-latest + arch: + - x64 + group: + - Core + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@latest + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: julia-actions/julia-buildpkg@latest + - uses: julia-actions/julia-runtest@latest + env: + GROUP: ${{ matrix.group }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb97e170..499223cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,15 @@ name: CI on: - - push - - pull_request + push: + branches: + - '**' + paths-ignore: + - 'README.md' + pull_request: + branches: + - master + paths-ignore: + - 'README.md' jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} @@ -10,19 +18,36 @@ jobs: fail-fast: false matrix: version: - - '1.3' - - 'nightly' + - '1.8' + - '1.9' + - '1.10' os: - ubuntu-latest - # - macOS-latest + - macOS-latest - windows-latest arch: - x64 + group: + - Core + include: + - arch: x86 + version: '1' + os: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: KyleMayes/install-llvm-action@v2 + with: + version: "17" + if: matrix.os == 'windows-latest' - uses: julia-actions/setup-julia@latest with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/julia-buildpkg@latest - uses: julia-actions/julia-runtest@latest + env: + GROUP: ${{ matrix.group }} + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v2 + with: + files: lcov.info diff --git a/.github/workflows/docs.yml b/.github/workflows/docsyml_backup similarity index 100% rename from .github/workflows/docs.yml rename to .github/workflows/docsyml_backup diff --git a/.gitignore b/.gitignore index 2ef6f28f..7d7ea3bc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,13 +2,15 @@ *.jl.cov *.jl.mem .DS_Store -Manifest.toml -!helpers/Manifest.toml /dev/ /test/standalone -/test/Manifest.toml /test/test.* +Manifest.toml +*.wasm +*.dll +*.o +*.so test.o test.so test.bc diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d735ad52..00000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Documentation: http://docs.travis-ci.com/user/languages/julia/ -language: julia -os: -- linux -- windows -julia: - - 1.3 - - nightly -matrix: - # extra linux test - include: - - os: linux - dist: bionic - - allow_failures: - - julia: nightly - fast_finish: true - -notifications: - email: false - -after_success: - - julia -e 'using Pkg; Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())' - - julia -e 'using Pkg; Pkg.add("Coverage"); using Coverage; Coveralls.submit(process_folder())' diff --git a/LICENSE b/LICENSE index acf45480..b4e5ab82 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,6 @@ -Copyright (c) 2019 Tom Short +Copyright (c) 2019-2022 Mason Protter, William Moses, Valentin Churavy, + McCoy R. Becker, Brenhin Keller, Julian Samaroo, + Tom Short, and other contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Project.toml b/Project.toml index 583ae233..b4d7d407 100644 --- a/Project.toml +++ b/Project.toml @@ -1,23 +1,28 @@ name = "StaticCompiler" uuid = "81625895-6c0f-48fc-b932-11a18313743c" -authors = ["Tom Short"] -version = "0.1.0" +authors = ["Tom Short and contributors"] +version = "0.7.2" + [deps] -Cassette = "7057c7e9-c182-5462-911a-8362d720325c" -DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +Clang_jll = "0ee61d77-7f21-5576-8119-9fcc46b10100" +CodeInfoTools = "bc773b8a-8374-437a-b9f2-0e9785855863" +GPUCompiler = "61eb1bfa-7361-4325-ad38-22787b887f55" +InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +LLD_jll = "d55e3150-da41-5e91-b323-ecfd1eec6109" LLVM = "929cbde3-209d-540e-8aea-75f648917ca0" Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -TypedCodeUtils = "687fb87b-adea-59d5-9be9-82253b54685d" +Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +StaticTools = "86c06d3c-3f03-46de-9781-57580aa96d0a" [compat] -DataStructures = "0.17" -Cassette = "0.3" -LLVM = "1.3" -TypedCodeUtils = "0.1" +CodeInfoTools = "0.3" +GPUCompiler = "0.21, 0.22, 0.23, 0.24, 0.25, 0.26" +LLVM = "6" MacroTools = "0.5" -julia = "1.2" +StaticTools = "0.8" +julia = "1.8, 1.9" [extras] Formatting = "59287772-0a20-5a39-b81b-1366585eb4c0" diff --git a/README.md b/README.md index 8b09b28b..78894700 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,113 @@ # StaticCompiler -[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://tshort.github.io/StaticCompiler.jl/dev) -[![Build Status](https://travis-ci.com/tshort/StaticCompiler.jl.svg?branch=master)](https://travis-ci.com/tshort/StaticCompiler.jl) -[![Build Status](https://ci.appveyor.com/api/projects/status/github/tshort/StaticCompiler.jl?svg=true)](https://ci.appveyor.com/project/tshort/StaticCompiler-jl) -[![Codecov](https://codecov.io/gh/tshort/StaticCompiler.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/tshort/StaticCompiler.jl) -[![Coveralls](https://coveralls.io/repos/github/tshort/StaticCompiler.jl/badge.svg?branch=master)](https://coveralls.io/github/tshort/StaticCompiler.jl?branch=master) +[![CI](https://github.com/tshort/StaticCompiler.jl/actions/workflows/ci.yml/badge.svg)](https://github.com/tshort/StaticCompiler.jl/actions/workflows/ci.yml) +[![CI (Integration)](https://github.com/tshort/StaticCompiler.jl/actions/workflows/ci-integration.yml/badge.svg)](https://github.com/tshort/StaticCompiler.jl/actions/workflows/ci-integration.yml) +[![CI (Julia nightly)](https://github.com/tshort/StaticCompiler.jl/workflows/CI%20(Julia%20nightly)/badge.svg)](https://github.com/tshort/StaticCompiler.jl/actions/workflows/ci-julia-nightly.yml) +[![CI (Integration nightly)](https://github.com/tshort/StaticCompiler.jl/actions/workflows/ci-integration-nightly.yml/badge.svg)](https://github.com/tshort/StaticCompiler.jl/actions/workflows/ci-integration-nightly.yml) +[![Coverage](https://codecov.io/gh/tshort/StaticCompiler.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/tshort/StaticCompiler.jl) -This is an experimental package to compile Julia code to standalone libraries. A system image is not needed. It is also meant for cross compilation, so Julia code can be compiled for other targets, including WebAssembly and embedded targets. +This is an experimental package to compile Julia code to standalone libraries. A system image is not needed. ## Installation and Usage +Installation is the same as any other registered Julia package ```julia using Pkg -Pkg.add(PackageSpec( url = "https://github.com/tshort/StaticCompiler.jl", rev = "master")) +Pkg.add("StaticCompiler") ``` + +### Standalone compilation +StaticCompiler.jl provides the functions `compile_executable` and `compile_shlib` for compiling a Julia function to a native executable or shared library for use from outside of Julia: ```julia -using StaticCompiler -``` -**Documentation**: [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://tshort.github.io/StaticCompiler.jl/dev) +julia> using StaticCompiler, StaticTools -## Approach -This package uses the [LLVM package](https://github.com/maleadt/LLVM.jl) to generate code in the same fashion as [CUDAnative](https://github.com/JuliaGPU/CUDAnative.jl). +julia> hello() = println(c"Hello, world!") +hello (generic function with 1 method) -Some of the key details of this approach are: +julia> compile_executable(hello, (), "./") +"/Users/user/hello" -* **ccalls and cglobal** -- When Julia compiles code CUDAnative style, `ccall` and `cglobal` references get compiled to a direct pointer. `StaticCompiler` converts these to symbol references for later linking. For `ccall` with a tuple call to a symbol in a library, `Cassette` is used to convert that to just a symbol reference (no dynamic library loading). +shell> ls -alh hello +-rwxrwxr-x. 1 user user 8.4K Oct 20 20:36 hello -* **Global variables** -- A lot of code gets compiled with global variables, and these get compiled to a direct pointer. `StaticCompiler` includes a basic serialize/deserialize approach. Right now, this is fairly basic, and it takes shortcuts for some objects by swapping in wrong types. This can work because many times, the objects are not really used in the code. Finding the global variable can be a little tricky because the pointer is converted to a Julia object with `unsafe_pointer_to_objref`, and that segfaults for some addresses. How to best handle cases like that is still to be determined. +shell> ./hello +Hello, world! +``` +This approach comes with substantial limitations compared to regular julia code, as you cannot rely on julia's runtime, `libjulia` (see, e.g., [StaticTools.jl](https://github.com/brenhinkeller/StaticTools.jl) for some ways to work around these limitations). -* **Initialization** -- If libjulia is used, some init code needs to be run to set up garbage collection and other things. For this, a basic `blank.ji` file is used to feed `jl_init_with_image`. +The low-level function `StaticCompiler.generate_obj` (not exported) generates object files. This can be used for more control of compilation. This can be used for example, to cross-compile to other targets. -Long term, a better approach may be to use Julia's standard compilation techniques with "tree shaking" to generate a reduced system image (see [here](https://github.com/JuliaLang/julia/issues/33670)). +### Method overlays -## Example -The API still needs work, but here is the general approach right now: +Sometimes, a julia function you want to statically compile will do things (such as throwing errors) that aren't supported natively by StaticCompiler. One tool provided for working around this is the `@device_override` macro which lets you swap out a method, but only inside of a StaticCompiler.jl compilation context. For example: ```julia -using StaticCompiler -m = irgen(cos, Tuple{Float64}) -write(m, "cos.bc") -write_object(m, "cos.o") +julia> using Libdl, StaticCompiler + +julia> f(x) = g(x) + 1; + +julia> g(x) = 2x + +julia> @device_override g(x::Int) = x - 10 + +julia> f(1) # Gives the expected answer in regular julia +3 + +julia> dlopen(compile_shlib(f, (Int,), "./")) do lib + fptr = dlsym(lib, "f") + # Now use the compiled version where g(x) = 2x is replaced with g(x) = x - 10 + @ccall $fptr(1::Int)::Int + end +-8 ``` +Typically, errors should be overrided and replaced with `@print_and_throw`, which is StaticCompiler friendly, i.e. +we define overrides such as +``` julia +@device_override @noinline Base.Math.throw_complex_domainerror(f::Symbol, x) = + @print_and_throw c"This operation requires a complex input to return a complex result" +``` + +If for some reason, you wish to use a different method table (defined with `Base.Experimental.@MethodTable` and `Base.Experimental.@overlay`) than the default one provided by StaticCompiler.jl, you can provide it to `compile_executable` and `compile_shlib` via a keyword argument `method_table`. + + +## Approach + +This package uses the [GPUCompiler package](https://github.com/JuliaGPU/GPUCompiler.jl) to generate code. + +## Limitations + +* GC-tracked allocations and global variables do *not* work with `compile_executable` or `compile_shlib`. This has some interesting consequences, including that all functions _within_ the function you want to compile must either be inlined or return only native types (otherwise Julia would have to allocate a place to put the results, which will fail). +* Since error handling relies on libjulia, you can only throw errors from standalone-compiled (`compile_executable` / `compile_shlib`) code if an explicit overload has been defined for that particular error with `@device_override` (see [quirks.jl](src/quirks.jl)). +* Type instability. Type unstable code cannot currently be statically compiled via this package. +* Extra experimental on Windows (PRs welcome if you encounter issues). Should work in WSL on Windows 10+. + +## Guide for Package Authors + +To enable code to be statically compiled, consider the following: + +* Use type-stable code. + +* Use Tuples, NamedTuples, StaticArrays, and other types where appropriate. These allocate on the stack and don't use Julia's heap allocation. + +* Avoid Julia's internal allocations. That means don't bake in use of Arrays or Strings or Dicts. Types from StaticTools can help, like StaticStrings and MallocArrays. + +* If need be, manage memory manually, using `malloc` and `free` from StaticTools.jl. This works with `StaticTools.MallocString` and `StaticTools.MallocArray`, or use [Bumper.jl](https://github.com/MasonProtter/Bumper.jl). + +* Don't use global variables that need to be allocated and initialized. Instead of global variables, use context structures that have an initialization function. It is okay to use global Tuples or NamedTuples as the use of these should be baked into compiled code. -`cos.o` should contain a function called `cos`. From there, you need to convert to link as needed with `libjulia`. +* Use context variables to store program state, inputs, and outputs. Parameterize these typese as needed, so your code can handle normal types (Arrays) and static-friendly types (StaticArrays, MallocArrays, or StrideArrays). The SciML ecosystem does this well ([example](https://github.com/SciML/OrdinaryDiffEq.jl/blob/e7f045950615352ddfcb126d13d92afd2bad05e4/src/integrators/type.jl#L82)). Use of these context variables also enables allocations and initialization to be centralized, so these could be managed by the calling routines in Julia, Python, JavaScript, or other language. -See the `test` directory for more information and types of code that currently run. The most advanced example that works is a call to an ODE solution using modified code from [ODE.jl](https://github.com/JuliaDiffEq/ODE.jl). For information on compiling and linking to an executable, see [test/standalone-exe.jl](./test/standalone-exe.jl). +* Arguments and returned values from `compile_shlib` must be native objects such as `Int`, `Float64`, or `Ptr`. They cannot be things like `Tuple{Int, Int}` because that is not natively sized. Such objects need to be passed by reference instead of by value. -## Known limitations +* If your code needs an array as a workspace, instead of directly creating it, create it as a function argument (where it could default to a standard array creation). That code could be statically compiled if that function argument is changed to a MallocArray or another static-friendly alternative. -* It won't work for recursive code. Jameson's [codegen-norecursion](https://github.com/JuliaLang/julia/tree/jn/codegen-norecursion) should fix that when merged. +## Guide for Statically Compiling Code -* `cfunction` is not supported. +If you're trying to statically compile generic code, you may run into issues if that code uses features not supported by StaticCompiler. One option is to change the code you're calling using the tips above. If that is not easy, you may by able to compile it anyway. One option is to use method overlays to change what methods are called. -* Generic code that uses `jl_apply_generic` does not work. One strategy for this is to use Cassette to swap out known code that uses dynamic calls. Another approach is to write something like `jl_apply_generic` to implement dynamic calls. +[Cthulhu](https://github.com/JuliaDebug/Cthulhu.jl) is a great help in digging into code, finding type instabilities, and finding other sources of code that may break static compilation. -* The use of Cassette makes it more difficult for Julia to infer some things, and only type-stable code can be statically compiled with this approach. +## Foreign Function Interfacing -* It's only been tested on Linux and Windows. +Because Julia objects follow C memory layouts, compiled libraries should be usable from most languages that can interface with C. For example, results should be usable with Python's [CFFI](https://cffi.readthedocs.io/en/latest/) package. -Finally, this whole approach is young and likely brittle. Do not expect it to work for your code. +For WebAssembly, interface helpers are available at [WebAssemblyInterfaces](https://github.com/tshort/WebAssemblyInterfaces.jl), and users should also see [WebAssemblyCompiler](https://github.com/tshort/WebAssemblyCompiler.jl) for a package more focused on compilation of WebAssebly in general. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..3ddada3f --- /dev/null +++ b/codecov.yml @@ -0,0 +1,12 @@ +comment: false + +coverage: + status: + project: + default: + threshold: 1% + target: 1% + patch: + default: + threshold: 1% + target: 1% diff --git a/docs/src/backend.md b/docs/src/backend.md index 523901c2..1e08a76c 100644 --- a/docs/src/backend.md +++ b/docs/src/backend.md @@ -7,4 +7,4 @@ Pages = ["backend.md"] ```@autodocs Modules = [StaticCompiler] Pages = readdir("../src") -``` +``` \ No newline at end of file diff --git a/docs/src/helpers.md b/docs/src/helpers.md index c07d4df5..e69de29b 100644 --- a/docs/src/helpers.md +++ b/docs/src/helpers.md @@ -1,23 +0,0 @@ -# Helpers -Note that the helpers defined here are used in tests, and they are useful to test out code in the REPL. - -```julia -twox(x) = 2x -# run code in the REPL -@jlrun twox(3) -# compile to an executable in a `standalone` directory -exegen([ (twox, Tuple{Int}, 4) ]) -``` - -These are not meant to be a permanent part of the API. They are just for testing. - - -```@index -Modules = [StaticCompiler] -Pages = ["helpers.md"] -``` - -```@autodocs -Modules = [StaticCompiler] -Pages = readdir("../src/helpers") -``` diff --git a/docs/src/index.md b/docs/src/index.md index 760d5e6e..e69de29b 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,64 +0,0 @@ -```@meta -CurrentModule = StaticCompiler -``` - -# StaticCompiler - -[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://tshort.github.io/StaticCompiler.jl/dev) -[![Build Status](https://travis-ci.com/tshort/StaticCompiler.jl.svg?branch=master)](https://travis-ci.com/tshort/StaticCompiler.jl) -[![Build Status](https://ci.appveyor.com/api/projects/status/github/tshort/StaticCompiler.jl?svg=true)](https://ci.appveyor.com/project/tshort/StaticCompiler-jl) -[![Codecov](https://codecov.io/gh/tshort/StaticCompiler.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/tshort/StaticCompiler.jl) -[![Coveralls](https://coveralls.io/repos/github/tshort/StaticCompiler.jl/badge.svg?branch=master)](https://coveralls.io/github/tshort/StaticCompiler.jl?branch=master) - -This is an experimental package to compile Julia code to standalone libraries. A system image is not needed. It is also meant for cross compilation, so Julia code can be compiled for other targets, including WebAssembly and embedded targets. - -## Installation and Usage -```julia -using Pkg -Pkg.add(PackageSpec( url = "https://github.com/tshort/StaticCompiler.jl", rev = "master")) -``` -```julia -using StaticCompiler -``` -**Documentation**: [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://tshort.github.io/StaticCompiler.jl/dev) - -## Approach -This package uses the [LLVM package](https://github.com/maleadt/LLVM.jl) to generate code in the same fashion as [CUDAnative](https://github.com/JuliaGPU/CUDAnative.jl). - -Some of the key details of this approach are: - -* **ccalls and cglobal** -- When Julia compiles code CUDAnative style, `ccall` and `cglobal` references get compiled to a direct pointer. `StaticCompiler` converts these to symbol references for later linking. For `ccall` with a tuple call to a symbol in a library, `Cassette` is used to convert that to just a symbol reference (no dynamic library loading). - -* **Global variables** -- A lot of code gets compiled with global variables, and these get compiled to a direct pointer. `StaticCompiler` includes a basic serialize/deserialize approach. Right now, this is fairly basic, and it takes shortcuts for some objects by swapping in wrong types. This can work because many times, the objects are not really used in the code. Finding the global variable can be a little tricky because the pointer is converted to a Julia object with `unsafe_pointer_to_objref`, and that segfaults for some addresses. How to best handle cases like that is still to be determined. - -* **Initialization** -- If libjulia is used, some init code needs to be run to set up garbage collection and other things. For this, a basic `blank.ji` file is used to feed `jl_init_with_image`. - -Long term, a better approach may be to use Julia's standard compilation techniques with "tree shaking" to generate a reduced system image (see [here](https://github.com/JuliaLang/julia/issues/33670)). - -## Example -The API still needs work, but here is the general approach right now: - -```julia -using StaticCompiler -m = irgen(cos, Tuple{Float64}) -write(m, "cos.bc") -write_object(m, "cos.o") -``` - -`cos.o` should contain a function called `cos`. From there, you need to convert to link as needed with `libjulia`. - -See the `test` directory for more information and types of code that currently run. The most advanced example that works is a call to an ODE solution using modified code from [ODE.jl](https://github.com/JuliaDiffEq/ODE.jl). For information on compiling and linking to an executable, see [test/standalone-exe.jl](https://github.com/tshort/StaticCompiler.jl/blob/master/test/standalone-exe.jl). - -## Known limitations - -* It won't work for recursive code. Jameson's [codegen-norecursion](https://github.com/JuliaLang/julia/tree/jn/codegen-norecursion) should fix that when merged. - -* `cfunction` is not supported. - -* Generic code that uses `jl_apply_generic` does not work. One strategy for this is to use Cassette to swap out known code that uses dynamic calls. Another approach is to write something like `jl_apply_generic` to implement dynamic calls. - -* The use of Cassette makes it more difficult for Julia to infer some things, and only type-stable code can be statically compiled with this approach. - -* It's only been tested on Linux and Windows. - -Finally, this whole approach is young and likely brittle. Do not expect it to work for your code. diff --git a/src/StaticCompiler.jl b/src/StaticCompiler.jl index 5f16b621..90e34040 100644 --- a/src/StaticCompiler.jl +++ b/src/StaticCompiler.jl @@ -1,28 +1,600 @@ module StaticCompiler +using InteractiveUtils +using GPUCompiler: GPUCompiler +using LLVM +using LLVM.Interop +using LLVM: API +using Libdl: Libdl, dlsym, dlopen +using Base: RefValue +using Serialization: serialize, deserialize +using Clang_jll: clang +using LLD_jll: lld +using StaticTools +using StaticTools: @symbolcall, @c_str, println +using Core: MethodTable +using Base:BinaryPlatforms.Platform, BinaryPlatforms.HostPlatform, BinaryPlatforms.arch, BinaryPlatforms.os_str, BinaryPlatforms.libc_str +using Base:BinaryPlatforms.platform_dlext +export load_function, compile_shlib, compile_executable +export static_code_llvm, static_code_typed, static_llvm_module, static_code_native +export @device_override, @print_and_throw +export StaticTarget -export irgen, write_object, @extern +include("interpreter.jl") +include("target.jl") +include("pointer_warning.jl") +include("quirks.jl") +include("dllexport.jl") -import Libdl +fix_name(f::Function) = fix_name(string(nameof(f))) +fix_name(s) = String(GPUCompiler.safe_name(s)) -using LLVM -using LLVM.Interop -using TypedCodeUtils -import TypedCodeUtils: reflect, lookthrough, canreflect, - DefaultConsumer, Reflection, Callsite, - identify_invoke, identify_call, identify_foreigncall, - process_invoke, process_call -using MacroTools -using DataStructures: MultiDict - - -include("serialize.jl") -include("utils.jl") -include("ccalls.jl") -include("globals.jl") -include("overdub.jl") -include("irgen.jl") -include("extern.jl") - -include("helpers/helpers.jl") + +""" +```julia +compile_executable(f::Function, types::Tuple, path::String, [name::String=string(nameof(f))]; + filename::String=name, + cflags=``, # Specify libraries you would like to link against, and other compiler options here + also_expose=[], + target::StaticTarget=StaticTarget(), + llvm_to_clang = Sys.iswindows(), + method_table=StaticCompiler.method_table, + kwargs... +) +``` +Attempt to compile a standalone executable that runs function `f` with a type signature given by the tuple of `types`. +If there are extra methods you would like to protect from name mangling in the produced binary for whatever reason, +you can provide them as a vector of tuples of functions and types, i.e. `[(f1, types1), (f2, types2), ...]` + +### Examples +```julia +julia> using StaticCompiler + +julia> function puts(s::Ptr{UInt8}) # Can't use Base.println because it allocates. + # Note, this `llvmcall` requires Julia 1.8+ + Base.llvmcall((\"\"\" + ; External declaration of the puts function + declare i32 @puts(i8* nocapture) nounwind + + define i32 @main(i8*) { + entry: + %call = call i32 (i8*) @puts(i8* %0) + ret i32 0 + } + \"\"\", "main"), Int32, Tuple{Ptr{UInt8}}, s) + end +puts (generic function with 1 method) + +julia> function print_args(argc::Int, argv::Ptr{Ptr{UInt8}}) + for i=1:argc + # Get pointer + p = unsafe_load(argv, i) + # Print string at pointer location (which fortunately already exists isn't tracked by the GC) + puts(p) + end + return 0 + end + +julia> compile_executable(print_args, (Int, Ptr{Ptr{UInt8}})) +""/Users/user/print_args"" + +shell> ./print_args 1 2 3 4 Five +./print_args +1 +2 +3 +4 +Five +``` +```julia +julia> using StaticTools # So you don't have to define `puts` and friends every time + +julia> hello() = println(c"Hello, world!") # c"..." makes a stack-allocated StaticString + +julia> compile_executable(hello) +"/Users/cbkeller/hello" + +shell> ls -alh hello +-rwxr-xr-x 1 user staff 33K Mar 20 21:11 hello + +shell> ./hello +Hello, world! +``` +""" +function compile_executable(f::Function, types=(), path::String=pwd(), name=fix_name(f); + also_expose=Tuple{Function, Tuple{DataType}}[], target::StaticTarget=StaticTarget(), + kwargs...) + compile_executable(vcat([(f, types)], also_expose), path, name; target, kwargs...) +end + +function compile_executable(funcs::Union{Array,Tuple}, path::String=pwd(), name=fix_name(first(first(funcs))); + filename = name, + demangle = true, + cflags = ``, + target::StaticTarget=StaticTarget(), + llvm_to_clang = Sys.iswindows(), + kwargs... + ) + + (f, types) = funcs[1] + tt = Base.to_tuple_type(types) + isexecutableargtype = tt == Tuple{} || tt == Tuple{Int, Ptr{Ptr{UInt8}}} + isexecutableargtype || @warn "input type signature $types should be either `()` or `(Int, Ptr{Ptr{UInt8}})` for standard executables" + + rt = last(only(static_code_typed(f, tt; target, kwargs...))) + isconcretetype(rt) || error("`$f$types` did not infer to a concrete type. Got `$rt`") + nativetype = isprimitivetype(rt) || isa(rt, Ptr) + nativetype || @warn "Return type `$rt` of `$f$types` does not appear to be a native type. Consider returning only a single value of a native machine type (i.e., a single float, int/uint, bool, or pointer). \n\nIgnoring this warning may result in Undefined Behavior!" + + generate_executable(funcs, path, name, filename; demangle, cflags, target, llvm_to_clang, kwargs...) + Sys.iswindows() && (filename *= ".exe") + joinpath(abspath(path), filename) +end + +""" +```julia +compile_shlib(f::Function, types::Tuple, [path::String=pwd()], [name::String=string(nameof(f))]; + filename::String=name, + cflags=``, + method_table=StaticCompiler.method_table, + target::StaticTarget=StaticTarget(), + kwargs...) + +compile_shlib(funcs::Array, [path::String=pwd()]; + filename="libfoo", + demangle=true, + cflags=``, + method_table=StaticCompiler.method_table, + target::StaticTarget=StaticTarget(), + kwargs...) +``` +As `compile_executable`, but compiling to a standalone `.dylib`/`.so` shared library. + +Arguments and returned values from `compile_shlib` must be native objects such as `Int`, `Float64`, or `Ptr`. They cannot be things like `Tuple{Int, Int}` because that is not natively sized. Such objects need to be passed by reference instead of by value. + +If `demangle` is set to `false`, compiled function names are prepended with "julia_". + +### Examples +```julia +julia> using StaticCompiler, LoopVectorization + +julia> function test(n) + r = 0.0 + @turbo for i=1:n + r += log(sqrt(i)) + end + return r/n + end +test (generic function with 1 method) + +julia> compile_shlib(test, (Int,)) +"/Users/user/test.dylib" + +julia> test(100_000) +5.2564961094956075 + +julia> ccall(("test", "test.dylib"), Float64, (Int64,), 100_000) +5.2564961094956075 +``` +""" +function compile_shlib(f::Function, types=(), path::String=pwd(), name=fix_name(f); + filename=name, + target::StaticTarget=StaticTarget(), + kwargs... + ) + compile_shlib(((f, types),), path; filename, target, kwargs...) +end +# As above, but taking an array of functions and returning a single shlib +function compile_shlib(funcs::Union{Array,Tuple}, path::String=pwd(); + filename = "libfoo", + demangle = true, + cflags = ``, + target::StaticTarget=StaticTarget(), + llvm_to_clang = Sys.iswindows(), + kwargs... + ) + for func in funcs + f, types = func + tt = Base.to_tuple_type(types) + isconcretetype(tt) || error("input type signature `$types` is not concrete") + + rt = last(only(static_code_typed(f, tt; target, kwargs...))) + isconcretetype(rt) || error("`$f$types` did not infer to a concrete type. Got `$rt`") + nativetype = isprimitivetype(rt) || isa(rt, Ptr) + nativetype || @warn "Return type `$rt` of `$f$types` does not appear to be a native type. Consider returning only a single value of a native machine type (i.e., a single float, int/uint, bool, or pointer). \n\nIgnoring this warning may result in Undefined Behavior!" + end + + generate_shlib(funcs, path, filename; demangle, cflags, target, llvm_to_clang, kwargs...) + + joinpath(abspath(path), filename * "." * Libdl.dlext) +end + + +""" +```julia +generate_shlib_fptr(path::String, name) +``` +Low level interface for obtaining a function pointer by `dlopen`ing a shared +library given the `path` and `name` of a `.so`/`.dylib` already compiled by +`generate_shlib`. + +See also `StaticCompiler.generate_shlib`. + +### Examples +```julia +julia> function test(n) + r = 0.0 + for i=1:n + r += log(sqrt(i)) + end + return r/n + end +test (generic function with 1 method) + +julia> path, name = StaticCompiler.generate_shlib(test, Tuple{Int64}, "./test"); + +julia> test_ptr = StaticCompiler.generate_shlib_fptr(path, name) +Ptr{Nothing} @0x000000015209f600 + +julia> ccall(test_ptr, Float64, (Int64,), 100_000) +5.256496109495593 + +julia> @ccall \$test_ptr(100_000::Int64)::Float64 # Equivalently +5.256496109495593 + +julia> test(100_000) +5.256496109495593 +``` +""" +function generate_shlib_fptr(path::String, name, filename::String=name) + lib_path = joinpath(abspath(path), "$filename.$(Libdl.dlext)") + ptr = Libdl.dlopen(lib_path, Libdl.RTLD_LOCAL) + fptr = Libdl.dlsym(ptr, name) + @assert fptr != C_NULL + fptr +end + +# As above, but also compile (maybe remove this method in the future?) +function generate_shlib_fptr(f, tt, path::String=tempname(), name=fix_name(f), filename::String=name; + temp::Bool=true, + kwargs...) + + generate_shlib(f, tt, false, path, name; kwargs...) + lib_path = joinpath(abspath(path), "$filename.$(Libdl.dlext)") + ptr = Libdl.dlopen(lib_path, Libdl.RTLD_LOCAL) + fptr = Libdl.dlsym(ptr, name) + @assert fptr != C_NULL + if temp + atexit(()->rm(path; recursive=true)) + end + fptr +end + +""" +```julia +generate_executable(f, tt, path::String, name, filename=string(name); kwargs...) +``` +Attempt to compile a standalone executable that runs `f`. +Low-level interface; you should generally use `compile_executable` instead. + +### Examples +```julia +julia> using StaticCompiler, StaticTools + +julia> hello() = println(c"Hello, world!") + +julia> path, name = StaticCompiler.generate_executable(hello, Tuple{}, "./") +("./", "hello") + +shell> ./hello +Hello, world! +``` +""" +generate_executable(f, tt, args...; kwargs...) = generate_executable(((f, tt),), args...; kwargs...) +function generate_executable(funcs::Union{Array,Tuple}, path=tempname(), name=fix_name(first(first(funcs))), filename=name; + demangle = true, + cflags = ``, + target::StaticTarget=StaticTarget(), + llvm_to_clang::Bool = Sys.iswindows(), + kwargs... + ) + exec_path = joinpath(path, filename) + _, obj_or_ir_path = generate_obj(funcs, path, filename; demangle, target, emit_llvm_only=llvm_to_clang, kwargs...) + # Pick a compiler + if !isnothing(target.compiler) + cc = `$(target.compiler)` + else + cc = Sys.isapple() ? `cc` : clang() + end + + # Compile! + if Sys.isapple() && !llvm_to_clang + # Apple no longer uses _start, so we can just specify a custom entry + entry = demangle ? "_$name" : "_julia_$name" + run(`$cc -e $entry $cflags $obj_or_ir_path -o $exec_path`) + else + fn = demangle ? "$name" : "julia_$name" + # Write a minimal wrapper to avoid having to specify a custom entry + wrapper_path = joinpath(path, "wrapper.c") + f = open(wrapper_path, "w") + print(f, """int $fn(int argc, char** argv); + void* __stack_chk_guard = (void*) $(rand(UInt) >> 1); + + int main(int argc, char** argv) + { + $fn(argc, argv); + return 0; + }""") + close(f) + if llvm_to_clang # (required on Windows) + # Use clang (llc) to generate an executable from the LLVM IR + cclang = if Sys.iswindows() + exec_path *= ".exe" + `clang` + elseif Sys.isapple() + `clang` + else + clang() + end + run(`$cclang -Wno-override-module $wrapper_path $obj_or_ir_path -o $exec_path`) + else + run(`$cc $wrapper_path $cflags $obj_or_ir_path -o $exec_path`) + end + + # Clean up + rm(wrapper_path) + end + path, name +end + + +""" +```julia +generate_shlib(f::Function, tt, [path::String], [name], [filename]; kwargs...) +generate_shlib(funcs::Array, [path::String], [filename::String]; demangle=true, target::StaticTarget=StaticTarget(), kwargs...) +``` +Low level interface for compiling a shared object / dynamically loaded library + (`.so` / `.dylib`) for function `f` given a tuple type `tt` characterizing +the types of the arguments for which the function will be compiled. + +If `demangle` is set to `false`, compiled function names are prepended with "julia_". + +### Examples +```julia +julia> using StaticCompiler, LoopVectorization + +julia> function test(n) + r = 0.0 + @turbo for i=1:n + r += log(sqrt(i)) + end + return r/n + end +test (generic function with 1 method) + +julia> path, name = StaticCompiler.generate_shlib(test, Tuple{Int64}, true, "./example") +("./example", "test") + +shell> tree \$path +./example +|-- test.dylib +`-- test.o +0 directories, 2 files + +julia> test(100_000) +5.2564961094956075 + +julia> ccall(("test", "example/test.dylib"), Float64, (Int64,), 100_000) +5.2564961094956075 +``` +""" +function generate_shlib(f::Function, tt, path::String=tempname(), name=fix_name(f), filename=name; target=StaticTarget(), kwargs...) + generate_shlib(((f, tt),), path, filename; target, kwargs...) +end +# As above, but taking an array of functions and returning a single shlib +function generate_shlib(funcs::Union{Array,Tuple}, path::String=tempname(), filename::String="libfoo"; + demangle = true, + cflags = ``, + target::StaticTarget=StaticTarget(), + llvm_to_clang::Bool = Sys.iswindows(), + kwargs... + ) + if !isnothing(target.platform) + lib_path = joinpath(path, "$filename.$(platform_dlext(target.platform))") + else + lib_path = joinpath(path, "$filename.$(Libdl.dlext)") + end + + _, obj_or_ir_path = generate_obj(funcs, path, filename; demangle, target, emit_llvm_only=llvm_to_clang, kwargs...) + # Pick a Clang + if !isnothing(target.compiler) + cc = `$(target.compiler)` + else + cc = Sys.isapple() ? `cc` : clang() + end + # Compile! + if llvm_to_clang # (required on Windows) + # Use clang (llc) to generate an executable from the LLVM IR + cclang = if Sys.iswindows() + add_dllexport(funcs, obj_or_ir_path; demangle) + `clang` + elseif Sys.isapple() + `clang` + else + clang() + end + run(`$cclang -shared -Wno-override-module $obj_or_ir_path -o $lib_path`) + else + run(`$cc -shared $cflags $obj_or_ir_path -o $lib_path `) + end + + path, name +end + +function static_code_llvm(@nospecialize(func), @nospecialize(types); target::StaticTarget=StaticTarget(), kwargs...) + job, kwargs = static_job(func, types; target, kwargs...) + GPUCompiler.code_llvm(stdout, job; libraries=false, kwargs...) +end + +function static_code_typed(@nospecialize(func), @nospecialize(types); target::StaticTarget=StaticTarget(), kwargs...) + job, kwargs = static_job(func, types; target, kwargs...) + GPUCompiler.code_typed(job; kwargs...) +end + +function static_code_native(@nospecialize(f), @nospecialize(tt), fname=fix_name(f); target::StaticTarget=StaticTarget(), kwargs...) + job, kwargs = static_job(f, tt; fname, target, kwargs...) + GPUCompiler.code_native(stdout, job; libraries=false, kwargs...) +end + +# Return an LLVM module +function static_llvm_module(f, tt, name=fix_name(f); demangle=true, target::StaticTarget=StaticTarget(), kwargs...) + if !demangle + name = "julia_"*name + end + job, kwargs = static_job(f, tt; name, target, kwargs...) + m = GPUCompiler.JuliaContext() do context + m, _ = GPUCompiler.codegen(:llvm, job; strip=true, only_entry=false, validate=false, libraries=false) + locate_pointers_and_runtime_calls(m) + m + end + return m +end + +#Return an LLVM module for multiple functions +function static_llvm_module(funcs::Union{Array,Tuple}; demangle=true, target::StaticTarget=StaticTarget(), kwargs...) + f,tt = funcs[1] + mod = GPUCompiler.JuliaContext() do context + name_f = fix_name(f) + if !demangle + name_f = "julia_"*name_f + end + job, kwargs = static_job(f, tt; name = name_f, target, kwargs...) + mod,_ = GPUCompiler.codegen(:llvm, job; strip=true, only_entry=false, validate=false, libraries=false) + if length(funcs) > 1 + for func in funcs[2:end] + f,tt = func + name_f = fix_name(f) + if !demangle + name_f = "julia_"*name_f + end + job, kwargs = static_job(f, tt; name = name_f, target, kwargs...) + tmod,_ = GPUCompiler.codegen(:llvm, job; strip=true, only_entry=false, validate=false, libraries=false) + link!(mod,tmod) + end + end + locate_pointers_and_runtime_calls(mod) + mod + end + # Just to be sure + for (modfunc, func) in zip(functions(mod), funcs) + fname = name(modfunc) + expectedname = (demangle ? "" : "julia_") * fix_name(func) + d = prefixlen(fname) - prefixlen(expectedname) + 1 + if d > 1 + name!(modfunc,fname[d:end]) + end + end + LLVM.ModulePassManager() do pass_manager #remove duplicate functions + LLVM.merge_functions!(pass_manager) + LLVM.run!(pass_manager, mod) + end + return mod +end + +function prefixlen(s) + m = match(r"^(?:julia_)+", s) + if m isa RegexMatch + length(m.match) + else + 0 + end +end + +""" +```julia +generate_obj(f, tt, path::String = tempname(), filenamebase::String="obj"; + target::StaticTarget=StaticTarget(), + demangle = true, + strip_llvm = false, + strip_asm = true, + kwargs...) +``` +Low level interface for compiling object code (`.o`) for for function `f` given +a tuple type `tt` characterizing the types of the arguments for which the +function will be compiled. + +`target` can be used to change the output target. This is useful for compiling to WebAssembly and embedded targets. +This is a struct of the type StaticTarget() +The defaults compile to the native target. + +If `demangle` is set to `false`, compiled function names are prepended with "julia_". + +### Examples +```julia +julia> fib(n) = n <= 1 ? n : fib(n - 1) + fib(n - 2) +fib (generic function with 1 method) + +julia> path, name, table = StaticCompiler.generate_obj(fib, Tuple{Int64}, "./test") +("./test", "fib", IdDict{Any, String}()) + +shell> tree \$path +./test +└── obj.o + +0 directories, 1 file +``` +""" +function generate_obj(f, tt, args...; kwargs...) + generate_obj(((f, tt),), args...; kwargs...) +end + + +""" +```julia +generate_obj(funcs::Union{Array,Tuple}, path::String = tempname(), filenamebase::String="obj"; + target::StaticTarget=StaticTarget(), + demangle = false, + emit_llvm_only = false, + strip_llvm = false, + strip_asm = true, + kwargs...) +``` +Low level interface for compiling object code (`.o`) for an array of Tuples +(f, tt) where each function `f` and tuple type `tt` determine the set of methods +which will be compiled. + +`target` can be used to change the output target. This is useful for compiling to WebAssembly and embedded targets. +This is a struct of the type StaticTarget() +The defaults compile to the native target. +""" +function generate_obj(funcs::Union{Array,Tuple}, path::String = tempname(), filenamebase::String="obj"; + target::StaticTarget=StaticTarget(), + demangle = true, + emit_llvm_only = false, + strip_llvm = false, + strip_asm = true, + kwargs...) + f, tt = funcs[1] + mkpath(path) + mod = static_llvm_module(funcs; demangle, kwargs...) + + if emit_llvm_only # (Required on Windows) + ir_path = joinpath(path, "$filenamebase.ll") + open(ir_path, "w") do io + write(io, string(mod)) + end + return path, ir_path + else + obj_path = joinpath(path, "$filenamebase.o") + obj = GPUCompiler.JuliaContext() do ctx + fakejob, _ = static_job(f, tt; target, kwargs...) + obj, _ = GPUCompiler.emit_asm(fakejob, mod; strip=strip_asm, validate=false, format=LLVM.API.LLVMObjectFile) + obj + end + open(obj_path, "w") do io + write(io, obj) + end + return path, obj_path + end +end end # module diff --git a/src/ccalls.jl b/src/ccalls.jl deleted file mode 100644 index 4314fab5..00000000 --- a/src/ccalls.jl +++ /dev/null @@ -1,90 +0,0 @@ - -""" - find_ccalls(f, tt) - -Returns a `Dict` mapping function addresses to symbol names for all `ccall`s and -`cglobal`s called from the method. This descends into other invocations -within the method. -""" -find_ccalls(@nospecialize(f), @nospecialize(tt)) = find_ccalls(reflect(f, tt)) - -function find_ccalls(ref::Reflection) - result = Dict{Ptr{Nothing}, Symbol}() - idx = VERSION > v"1.2" ? 5 : 4 - foreigncalls = TypedCodeUtils.filter((c) -> lookthrough((c) -> c.head === :foreigncall && !(c.args[idx] isa QuoteNode && c.args[idx].value == :llvmcall), c), ref.CI.code) - # foreigncalls = TypedCodeUtils.filter((c) -> lookthrough((c) -> c.head === :foreigncall, c), ref.CI.code) - for fc in foreigncalls - sym = getsym(fc[2].args[1]) - address = eval(:(cglobal($(sym)))) - result[address] = Symbol(sym isa Tuple ? sym[1] : sym.value) - end - cglobals = TypedCodeUtils.filter((c) -> lookthrough(c -> c.head === :call && iscglobal(c.args[1]), c), ref.CI.code) - for fc in cglobals - sym = getsym(fc[2].args[2]) - address = eval(:(cglobal($(sym)))) - result[address] = Symbol(sym isa Tuple ? sym[1] : sym.value) - end - invokes = TypedCodeUtils.filter((c) -> lookthrough(identify_invoke, c), ref.CI.code) - invokes = map((arg) -> process_invoke(DefaultConsumer(), ref, arg...), invokes) - for fi in invokes - canreflect(fi) || continue - merge!(result, find_ccalls(reflect(fi))) - end - return result -end - -getsym(x) = x -getsym(x::String) = QuoteNode(Symbol(x)) -getsym(x::QuoteNode) = x -getsym(x::Expr) = eval.((x.args[2], x.args[3])) - -iscglobal(x) = x == cglobal || x isa GlobalRef && x.name == :cglobal - - -""" - fix_ccalls!(mod::LLVM.Module, d) - -Replace function addresses with symbol names in `mod`. The symbol names are -meant to be linked to `libjulia` or other libraries. -`d` is a `Dict` mapping a function address to symbol name for `ccall`s. -""" -function fix_ccalls!(mod::LLVM.Module, d) - for fun in functions(mod), blk in blocks(fun), instr in instructions(blk) - if instr isa LLVM.CallInst - dest = called_value(instr) - if dest isa ConstantExpr && occursin("inttoptr", string(dest)) - # @show instr - # @show dest - argtypes = [llvmtype(op) for op in operands(instr)] - nargs = length(parameters(eltype(argtypes[end]))) - # num_extra_args = 1 + length(collect(eachmatch(r"jl_roots", string(instr)))) - ptr = Ptr{Cvoid}(convert(Int, first(operands(dest)))) - if haskey(d, ptr) - sym = d[ptr] - newdest = LLVM.Function(mod, string(sym), LLVM.FunctionType(llvmtype(instr), argtypes[1:nargs])) - LLVM.linkage!(newdest, LLVM.API.LLVMExternalLinkage) - replace_uses!(dest, newdest) - end - end - elseif instr isa LLVM.LoadInst && occursin("inttoptr", string(instr)) - # dest = called_value(instr) - for op in operands(instr) - lastop = op - if occursin("inttoptr", string(op)) - # @show instr - if occursin("addrspacecast", string(op)) || occursin("getelementptr", string(op)) - op = first(operands(op)) - end - first(operands(op)) isa LLVM.ConstantInt || continue - ptr = Ptr{Cvoid}(convert(Int, first(operands(op)))) - if haskey(d, ptr) - obj = d[ptr] - newdest = GlobalVariable(mod, llvmtype(instr), string(d[ptr])) - LLVM.linkage!(newdest, LLVM.API.LLVMExternalLinkage) - replace_uses!(op, newdest) - end - end - end - end - end -end diff --git a/src/dllexport.jl b/src/dllexport.jl new file mode 100644 index 00000000..b957e7b5 --- /dev/null +++ b/src/dllexport.jl @@ -0,0 +1,11 @@ +function add_dllexport(funcs, ir_path; demangle=true) + ir = read(ir_path, String) + + for (f, _) in funcs + name_f = (demangle ? "" : "julia_") * fix_name(f) + pattern = Regex("^define(.*?@$name_f\\()", "m") + ir = replace(ir, pattern => s"define dllexport\1") + end + + write(ir_path, ir) +end \ No newline at end of file diff --git a/src/extern.jl b/src/extern.jl deleted file mode 100644 index 6c0424a9..00000000 --- a/src/extern.jl +++ /dev/null @@ -1,16 +0,0 @@ -""" - @extern(fun, returntype, argtypes, args...) - -Creates a call to an external function meant to be included at link time. -Use the same conventions as `ccall`. - -This transforms into the following `ccall`: - - ccall("extern fun", llvmcall, returntype, argtypes, args...) -""" -macro extern(name, rettyp, argtyp, args...) - externfun = string("extern ", name isa AbstractString || name isa Symbol ? name : name.value) - Expr(:call, :ccall, externfun, esc(:llvmcall), esc(rettyp), - Expr(:tuple, esc.(argtyp.args)...), esc.(args)...) -end - diff --git a/src/globals.jl b/src/globals.jl deleted file mode 100644 index e2583388..00000000 --- a/src/globals.jl +++ /dev/null @@ -1,164 +0,0 @@ -struct GlobalsContext - invokes::Set{Any} -end -GlobalsContext() = GlobalsContext(Set()) - - -""" - fix_globals!(mod::LLVM.Module) - -Replace function addresses in `mod` with references to global data structures. -For each global variable, two LLVM global objects are created: - -* `jl.global.data` -- An LLVM 'i8' vector holding a serialized version of the Julia object. -* `jl.global` -- A pointer to the unserialized Julia object. - -The `inttopt` with the function address is replaced by `jl.global`. - -A function `jl_init_globals` is added to `mod`. This function deserializes the data in -`jl.global.data` and updates `jl.global`. -""" - -_opcode(x::LLVM.ConstantExpr) = LLVM.API.LLVMGetConstOpcode(LLVM.ref(x)) - -function fix_globals!(mod::LLVM.Module) - # Create a `jl_init_globals` function. - jl_init_globals_func = LLVM.Function(mod, "jl_init_globals", - LLVM.FunctionType(julia_to_llvm(Cvoid), LLVMType[])) - jl_init_global_entry = BasicBlock(jl_init_globals_func, "entry", context(mod)) - - # Definitions for utility functions - func_type = LLVM.FunctionType(julia_to_llvm(Any), LLVMType[LLVM.PointerType(julia_to_llvm(Int8))]) - deserialize_funs = Dict() - - uint8_t = julia_to_llvm(UInt8) - - ctx = SerializeContext() - es = [] - objs = Set() - gptridx = Dict() - instrs = [] - gptrs = [] - j = 1 # counter for position in gptridx - Builder(context(mod)) do builder - toinstr!(x) = x - function toinstr!(x::LLVM.ConstantExpr) - if _opcode(x) == LLVM.API.LLVMAddrSpaceCast - val = toinstr!(first(operands(x))) - ret = addrspacecast!(builder, val, llvmtype(x)) - return ret - elseif _opcode(x) == LLVM.API.LLVMGetElementPtr - ops = operands(x) - val = toinstr!(first(ops)) - ret = gep!(builder, val, [ops[i] for i in 2:length(ops)]) - return ret - elseif _opcode(x) == LLVM.API.LLVMBitCast - ops = operands(x) - val = toinstr!(first(ops)) - ret = pointercast!(builder, val, llvmtype(x)) - return ret - elseif _opcode(x) == LLVM.API.LLVMIntToPtr - ptr = Ptr{Any}(convert(Int, first(operands(x)))) - obj = unsafe_pointer_to_objref(ptr) - if !in(obj, objs) - push!(es, serialize(ctx, obj)) - push!(objs, obj) - # Create pointers to the data. - gptr = GlobalVariable(mod, julia_to_llvm(Any), "jl.global") - linkage!(gptr, LLVM.API.LLVMInternalLinkage) - LLVM.API.LLVMSetInitializer(LLVM.ref(gptr), LLVM.ref(null(julia_to_llvm(Any)))) - push!(gptrs, gptr) - gptridx[obj] = j - j += 1 - end - gptr = gptrs[gptridx[obj]] - gptr2 = load!(builder, gptr) - ret = pointercast!(builder, gptr2, llvmtype(x)) - return ret - end - return x - end - for fun in functions(mod) - if startswith(LLVM.name(fun), "jfptr") - unsafe_delete!(mod, fun) - continue - end - - for blk in blocks(fun), instr in instructions(blk) - # Set up functions to walk the operands of the instruction - # and convert appropriate ConstantExpr's to instructions. - # Look for `LLVMIntToPtr` expressions. - position!(builder, instr) - ops = operands(instr) - N = opcode(instr) == LLVM.API.LLVMCall ? length(ops) - 1 : length(ops) - if opcode(instr) == LLVM.API.LLVMCall && name(last(operands(instr))) == "jl_type_error" - continue - end - for i in 1:N - try - if opcode(instr) == LLVM.API.LLVMPHI - position!(builder, last(instructions(LLVM.incoming(instr)[i][2]))) - end - ops[i] = toinstr!(ops[i]) - catch x - end - end - end - end - end - nglobals = length(es) - #@show mod - #verify(mod) - for i in 1:nglobals - # Assign the appropriate function argument to the appropriate global. - es[i] = :(unsafe_store!($((Symbol("global", i))), $(es[i]))) - end - # Define the deserializing function. - fune = quote - function _deserialize_globals(Vptr, $((Symbol("global", i) for i in 1:nglobals)...)) - $(ctx.init...) - $(es...) - return - end - end - # @show fune - # Execute the deserializing function. - deser_fun = eval(fune) - v = take!(ctx.io) - gv_typ = LLVM.ArrayType(uint8_t, length(v)) - data = LLVM.GlobalVariable(mod, gv_typ, "jl.global.data") - linkage!(data, LLVM.API.LLVMExternalLinkage) - constant!(data, true) - LLVM.API.LLVMSetInitializer(LLVM.ref(data), - LLVM.API.LLVMConstArray(LLVM.ref(uint8_t), - [LLVM.ref(ConstantInt(uint8_t, x)) for x in v], - UInt32(length(v)))) - Builder(context(mod)) do builder - dataptr = gep!(builder, data, [ConstantInt(0, context(mod)), ConstantInt(0, context(mod))]) - - # Create the Julia object from `data` and include that in `init_fun`. - position!(builder, jl_init_global_entry) - gfunc_type = LLVM.FunctionType(julia_to_llvm(Cvoid), - LLVMType[LLVM.PointerType(julia_to_llvm(Int8)), - Iterators.repeated(LLVM.FunctionType(julia_to_llvm(Any)), nglobals)...]) - deserialize_globals_func = LLVM.Function(mod, "_deserialize_globals", gfunc_type) - LLVM.linkage!(deserialize_globals_func, LLVM.API.LLVMExternalLinkage) - for i in 1:nglobals - # The following fix is to match the argument types which are an integer, not a %jl_value_t**. - gptrs[i] = LLVM.ptrtoint!(builder, gptrs[i], julia_to_llvm(Csize_t)) - end - LLVM.call!(builder, deserialize_globals_func, LLVM.Value[dataptr, gptrs...]) - ret!(builder) - end - tt = Tuple{Ptr{UInt8}, Iterators.repeated(Ptr{Any}, nglobals)...} - deser_mod = irgen(deser_fun, tt, overdub = false, fix_globals = false, optimize_llvm = false) - d = find_ccalls(deser_fun, tt) - fix_ccalls!(deser_mod, d) - # rename deserialization function to "_deserialize_globals" - fun = first(TypedCodeUtils.filter(x -> LLVM.name(x) == "_deserialize_globals", functions(deser_mod)))[2] - # LLVM.name!(fun, "_deserialize_globals") - linkage!(fun, LLVM.API.LLVMExternalLinkage) - # link into the main module - LLVM.link!(mod, deser_mod) - return -end diff --git a/src/helpers/README.md b/src/helpers/README.md deleted file mode 100644 index e443db6a..00000000 --- a/src/helpers/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Helpers -Note that the helpers defined here are used in tests, and they are useful to test out code in the REPL. - -```julia -twox(x) = 2x -# run code in the REPL -@jlrun twox(3) -# compile to an executable in a `standalone` directory -exegen([ (twox, Tuple{Int}, 4) ]) -``` - -These are not meant to be a permanent part of the API. They are just for testing. diff --git a/src/helpers/helpers.jl b/src/helpers/helpers.jl deleted file mode 100644 index 1f041540..00000000 --- a/src/helpers/helpers.jl +++ /dev/null @@ -1,37 +0,0 @@ -""" -Returns shellcmd string for different OS. Optionally, checks for gcc installation. -""" -function _shellcmd(checkInstallation::Bool = false) - - if Sys.isunix() - shellcmd = "gcc" - elseif Sys.iswindows() - shellcmd = ["cmd", "/c", "gcc"] - else - error("run command not defined") - end - - if checkInstallation - # Checking gcc installation - try - run(`$shellcmd -v`) - catch - @warn "Make sure gcc compiler is installed: https://gcc.gnu.org/install/binaries.html and is on the path, othetwise some of the functions will return errors" - return nothing - end - end - - return shellcmd -end - -shellcmd = _shellcmd(true) # is used in @jlrun and exegen() - - -export @jlrun -include("jlrun.jl") - -export ldflags, ldlibs, cflags # are used in exegen -include("juliaconfig.jl") - -export exegen -include("standalone-exe.jl") diff --git a/src/helpers/jlrun.jl b/src/helpers/jlrun.jl deleted file mode 100644 index f2e8e165..00000000 --- a/src/helpers/jlrun.jl +++ /dev/null @@ -1,61 +0,0 @@ -using Libdl, LLVM - -function show_inttoptr(mod) - for fun in LLVM.functions(mod), - blk in LLVM.blocks(fun), - instr in LLVM.instructions(blk) - - s = string(instr) - if occursin("inttoptr", s) && occursin(r"[0-9]{8,30}", s) - println(LLVM.name(fun), " ---------------------------") - @show instr - println() - end - end -end - -""" -Compiles function call provided and calls it with `ccall` using the shared library that was created. -""" -macro jlrun(e) - - fun = e.args[1] - efun = esc(fun) - args = length(e.args) > 1 ? e.args[2:end] : Any[] - libpath = abspath("test.o") - dylibpath = abspath("test.so") - tt = Tuple{(typeof(eval(a)) for a in args)...} - if length(e.args) > 1 - ct = code_typed(Base.eval(__module__, fun), tt) - else - ct = code_typed(Base.eval(__module__, fun)) - end - rettype = ct[1][2] - bindir = joinpath(dirname(Sys.BINDIR), "tools") - libdir = joinpath(dirname(Sys.BINDIR), "lib") - - runCommand = :(run( - $(`$shellcmd -shared -fPIC -o test.so -L$libdir test.o -ljulia`), - wait = true, - )) - - quote - m = irgen($efun, $tt) - # m = irgen($efun, $tt, overdub = false) - LLVM.verify(m) - # show_inttoptr(m) - write(m, "test.bc") - write_object(m, "test.o") - $runCommand - dylib = Libdl.dlopen($dylibpath) - ccall(Libdl.dlsym(dylib, "jl_init_globals"), Cvoid, ()) - res = ccall( - Libdl.dlsym(dylib, $(Meta.quot(fun))), - $rettype, - ($((typeof(eval(a)) for a in args)...),), - $(eval.(args)...), - ) - Libdl.dlclose(dylib) - res - end -end diff --git a/src/helpers/juliaconfig.jl b/src/helpers/juliaconfig.jl deleted file mode 100644 index 039e0905..00000000 --- a/src/helpers/juliaconfig.jl +++ /dev/null @@ -1,63 +0,0 @@ -# from PackageCompilerX: https://github.com/KristofferC/PackageCompilerX.jl/blob/c1a90edfaa28907edf2edbbc734ef8afdeeaca80/src/juliaconfig.jl -# adopted from https://github.com/JuliaLang/julia/blob/release-0.6/contrib/julia-config.jl - -function shell_escape(str) - str = replace(str, "'" => "'\''") - return "'$str'" -end - -function julia_libdir() - return if ccall(:jl_is_debugbuild, Cint, ()) != 0 - dirname(abspath(Libdl.dlpath("libjulia-debug"))) - else - dirname(abspath(Libdl.dlpath("libjulia"))) - end -end - -function julia_private_libdir() - @static if Sys.iswindows() - return julia_libdir() - else - return abspath(Sys.BINDIR, Base.PRIVATE_LIBDIR) - end -end - -julia_includedir() = abspath(Sys.BINDIR, Base.INCLUDEDIR, "julia") - -function ldflags() - fl = "-L$(shell_escape(julia_libdir()))" - if Sys.iswindows() - fl = fl * " -Wl,--stack,8388608" - fl = fl * " -Wl,--export-all-symbols" - elseif Sys.islinux() - fl = fl * " -Wl,--export-dynamic" - end - return fl -end - -# TODO -function ldlibs(relative_path=nothing) - libname = if ccall(:jl_is_debugbuild, Cint, ()) != 0 - "julia-debug" - else - "julia" - end - if Sys.islinux() - return "-Wl,-rpath-link,$(shell_escape(julia_libdir())) -Wl,-rpath-link,$(shell_escape(julia_private_libdir())) -l$libname" - elseif Sys.iswindows() - return "-l$libname -lopenlibm" - else - return "-l$libname" - end -end - -function cflags() - flags = IOBuffer() - print(flags, "-std=gnu99") - include = shell_escape(julia_includedir()) - print(flags, " -I", include) - if Sys.isunix() - print(flags, " -fPIC") - end - return String(take!(flags)) -end diff --git a/src/helpers/standalone-exe.jl b/src/helpers/standalone-exe.jl deleted file mode 100644 index 40d026bb..00000000 --- a/src/helpers/standalone-exe.jl +++ /dev/null @@ -1,148 +0,0 @@ -Ctemplate = """ -#include -#include -extern CRETTYPE FUNNAME(CARGTYPES); -extern void jl_init_with_image(const char *, const char *); -extern void jl_init_globals(void); -int main() -{ - jl_init_with_image(".", "blank.ji"); - jl_init_globals(); - printf("RETFORMAT", FUNNAME(FUNARG)); - jl_atexit_hook(0); - return 0; -} -""" - -# "signed" is removed from signed types -# duplicates will remove automatically -Cmap = Dict( - Cchar => "char", #Int8 - Cuchar => "unsigned char", #UInt8 - Cshort => "short", #Int16 - # Cstring => - Cushort => "unsigned short", #UInt16 - Cint => "int", #Int32 - Cuint => "unsigned int", #UInt32 - Clong => "long", #Int32 - Culong => "unsigned long", #UInt32 - Clonglong => "long long", #Int64 - Culonglong => "unsigned long long", #UInt64 - # Cintmax_t => "intmax_t", #Int64 - # Cuintmax_t => "uintmax_t", #UInt64 - # Csize_t => "size_t", #UInt - # Cssize_t => "ssize_t", #Int - # Cptrdiff_t => "ptrdiff_t", #Int - # Cwchar_t => "wchar_t", #Int32 - # Cwstring => - Cfloat => "float", #Float32 - Cdouble => "double", #Float64 - Nothing => "void", -) - -Cformatmap = Dict( - Cchar => "%c", #Int8 - # Cuchar => "unsigned char", #UInt8 - # Cshort => "short", #Int16 - Cstring => "%s", - # Cushort => "unsigned short", #UInt16 - Cint => "%d", #"i" #Int32 - Cuint => "%u", #UInt32 - Clong => "%ld", #Int32 - # Culong => "unsigned long", #UInt32 - Clonglong => "%lld", #Int64 - # Culonglong => "unsigned long long", #UInt64 - # Cintmax_t => "intmax_t", #Int64 - # Cuintmax_t => "uintmax_t", #UInt64 - # Csize_t => "size_t", #UInt - # Cssize_t => "ssize_t", #Int - # Cptrdiff_t => "ptrdiff_t", #Int - # Cwchar_t => "wchar_t", #Int32 - # Cwstring => - # Cfloat => "%f", #Float32 - Cdouble => "%f", #%e #Float64 -) - -""" -converts to text. returns "" for Nothing and empty Tuple. -""" -totext(x) = string(x) -totext(x::Nothing) = "" -totext(x::Tuple{}) = "" - -""" -Makes standalone executable. -""" -function exegen(funcalls) - - cd(mkpath("standalone")) do - # create `blank.ji` for initialization - julia_path = joinpath(Sys.BINDIR, Base.julia_exename()) - base_dir = dirname(Base.find_source_file("sysimg.jl")) - wd = pwd() - open(println, "blank.jl", "w") - cd(base_dir) do - run(`$(julia_path) --output-ji $(wd)/blank.ji $(wd)/blank.jl`) - end - - dir = pwd() - standalonedir = dir - bindir = string(Sys.BINDIR) - libdir = joinpath(dirname(Sys.BINDIR), "lib") - includedir = joinpath(dirname(Sys.BINDIR), "include", "julia") - if Sys.iswindows() - for fn in readdir(bindir) - if splitext(fn)[end] == ".dll" - cp(joinpath(bindir, fn), fn, force = true) - end - end - end - - flags = join((cflags(), ldflags(), ldlibs()), " ") - flags = Base.shell_split(flags) - wrapper = joinpath(@__DIR__, "embedding_wrapper.c") - if Sys.iswindows() - rpath = `` - elseif Sys.isapple() - rpath = `-Wl,-rpath,'@executable_path' -Wl,-rpath,'@executable_path/../lib'` - else - rpath = `-Wl,-rpath,\$ORIGIN:\$ORIGIN/../lib` - end - - for (func, tt, val) in funcalls - fname = nameof(func) - rettype = Base.return_types(func, tt)[1] - argtype = length(tt.types) > 0 ? tt.types[1] : Nothing - fmt = Cformatmap[rettype] - Ctxt = foldl(replace, - ( - "FUNNAME" => fname, - "CRETTYPE" => Cmap[rettype], - "RETFORMAT" => fmt, - "CARGTYPES" => Cmap[argtype], - "FUNARG" => totext(val), - ), - init = Ctemplate) - write("$fname.c", Ctxt) - m = StaticCompiler.irgen(func, tt) - # StaticCompiler.show_inttoptr(m) - # @show m - dlext = Libdl.dlext - exeext = Sys.iswindows() ? ".exe" : "" - if Sys.isapple() - o_file = `-Wl,-all_load $fname.o` - else - o_file = `-Wl,--whole-archive $fname.o -Wl,--no-whole-archive` - end - extra = Sys.iswindows() ? `-Wl,--export-all-symbols` : `` - write(m, "$fname.bc") - write_object(m, "$fname.o") - - run(`$shellcmd -shared -fpic -L$libdir -o lib$fname.$dlext $o_file -Wl,-rpath,$libdir -ljulia $extra`) - run(`$shellcmd -c -std=gnu99 -I$includedir -DJULIA_ENABLE_THREADING=1 -fPIC $fname.c`) - #run(`$shellcmd -o $fname $fname.o -L$libdir -L$standalonedir -Wl,--unresolved-symbols=ignore-in-object-files -Wl,-rpath,'.' -Wl,-rpath,$libdir -ljulia -l$fname -O2 $rpath $flags`) - run(`$shellcmd -o $fname $fname.o -L$libdir -L$standalonedir -Wl,-rpath,'.' -Wl,-rpath,$libdir -ljulia -l$fname -O2 $rpath $flags`) - end - end - -end diff --git a/src/interpreter.jl b/src/interpreter.jl new file mode 100644 index 00000000..344cc53d --- /dev/null +++ b/src/interpreter.jl @@ -0,0 +1,134 @@ +## interpreter + +using Core.Compiler: + AbstractInterpreter, InferenceResult, InferenceParams, InferenceState, MethodInstance, OptimizationParams, WorldView, get_world_counter +using GPUCompiler: + @safe_debug, AbstractCompilerParams, CodeCache, CompilerJob, methodinstance +using CodeInfoTools +using CodeInfoTools: resolve + +struct StaticInterpreter <: AbstractInterpreter + global_cache::CodeCache + method_table::Union{Nothing,Core.MethodTable} + + # Cache of inference results for this particular interpreter + local_cache::Vector{InferenceResult} + # The world age we're working inside of + world::UInt + + # Parameters for inference and optimization + inf_params::InferenceParams + opt_params::OptimizationParams + + function StaticInterpreter(cache::CodeCache, mt::Union{Nothing,Core.MethodTable}, world::UInt, ip::InferenceParams, op::OptimizationParams) + @assert world <= Base.get_world_counter() + + return new( + cache, + mt, + + # Initially empty cache + Vector{InferenceResult}(), + + # world age counter + world, + + # parameters for inference and optimization + ip, + op + ) + end +end + + +Core.Compiler.InferenceParams(interp::StaticInterpreter) = interp.inf_params +Core.Compiler.OptimizationParams(interp::StaticInterpreter) = interp.opt_params +Core.Compiler.get_world_counter(interp::StaticInterpreter) = interp.world +Core.Compiler.get_inference_cache(interp::StaticInterpreter) = interp.local_cache +Core.Compiler.code_cache(interp::StaticInterpreter) = WorldView(interp.global_cache, interp.world) + +# No need to do any locking since we're not putting our results into the runtime cache +Core.Compiler.lock_mi_inference(interp::StaticInterpreter, mi::MethodInstance) = nothing +Core.Compiler.unlock_mi_inference(interp::StaticInterpreter, mi::MethodInstance) = nothing + +function Core.Compiler.add_remark!(interp::StaticInterpreter, sv::InferenceState, msg) + @safe_debug "Inference remark during static compilation of $(sv.linfo): $msg" +end + + +##### +##### Pre-inference +##### + +function resolve_generic(a) + if a isa Type && a <: Function && isdefined(a, :instance) + return a.instance + else + return resolve(a) + end +end + +function custom_pass!(interp::StaticInterpreter, result::InferenceResult, mi::Core.MethodInstance, src) + src === nothing && return src + mi.specTypes isa UnionAll && return src + sig = Tuple(mi.specTypes.parameters) + as = map(resolve_generic, sig) + return src +end + +function Core.Compiler.InferenceState(result::InferenceResult, cache::Symbol, interp::StaticInterpreter) + world = get_world_counter(interp) + src = @static if VERSION >= v"1.10.0-DEV.873" + Core.Compiler.retrieve_code_info(result.linfo, world) + else + Core.Compiler.retrieve_code_info(result.linfo) + end + mi = result.linfo + src = custom_pass!(interp, result, mi, src) + src === nothing && return nothing + Core.Compiler.validate_code_in_debug_mode(result.linfo, src, "lowered") + return InferenceState(result, src, cache, interp) +end + +Core.Compiler.may_optimize(interp::StaticInterpreter) = true +Core.Compiler.may_compress(interp::StaticInterpreter) = true +Core.Compiler.may_discard_trees(interp::StaticInterpreter) = true +Core.Compiler.verbose_stmt_info(interp::StaticInterpreter) = false + + +if isdefined(Base.Experimental, Symbol("@overlay")) + using Core.Compiler: OverlayMethodTable + if v"1.8-beta2" <= VERSION < v"1.9-" || VERSION >= v"1.9.0-DEV.120" + Core.Compiler.method_table(interp::StaticInterpreter) = + OverlayMethodTable(interp.world, interp.method_table) + else + Core.Compiler.method_table(interp::StaticInterpreter, sv::InferenceState) = + OverlayMethodTable(interp.world, interp.method_table) + end +else + Core.Compiler.method_table(interp::StaticInterpreter, sv::InferenceState) = + WorldOverlayMethodTable(interp.world) +end + +# semi-concrete interepretation is broken with overlays (JuliaLang/julia#47349) +@static if VERSION >= v"1.9.0-DEV.1248" +function Core.Compiler.concrete_eval_eligible(interp::StaticInterpreter, + @nospecialize(f), result::Core.Compiler.MethodCallResult, arginfo::Core.Compiler.ArgInfo) + ret = @invoke Core.Compiler.concrete_eval_eligible(interp::AbstractInterpreter, + f::Any, result::Core.Compiler.MethodCallResult, arginfo::Core.Compiler.ArgInfo) + ret === false && return nothing + return ret +end +end + +struct StaticCompilerParams <: AbstractCompilerParams + opt::Bool + optlevel::Int + cache::CodeCache +end + +function StaticCompilerParams(; opt = false, + optlevel = Base.JLOptions().opt_level, + cache = CodeCache()) + return StaticCompilerParams(opt, optlevel, cache) +end diff --git a/src/irgen.jl b/src/irgen.jl deleted file mode 100644 index cc9811c4..00000000 --- a/src/irgen.jl +++ /dev/null @@ -1,300 +0,0 @@ - -struct LLVMNativeCode # thin wrapper - p::Ptr{Cvoid} -end - -function xlinfo(f, tt) - # get the method instance - world = typemax(UInt) - g = (args...) -> Cassette.overdub(ctx, f, args...) - meth = which(g, tt) - sig_tt = Tuple{typeof(g), tt.parameters...} - (ti, env) = ccall(:jl_type_intersection_with_env, Any, - (Any, Any), sig_tt, meth.sig)::Core.SimpleVector - - if VERSION >= v"1.2.0-DEV.320" - meth = Base.func_for_method_checked(meth, ti, env) - else - meth = Base.func_for_method_checked(meth, ti) - end - - return ccall(:jl_specializations_get_linfo, Ref{Core.MethodInstance}, - (Any, Any, Any, UInt), meth, ti, env, world) -end - -""" -Returns an LLVMNativeCode object for the function call `f` with TupleTypes `tt`. -""" -function raise_exception(insblock::BasicBlock, ex::Value) -end - -# const jlctx = Ref{LLVM.Context}() - -# function __init__() -# jlctx[] = LLVM.Context(convert(LLVM.API.LLVMContextRef, -# cglobal(:jl_LLVMContext, Nothing))) -# end - -""" - irgen(func, tt; - optimize = true, - optimize_llvm = true, - fix_globals = true, - overdub = true, - module_setup = (m) -> nothing) - -Generates Julia IR targeted for static compilation. -`ccall` and `cglobal` uses have pointer references changed to symbols -meant to be linked with libjulia and other libraries. - -`optimize` controls Julia-side optimization. `optimize_llvm` controls -optimization on the LLVM side. - -If `overdub == true` (the default), Cassette is used to swap out -`ccall`s with a tuple of library and symbol. - -`module_setup` is an optional function to control setup of modules. It takes an LLVM -module as input. -""" -function irgen(@nospecialize(func), @nospecialize(tt); - optimize = true, - optimize_llvm = true, - fix_globals = true, - overdub = true, - module_setup = (m) -> nothing) - # get the method instance - isa(func, Core.Builtin) && error("function is not a generic function") - world = typemax(UInt) - gfunc = overdub ? (args...) -> Cassette.overdub(ctx, func, args...) : func - meth = which(gfunc, tt) - sig_tt = Tuple{typeof(gfunc), tt.parameters...} - (ti, env) = ccall(:jl_type_intersection_with_env, Any, - (Any, Any), sig_tt, meth.sig)::Core.SimpleVector - - if VERSION >= v"1.2.0-DEV.320" - meth = Base.func_for_method_checked(meth, ti, env) - else - meth = Base.func_for_method_checked(meth, ti) - end - - linfo = ccall(:jl_specializations_get_linfo, Ref{Core.MethodInstance}, - (Any, Any, Any, UInt), meth, ti, env, world) - - current_method = nothing - last_method_instance = nothing - call_stack = Vector{Core.MethodInstance}() - global method_map = Dict{String,Core.MethodInstance}() - global dependencies = MultiDict{Core.MethodInstance,LLVM.Function}() - # set-up the compiler interface - function hook_module_setup(ref::Ptr{Cvoid}) - ref = convert(LLVM.API.LLVMModuleRef, ref) - module_setup(LLVM.Module(ref)) - end - function hook_raise_exception(insblock::Ptr{Cvoid}, ex::Ptr{Cvoid}) - insblock = convert(LLVM.API.LLVMValueRef, insblock) - ex = convert(LLVM.API.LLVMValueRef, ex) - raise_exception(BasicBlock(insblock), Value(ex)) - end - function postprocess(ir) - # get rid of jfptr wrappers - for llvmf in functions(ir) - startswith(LLVM.name(llvmf), "jfptr_") && unsafe_delete!(ir, llvmf) - end - - return - end - function hook_module_activation(ref::Ptr{Cvoid}) - ref = convert(LLVM.API.LLVMModuleRef, ref) - global ir = LLVM.Module(ref) - postprocess(ir) - - # find the function that this module defines - llvmfs = filter(llvmf -> !isdeclaration(llvmf) && - linkage(llvmf) == LLVM.API.LLVMExternalLinkage, - collect(functions(ir))) - llvmf = nothing - if length(llvmfs) == 1 - llvmf = first(llvmfs) - elseif length(llvmfs) > 1 - llvmfs = filter!(llvmf -> startswith(LLVM.name(llvmf), "julia_"), llvmfs) - if length(llvmfs) == 1 - llvmf = first(llvmfs) - end - end - insert!(dependencies, last_method_instance, llvmf) - method_map[name(llvmf)] = current_method - end - function hook_emit_function(method_instance, code, world) - push!(call_stack, method_instance) - end - function hook_emitted_function(method, code, world) - current_method = method - last_method_instance = pop!(call_stack) - # @show code - # dump(method, maxdepth=2) - # global mymeth = method - end - - params = Base.CodegenParams(cached=false, - track_allocations=false, - code_coverage=false, - static_alloc=false, - prefer_specsig=true, - module_setup=hook_module_setup, - module_activation=hook_module_activation, - raise_exception=hook_raise_exception, - emit_function=hook_emit_function, - emitted_function=hook_emitted_function, - ) - - # get the code - mod = let - ref = ccall(:jl_get_llvmf_defn, LLVM.API.LLVMValueRef, - (Any, UInt, Bool, Bool, Base.CodegenParams), - linfo, world, #=wrapper=#false, #=optimize=#false, params) - if ref == C_NULL - # error(jlctx[], "the Julia compiler could not generate LLVM IR") - end - - llvmf = LLVM.Function(ref) - LLVM.parent(llvmf) - end - - # the main module should contain a single jfptr_ function definition, - # e.g. jlcall_kernel_vadd_62977 - - # definitions = filter(f->!isdeclaration(f), functions(mod)) - definitions = Iterators.filter(f->!isdeclaration(f), collect(functions(mod))) - # definitions = collect(functions(mod)) - wrapper = let - fs = collect(Iterators.filter(f->startswith(LLVM.name(f), "jfptr_"), definitions)) - @assert length(fs) == 1 - fs[1] - end - - # the jlcall wrapper function should point us to the actual entry-point, - # e.g. julia_kernel_vadd_62984 - entry_tag = let - m = match(r"jfptr_(.+)_\d+", LLVM.name(wrapper)) - @assert m != nothing - m.captures[1] - end - unsafe_delete!(mod, wrapper) - entry = let - re = Regex("julia_$(entry_tag)_\\d+") - llvmcall_re = Regex("julia_$(entry_tag)_\\d+u\\d+") - fs = collect(Iterators.filter(f->occursin(re, LLVM.name(f)) && - !occursin(llvmcall_re, LLVM.name(f)), definitions)) - if length(fs) != 1 - compiler_error(func, tt, cap, "could not find single entry-point"; - entry=>entry_tag, available=>[LLVM.name.(definitions)]) - end - fs[1] - end - - LLVM.name!(entry, string(nameof(func))) - - # link in dependent modules - cache = Dict{String,String}() - for called_method_instance in keys(dependencies) - llvmfs = dependencies[called_method_instance] - - # link the first module - llvmf = popfirst!(llvmfs) - llvmfn = LLVM.name(llvmf) - link!(mod, LLVM.parent(llvmf)) - # process subsequent duplicate modules - for dup_llvmf in llvmfs - if Base.JLOptions().debug_level >= 2 - # link them too, to ensure accurate backtrace reconstruction - link!(mod, LLVM.parent(dup_llvmf)) - else - # don't link them, but note the called function name in a cache - dup_llvmfn = LLVM.name(dup_llvmf) - cache[dup_llvmfn] = llvmfn - end - end - end - # resolve function declarations with cached entries - for llvmf in filter(isdeclaration, collect(functions(mod))) - llvmfn = LLVM.name(llvmf) - if haskey(cache, llvmfn) - def_llvmfn = cache[llvmfn] - replace_uses!(llvmf, functions(mod)[def_llvmfn]) - unsafe_delete!(LLVM.parent(llvmf), llvmf) - end - end - # rename functions to something easier to decipher - # especially helps with overdubbed functions - for (fname, mi) in method_map - id = split(fname, "_")[end] - basename = mi.def.name - args = join(collect(mi.specTypes.parameters)[2:end], "_") - if basename == :overdub # special handling for Cassette - basename = string(mi.specTypes.parameters[3]) - basename = replace(basename, r"^typeof\(" => "") - basename = replace(basename, r"\)$" => "") - args = join(collect(mi.specTypes.parameters)[4:end], "_") - end - newname = join([basename, args, id], "_") - if haskey(functions(mod), fname) - name!(functions(mod)[fname], newname) - end - end - - d = find_ccalls(gfunc, tt) - fix_ccalls!(mod, d) - if fix_globals - fix_globals!(mod) - end - if optimize_llvm - optimize!(mod) - end - return mod -end - - -""" - optimize!(mod::LLVM.Module) - -Optimize the LLVM module `mod`. Crude for now. -Returns nothing. -""" -function optimize!(mod::LLVM.Module) - for llvmf in functions(mod) - startswith(LLVM.name(llvmf), "jfptr_") && unsafe_delete!(mod, llvmf) - startswith(LLVM.name(llvmf), "julia_") && LLVM.linkage!(llvmf, LLVM.API.LLVMExternalLinkage) - end - # triple = "wasm32-unknown-unknown-wasm" - # triple!(mod, triple) - # datalayout!(mod, "e-m:e-p:32:32-i64:64-n32:64-S128") - # LLVM.API.@apicall(:LLVMInitializeWebAssemblyTarget, Cvoid, ()) - # LLVM.API.@apicall(:LLVMInitializeWebAssemblyTargetMC, Cvoid, ()) - # LLVM.API.@apicall(:LLVMInitializeWebAssemblyTargetInfo, Cvoid, ()) - triple = "i686-pc-linux-gnu" - tm = TargetMachine(Target(triple), triple) - - ModulePassManager() do pm - # add_library_info!(pm, triple(mod)) - add_transform_info!(pm, tm) - ccall(:jl_add_optimization_passes, Cvoid, - (LLVM.API.LLVMPassManagerRef, Cint, Cint), - LLVM.ref(pm), Base.JLOptions().opt_level, 1) - - dead_arg_elimination!(pm) - global_optimizer!(pm) - global_dce!(pm) - strip_dead_prototypes!(pm) - - run!(pm, mod) - end - mod -end - -function write_object(mod::LLVM.Module, path) - host_triple = triple() - host_t = Target(host_triple) - TargetMachine(host_t, host_triple, "", "", LLVM.API.LLVMCodeGenLevelDefault, LLVM.API.LLVMRelocPIC) do tm - emit(tm, mod, LLVM.API.LLVMObjectFile, path) - end -end diff --git a/src/overdub.jl b/src/overdub.jl deleted file mode 100644 index 9a0015be..00000000 --- a/src/overdub.jl +++ /dev/null @@ -1,33 +0,0 @@ -# Implements contextual dispatch through Cassette.jl - -using Cassette - -## -# Convert two-arg `ccall` to single arg. -## -function transform(ctx, ref) - CI = ref.code_info - ismatch = x -> begin - Base.Meta.isexpr(x, :foreigncall) && - Base.Meta.isexpr(x.args[1], :call) - end - replace = x -> begin - y = Expr(x.head, Any[x.args[1].args[2], x.args[2:end]...]) - Expr(x.head, x.args[1].args[2], x.args[2:end]...) - end - Cassette.replace_match!(replace, ismatch, CI.code) - return CI -end - -const Pass = Cassette.@pass transform - -Cassette.@context Ctx -const ctx = Cassette.disablehooks(Ctx(pass = Pass)) - -### -# Rewrite functions -### - -#@inline Cassette.overdub(ctx::Ctx, ::typeof(+), a::T, b::T) where T<:Union{Float32, Float64} = add_float_contract(a, b) - -contextualize(f::F) where F = (args...) -> Cassette.overdub(ctx, f, args...) diff --git a/src/pointer_warning.jl b/src/pointer_warning.jl new file mode 100644 index 00000000..9f8f30c8 --- /dev/null +++ b/src/pointer_warning.jl @@ -0,0 +1,72 @@ +function locate_pointers_and_runtime_calls(mod) + i64 = LLVM.IntType(64) + # d = IdDict{Any, Tuple{String, LLVM.GlobalVariable}}() + for func ∈ LLVM.functions(mod), bb ∈ LLVM.blocks(func), inst ∈ LLVM.instructions(bb) + warned = false + if isa(inst, LLVM.LoadInst) && occursin("inttoptr", string(inst)) + warned = inspect_pointers(mod, inst) + elseif isa(inst, LLVM.StoreInst) && occursin("inttoptr", string(inst)) + @debug "Inspecting StoreInst" inst + warned = inspect_pointers(mod, inst) + elseif inst isa LLVM.RetInst && occursin("inttoptr", string(inst)) + @debug "Inspecting RetInst" inst LLVM.operands(inst) + warned = inspect_pointers(mod, inst) + elseif isa(inst, LLVM.BitCastInst) && occursin("inttoptr", string(inst)) + @debug "Inspecting BitCastInst" inst LLVM.operands(inst) + warned = inspect_pointers(mod, inst) + elseif isa(inst, LLVM.CallInst) + @debug "Inspecting CallInst" inst LLVM.operands(inst) + dest = LLVM.called_operand(inst) + if occursin("inttoptr", string(dest)) && length(LLVM.operands(dest)) > 0 + @debug "Inspecting CallInst inttoptr" dest LLVM.operands(dest) LLVM.operands(inst) + ptr_arg = first(LLVM.operands(dest)) + ptr_val = convert(Int, ptr_arg) + ptr = Ptr{Cvoid}(ptr_val) + + frames = ccall(:jl_lookup_code_address, Any, (Ptr{Cvoid}, Cint,), ptr, 0) + + data_warnings(inst, frames) + warned = true + end + end + if warned + @warn("LLVM function generated warnings due to raw pointers embedded in the code. This will likely cause errors or undefined behaviour.", + func = func) + end + end +end + +function inspect_pointers(mod, inst) + warned = false + jl_t = (LLVM.StructType(LLVM.LLVMType[])) + for (i, arg) ∈ enumerate(LLVM.operands(inst)) + if occursin("inttoptr", string(arg)) && arg isa LLVM.ConstantExpr + op1 = LLVM.Value(LLVM.API.LLVMGetOperand(arg, 0)) + if op1 isa LLVM.ConstantExpr + op1 = LLVM.Value(LLVM.API.LLVMGetOperand(op1, 0)) + end + ptr = Ptr{Cvoid}(convert(Int, op1)) + frames = ccall(:jl_lookup_code_address, Any, (Ptr{Cvoid}, Cint,), ptr, 0) + data_warnings(inst, frames) + warned = true + end + end + warned +end + +data_warnings(inst, frames) = for frame ∈ frames + fn, file, line, linfo, fromC, inlined = frame + @warn("Found pointer references to julia data", + "llvm instruction" = inst, + name = fn, + file = file, + line = line, + fromC = fromC, + inlined = inlined) +end + +llvmeltype(x::LLVM.Value) = eltype(LLVM.value_type(x)) + + + + diff --git a/src/quirks.jl b/src/quirks.jl new file mode 100644 index 00000000..f279d94e --- /dev/null +++ b/src/quirks.jl @@ -0,0 +1,58 @@ +libcexit(x::Int32) = @symbolcall exit(x::Int32)::Nothing +macro print_and_throw(err) + quote + println($err) + libcexit(Int32(1)) + end +end + +# math.jl +@device_override @noinline Base.Math.throw_complex_domainerror(f::Symbol, x) = + @print_and_throw c"This operation requires a complex input to return a complex result" +@device_override @noinline Base.Math.throw_exp_domainerror(x) = + @print_and_throw c"Exponentiation yielding a complex result requires a complex argument" + +# intfuncs.jl +@device_override @noinline Base.throw_domerr_powbysq(::Any, p) = + @print_and_throw c"Cannot raise an integer to a negative power" +@device_override @noinline Base.throw_domerr_powbysq(::Integer, p) = + @print_and_throw c"Cannot raise an integer to a negative power" +@device_override @noinline Base.throw_domerr_powbysq(::AbstractMatrix, p) = + @print_and_throw c"Cannot raise an integer to a negative power" +@device_override @noinline Base.__throw_gcd_overflow(a, b) = + @print_and_throw c"gcd overflow" + +# checked.jl +@device_override @noinline Base.Checked.throw_overflowerr_binaryop(op, x, y) = + @print_and_throw c"Binary operation overflowed" +@device_override @noinline Base.Checked.throw_overflowerr_negation(x) = + @print_and_throw c"Negation overflowed" +@device_override function Base.Checked.checked_abs(x::Base.Checked.SignedInt) + r = ifelse(x < 0, -x, x) + r < 0 && @print_and_throw(c"checked arithmetic: cannot compute |x|") + r +end + +# boot.jl +@device_override @noinline Core.throw_inexacterror(f::Symbol, ::Type{T}, val) where {T} = + @print_and_throw c"Inexact conversion" + +# abstractarray.jl +@device_override @noinline Base.throw_boundserror(A, I) = + @print_and_throw c"Out-of-bounds array access" + +# trig.jl +@device_override @noinline Base.Math.sincos_domain_error(x) = + @print_and_throw c"sincos(x) is only defined for finite x." + +@static if isdefined(StaticTools, :Bumper) + Bumper = StaticTools.Bumper + @device_override @noinline Bumper.AllocBufferImpl.oom_error() = + @print_and_throw c"alloc: Buffer out of memory. This might be a sign of a memory leak." + @device_override @noinline Bumper.Internals.esc_err() = + @print_and_throw c"Tried to return a PtrArray from a `no_escape` block. If you really want to do this, evaluate Bumper.allow_ptrarray_to_escape() = true" + + # Just to make the compiler's life a little easier, let's not make it fetch and elide the current task + # since tasks don't actually exist on-device. + @device_override Bumper.Internals.get_task() = 0 +end diff --git a/src/serialize.jl b/src/serialize.jl deleted file mode 100644 index 57256501..00000000 --- a/src/serialize.jl +++ /dev/null @@ -1,242 +0,0 @@ - -""" -A context structure for holding state related to serializing Julia -objects. A key component is an `IOBuffer` used to hold the serialized -result. -""" -struct SerializeContext - io::IOBuffer - store::Dict{Any,Any} # Meant to map Julia object to variable name - init::Vector{Any} # Expressions to run initially -end -SerializeContext(io::IOBuffer = IOBuffer()) = SerializeContext(io, Dict(), Vector{Expr}()) - -const _td = IdDict( - Any => :jl_any_type, - Float64 => :jl_float64_type, - Float32 => :jl_float32_type, - Int64 => :jl_int64_type, - Int32 => :jl_int32_type, - Int16 => :jl_int16_type, - Int8 => :jl_int8_type, - UInt64 => :jl_uint64_type, - UInt32 => :jl_uint32_type, - UInt16 => :jl_uint16_type, - UInt8 => :jl_uint8_type, - Cint => :jl_int32_type, - Cvoid => :jl_any_type, - Array => :jl_array_type, - Array{Any,1} => :jl_array_any_type, - Array{Int32,1} => :jl_array_int32_type, - Array{UInt8,1} => :jl_array_uint8_type, - ErrorException => :jl_errorexception_type, - DataType => :jl_datatype_type, - UnionAll => :jl_unionall_type, - Union => :jl_union_type, - Core.TypeofBottom => :jl_typeofbottom_type, - TypeVar => :jl_tvar_type, -) - -const _t = IdDict() - -for (t,s) in _td - _t[t] = :(unsafe_load(cglobal($(QuoteNode(s)), Type))) -end - -const _gd = IdDict( - Core => :jl_core_module, - Main => :jl_main_module, - nothing => :jl_nothing, - () => :jl_emptytuple, - Core.svec() => :jl_emptysvec, - UndefRefError() => :jl_undefref_exception, -) - -const _g = IdDict() - -for (x,s) in _gd - _g[x] = :(unsafe_load(cglobal($(QuoteNode(s)), Any))) -end - -""" - serialize(ctx::SerializeContext, x) - -Serialize `x` into the context object `ctx`. `ctx.io` is the `IOBuffer` where the -serialized results are stored. Get the result with `take!(ctx.io)`. - -This function returns an expression that will deserialize the object. Several `serialize` -methods can be called recursively to build up deserialization code for nested objects. -The expression returned is meant to be `eval`ed into a function that can be called -to do the serialization. - -The deserialization code should be pretty low-level code that can be compiled -relatively easily. It especially shouldn't use global variables. - -Serialization / deserialization code can use `ctx` to hold state information. - -Some simple types like boxed variables do not need to write anything to `ctx.io`. -They can return an expression that directly creates the object. -""" -function serialize(ctx::SerializeContext, @nospecialize(x)) - haskey(_g, x) && return _g[x] - # TODO: fix this major kludge. - if nfields(x) > 0 - return Expr(:tuple, (serialize(ctx, getfield(x,i)) for i in 1:nfields(x))...) - end - return :(unsafe_load(cglobal(:jl_emptytuple, Any))) -end - -function serialize(ctx::SerializeContext, @nospecialize(t::DataType)) - if haskey(_t, t) - return _t[t] - elseif haskey(ctx.store, t) - return ctx.store[t] - else - # primary = unwrap_unionall(t.wrapper) - name = gensym(Symbol(:type, "-", t.name.name)) - ctx.store[t] = name - e = quote - $name = let - local tn = $(serialize(ctx, t.name)) - # names = $(serialize(ctx, t.names)) - local super = $(serialize(ctx, t.super)) - local parameters = $(serialize(ctx, t.parameters)) - local types = $(serialize(ctx, t.types)) - local ndt = ccall(:jl_new_datatype, Any, - (Any, Any, Any, Any, Any, Any, Cint, Cint, Cint), - tn, tn.module, super, parameters, #=names=# unsafe_load(cglobal(:jl_any_type, Any)), types, - $(t.abstract), $(t.mutable), $(t.ninitialized)) - # tn.wrapper = ndt.name.wrapper - # ccall(:jl_set_const, Cvoid, (Any, Any, Any), tn.module, tn.name, tn.wrapper) - ndt - # ty = tn.wrapper - # $(ctx.types[string(t)]) = ndt - # hasinstance = serialize(ctx, ) - # $(if isdefined(primary, :instance) && !isdefined(t, :instance) - # # use setfield! directly to avoid `fieldtype` lowering expecting to see a Singleton object already on ty - # :(Core.setfield!(ty, :instance, ccall(:jl_new_struct, Any, (Any, Any...), ty))) - # end) - end - end - push!(ctx.init, e) - return name - end -end - -function serialize(ctx::SerializeContext, tn::Core.TypeName) - haskey(ctx.store, tn) && return ctx.store[tn] - name = gensym(Symbol(:typename, "-", tn.name)) - ctx.store[tn] = name - e = quote - $name = ccall(:jl_new_typename_in, Ref{Core.TypeName}, (Any, Any), - # $(serialize(ctx, tn.name)), Main #=__deserialized_types__ =# ) - $(serialize(ctx, tn.name)), unsafe_load(cglobal(:jl_main_module, Any)) #=__deserialized_types__ =# ) - end - push!(ctx.init, e) - return name -end - -function serialize(ctx::SerializeContext, mi::Core.MethodInstance) - return :(unsafe_load(cglobal(:jl_emptytuple, Any))) -end - -function serialize(ctx::SerializeContext, x::String) - advance!(ctx.io) - v = Vector{UInt8}(x) - ioptr = ctx.io.ptr - write(ctx.io, v) - quote - unsafe_string(Vptr + $(ioptr - 1), $(length(v))) - end -end - -function serialize(ctx::SerializeContext, x::Symbol) - haskey(ctx.store, x) && return ctx.store[x] - name = gensym(Symbol(:symbol, "-", x)) - ctx.store[x] = name - e = quote - $name = ccall(:jl_symbol_n, Any, (Ptr{UInt8}, Csize_t), $(serialize(ctx, string(x))), $(length(string(x)))) - # ccall(:jl_set_global, Cvoid, (Any, Any, Any), unsafe_load(cglobal(:jl_main_module, Any)), $(QuoteNode(name)), x) - end - push!(ctx.init, e) - return name -end - - - -# Define functions that return an expression. Example: -# serialize(ctx::SerializeContext, x::Int) = :(ccall(:jl_box_int64, Any, (Int,), $x)) -for (fun, type) in (:jl_box_int64 => Int64, :jl_box_int32 => Int32, :jl_box_int8 => Int16, :jl_box_int8 => Int8, - :jl_box_uint64 => UInt64, :jl_box_uint32 => UInt32, :jl_box_uint8 => UInt16, :jl_box_uint8 => UInt8, - :jl_box_voidpointer => Ptr{Cvoid}, - :jl_box_float64 => Float64, :jl_box_float32 => Float32) - @eval serialize(ctx::SerializeContext, x::$type) = Expr(:call, :ccall, QuoteNode($(QuoteNode(fun))), Any, Expr(:tuple, $type), x) -end -serialize(ctx::SerializeContext, x::Char) = :(ccall(:jl_box_char, Any, (UInt32,), $x)) -serialize(ctx::SerializeContext, x::Bool) = :(ccall(:jl_box_bool, Any, (UInt8,), $x)) - -function serialize(ctx::SerializeContext, a::Tuple) - length(a) == 0 && return :(unsafe_load(cglobal(:jl_emptytuple, Any))) - Expr(:tuple, (serialize(ctx, x) for x in a)...) -end - -function serialize(ctx::SerializeContext, a::Core.SimpleVector) - length(a) == 0 && return :(unsafe_load(cglobal(:jl_emptysvec, Any))) - Expr(:call, Expr(:., :Core, QuoteNode(:svec)), (serialize(ctx, x) for x in a)...) -end - -advance!(io) = write(io, repeat('\0', -rem(io.ptr - 1, 8, RoundUp))) # Align data to 8 bytes - -function serialize(ctx::SerializeContext, a::Array{T,N}) where {T,N} - elty = eltype(a) - aty = typeof(a) - dims = size(a) - atys = serialize(ctx, aty) - if isbitstype(elty) - advance!(ctx.io) - ioptr = ctx.io.ptr - write(ctx.io, a) - if N == 1 - advance!(ctx.io) - ioptr = ctx.io.ptr - write(ctx.io, a) - quote - p = Vptr + $ioptr - 1 - ccall(:jl_ptr_to_array_1d, $aty, (Any, Ptr{Cvoid}, Csize_t, Cint), $atys, p, $(length(a)), false) - end - else - dms = serialize(ctx, dims) - advance!(ctx.io) - ioptr = ctx.io.ptr - write(ctx.io, a) - quote - p = Vptr + $ioptr - 1 - ccall(:jl_ptr_to_array, $aty, (Any, Ptr{Cvoid}, Any, Int32), $atys, p, $dms, false) - end - end - else - idx = Int[] - e = Array{Any}(undef, length(a)) - @inbounds for i in eachindex(a) - if isassigned(a, i) - e[i] = serialize(ctx, a[i]) - push!(idx, i) - end - end - aname = gensym() - resulte = [quote - # $aname = Array{$elty, $(length(dims))}(undef, $dims) - $aname = ccall(:jl_new_array, $aty, (Any, Any), $atys, $(serialize(ctx, dims))) - end] - for i in idx - push!(resulte, quote - # unsafe_store!(pointer($aname), $(e[i]), $i) - unsafe_store!(convert(Ptr{Any}, pointer($aname)), $(e[i]), $i) - # unsafe_store!(convert(Ptr{Csize_t}, pointer($aname)), pointer_from_objref($(e[i])), $i) - # @inbounds $aname[$i] = $(e[i]) - end) - end - push!(resulte, :($aname = $aname)) - Expr(:block, resulte...) - end -end diff --git a/src/target.jl b/src/target.jl new file mode 100644 index 00000000..5faec0d1 --- /dev/null +++ b/src/target.jl @@ -0,0 +1,158 @@ +@static if isdefined(Base.Experimental, Symbol("@overlay")) + Base.Experimental.@MethodTable(method_table) +else + const method_table = nothing +end + +""" +```julia + StaticTarget() # Native target + StaticTarget(platform::Base.BinaryPlatforms.Platform) # Specific target with generic CPU + StaticTarget(platform::Platform, cpu::String) # Specific target with specific CPU + StaticTarget(platform::Platform, cpu::String, features::String) # Specific target with specific CPU and features +``` +Struct that defines a target for the compilation +Beware that currently the compilation assumes that the code is on the host so platform specific code like: +```julia + Sys.isapple() ... +``` +does not behave as expected. +By default `StaticTarget()` is the native target. + +For cross-compilation of executables and shared libraries, one also needs to call `set_compiler!` with the path to a valid C compiler +for the target platform. For example, to cross-compile for aarch64 using a compiler from homebrew, one can use: +```julia + set_compiler!(StaticTarget(parse(Platform,"aarch64-gnu-linux")), "/opt/homebrew/bin/aarch64-unknown-linux-gnu-gcc") +``` +""" +mutable struct StaticTarget + platform::Union{Platform,Nothing} + tm::LLVM.TargetMachine + compiler::Union{String,Nothing} + julia_runtime::Bool +end + +clean_triple(platform::Platform) = arch(platform) * os_str(platform) * libc_str(platform) +StaticTarget() = StaticTarget(HostPlatform(), unsafe_string(LLVM.API.LLVMGetHostCPUName()), unsafe_string(LLVM.API.LLVMGetHostCPUFeatures())) +StaticTarget(platform::Platform) = StaticTarget(platform, LLVM.TargetMachine(LLVM.Target(triple = clean_triple(platform)), clean_triple(platform)), nothing, false) +StaticTarget(platform::Platform, cpu::String) = StaticTarget(platform, LLVM.TargetMachine(LLVM.Target(triple = clean_triple(platform)), clean_triple(platform), cpu), nothing, false) +StaticTarget(platform::Platform, cpu::String, features::String) = StaticTarget(platform, LLVM.TargetMachine(LLVM.Target(triple = clean_triple(platform)), clean_triple(platform), cpu, features), nothing, false) + +function StaticTarget(triple::String, cpu::String, features::String) + platform = tryparse(Platform, triple) + StaticTarget(platform, LLVM.TargetMachine(LLVM.Target(triple = triple), triple, cpu, features), nothing) +end + +""" +Set the compiler for cross compilation + ```julia + set_compiler!(StaticTarget(parse(Platform,"aarch64-gnu-linux")), "/opt/homebrew/bin/aarch64-elf-gcc") +``` +""" +set_compiler!(target::StaticTarget, compiler::String) = (target.compiler = compiler) + + +set_runtime!(target::StaticTarget, julia_runtime::Bool) = (target.julia_runtime = julia_runtime) + +""" +```julia +@device_override old_bad_method(arg1::Type1, arg2::Type2) = new_good_method(arg1, arg2) +``` +Override a non-static-compilable method (e.g. `old_bad_method(::Type1, ::Type2)`) +with a more compileable replacement. +### Examples +``` +@device_override @noinline Core.throw_inexacterror(f::Symbol, ::Type{T}, val) where {T} = + @print_and_throw c"Inexact conversion" +``` +""" +macro device_override(ex) + ex = macroexpand(__module__, ex) + if Meta.isexpr(ex, :call) + @show ex = eval(ex) + error() + end + code = quote + $Base.Experimental.@overlay($StaticCompiler.method_table, $ex) + end + return esc(code) +end + +# Default to native +struct StaticCompilerTarget{MT} <: GPUCompiler.AbstractCompilerTarget + triple::String + cpu::String + features::String + julia_runtime::Bool + method_table::MT +end + +module StaticRuntime + # the runtime library + signal_exception() = return + malloc(sz) = ccall("extern malloc", llvmcall, Csize_t, (Csize_t,), sz) + report_oom(sz) = return + report_exception(ex) = return + report_exception_name(ex) = return + report_exception_frame(idx, func, file, line) = return +end + + +GPUCompiler.llvm_triple(target::StaticCompilerTarget) = target.triple + +function GPUCompiler.llvm_machine(target::StaticCompilerTarget) + triple = GPUCompiler.llvm_triple(target) + + t = LLVM.Target(triple=triple) + + tm = LLVM.TargetMachine(t, triple, target.cpu, target.features, reloc=LLVM.API.LLVMRelocPIC) + GPUCompiler.asm_verbosity!(tm, true) + + return tm +end + +GPUCompiler.runtime_slug(job::GPUCompiler.CompilerJob{<:StaticCompilerTarget}) = "static_$(job.config.target.cpu)-$(hash(job.config.target.features))" + +GPUCompiler.runtime_module(::GPUCompiler.CompilerJob{<:StaticCompilerTarget}) = StaticRuntime +GPUCompiler.runtime_module(::GPUCompiler.CompilerJob{<:StaticCompilerTarget, StaticCompilerParams}) = StaticRuntime + + +GPUCompiler.can_throw(job::GPUCompiler.CompilerJob{<:StaticCompilerTarget, StaticCompilerParams}) = true +GPUCompiler.can_throw(job::GPUCompiler.CompilerJob{<:StaticCompilerTarget}) = true + +GPUCompiler.uses_julia_runtime(job::GPUCompiler.CompilerJob{<:StaticCompilerTarget}) = job.config.target.julia_runtime +GPUCompiler.get_interpreter(job::GPUCompiler.CompilerJob{<:StaticCompilerTarget, StaticCompilerParams}) = + StaticInterpreter(job.config.params.cache, GPUCompiler.method_table(job), job.world, + GPUCompiler.inference_params(job), GPUCompiler.optimization_params(job)) +GPUCompiler.ci_cache(job::GPUCompiler.CompilerJob{<:StaticCompilerTarget, StaticCompilerParams}) = job.config.params.cache +GPUCompiler.method_table(@nospecialize(job::GPUCompiler.CompilerJob{<:StaticCompilerTarget})) = job.config.target.method_table + + +function static_job(@nospecialize(func::Function), @nospecialize(types::Type); + name = fix_name(func), + kernel::Bool = false, + target::StaticTarget = StaticTarget(), + method_table=method_table, + kwargs... + ) + source = methodinstance(typeof(func), Base.to_tuple_type(types)) + tm = target.tm + gputarget = StaticCompilerTarget(LLVM.triple(tm), LLVM.cpu(tm), LLVM.features(tm), target.julia_runtime, method_table) + params = StaticCompilerParams() + config = GPUCompiler.CompilerConfig(gputarget, params, name = name, kernel = kernel) + StaticCompiler.CompilerJob(source, config), kwargs +end +function static_job(@nospecialize(func), @nospecialize(types); + name = fix_name(func), + kernel::Bool = false, + target::StaticTarget = StaticTarget(), + method_table=method_table, + kwargs... +) + source = methodinstance(typeof(func), Base.to_tuple_type(types)) + tm = target.tm + gputarget = StaticCompilerTarget(LLVM.triple(tm), LLVM.cpu(tm), LLVM.features(tm), target.julia_runtime, method_table) + params = StaticCompilerParams() + config = GPUCompiler.CompilerConfig(gputarget, params, name = name, kernel = kernel) + StaticCompiler.CompilerJob(source, config), kwargs +end \ No newline at end of file diff --git a/src/utils.jl b/src/utils.jl deleted file mode 100644 index 0fdf7a7c..00000000 --- a/src/utils.jl +++ /dev/null @@ -1,56 +0,0 @@ - -function julia_to_llvm(@nospecialize x) - isboxed = Ref{UInt8}() - # LLVMType(ccall(:jl_type_to_llvm, LLVM.API.LLVMTypeRef, (Any, Ref{UInt8}), x, isboxed)) # noserialize - LLVMType(ccall(:julia_type_to_llvm, LLVM.API.LLVMTypeRef, (Any, Ref{UInt8}), x, isboxed)) # julia v1.1.1 -end - -const jl_value_t_ptr = julia_to_llvm(Any) -const jl_value_t = eltype(jl_value_t_ptr) -# const jl_value_t_ptr_ptr = LLVM.PointerType(jl_value_t_ptr) -# # cheat on these for now: -# const jl_datatype_t_ptr = jl_value_t_ptr -# const jl_unionall_t_ptr = jl_value_t_ptr -# const jl_typename_t_ptr = jl_value_t_ptr -# const jl_sym_t_ptr = jl_value_t_ptr -# const jl_svec_t_ptr = jl_value_t_ptr -# const jl_module_t_ptr = jl_value_t_ptr -# const jl_array_t_ptr = jl_value_t_ptr -# -# const bool_t = julia_to_llvm(Bool) -# const int8_t = julia_to_llvm(Int8) -# const int16_t = julia_to_llvm(Int16) -# const int32_t = julia_to_llvm(Int32) -# const int64_t = julia_to_llvm(Int64) -# const uint8_t = julia_to_llvm(UInt8) -# const uint16_t = julia_to_llvm(UInt16) -# const uint32_t = julia_to_llvm(UInt32) -# const uint64_t = julia_to_llvm(UInt64) -# const float_t = julia_to_llvm(Float32) -# const double_t = julia_to_llvm(Float64) -# const float32_t = julia_to_llvm(Float32) -# const float64_t = julia_to_llvm(Float64) -# const void_t = julia_to_llvm(Nothing) -# const size_t = julia_to_llvm(Int) -# -# const int8_t_ptr = LLVM.PointerType(int8_t) -# const void_t_ptr = LLVM.PointerType(void_t) - -function module_setup(mod::LLVM.Module) -# triple!(mod, "wasm32-unknown-unknown-wasm") -# datalayout!(mod, "e-m:e-p:32:32-i64:64-n32:64-S128") -end - -llvmmod(native_code) = - LLVM.Module(ccall(:jl_get_llvm_module, LLVM.API.LLVMModuleRef, - (Ptr{Cvoid},), native_code.p)) - -function Base.write(mod::LLVM.Module, path::String) - open(io -> write(io, mod), path, "w") -end - - -walk(f, x) = true -# walk(f, x::Instruction) = foreach(c->walk(f,c), operands(x)) -# walk(f, x::Instruction) = f(x) || foreach(c->walk(f,c), operands(x)) -walk(f, x::ConstantExpr) = f(x) || foreach(c->walk(f,c), operands(x)) diff --git a/test/Project.toml b/test/Project.toml index 6e781cc6..a36e2086 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,6 +1,19 @@ [deps] +Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Formatting = "59287772-0a20-5a39-b81b-1366585eb4c0" +GPUCompiler = "61eb1bfa-7361-4325-ad38-22787b887f55" +LLD_jll = "d55e3150-da41-5e91-b323-ecfd1eec6109" LLVM = "929cbde3-209d-540e-8aea-75f648917ca0" Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +LoopVectorization = "bdcacae8-1622-11e9-2a5c-532679323890" +MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +ManualMemory = "d125e4d3-2237-4719-b19c-fa641b8a4667" +StaticTools = "86c06d3c-3f03-46de-9781-57580aa96d0a" +StrideArraysCore = "7792a7ef-975c-4747-a70f-980b88e8d1da" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Bumper = "8ce10254-0962-460f-a3d8-1f77fea1446e" + +[compat] +Bumper = "0.6" \ No newline at end of file diff --git a/test/ccalls.jl b/test/ccalls.jl deleted file mode 100644 index dd5663c6..00000000 --- a/test/ccalls.jl +++ /dev/null @@ -1,35 +0,0 @@ -# d = find_ccalls(Threads.nthreads, Tuple{}) -# d = find_ccalls(time, Tuple{}) -# d = find_ccalls(muladd, Tuple{Array{Float64,2},Array{Float64,2},Array{Float64,2}}) - -f1() = ccall(:jl_errno, Int, (Int,), 11) -f2() = ccall(:jl_errno, Int, (Int, Int), 21, 22) -f3() = ccall(:jl_errno, Int, (Int, Int, Int), 31, 32, 33) - -@testset "ccalls" begin - m1 = irgen(f1, Tuple{}) - m2 = irgen(f2, Tuple{}) - m3 = irgen(f3, Tuple{}) - LLVM.verify(m1) - LLVM.verify(m2) - LLVM.verify(m3) -end - - -function f() - n = Int(unsafe_load(cglobal(:jl_n_threads, Cint))) - return 2n -end - -@testset "cglobal" begin - m = irgen(f, Tuple{}) - LLVM.verify(m) - @test f() == @jlrun f() -end - -@testset "extern" begin - f() = @extern(:time, Cvoid, (Ptr{Cvoid},), C_NULL) - m = irgen(f, Tuple{}) - LLVM.verify(m) - @test "time" in [name(f) for f in LLVM.functions(m)] -end diff --git a/test/globals.jl b/test/globals.jl deleted file mode 100644 index 43c70e84..00000000 --- a/test/globals.jl +++ /dev/null @@ -1,36 +0,0 @@ -# @testset "serialize" begin -# ctx = StaticCompiler.SerializeContext() -# a = Any["abcdg", ["hi", "bye"], 3333, Int32(44), 314f0, 3.14, (1, 3.3f0), Core.svec(9.9, 9), :sym, :sym, :a] -# e = StaticCompiler.serialize(ctx, a) -# g = eval(:(Vptr -> $e)) -# v = take!(ctx.io) -# GC.enable(false) -# res = g(pointer(v)) -# GC.enable(true) -# @test res == a -# end - - -# const a = ["abcdg", "asdfl", 123, 3.14, ["a", "asdf"], (1, 3.63), [1, 3.63]] -const a = ["abcdg", "asdxf"] -const b = "B" -const x = [1.33, 35.0] -const xi = [3, 5] - -f(x) = @inbounds a[1][3] > b[1] ? 2x : x -g(i) = @inbounds x[1] > x[2] ? 2i : i -h(i) = @inbounds xi[1] == 3 ? i : 2i - -@testset "globals" begin - @test f(3) == @jlrun f(3) - @test g(3) == @jlrun g(3) - @test h(3) == @jlrun h(3) -end - -f() = Complex{Float64} -g(@nospecialize(x)) = isa(x, Number) ? 1 : 0 - -@testset "type" begin - @test string(@jlrun f()) == "Complex{Float64}" - res = g(4.0im) -end diff --git a/test/ode.jl b/test/ode.jl deleted file mode 100644 index b270a544..00000000 --- a/test/ode.jl +++ /dev/null @@ -1,182 +0,0 @@ -# Adapted from: https://github.com/JuliaDiffEq/ODE.jl/blob/8954872f956116e78b6c04690f899fe2db696b4e/src/ODE.jl#L84-L360 -# MIT license -# Copyright (c) 2009-2015: various contributors: https://github.com/JuliaLang/ODE.jl/contributors - -using LinearAlgebra - -function hinit(F, x0, t0::T, tend, p, reltol, abstol) where T - # Returns first step, direction of integration and F evaluated at t0 - tdir = sign(tend-t0) - tdir==0 && error("Zero time span") - tau = max(reltol*norm(x0, Inf), abstol) - d0 = norm(x0, Inf)/tau - f0 = F(t0, x0) - d1 = norm(f0, Inf)/tau - if d0 < 1e-5 || d1 < 1e-5 - h0 = 1e-6 - else - h0 = 0.01*(d0/d1) - end - h0 = convert(T,h0) - # perform Euler step - x1 = x0 + tdir*h0*f0 - f1 = F(t0 + tdir*h0, x1) - # estimate second derivative - d2 = norm(f1 - f0, Inf)/(tau*h0) - if max(d1, d2) <= 1e-15 - h1 = max(T(10)^(-6), T(10)^(-3)*h0) - else - pow = -(2 + log10(max(d1, d2)))/(p + 1) - h1 = 10^pow - end - h1 = convert(T,h1) - return tdir*min(100*h0, h1, tdir*(tend-t0)), tdir, f0 -end - -function fdjacobian(F, x::Number, t) - ftx = F(t, x) - - # The 100 below is heuristic - dx = (x .+ (x==0))./100 - dFdx = (F(t,x+dx)-ftx)./dx - - return dFdx -end - -function fdjacobian(F, x, t) - ftx = F(t, x) - lx = max(length(x),1) - dFdx = zeros(eltype(x), lx, lx) - for j = 1:lx - # The 100 below is heuristic - dx = zeros(eltype(x), lx) - dx[j] = (x[j] .+ (x[j]==0))./100 - dFdx[:,j] = (F(t,x+dx)-ftx)./dx[j] - end - return dFdx -end - -# ODE23S Solve stiff systems based on a modified Rosenbrock triple -# (also used by MATLAB's ODE23s); see Sec. 4.1 in -# -# [SR97] L.F. Shampine and M.W. Reichelt: "The MATLAB ODE Suite," SIAM Journal on Scientific Computing, Vol. 18, 1997, pp. 1–22 -# -# supports keywords: points = :all | :specified (using dense output) -# jacobian = G(t,y)::Function | nothing (FD) -function ode23s(F, y0, tspan; - reltol = 1.0e-5, abstol = 1.0e-8, - jacobian=nothing, - points=:all, - norm=LinearAlgebra.norm, - minstep=abs(tspan[end] - tspan[1])/1e18, - maxstep=abs(tspan[end] - tspan[1])/2.5, - initstep=0.) - - # select method for computing the Jacobian - if typeof(jacobian) == Function - jac = jacobian - else - # fallback finite-difference - jac = (t, y)->fdjacobian(F, y, t) - end - - # constants - d = 1/(2 + sqrt(2)) - e32 = 6 + sqrt(2) - - - # initialization - t = tspan[1] - - tfinal = tspan[end] - - h = initstep - if h == 0. - # initial guess at a step size - h, tdir, F0 = hinit(F, y0, t, tfinal, 3, reltol, abstol) - else - tdir = sign(tfinal - t) - F0 = F(t,y0) - end - h = tdir * min(abs(h), maxstep) - - y = y0 - tout = [t] # first output time - yout = [deepcopy(y)] # first output solution - - J = jac(t,y) # get Jacobian of F wrt y -# Core.print(t, " ", tfinal, " ", minstep, " ", h) - while abs(t - tfinal) > 0 && minstep < abs(h) - if abs(t-tfinal) < abs(h) - h = tfinal - t - end - - if size(J,1) == 1 - W = I - h*d*J - else - # note: if there is a mass matrix M on the lhs of the ODE, i.e., - # M * dy/dt = F(t,y) - # we can simply replace eye(J) by M in the following expression - # (see Sec. 5 in [SR97]) - - W = lu( I - h*d*J ) - end - - # approximate time-derivative of F - T = h*d*(F(t + h/100, y) - F0)/(h/100) - - # modified Rosenbrock formula - k1 = W\(F0 + T) - F1 = F(t + 0.5*h, y + 0.5*h*k1) - k2 = W\(F1 - k1) + k1 - ynew = y + h*k2 - F2 = F(t + h, ynew) - k3 = W\(F2 - e32*(k2 - F1) - 2*(k1 - F0) + T ) - - err = (abs(h)/6)*norm(k1 - 2*k2 + k3) # error estimate - delta = max(reltol*max(norm(y),norm(ynew)), abstol) # allowable error - - # check if new solution is acceptable - if err <= delta - - # # if points==:specified || points==:all - # # only points in tspan are requested - # # -> find relevant points in (t,t+h] - # for toi in tspan[(tspan.>t) .& (tspan.<=t+h)] - # # rescale to (0,1] - # s = (toi-t)/h - - # # use interpolation formula to get solutions at t=toi - # push!(tout, toi) - # push!(yout, y + h*( k1*s*(1-s)/(1-2*d) + k2*s*(s-2*d)/(1-2*d))) - # end - # # Core.print("First\n") - # # end - # if points==:all - if (tout[end]!=t+h) - # # add the intermediate points - push!(tout, t + h) - push!(yout, ynew) - end - - # update solution - t = t + h - y = ynew - - F0 = F2 # use FSAL property - J = jac(t,y) # get Jacobian of F wrt y - # for new solution - end - - # update of the step size - h = tdir*min( maxstep, abs(h)*0.8*(delta/err)^(1/3) ) - end - - return tout, yout -end - - -# fode() = ode23s((t,y)->2.0t^2, 0.0, Float64[0:.001:2;], initstep = 1e-4)[2][end] -# fode() = ode23s((t,y)->2.0t^2, 0.0, [0:.001:2;], initstep = 1e-4)[2][end] - -# @show fode() diff --git a/test/others.jl b/test/others.jl deleted file mode 100644 index 4481e77a..00000000 --- a/test/others.jl +++ /dev/null @@ -1,73 +0,0 @@ -mutable struct AAA - aaa::Int - bbb::Int -end -@noinline ssum(x) = x.aaa + x.bbb -fstruct(x) = ssum(AAA(x, 99)) -@test fstruct(10) == @jlrun fstruct(10) - -module ZZ -mutable struct AAA - aaa::Int - bbb::Int -end -@noinline ssum(x) = x.aaa + x.bbb -fstruct(x) = ssum(AAA(x, 99)) -end # module -ffstruct(x) = ZZ.fstruct(x) -@test ffstruct(10) == @jlrun ffstruct(10) - -const ag = Ref(0x80808080) -jglobal() = ag -@show bg = @jlrun jglobal() -# @test jglobal()[] == bg[] # Something's broken with mutable's - -arraysum(x) = sum([x, 1]) -# @test arraysum(6) == @jlrun arraysum(6) - -fsin(x) = sin(x) -@test fsin(0.5) == @jlrun fsin(0.5) - -fccall() = ccall(:jl_ver_major, Cint, ()) -@test fccall() == @jlrun fccall() - -fcglobal() = cglobal(:jl_n_threads, Cint) -@test fcglobal() == @jlrun fcglobal() - -const sv = Core.svec(1,2,3,4) -fsv() = sv -@test fsv() == @jlrun fsv() - -const arr = [9,9,9,9] -farray() = arr -@show @jlrun farray() -@show farray() -# @test farray() == @jlrun farray() - -@noinline f0(x) = 3x -@noinline fop(f, x) = 2f(x) -funcall(x) = fop(f0, x) -@test funcall(2) == @jlrun funcall(2) - -hi() = print(Core.stdout, 'X') -@jlrun hi() - -hello() = print(Core.stdout, "Hello world...\n") -@jlrun hello() - -function gx(i) - a = 2.0:0.1:10.0 - @inbounds i > 3 ? a[1] : a[5] -end - -@test_skip gx(4) == @jlrun gx(4) - -fsimple() = [0:.001:2;][end] - -@test fsimple() == @jlrun fsimple() - -@noinline function fsym(x; l = :hello, s = :x) - s == :asdf ? x : 2x -end -gsym(x) = fsym(x, l = :hello, s = :asdf) + 1 -@test gsym(3) == @jlrun gsym(3) diff --git a/test/runtests.jl b/test/runtests.jl index 3381a873..542659c2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,21 +1,25 @@ -using StaticCompiler using Test -using LLVM +using StaticCompiler using Libdl +using LinearAlgebra +using LoopVectorization +using ManualMemory +using Distributed +using StaticTools +using StrideArraysCore +using MacroTools +using LLD_jll +using Bumper -cd(@__DIR__) -@testset "ccalls" begin - include("ccalls.jl") -end +addprocs(1) +@everywhere using StaticCompiler, StrideArraysCore -@testset "globals" begin - include("globals.jl") -end +const GROUP = get(ENV, "GROUP", "All") -@testset "others" begin - include("others.jl") +if GROUP == "Core" || GROUP == "All" + include("testcore.jl") end -@testset "standalone" begin - include("standalone-exe.jl") +if GROUP == "Integration" || GROUP == "All" + include("testintegration.jl") end diff --git a/test/scripts/interop.jl b/test/scripts/interop.jl new file mode 100644 index 00000000..b601e090 --- /dev/null +++ b/test/scripts/interop.jl @@ -0,0 +1,16 @@ +using StaticCompiler +using StaticTools + +function interop(argc, argv) + lib = StaticTools.dlopen(c"libm") + printf(lib) + sin = StaticTools.dlsym(lib, c"sin") + printf(sin) + x = @ptrcall sin(5.0::Float64)::Float64 + printf(x) + newline() + StaticTools.dlclose(lib) +end + +# Attempt to compile +path = compile_executable(interop, (Int64, Ptr{Ptr{UInt8}}), "./", cflags=`-ldl -lm`) diff --git a/test/scripts/loopvec_matrix.jl b/test/scripts/loopvec_matrix.jl new file mode 100644 index 00000000..7b19ce86 --- /dev/null +++ b/test/scripts/loopvec_matrix.jl @@ -0,0 +1,53 @@ +using StaticCompiler +using StaticTools +using LoopVectorization + +@inline function mul!(C::MallocArray, A::MallocArray, B::MallocArray) + @turbo for n ∈ indices((C,B), 2), m ∈ indices((C,A), 1) + Cmn = zero(eltype(C)) + for k ∈ indices((A,B), (2,1)) + Cmn += A[m,k] * B[k,n] + end + C[m,n] = Cmn + end + return C +end + +function loopvec_matrix(argc::Int, argv::Ptr{Ptr{UInt8}}) + argc == 3 || return printf(stderrp(), c"Incorrect number of command-line arguments\n") + rows = argparse(Int64, argv, 2) # First command-line argument + cols = argparse(Int64, argv, 3) # Second command-line argument + + # LHS + A = MallocArray{Float64}(undef, rows, cols) + @turbo for i ∈ axes(A, 1) + for j ∈ axes(A, 2) + A[i,j] = i*j + end + end + + # RHS + B = MallocArray{Float64}(undef, cols, rows) + @turbo for i ∈ axes(B, 1) + for j ∈ axes(B, 2) + B[i,j] = i*j + end + end + + # # Matrix multiplication + C = MallocArray{Float64}(undef, cols, cols) + mul!(C, B, A) + + # Print to stdout + printf(C) + # Also print to file + printdlm(c"table.tsv", C, '\t') + fwrite(c"table.b", C) + # Clean up matrices + free(A) + free(B) + free(C) +end + +# Attempt to compile +path = compile_executable(loopvec_matrix, (Int64, Ptr{Ptr{UInt8}}), "./") diff --git a/test/scripts/loopvec_matrix_stack.jl b/test/scripts/loopvec_matrix_stack.jl new file mode 100644 index 00000000..5e0c90d7 --- /dev/null +++ b/test/scripts/loopvec_matrix_stack.jl @@ -0,0 +1,49 @@ +using StaticCompiler +using StaticTools +using LoopVectorization + +@inline function mul!(C::StackArray, A::StackArray, B::StackArray) + @turbo for n ∈ indices((C,B), 2), m ∈ indices((C,A), 1) + Cmn = zero(eltype(C)) + for k ∈ indices((A,B), (2,1)) + Cmn += A[m,k] * B[k,n] + end + C[m,n] = Cmn + end + return C +end + +function loopvec_matrix_stack() + rows = 10 + cols = 5 + + # LHS + A = StackArray{Float64}(undef, rows, cols) + @turbo for i ∈ axes(A, 1) + for j ∈ axes(A, 2) + A[i,j] = i*j + end + end + + # RHS + B = StackArray{Float64}(undef, cols, rows) + @turbo for i ∈ axes(B, 1) + for j ∈ axes(B, 2) + B[i,j] = i*j + end + end + + # # Matrix multiplication + C = StackArray{Float64}(undef, cols, cols) + mul!(C, B, A) + + # Print to stdout + printf(C) + # Also print to file + fp = fopen(c"table.tsv",c"w") + printf(fp, C) + fclose(fp) +end + +# Attempt to compile +path = compile_executable(loopvec_matrix_stack, (), "./") diff --git a/test/scripts/loopvec_product.jl b/test/scripts/loopvec_product.jl new file mode 100644 index 00000000..83424504 --- /dev/null +++ b/test/scripts/loopvec_product.jl @@ -0,0 +1,25 @@ +using StaticCompiler +using StaticTools +using LoopVectorization + +function loopvec_product(argc::Int, argv::Ptr{Ptr{UInt8}}) + argc == 3 || return printf(stderrp(), c"Incorrect number of command-line arguments\n") + rows = argparse(Int64, argv, 2) # First command-line argument + cols = argparse(Int64, argv, 3) # Second command-line argument + + s = 0 + @turbo for i=1:rows + for j=1:cols + s += i*j + end + end + # Print result to stdout + printf(s) + # Also print to file + fp = fopen(c"product.tsv",c"w") + printf(fp, s) + fclose(fp) +end + +# Attempt to compile +path = compile_executable(loopvec_product, (Int64, Ptr{Ptr{UInt8}}), "./") diff --git a/test/scripts/print_args.jl b/test/scripts/print_args.jl new file mode 100644 index 00000000..c3d05fbf --- /dev/null +++ b/test/scripts/print_args.jl @@ -0,0 +1,33 @@ +using StaticCompiler +using StaticTools + +function print_args(argc::Int, argv::Ptr{Ptr{UInt8}}) + printf(c"Argument count is %d:\n", argc) + for i=1:argc + # iᵗʰ input argument string + pᵢ = unsafe_load(argv, i) # Get pointer + strᵢ = MallocString(pᵢ) # Can wrap to get high-level interface + println(strᵢ) + # No need to `free` since we didn't allocate this memory + end + newline() + println(c"Testing string indexing and substitution") + m = m"Hello world!" + println(m[1:5]) + println(m) + m[7:11] = c"there" + println(m) + free(m) + + s = m"Hello world!" + println(s[1:5]) + println(s) + s[7:11] = c"there" + println(s) + + println(c"That was fun, see you next time!") + return 0 +end + +# Attempt to compile +path = compile_executable(print_args, (Int64, Ptr{Ptr{UInt8}}), "./") diff --git a/test/scripts/rand_matrix.jl b/test/scripts/rand_matrix.jl new file mode 100644 index 00000000..604e44a0 --- /dev/null +++ b/test/scripts/rand_matrix.jl @@ -0,0 +1,23 @@ +using StaticCompiler +using StaticTools + +function rand_matrix(argc::Int, argv::Ptr{Ptr{UInt8}}) + argc == 3 || return printf(stderrp(), c"Incorrect number of command-line arguments\n") + rows = argparse(Int64, argv, 2) # First command-line argument + cols = argparse(Int64, argv, 3) # Second command-line argument + + # Manually fil matrix + M = MallocArray{Float64}(undef, rows, cols) + rng = static_rng() + @inbounds for i=1:rows + for j=1:cols + M[i,j] = rand(rng) + end + end + printf(M) + free(M) +end + +# Attempt to compile +# cflags=`-lm`: need to explicitly include libm math library on linux +path = compile_executable(rand_matrix, (Int64, Ptr{Ptr{UInt8}}), "./", cflags=`-lm`) diff --git a/test/scripts/randn_matrix.jl b/test/scripts/randn_matrix.jl new file mode 100644 index 00000000..065d4662 --- /dev/null +++ b/test/scripts/randn_matrix.jl @@ -0,0 +1,22 @@ +using StaticCompiler +using StaticTools + +function randn_matrix(argc::Int, argv::Ptr{Ptr{UInt8}}) + argc == 3 || return printf(stderrp(), c"Incorrect number of command-line arguments\n") + rows = argparse(Int64, argv, 2) # First command-line argument + cols = argparse(Int64, argv, 3) # Second command-line argument + + M = MallocArray{Float64}(undef, rows, cols) + rng = MarsagliaPolar(static_rng()) + @inbounds for i=1:rows + for j=1:cols + M[i,j] = randn(rng) + end + end + printf(M) + free(M) +end + +# Attempt to compile +# cflags=`-lm`: need to explicitly include libm math library on linux +path = compile_executable(randn_matrix, (Int64, Ptr{Ptr{UInt8}}), "./", cflags=`-lm`) diff --git a/test/scripts/throw_errors.jl b/test/scripts/throw_errors.jl new file mode 100644 index 00000000..fb823a0d --- /dev/null +++ b/test/scripts/throw_errors.jl @@ -0,0 +1,15 @@ +using StaticCompiler +using StaticTools + +function maybe_throw(argc::Int, argv::Ptr{Ptr{UInt8}}) + printf(c"Argument count is %d:\n", argc) + argc > 1 || return printf(stderrp(), c"Too few command-line arguments\n") + n = argparse(Int64, argv, 2) # First command-line argument + printf((c"Input:\n", n, c"\n")) + printf(c"\nAttempting to represent input as UInt64:\n") + x = UInt64(n) + printf(x) +end + +# Attempt to compile +path = compile_executable(maybe_throw, (Int64, Ptr{Ptr{UInt8}}), "./") diff --git a/test/scripts/times_table.jl b/test/scripts/times_table.jl new file mode 100644 index 00000000..7d295eb0 --- /dev/null +++ b/test/scripts/times_table.jl @@ -0,0 +1,25 @@ +using StaticCompiler +using StaticTools + +function times_table(argc::Int, argv::Ptr{Ptr{UInt8}}) + argc == 3 || return printf(stderrp(), c"Incorrect number of command-line arguments\n") + rows = argparse(Int64, argv, 2) # First command-line argument + cols = argparse(Int64, argv, 3) # Second command-line argument + + M = MallocArray{Int64}(undef, rows, cols) + @inbounds for i=1:rows + for j=1:cols + M[i,j] = i*j + end + end + # Print to stdout + printf(M) + # Also print to file + fwrite(c"table.b", M) + printdlm(c"table.tsv", M) + # Clean up matrix + free(M) +end + +# Attempt to compile +path = compile_executable(times_table, (Int64, Ptr{Ptr{UInt8}}), "./") diff --git a/test/scripts/wasm.jl b/test/scripts/wasm.jl new file mode 100644 index 00000000..beff3b7d --- /dev/null +++ b/test/scripts/wasm.jl @@ -0,0 +1,16 @@ +# Test that we can compile an object to wasm +# WebAssemblyCompiler.jl is a better tool for this, but this exercises the cross compilation pipeline + +using StaticCompiler +using LLVM +InitializeAllTargets() +InitializeAllTargetInfos() +InitializeAllAsmPrinters() +InitializeAllAsmParsers() +InitializeAllTargetMCs() + +fib(n) = n <= 1 ? n : fib(n - 1) + fib(n - 2) + +target=StaticTarget("wasm32","","") + +StaticCompiler.generate_obj(fib, Tuple{Int64}, "./test", target=target) diff --git a/test/scripts/withmallocarray.jl b/test/scripts/withmallocarray.jl new file mode 100644 index 00000000..29301214 --- /dev/null +++ b/test/scripts/withmallocarray.jl @@ -0,0 +1,31 @@ +using StaticCompiler +using StaticTools + +function withmallocarray(argc::Int, argv::Ptr{Ptr{UInt8}}) + argc == 3 || return printf(stderrp(), c"Incorrect number of command-line arguments\n") + rows = argparse(Int64, argv, 2) # First command-line argument + cols = argparse(Int64, argv, 3) # Second command-line argument + + mzeros(rows, cols) do A + printf(A) + end + mones(Int, rows, cols) do A + printf(A) + end + mfill(3.141592, rows, cols) do A + printf(A) + end + + # Random number generation + rng = MarsagliaPolar() + mrand(rng, rows, cols) do A + printf(A) + end + mrandn(rng, rows, cols) do A + printf(A) + end +end + +# Attempt to compile +# cflags=`-lm`: need to explicitly include libm math library on linux +path = compile_executable(withmallocarray, (Int64, Ptr{Ptr{UInt8}}), "./", cflags=`-lm`) diff --git a/test/standalone-exe.jl b/test/standalone-exe.jl deleted file mode 100644 index fb97de52..00000000 --- a/test/standalone-exe.jl +++ /dev/null @@ -1,41 +0,0 @@ -# Definitions of functions to compile -twox(x) = 2x - -const aa = [4, 5] -arrayfun(x) = x + aa[1] + aa[2] - -jsin(x) = sin(x) - -function arridx(i) - a = collect(1.0:0.1:10.0) - @inbounds i > 3 ? a[1] : a[5] -end - -fsimple() = [0:.001:2;][end] - -include("ode.jl") -fode() = ode23s((t,y)->2.0t^2, 0.0, [0:.001:2;], initstep = 1e-4)[2][end] - -# Functions to compile and arguments to pass -funcalls = [ - (twox, Tuple{Int}, 4), - (arrayfun, Tuple{Int}, 4), - (jsin, Tuple{Float64}, 0.5), - (arridx, Tuple{Int}, 4), - (fsimple, Tuple{}, ()), - (fode, Tuple{}, ()), # Broken on Julia v1.2.0; works on Julia v1.3.0-rc3 -] - -StaticCompiler.exegen(funcalls) - -using Formatting -@testset "exegen" begin - cd("standalone") do - for (func, tt, val) in funcalls - fname = nameof(func) - rettype = Base.return_types(func, tt)[1] - fmt = StaticCompiler.Cformatmap[rettype] - @test Formatting.sprintf1(fmt, func(val...)) == read(`./$fname`, String) - end - end -end diff --git a/test/testcore.jl b/test/testcore.jl new file mode 100644 index 00000000..064f0109 --- /dev/null +++ b/test/testcore.jl @@ -0,0 +1,168 @@ +workdir = tempdir() + + + +fib(n) = n <= 1 ? n : fib(n - 1) + fib(n - 2) # This needs to be defined globally due to https://github.com/JuliaLang/julia/issues/40990 + +@testset "Standalone Dylibs" begin + # Test function + # (already defined) + # fib(n) = n <= 1 ? n : fib(n - 1) + fib(n - 2) + + #Compile dylib + name = repr(fib) + filepath = compile_shlib(fib, (Int,), workdir, name, demangle=true) + @test occursin("fib.$(Libdl.dlext)", filepath) + # Open dylib manually + ptr = Libdl.dlopen(filepath, Libdl.RTLD_LOCAL) + fptr = Libdl.dlsym(ptr, name) + @test fptr != C_NULL + @test ccall(fptr, Int, (Int,), 10) == 55 + Libdl.dlclose(ptr) + + # As above, but without demangling + filepath = compile_shlib(fib, (Int,), workdir, name, demangle=false) + ptr = Libdl.dlopen(filepath, Libdl.RTLD_LOCAL) + fptr = Libdl.dlsym(ptr, "julia_"*name) + @test fptr != C_NULL + @test ccall(fptr, Int, (Int,), 10) == 55 + Libdl.dlclose(ptr) +end + +@testset "Standalone Executables" begin + # Minimal test with no `llvmcall` + @inline function foo() + v = 0.0 + n = 1000 + for i=1:n + v += sqrt(n) + end + return 0 + end + + filepath = compile_executable(foo, (), workdir, demangle=false) + r = run(`$filepath`); + @test isa(r, Base.Process) + @test r.exitcode == 0 + + filepath = compile_executable(foo, (), workdir, demangle=true) + r = run(`$filepath`); + @test isa(r, Base.Process) + @test r.exitcode == 0 + + filepath = compile_executable(foo, (), workdir, llvm_to_clang=true) + r = run(`$filepath`); + @test isa(r, Base.Process) + @test r.exitcode == 0 + + + @inline function _puts(s::Ptr{UInt8}) # Can't use Base.println because it allocates + Base.llvmcall((""" + ; External declaration of the puts function + declare i32 @puts(i8* nocapture) nounwind + + define i32 @main(i64) { + entry: + %ptr = inttoptr i64 %0 to i8* + %status = call i32 (i8*) @puts(i8* %ptr) + ret i32 %status + } + """, "main"), Int32, Tuple{Ptr{UInt8}}, s) + end + + @inline function print_args(argc::Int, argv::Ptr{Ptr{UInt8}}) + for i=1:argc + # Get pointer + p = unsafe_load(argv, i) + # Print string at pointer location (which fortunately already exists isn't tracked by the GC) + _puts(p) + end + return 0 + end + + filepath = compile_executable(print_args, (Int, Ptr{Ptr{UInt8}}), workdir, demangle=false) + r = run(`$filepath Hello, world!`); + @test isa(r, Base.Process) + @test r.exitcode == 0 + + filepath = compile_executable(print_args, (Int, Ptr{Ptr{UInt8}}), workdir, demangle=true) + r = run(`$filepath Hello, world!`); + @test isa(r, Base.Process) + @test r.exitcode == 0 + + filepath = compile_executable(print_args, (Int, Ptr{Ptr{UInt8}}), workdir, llvm_to_clang=true) + r = run(`$filepath Hello, world!`); + @test isa(r, Base.Process) + @test r.exitcode == 0 + + + # Compile a function that definitely fails + @inline foo_err() = UInt64(-1) + filepath = compile_executable(foo_err, (), workdir, demangle=true) + @test isfile(filepath) + status = -1 + try + status = run(`filepath`) + catch + @info "foo_err: Task failed successfully!" + end + @test status === -1 + +end + +@noinline square(n) = n*n + +function squaresquare(n) + square(square(n)) +end + +function squaresquaresquare(n) + square(squaresquare(n)) +end + +@testset "Multiple Function Dylibs" begin + + funcs = [(squaresquare,(Float64,)), (squaresquaresquare,(Float64,))] + filepath = compile_shlib(funcs, workdir, demangle=true) + + ptr = Libdl.dlopen(filepath, Libdl.RTLD_LOCAL) + + fptr2 = Libdl.dlsym(ptr, "squaresquare") + @test ccall(fptr2, Float64, (Float64,), 10.) == squaresquare(10.) + + fptr = Libdl.dlsym(ptr, "squaresquaresquare") + @test ccall(fptr, Float64, (Float64,), 10.) == squaresquaresquare(10.) + #Compile dylib +end + + +# Overlays + +module SubFoo + +rand(args...) = Base.rand(args...) + +function f() + x = rand() + y = rand() + return x + y +end + +end + +@device_override SubFoo.rand() = 2 + +# Lets test having another method table around +Base.Experimental.@MethodTable AnotherTable +Base.Experimental.@overlay AnotherTable SubFoo.rand() = 3 + +@testset "Overlays" begin + Libdl.dlopen(compile_shlib(SubFoo.f, (), workdir)) do lib + fptr = Libdl.dlsym(lib, "f") + @test @ccall($fptr()::Int) == 4 + end + Libdl.dlopen(compile_shlib(SubFoo.f, (), workdir; method_table=AnotherTable)) do lib + fptr = Libdl.dlsym(lib, "f") + @test @ccall($fptr()::Int) == 6 + end +end diff --git a/test/testintegration.jl b/test/testintegration.jl new file mode 100644 index 00000000..d2a98da2 --- /dev/null +++ b/test/testintegration.jl @@ -0,0 +1,348 @@ +# Setup +testpath = pwd() +scratch = tempdir() +cd(scratch) + +if VERSION >= v"1.9" + # Bumper uses PackageExtensions to work with StaticCompiler, so let's just skip this test on 1.8 + function bumper_test(N::Int) + buf = AllocBuffer(MallocVector, sizeof(Float64) * N) + s = 0.0 + for i ∈ 1:N + # some excuse to reuse the same memory a bunch of times + @no_escape buf begin + v = @alloc(Float64, N) + v .= i + s += sum(v) + end + end + free(buf) + s + end + + @testset "Bumper.jl integration" begin + + path = compile_shlib(bumper_test, (Int,), "./") + ptr = Libdl.dlopen(path, Libdl.RTLD_LOCAL) + + fptr = Libdl.dlsym(ptr, "bumper_test") + + @test bumper_test(8) == @ccall($fptr(8::Int)::Float64) + end +end + +@testset "Standalone Executable Integration" begin + + jlpath = joinpath(Sys.BINDIR, Base.julia_exename()) # Get path to julia executable + + ## --- Times table, file IO, mallocarray + let + # Attempt to compile + # We have to start a new Julia process to get around the fact that Pkg.test + # disables `@inbounds`, but ironically we can use `--compile=min` to make that + # faster. + status = -1 + try + isfile("times_table") && rm("times_table") + status = run(`$jlpath --startup=no --compile=min $testpath/scripts/times_table.jl`) + catch e + @warn "Could not compile $testpath/scripts/times_table.jl" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + + # Attempt to run + println("5x5 times table:") + status = -1 + try + status = run(`./times_table 5 5`) + catch e + @warn "Could not run $(scratch)/times_table" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + # Test ascii output + # @test parsedlm(Int, c"table.tsv", '\t') == (1:5)*(1:5)' broken=Sys.isapple() + # Test binary output + @test fread!(szeros(Int, 5,5), c"table.b") == (1:5)*(1:5)' + end + + ## --- "withmallocarray"-type do-block pattern + let + # Compile... + status = -1 + try + isfile("withmallocarray") && rm("withmallocarray") + status = run(`$jlpath --startup=no --compile=min $testpath/scripts/withmallocarray.jl`) + catch e + @warn "Could not compile $testpath/scripts/withmallocarray.jl" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + + # Run... + println("3x3 malloc arrays via do-block syntax:") + status = -1 + try + status = run(`./withmallocarray 3 3`) + catch e + @warn "Could not run $(scratch)/withmallocarray" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + end + + ## --- Random number generation + let + # Compile... + status = -1 + try + isfile("rand_matrix") && rm("rand_matrix") + status = run(`$jlpath --startup=no --compile=min $testpath/scripts/rand_matrix.jl`) + catch e + @warn "Could not compile $testpath/scripts/rand_matrix.jl" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + + # Run... + println("5x5 uniform random matrix:") + status = -1 + try + status = run(`./rand_matrix 5 5`) + catch e + @warn "Could not run $(scratch)/rand_matrix" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + end + + let + # Compile... + status = -1 + try + isfile("randn_matrix") && rm("randn_matrix") + status = run(`$jlpath --startup=no --compile=min $testpath/scripts/randn_matrix.jl`) + catch e + @warn "Could not compile $testpath/scripts/randn_matrix.jl" + println(e) + end + if Sys.isbsd() + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + end + + # Run... + println("5x5 Normal random matrix:") + status = -1 + try + status = run(`./randn_matrix 5 5`) + catch e + @warn "Could not run $(scratch)/randn_matrix" + println(e) + end + if Sys.isbsd() + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + end + end + + ## --- Test LoopVectorization integration + if Bool(LoopVectorization.VectorizationBase.has_feature(Val{:x86_64_avx2})) + let + # Compile... + status = -1 + try + isfile("loopvec_product") && rm("loopvec_product") + status = run(`$jlpath --startup=no --compile=min $testpath/scripts/loopvec_product.jl`) + catch e + @warn "Could not compile $testpath/scripts/loopvec_product.jl" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + + # Run... + println("10x10 table sum:") + status = -1 + try + status = run(`./loopvec_product 10 10`) + catch e + @warn "Could not run $(scratch)/loopvec_product" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + # @test parsedlm(c"product.tsv",'\t')[] == 3025 + end + end + + let + # Compile... + status = -1 + try + isfile("loopvec_matrix") && rm("loopvec_matrix") + status = run(`$jlpath --startup=no $testpath/scripts/loopvec_matrix.jl`) + catch e + @warn "Could not compile $testpath/scripts/loopvec_matrix.jl" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + + # Run... + println("10x5 matrix product:") + status = -1 + try + status = run(`./loopvec_matrix 10 5`) + catch e + @warn "Could not run $(scratch)/loopvec_matrix" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + A = (1:10) * (1:5)' + # Check ascii output + # @test parsedlm(c"table.tsv",'\t') == A' * A broken=Sys.isapple() + # Check binary output + @test fread!(szeros(5,5), c"table.b") == A' * A + end + + let + # Compile... + status = -1 + try + isfile("loopvec_matrix_stack") && rm("loopvec_matrix_stack") + status = run(`$jlpath --startup=no $testpath/scripts/loopvec_matrix_stack.jl`) + catch e + @warn "Could not compile $testpath/scripts/loopvec_matrix_stack.jl" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + + # Run... + println("10x5 matrix product:") + status = -1 + try + status = run(`./loopvec_matrix_stack`) + catch e + @warn "Could not run $(scratch)/loopvec_matrix_stack" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + A = (1:10) * (1:5)' + # @test parsedlm(c"table.tsv",'\t') == A' * A broken=Sys.isapple() + end + + + ## --- Test string handling + + let + # Compile... + status = -1 + try + isfile("print_args") && rm("print_args") + status = run(`$jlpath --startup=no --compile=min $testpath/scripts/print_args.jl`) + catch e + @warn "Could not compile $testpath/scripts/print_args.jl" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + + # Run... + println("String indexing and handling:") + status = -1 + try + status = run(`./print_args foo bar`) + catch e + @warn "Could not run $(scratch)/print_args" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + end + + ## --- Test error throwing + + let + # Compile... + status = -1 + try + isfile("maybe_throw") && rm("maybe_throw") + status = run(`$jlpath --startup=no --compile=min $testpath/scripts/throw_errors.jl`) + catch e + @warn "Could not compile $testpath/scripts/throw_errors.jl" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + + # Run... + println("Error handling:") + status = -1 + try + status = run(`./maybe_throw 10`) + catch e + @warn "Could not run $(scratch)/maybe_throw" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + status = -1 + try + status = run(`./maybe_throw -10`) + catch e + @info "maybe_throw: task failed sucessfully!" + end + if Sys.iswindows() + @info "maybe_throw: task doesn't fail on Windows." + @test status.exitcode == 0 + else + @test status === -1 + end + end + + ## --- Test interop + + if Sys.isbsd() + let + # Compile... + status = -1 + try + isfile("interop") && rm("interop") + status = run(`$jlpath --startup=no --compile=min $testpath/scripts/interop.jl`) + catch e + @warn "Could not compile $testpath/scripts/interop.jl" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + + # Run... + println("Interop:") + status = -1 + try + status = run(`./interop`) + catch e + @warn "Could not run $(scratch)/interop" + println(e) + end + @test isa(status, Base.Process) + @test isa(status, Base.Process) && status.exitcode == 0 + end + end + +end + +## --- Clean up + +cd(testpath)