├── .gitignore ├── test ├── ir_tests.jl ├── runtests.jl ├── modules.jl ├── desugaring.jl ├── import.jl ├── generators.jl ├── assignments.jl ├── scopes.jl ├── quoting_ir.jl ├── repl_mode.jl ├── import_ir.jl ├── hooks.jl ├── arrays.jl ├── loops_ir.jl ├── decls.jl ├── syntax_graph.jl ├── branching_ir.jl ├── destructuring.jl ├── loops.jl ├── macros_ir.jl ├── branching.jl ├── closures.jl ├── exceptions.jl ├── quoting.jl ├── exceptions_ir.jl ├── decls_ir.jl ├── misc.jl ├── assignments_ir.jl ├── typedefs.jl ├── generators_ir.jl ├── destructuring_ir.jl └── utils.jl ├── .github └── workflows │ ├── CompatHelper.yml │ ├── TagBot.yml │ └── CI.yml ├── Project.toml ├── src ├── precompile.jl ├── JuliaLowering.jl ├── hooks.jl ├── utils.jl ├── kinds.jl ├── bindings.jl └── syntax_macros.jl └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /Manifest.toml 2 | -------------------------------------------------------------------------------- /test/ir_tests.jl: -------------------------------------------------------------------------------- 1 | @testset "IR tests" begin 2 | testdir = @__DIR__ 3 | for filename in readdir(testdir) 4 | if endswith(filename, "_ir.jl") 5 | @testset "$filename" begin 6 | test_ir_cases(joinpath(testdir, filename)) 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /.github/workflows/CompatHelper.yml: -------------------------------------------------------------------------------- 1 | name: CompatHelper 2 | on: 3 | schedule: 4 | - cron: 0 0 * * * 5 | workflow_dispatch: 6 | jobs: 7 | CompatHelper: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Pkg.add("CompatHelper") 11 | run: julia -e 'using Pkg; Pkg.add("CompatHelper")' 12 | - name: CompatHelper.main() 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} 16 | run: julia -e 'using CompatHelper; CompatHelper.main()' 17 | -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "JuliaLowering" 2 | uuid = "f3c80556-a63f-4383-b822-37d64f81a311" 3 | authors = ["Claire Foster and contributors"] 4 | version = "1.0.0-DEV" 5 | 6 | [deps] 7 | JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4" 8 | 9 | [sources] 10 | JuliaSyntax = {rev = "99e975a7", url = "https://github.com/JuliaLang/JuliaSyntax.jl"} 11 | 12 | [compat] 13 | julia = "1" 14 | 15 | [extras] 16 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 17 | Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" 18 | FileWatching = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" 19 | REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" 20 | 21 | [targets] 22 | test = ["Test", "FileWatching", "Markdown", "REPL"] 23 | -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | include("utils.jl") 2 | 3 | @testset "JuliaLowering.jl" begin 4 | include("syntax_graph.jl") 5 | 6 | include("ir_tests.jl") 7 | 8 | include("arrays.jl") 9 | include("assignments.jl") 10 | include("branching.jl") 11 | include("closures.jl") 12 | include("decls.jl") 13 | include("destructuring.jl") 14 | include("desugaring.jl") 15 | include("exceptions.jl") 16 | include("functions.jl") 17 | include("generators.jl") 18 | include("import.jl") 19 | include("loops.jl") 20 | @testset "macros" include("macros.jl") 21 | include("misc.jl") 22 | include("modules.jl") 23 | include("quoting.jl") 24 | include("scopes.jl") 25 | include("typedefs.jl") 26 | include("compat.jl") 27 | include("hooks.jl") 28 | end 29 | -------------------------------------------------------------------------------- /.github/workflows/TagBot.yml: -------------------------------------------------------------------------------- 1 | name: TagBot 2 | on: 3 | issue_comment: 4 | types: 5 | - created 6 | workflow_dispatch: 7 | inputs: 8 | lookback: 9 | default: 3 10 | permissions: 11 | actions: read 12 | checks: read 13 | contents: write 14 | deployments: read 15 | issues: read 16 | discussions: read 17 | packages: read 18 | pages: read 19 | pull-requests: read 20 | repository-projects: read 21 | security-events: read 22 | statuses: read 23 | jobs: 24 | TagBot: 25 | if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: JuliaRegistries/TagBot@v1 29 | with: 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | ssh: ${{ secrets.DOCUMENTER_KEY }} 32 | -------------------------------------------------------------------------------- /src/precompile.jl: -------------------------------------------------------------------------------- 1 | # exercise the whole lowering pipeline 2 | if Base.get_bool_env("JULIA_LOWERING_PRECOMPILE", true) 3 | thunks = String[ 4 | """ 5 | function foo(xxx, yyy) 6 | @nospecialize xxx 7 | return Pair{Any,Any}(typeof(xxx), typeof(yyy)) 8 | end 9 | """ 10 | 11 | """ 12 | struct Foo 13 | x::Int 14 | Foo(x::Int) = new(x) 15 | # Foo() = new() 16 | end 17 | """ 18 | ] 19 | for thunk in thunks 20 | stream = JuliaSyntax.ParseStream(thunk) 21 | JuliaSyntax.parse!(stream; rule=:all) 22 | st0 = JuliaSyntax.build_tree(SyntaxTree, stream; filename=@__FILE__) 23 | lwrst = lower(@__MODULE__, st0[1]) 24 | lwr = to_lowered_expr(lwrst) 25 | @assert Meta.isexpr(lwr, :thunk) && only(lwr.args) isa Core.CodeInfo 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: ['*'] 7 | pull_request: 8 | workflow_dispatch: 9 | concurrency: 10 | # Skip intermediate builds: always. 11 | # Cancel intermediate builds: only if it is a pull request build. 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} 14 | jobs: 15 | test: 16 | name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | version: 22 | - 'nightly' 23 | os: 24 | - ubuntu-latest 25 | arch: 26 | - x64 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: julia-actions/setup-julia@v1 30 | with: 31 | version: ${{ matrix.version }} 32 | arch: ${{ matrix.arch }} 33 | - uses: julia-actions/cache@v1 34 | - uses: julia-actions/julia-buildpkg@v1 35 | - uses: julia-actions/julia-runtest@v1 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 JuliaHub and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/modules.jl: -------------------------------------------------------------------------------- 1 | @testset "modules" begin 2 | 3 | test_mod = Module() 4 | 5 | A = JuliaLowering.include_string(test_mod, """ 6 | module A 7 | function g() 8 | return "hi" 9 | end 10 | end 11 | """, "module_test") 12 | @test A isa Module 13 | @test A.g() == "hi" 14 | @test A.include isa Base.IncludeInto 15 | @test A.eval isa Core.EvalInto 16 | @test A.Base === Base 17 | @test A.eval(:(x = -2)) == -2 18 | @test A.x == -2 19 | 20 | B = JuliaLowering.include_string(test_mod, """ 21 | baremodule B 22 | end 23 | """, "baremodule_test") 24 | @test B.Core === Core 25 | @test !isdefined(B, :include) 26 | @test !isdefined(B, :eval) 27 | @test !isdefined(B, :Base) 28 | 29 | # Module init order 30 | Amod = JuliaLowering.include_string(test_mod, """ 31 | module A 32 | init_order = [] 33 | __init__() = push!(init_order, "A") 34 | module B 35 | using ..A 36 | __init__() = push!(A.init_order, "B") 37 | end 38 | module C 39 | using ..A 40 | __init__() = push!(A.init_order, "C") 41 | module D 42 | using ...A 43 | __init__() = push!(A.init_order, "D") 44 | end 45 | module E 46 | using ...A 47 | __init__() = push!(A.init_order, "E") 48 | end 49 | end 50 | end 51 | """) 52 | @test Amod.init_order == ["B", "D", "E", "C", "A"] 53 | 54 | end 55 | -------------------------------------------------------------------------------- /src/JuliaLowering.jl: -------------------------------------------------------------------------------- 1 | # Use a baremodule because we're implementing `include` and `eval` 2 | baremodule JuliaLowering 3 | 4 | using Base 5 | # We define a separate _include() for use in this module to avoid mixing method 6 | # tables with the public `JuliaLowering.include()` API 7 | const _include = Base.IncludeInto(JuliaLowering) 8 | 9 | if parentmodule(JuliaLowering) === Base 10 | using Base.JuliaSyntax 11 | else 12 | using JuliaSyntax 13 | end 14 | 15 | using .JuliaSyntax: highlight, Kind, @KSet_str, is_leaf, children, numchildren, 16 | head, kind, flags, has_flags, numeric_flags, filename, first_byte, 17 | last_byte, byte_range, sourcefile, source_location, span, sourcetext, 18 | is_literal, is_number, is_operator, is_prec_assignment, is_prefix_call, 19 | is_infix_op_call, is_postfix_op_call, is_error 20 | 21 | _include("kinds.jl") 22 | _register_kinds() 23 | 24 | _include("syntax_graph.jl") 25 | _include("ast.jl") 26 | _include("bindings.jl") 27 | _include("utils.jl") 28 | 29 | _include("macro_expansion.jl") 30 | _include("desugaring.jl") 31 | _include("scope_analysis.jl") 32 | _include("closure_conversion.jl") 33 | _include("linear_ir.jl") 34 | _include("runtime.jl") 35 | _include("syntax_macros.jl") 36 | 37 | _include("eval.jl") 38 | _include("compat.jl") 39 | _include("hooks.jl") 40 | 41 | function __init__() 42 | _register_kinds() 43 | end 44 | 45 | _include("precompile.jl") 46 | 47 | end 48 | -------------------------------------------------------------------------------- /test/desugaring.jl: -------------------------------------------------------------------------------- 1 | @testset "Desugaring" begin 2 | 3 | test_mod = Module(:TestMod) 4 | 5 | # @test desugar(test_mod, """ 6 | # let 7 | # y = 0 8 | # x = 1 9 | # let x = x + 1 10 | # y = x 11 | # end 12 | # (x, y) 13 | # end 14 | # """) ≈ @ast_ [K"block" 15 | # [K"block" 16 | # [K"=" 17 | # "y"::K"Identifier" 18 | # 0::K"Integer" 19 | # ] 20 | # [K"=" 21 | # "x"::K"Identifier" 22 | # 1::K"Integer" 23 | # ] 24 | # [K"block" 25 | # [K"=" 26 | # 1::K"BindingId" 27 | # [K"call" 28 | # "+"::K"Identifier" 29 | # "x"::K"Identifier" 30 | # 1::K"Integer" 31 | # ] 32 | # ] 33 | # [K"block" 34 | # [K"local_def" 35 | # "x"::K"Identifier" 36 | # ] 37 | # [K"=" 38 | # "x"::K"Identifier" 39 | # 1::K"BindingId" 40 | # ] 41 | # [K"block" 42 | # [K"=" 43 | # "y"::K"Identifier" 44 | # "x"::K"Identifier" 45 | # ] 46 | # ] 47 | # ] 48 | # ] 49 | # [K"call" 50 | # "tuple"::K"core" 51 | # "x"::K"Identifier" 52 | # "y"::K"Identifier" 53 | # ] 54 | # ] 55 | # ] 56 | 57 | end 58 | -------------------------------------------------------------------------------- /test/import.jl: -------------------------------------------------------------------------------- 1 | @testset "using / import" begin 2 | 3 | test_mod = Module() 4 | 5 | # Test attributes are correctly set for export/public 6 | JuliaLowering.include_string(test_mod, """ 7 | x = 1 8 | y = 2 9 | export x 10 | public y 11 | """) 12 | @test Base.isexported(test_mod, :x) 13 | @test Base.ispublic(test_mod, :x) 14 | @test Base.ispublic(test_mod, :y) 15 | @test !Base.isexported(test_mod, :y) 16 | 17 | # Test various forms of `using` 18 | C = JuliaLowering.include_string(test_mod, """ 19 | module C 20 | module D 21 | export x 22 | public y, f 23 | x = [101] 24 | y = [202] 25 | 26 | function f() 27 | "hi" 28 | end 29 | end 30 | module E 31 | using ..D: f 32 | using ..D 33 | using .D: y as D_y 34 | using .D: x as D_x_2, y as D_y_2 35 | import .D.y as D_y_3 36 | end 37 | end 38 | """) 39 | @test C.D.f === C.E.f 40 | @test C.D.x === C.E.x 41 | @test C.D.y === C.E.D_y 42 | @test C.D.x === C.E.D_x_2 43 | @test C.D.y === C.E.D_y_2 44 | @test C.D.y === C.E.D_y_3 45 | 46 | # Test that using F brings in the exported symbol G immediately and that it can 47 | # be used next in the import list. 48 | F = JuliaLowering.include_string(test_mod, """ 49 | module F 50 | export G 51 | module G 52 | export G_global 53 | G_global = "exported from G" 54 | end 55 | end 56 | """) 57 | JuliaLowering.include_string(test_mod, """ 58 | using .F, .G 59 | """) 60 | @test test_mod.F === F 61 | @test test_mod.G === F.G 62 | @test test_mod.G_global === "exported from G" 63 | 64 | # Similarly, that import makes symbols available immediately 65 | H = JuliaLowering.include_string(test_mod, """ 66 | module H 67 | module I 68 | module J 69 | end 70 | end 71 | end 72 | """) 73 | JuliaLowering.include_string(test_mod, """ 74 | import .H.I, .I.J 75 | """) 76 | @test test_mod.I === H.I 77 | @test test_mod.J === H.I.J 78 | @test test_mod.G_global === "exported from G" 79 | 80 | end 81 | -------------------------------------------------------------------------------- /test/generators.jl: -------------------------------------------------------------------------------- 1 | @testset "Generators" begin 2 | 3 | test_mod = Module() 4 | 5 | @test JuliaLowering.include_string(test_mod, """ 6 | collect(x^2 for x in 1:3) 7 | """) == [1,4,9] 8 | 9 | @test JuliaLowering.include_string(test_mod, """ 10 | collect(x for x in 1:5 if isodd(x)) 11 | """) == [1,3,5] 12 | 13 | @test JuliaLowering.include_string(test_mod, """ 14 | collect((y,x) for (x,y) in zip(1:3, 2:4) if y != 3) 15 | """) == [(2,1), (4,3)] 16 | 17 | # product iterator 18 | @test JuliaLowering.include_string(test_mod, """ 19 | collect((x,y) for x in 1:3, y in 1:2) 20 | """) == [(1,1) (1,2) 21 | (2,1) (2,2) 22 | (3,1) (3,2)] 23 | 24 | # flattened iterator 25 | @test JuliaLowering.include_string(test_mod, """ 26 | collect((x,y,z) for x in 1:3, y in 4:5 for z in 6:7) 27 | """) == [ 28 | (1,4,6) 29 | (1,4,7) 30 | (2,4,6) 31 | (2,4,7) 32 | (3,4,6) 33 | (3,4,7) 34 | (1,5,6) 35 | (1,5,7) 36 | (2,5,6) 37 | (2,5,7) 38 | (3,5,6) 39 | (3,5,7) 40 | ] 41 | 42 | # Duplicate iteration variables - body sees only innermost 43 | @test JuliaLowering.include_string(test_mod, """ 44 | collect(x for x in 1:3 for x in 1:2) 45 | """) == [1, 2, 1, 2, 1, 2] 46 | 47 | # Outer iteration variables are protected from mutation 48 | @test JuliaLowering.include_string(test_mod, """ 49 | collect((z=y; y=100; z) for y in 1:3 for x in 1:2) 50 | """) == [1, 1, 2, 2, 3, 3] 51 | 52 | # Simple typed comprehension lowered to for loops 53 | @test JuliaLowering.include_string(test_mod, """ 54 | Tuple{Int,Int}[(x,y) for x in 1:2, y in 1:3] 55 | """) == [(1,1) (1,2) (1,3) 56 | (2,1) (2,2) (2,3)] 57 | 58 | # Triply nested comprehension 59 | @test JuliaLowering.include_string(test_mod, """ 60 | [(x,y,z) for x in 1:3 for y in 4:5 for z in 6:7] 61 | """) == [ 62 | (1, 4, 6) 63 | (1, 4, 7) 64 | (1, 5, 6) 65 | (1, 5, 7) 66 | (2, 4, 6) 67 | (2, 4, 7) 68 | (2, 5, 6) 69 | (2, 5, 7) 70 | (3, 4, 6) 71 | (3, 4, 7) 72 | (3, 5, 6) 73 | (3, 5, 7) 74 | ] 75 | 76 | end 77 | -------------------------------------------------------------------------------- /test/assignments.jl: -------------------------------------------------------------------------------- 1 | @testset "assignments" begin 2 | 3 | test_mod = Module() 4 | 5 | Base.include_string(test_mod, 6 | """ 7 | mutable struct X 8 | a 9 | b 10 | end 11 | """) 12 | 13 | # TODO: Desugaring of assignment done, but needs `where` lowering 14 | JuliaLowering.include_string(test_mod, """ 15 | MyVector{T} = Array{1,T} 16 | """) 17 | @test test_mod.MyVector{Int} == Array{1,Int} 18 | 19 | # Chained assignment 20 | @test JuliaLowering.include_string(test_mod, """ 21 | let 22 | a = b = 42 23 | end 24 | """) == 42 25 | 26 | # Assignment in value but not tail position 27 | @test JuliaLowering.include_string(test_mod, """ 28 | let 29 | x = begin 30 | y = 42 31 | end 32 | x 33 | end 34 | """) == 42 35 | 36 | @test JuliaLowering.include_string(test_mod, """ 37 | let 38 | x = [] 39 | a = b = (push!(x, 1); 42) 40 | (a,b,x) 41 | end 42 | """) == (42,42,[1]) 43 | 44 | # setproperty! 45 | @test JuliaLowering.include_string(test_mod, """ 46 | let 47 | x = X(1,2) 48 | x.a = 10 49 | (x.a, x.b) 50 | end 51 | """) == (10,2) 52 | 53 | # Declarations 54 | @test JuliaLowering.include_string(test_mod, """ 55 | let 56 | x::Int = 1 57 | x = 10.0 58 | x 59 | end 60 | """) === 10 61 | 62 | # Updating assignments 63 | @test JuliaLowering.include_string(test_mod, """ 64 | let x = "hi" 65 | x *= " ho" 66 | x 67 | end 68 | """) == "hi ho" 69 | 70 | @test JuliaLowering.include_string(test_mod, """ 71 | let x = [1,3] 72 | x .-= [0,1] 73 | x 74 | end 75 | """) == [1,2] 76 | 77 | @test JuliaLowering.include_string(test_mod, """ 78 | let x = [1 2; 3 4] 79 | x[begin, 1:end] .-= 1 80 | x 81 | end 82 | """) == [0 1 ; 3 4] 83 | 84 | # Test that side effects of computing indices in left hand side only occur 85 | # once. 86 | @test JuliaLowering.include_string(test_mod, """ 87 | let 88 | x = [1, 2] 89 | n_calls = 0 90 | the_index() = (n_calls = n_calls + 1; 1) 91 | x[the_index()] += 1 92 | x[the_index()]::Int += 1 93 | x[the_index():end] .+= 1 94 | n_calls 95 | end 96 | """) == 3 97 | 98 | end 99 | -------------------------------------------------------------------------------- /test/scopes.jl: -------------------------------------------------------------------------------- 1 | @testset "Scopes" begin 2 | 3 | test_mod = Module() 4 | 5 | #------------------------------------------------------------------------------- 6 | # Scopes 7 | @test JuliaLowering.include_string(test_mod, 8 | """ 9 | let 10 | y = 0 11 | x = 1 12 | let x = x + 1 13 | y = x 14 | end 15 | (x, y) 16 | end 17 | """) == (1, 2) 18 | 19 | JuliaLowering.include_string(test_mod, """ 20 | x = 101 21 | y = 202 22 | """) 23 | @test test_mod.x == 101 24 | @test test_mod.y == 202 25 | @test JuliaLowering.include_string(test_mod, "x + y") == 303 26 | 27 | @test JuliaLowering.include_string(test_mod, """ 28 | begin 29 | local x = 1 30 | local x = 2 31 | let (x,y) = (:x,:y) 32 | (y,x) 33 | end 34 | end 35 | """) === (:y,:x) 36 | 37 | # Types on left hand side of type decls refer to the outer scope 38 | # (In the flisp implementation they refer to the inner scope, but this seems 39 | # like a bug.) 40 | @test JuliaLowering.include_string(test_mod, """ 41 | let x::Int = 10.0 42 | local Int = Float64 43 | x 44 | end 45 | """) === 10 46 | 47 | # Closures in let syntax can only capture values from the outside 48 | # (In the flisp implementation it captures from inner scope, but this is 49 | # inconsistent with let assignment where the rhs refers to the outer scope and 50 | # thus seems like a bug.) 51 | @test JuliaLowering.include_string(test_mod, """ 52 | begin 53 | local y = :outer_y 54 | let f() = y 55 | local y = :inner_y 56 | f() 57 | end 58 | end 59 | """) === :outer_y 60 | 61 | # wrap expression in scope block of `scope_type` 62 | function wrapscope(ex, scope_type) 63 | g = JuliaLowering.ensure_attributes(ex._graph, scope_type=Symbol) 64 | ex = JuliaLowering.reparent(g, ex) 65 | makenode(ex, ex, K"scope_block", ex; scope_type=scope_type) 66 | end 67 | 68 | assign_z_2 = parsestmt(SyntaxTree, "begin z = 2 end", filename="foo.jl") 69 | Base.eval(test_mod, :(z=1)) 70 | @test test_mod.z == 1 71 | # neutral (eg, for loops) and hard (eg, let) scopes create a new binding for z 72 | JuliaLowering.eval(test_mod, wrapscope(assign_z_2, :neutral)) 73 | @test test_mod.z == 1 74 | JuliaLowering.eval(test_mod, wrapscope(assign_z_2, :hard)) 75 | @test test_mod.z == 1 76 | # but wrapping neutral scope in soft scope uses the existing binding in test_mod 77 | JuliaLowering.eval(test_mod, wrapscope(wrapscope(assign_z_2, :neutral), :soft)) 78 | @test test_mod.z == 2 79 | 80 | end 81 | -------------------------------------------------------------------------------- /test/quoting_ir.jl: -------------------------------------------------------------------------------- 1 | ######################################## 2 | # Simple interpolation 3 | quote 4 | $x + 1 5 | end 6 | #--------------------- 7 | 1 TestMod.x 8 | 2 (call core.tuple %₁) 9 | 3 (call JuliaLowering.interpolate_ast SyntaxTree (inert (block (call-i ($ x) + 1))) %₂) 10 | 4 (return %₃) 11 | 12 | ######################################## 13 | # Trivial interpolation 14 | :($x) 15 | #--------------------- 16 | 1 TestMod.x 17 | 2 (call core.tuple %₁) 18 | 3 (call JuliaLowering.interpolate_ast SyntaxTree (inert ($ x)) %₂) 19 | 4 (return %₃) 20 | 21 | ######################################## 22 | # Double escape 23 | quote 24 | quote 25 | $$x + 1 26 | end 27 | end 28 | #--------------------- 29 | 1 TestMod.x 30 | 2 (call core.tuple %₁) 31 | 3 (call JuliaLowering.interpolate_ast SyntaxTree (inert (block (quote (block (call-i ($ ($ x)) + 1))))) %₂) 32 | 4 (return %₃) 33 | 34 | ######################################## 35 | # Symbols on `.` right hand side need to be scoped correctly 36 | let x = 1 37 | :(A.$x) 38 | end 39 | #--------------------- 40 | 1 1 41 | 2 (= slot₁/x %₁) 42 | 3 slot₁/x 43 | 4 (call core.tuple %₃) 44 | 5 (call JuliaLowering.interpolate_ast SyntaxTree (inert (. A ($ x))) %₄) 45 | 6 (return %₅) 46 | 47 | ######################################## 48 | # Error: Double escape 49 | quote 50 | $$x + 1 51 | end 52 | #--------------------- 53 | LoweringError: 54 | quote 55 | $$x + 1 56 | # └┘ ── `$` expression outside string or quote block 57 | end 58 | 59 | ######################################## 60 | # Quoted property access with identifier 61 | Core.:(foo) 62 | #--------------------- 63 | 1 TestMod.Core 64 | 2 (call top.getproperty %₁ :foo) 65 | 3 (return %₂) 66 | 67 | ######################################## 68 | # Quoted property access with operator 69 | Core.:(!==) 70 | #--------------------- 71 | 1 TestMod.Core 72 | 2 (call top.getproperty %₁ :!==) 73 | 3 (return %₂) 74 | 75 | ######################################## 76 | # Quoted operator function definition (issue #20) 77 | function Base.:(==)() end 78 | #--------------------- 79 | 1 TestMod.Base 80 | 2 (call top.getproperty %₁ :==) 81 | 3 (call core.Typeof %₂) 82 | 4 (call core.svec %₃) 83 | 5 (call core.svec) 84 | 6 SourceLocation::1:10 85 | 7 (call core.svec %₄ %₅ %₆) 86 | 8 --- method core.nothing %₇ 87 | slots: [slot₁/#self#(!read)] 88 | 1 (return core.nothing) 89 | 9 latestworld 90 | 10 (return core.nothing) 91 | -------------------------------------------------------------------------------- /test/repl_mode.jl: -------------------------------------------------------------------------------- 1 | # JuliaLowering REPL mode: an interactive test utility for lowering code (not 2 | # part of the unit tests) 3 | 4 | module JuliaLoweringREPL 5 | 6 | import ReplMaker 7 | import REPL 8 | 9 | using JuliaLowering: JuliaLowering, SyntaxTree, children 10 | using JuliaSyntax 11 | 12 | function is_incomplete(prompt_state) 13 | str = String(take!(copy(REPL.LineEdit.buffer(prompt_state)))) 14 | stream = JuliaSyntax.ParseStream(str) 15 | JuliaSyntax.parse!(stream, rule=:all) 16 | if JuliaSyntax.any_error(stream) 17 | tree = JuliaSyntax.build_tree(SyntaxNode, stream) 18 | tag = JuliaSyntax._incomplete_tag(tree, 1) 19 | return tag != :none 20 | else 21 | return false 22 | end 23 | end 24 | 25 | function eval_ish(mod::Module, ex::SyntaxTree, do_eval::Bool, do_print_ir::Bool) 26 | k = kind(ex) 27 | if k == K"toplevel" 28 | x = nothing 29 | for e in children(ex) 30 | x = eval_ish(mod, e, do_eval, do_print_ir) 31 | end 32 | return x 33 | end 34 | linear_ir = JuliaLowering.lower(mod, ex) 35 | if do_print_ir 36 | JuliaLowering.print_ir(stdout, linear_ir) 37 | end 38 | if do_eval 39 | println(stdout, "#----------------------") 40 | expr_form = JuliaLowering.to_lowered_expr(linear_ir) 41 | Base.eval(mod, expr_form) 42 | end 43 | end 44 | 45 | PRINT_IR::Bool = true 46 | DO_EVAL::Bool = false 47 | function opts(; do_eval=false, print_ir=false) 48 | global DO_EVAL = do_eval 49 | global PRINT_IR = print_ir 50 | end 51 | 52 | function handle_input(str) 53 | global DO_EVAL, PRINT_IR 54 | if str == "DO_EVAL" 55 | DO_EVAL = true 56 | return 57 | elseif str == "!DO_EVAL" 58 | DO_EVAL = false 59 | return 60 | elseif str == "PRINT_IR" 61 | PRINT_IR = true 62 | return 63 | elseif str == "!PRINT_IR" 64 | PRINT_IR = false 65 | return 66 | end 67 | ex = parseall(SyntaxTree, str; filename="REPL") 68 | eval_ish(Main, ex, DO_EVAL, PRINT_IR) 69 | end 70 | 71 | function init() 72 | ReplMaker.initrepl(handle_input, 73 | valid_input_checker = !is_incomplete, 74 | prompt_text="Lowering> ", 75 | prompt_color = :blue, 76 | start_key=")", 77 | mode_name=:JuliaLowering) 78 | end 79 | 80 | function __init__() 81 | init() 82 | end 83 | 84 | end 85 | -------------------------------------------------------------------------------- /test/import_ir.jl: -------------------------------------------------------------------------------- 1 | ######################################## 2 | # Basic import 3 | import A: b 4 | #--------------------- 5 | 1 (call JuliaLowering.eval_import true TestMod :($(QuoteNode(:($(Expr(:., :A)))))) :($(QuoteNode(:($(Expr(:., :b))))))) 6 | 2 latestworld 7 | 3 (return core.nothing) 8 | 9 | ######################################## 10 | # Import with paths and `as` 11 | import A.B.C: b, c.d as e 12 | #--------------------- 13 | 1 (call JuliaLowering.eval_import true TestMod :($(QuoteNode(:($(Expr(:., :A, :B, :C)))))) :($(QuoteNode(:($(Expr(:., :b)))))) :($(QuoteNode(:(c.d as e))))) 14 | 2 latestworld 15 | 3 (return core.nothing) 16 | 17 | ######################################## 18 | # Imports without `from` module need separating with latestworld 19 | import A, B 20 | #--------------------- 21 | 1 (call JuliaLowering.eval_import true TestMod top.nothing :($(QuoteNode(:($(Expr(:., :A))))))) 22 | 2 latestworld 23 | 3 (call JuliaLowering.eval_import true TestMod top.nothing :($(QuoteNode(:($(Expr(:., :B))))))) 24 | 4 latestworld 25 | 5 (return core.nothing) 26 | 27 | ######################################## 28 | # Multiple usings need separating with latestworld 29 | using A, B 30 | #--------------------- 31 | 1 (call JuliaLowering.eval_using TestMod :($(QuoteNode(:($(Expr(:., :A))))))) 32 | 2 latestworld 33 | 3 (call JuliaLowering.eval_using TestMod :($(QuoteNode(:($(Expr(:., :B))))))) 34 | 4 latestworld 35 | 5 (return core.nothing) 36 | 37 | ######################################## 38 | # Using with paths and `as` 39 | using A.B.C: b, c.d as e 40 | #--------------------- 41 | 1 (call JuliaLowering.eval_import false TestMod :($(QuoteNode(:($(Expr(:., :A, :B, :C)))))) :($(QuoteNode(:($(Expr(:., :b)))))) :($(QuoteNode(:(c.d as e))))) 42 | 2 latestworld 43 | 3 (return core.nothing) 44 | 45 | ######################################## 46 | # Error: Import not at top level 47 | function f() 48 | import A: b 49 | end 50 | #--------------------- 51 | LoweringError: 52 | function f() 53 | import A: b 54 | # └─────────┘ ── this syntax is only allowed in top level code 55 | end 56 | 57 | ######################################## 58 | # Export 59 | export a, b, c 60 | #--------------------- 61 | 1 (call JuliaLowering.eval_public TestMod true ["a", "b", "c"]) 62 | 2 (return %₁) 63 | 64 | ######################################## 65 | # Public 66 | public a, b, c 67 | #--------------------- 68 | 1 (call JuliaLowering.eval_public TestMod false ["a", "b", "c"]) 69 | 2 (return %₁) 70 | -------------------------------------------------------------------------------- /src/hooks.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Becomes `Core._lower()` upon activating JuliaLowering. 3 | 4 | Returns an svec with the lowered code (usually expr) as its first element, and 5 | (until integration is less experimental) whatever we want after it 6 | """ 7 | function core_lowering_hook(@nospecialize(code), mod::Module, 8 | file="none", line=0, world=typemax(Csize_t), warn=false) 9 | if !(code isa SyntaxTree || code isa Expr) 10 | # e.g. LineNumberNode, integer... 11 | return Core.svec(code) 12 | end 13 | 14 | # TODO: fix in base 15 | file = file isa Ptr{UInt8} ? unsafe_string(file) : file 16 | line = !(line isa Int) ? Int(line) : line 17 | 18 | local st0 = nothing 19 | try 20 | st0 = code isa Expr ? expr_to_syntaxtree(code, LineNumberNode(line, file)) : code 21 | if kind(st0) in KSet"toplevel module" 22 | return Core.svec(code) 23 | elseif kind(st0) === K"doc" && numchildren(st0) >= 2 && kind(st0[2]) === K"module" 24 | # TODO: this ignores module docstrings for now 25 | return Core.svec(Expr(st0[2])) 26 | end 27 | ctx1, st1 = expand_forms_1( mod, st0, true, world) 28 | ctx2, st2 = expand_forms_2( ctx1, st1) 29 | ctx3, st3 = resolve_scopes( ctx2, st2) 30 | ctx4, st4 = convert_closures(ctx3, st3) 31 | ctx5, st5 = linearize_ir( ctx4, st4) 32 | ex = to_lowered_expr(st5) 33 | return Core.svec(ex, st5, ctx5) 34 | catch exc 35 | @info("JuliaLowering threw given input:", code=code, st0=st0, file=file, line=line, mod=mod) 36 | rethrow(exc) 37 | 38 | # TODO: Re-enable flisp fallback once we're done collecting errors 39 | # @error("JuliaLowering failed — falling back to flisp!", 40 | # exception=(exc,catch_backtrace()), 41 | # code=code, file=file, line=line, mod=mod) 42 | # return Base.fl_lower(code, mod, file, line, world, warn) 43 | end 44 | end 45 | 46 | # TODO: Write a parser hook here. The input to `core_lowering_hook` should 47 | # eventually be a (convertible to) SyntaxTree, but we need to make updates to 48 | # the parsing API to include a parameter for AST type. 49 | 50 | const _has_v1_13_hooks = isdefined(Core, :_lower) 51 | 52 | function activate!(enable=true) 53 | if !_has_v1_13_hooks 54 | error("Cannot use JuliaLowering without `Core._lower` binding or in $VERSION < 1.13") 55 | end 56 | 57 | if enable 58 | Core._setlowerer!(core_lowering_hook) 59 | else 60 | Core._setlowerer!(Base.fl_lower) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/hooks.jl: -------------------------------------------------------------------------------- 1 | const JL = JuliaLowering 2 | 3 | @testset "hooks" begin 4 | test_mod = Module() 5 | 6 | @testset "`core_lowering_hook`" begin 7 | # Non-AST types are often sent through lowering 8 | stuff = Any[LineNumberNode(1), 123, 123.123, true, "foo", test_mod] 9 | for s in stuff 10 | @test JL.core_lowering_hook(s, test_mod) == Core.svec(s) 11 | end 12 | 13 | for ast_type in (Expr, JL.SyntaxTree) 14 | ex = parsestmt(ast_type, "[1,2,3] .+= 1") 15 | out = JL.core_lowering_hook(ex, test_mod) 16 | @test out isa Core.SimpleVector && out[1] isa Expr 17 | val = Core.eval(test_mod, out[1]) 18 | @test val == [2,3,4] 19 | end 20 | 21 | # file argument mismatch with embedded linenumbernodes shouldn't crash 22 | ex = Expr(:block, LineNumberNode(111), :(x = 1), LineNumberNode(222), :(x + 1)) 23 | lwr = JuliaLowering.core_lowering_hook(ex, test_mod, "foo.jl", 333)[1] 24 | @test Core.eval(test_mod, lwr) === 2 25 | end 26 | 27 | if isdefined(Core, :_lower) 28 | function jeval(str) 29 | prog = parseall(Expr, str) 30 | local out 31 | try 32 | JL.activate!() 33 | out = Core.eval(test_mod, prog) 34 | finally 35 | JL.activate!(false) 36 | end 37 | end 38 | @testset "integration: `JuliaLowering.activate!`" begin 39 | out = jeval("global asdf = 1") 40 | @test out === 1 41 | @test isdefined(test_mod, :asdf) 42 | 43 | out = jeval("module M; x = 1; end") 44 | @test out isa Module 45 | @test isdefined(test_mod, :M) 46 | @test isdefined(test_mod.M, :x) 47 | 48 | # Tricky cases with symbols 49 | out = jeval("""module M2 50 | Base.@constprop :aggressive function f(x); x; end 51 | const what = ccall(:jl_value_ptr, Ptr{Cvoid}, (Any,), Core.nothing) 52 | end""") 53 | @test out isa Module 54 | @test isdefined(test_mod, :M2) 55 | @test isdefined(test_mod.M2, :f) 56 | @test isdefined(test_mod.M2, :what) 57 | 58 | out = jeval(""" "docstring" module M3 end """) 59 | @test out isa Module 60 | @test isdefined(test_mod, :M3) 61 | 62 | # Macros may produce toplevel expressions. Note that julia handles 63 | # this case badly (macro expansion replaces M5_inner with a 64 | # globalref) and we handle esc(:M5_inner) badly 65 | out = jeval("""module M5 66 | macro newmod() 67 | return quote 68 | let a = 1 69 | $(Expr(:toplevel, 70 | Expr(:module, true, :M5_inner, 71 | Expr(:block, :(global asdf = 1))))) 72 | end 73 | end 74 | end 75 | @newmod() 76 | end""") 77 | @test out isa Module 78 | @test isdefined(test_mod, :M5) 79 | @test isdefined(test_mod.M5, :M5_inner) 80 | @test isdefined(test_mod.M5.M5_inner, :asdf) 81 | 82 | # TODO: broken, commented to prevent error logging 83 | # @test jeval("Base.@propagate_inbounds @inline meta_double_quote_issue(x) = x") isa Function 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /test/arrays.jl: -------------------------------------------------------------------------------- 1 | using Test, JuliaLowering 2 | 3 | @testset "Array syntax" begin 4 | 5 | test_mod = Module() 6 | 7 | # Test that two array element types are equal and that they are also equal 8 | # elementwise 9 | function ≅(a, b) 10 | eltype(a) == eltype(b) && a == b 11 | end 12 | 13 | # vect 14 | @test JuliaLowering.include_string(test_mod, """ 15 | [1,2,3] 16 | """) ≅ [1,2,3] 17 | 18 | # hcat 19 | @test JuliaLowering.include_string(test_mod, """ 20 | [1 2 3] 21 | """) ≅ [1 2 3] 22 | 23 | # typed_hcat 24 | @test JuliaLowering.include_string(test_mod, """ 25 | Int[1.0 2.0 3.0] 26 | """) ≅ [1 2 3] 27 | 28 | # splat with vect/hcat/typed_hcat 29 | @test JuliaLowering.include_string(test_mod, """ 30 | let xs = [1,2,3] 31 | [0, xs...] 32 | end 33 | """) ≅ [0,1,2,3] 34 | @test JuliaLowering.include_string(test_mod, """ 35 | let xs = [1,2,3] 36 | [0 xs...] 37 | end 38 | """) ≅ [0 1 2 3] 39 | @test JuliaLowering.include_string(test_mod, """ 40 | let xs = [1,2,3] 41 | Int[0 xs...] 42 | end 43 | """) ≅ Int[0 1 2 3] 44 | 45 | # vcat 46 | @test JuliaLowering.include_string(test_mod, """ 47 | [1;2;3] 48 | """) ≅ [1; 2; 3] 49 | 50 | @test JuliaLowering.include_string(test_mod, """ 51 | let 52 | xs = (1,2) 53 | [xs...; xs...] 54 | end 55 | """) ≅ [1,2,1,2] 56 | 57 | # hvcat 58 | @test JuliaLowering.include_string(test_mod, """ 59 | [1 2 3; 4 5 6] 60 | """) ≅ [1 2 3; 61 | 4 5 6] 62 | 63 | # hvcat_rows 64 | @test JuliaLowering.include_string(test_mod, """ 65 | let 66 | xs = (1,2) 67 | [xs... 3; 4 xs...] 68 | end 69 | """) ≅ [1 2 3; 70 | 4 1 2] 71 | 72 | # typed_vcat 73 | @test JuliaLowering.include_string(test_mod, """ 74 | Int[1.0; 2.0; 3.0] 75 | """) ≅ [1; 2; 3] 76 | 77 | # typed_hvcat 78 | @test JuliaLowering.include_string(test_mod, """ 79 | Int[1.0 2.0 3.0; 4.0 5.0 6.0] 80 | """) ≅ [1 2 3; 81 | 4 5 6] 82 | 83 | # typed_hvcat_rows 84 | @test JuliaLowering.include_string(test_mod, """ 85 | let 86 | xs = (1.0,2.0) 87 | Int[xs... 3; 4 xs...] 88 | end 89 | """) ≅ [1 2 3; 90 | 4 1 2] 91 | 92 | # ncat with a single dimension 93 | @test JuliaLowering.include_string(test_mod, """ 94 | [1 ;;; 2 ;;; 3] 95 | """) ≅ [1 ;;; 2 ;;; 3] 96 | 97 | @test JuliaLowering.include_string(test_mod, """ 98 | Int[1.0 ;;; 2.0 ;;; 3.0] 99 | """) ≅ [1 ;;; 2 ;;; 3] 100 | 101 | # Lowering of ref to setindex 102 | @test JuliaLowering.include_string(test_mod, """ 103 | let 104 | as = [0,0,0,0] 105 | as[begin] = 1 106 | as[2] = 2 107 | as[end] = 4 108 | as 109 | end 110 | """) == [1, 2, 0, 4] 111 | 112 | @test JuliaLowering.include_string(test_mod, """ 113 | let 114 | as = zeros(Int, 2,3) 115 | as[begin, end] = 1 116 | as[end, begin] = 2 117 | js = (2,) 118 | as[js..., end] = 3 119 | as 120 | end 121 | """) == [0 0 1; 122 | 2 0 3] 123 | 124 | # getindex 125 | @test JuliaLowering.include_string(test_mod, """ 126 | let 127 | x = [1 2; 128 | 3 4] 129 | (x[end,begin], x[begin,end]) 130 | end 131 | """) == (3, 2) 132 | 133 | # getindex with splats 134 | @test JuliaLowering.include_string(test_mod, """ 135 | let 136 | x = [1 2; 137 | 3 4 138 | ;;; 139 | 5 6; 140 | 7 8] 141 | inds = (2,1) 142 | ind1 = (1,) 143 | (x[inds..., begin], x[inds..., end], x[1, inds...], 144 | x[ind1..., ind1..., end]) 145 | end 146 | """) == (3, 7, 2, 5) 147 | 148 | end # @testset "Array syntax" begin 149 | -------------------------------------------------------------------------------- /test/loops_ir.jl: -------------------------------------------------------------------------------- 1 | ######################################## 2 | # Basic while loop 3 | while f(a) 4 | body1 5 | body2 6 | end 7 | #--------------------- 8 | 1 TestMod.f 9 | 2 TestMod.a 10 | 3 (call %₁ %₂) 11 | 4 (gotoifnot %₃ label₈) 12 | 5 TestMod.body1 13 | 6 TestMod.body2 14 | 7 (goto label₁) 15 | 8 (return core.nothing) 16 | 17 | ######################################## 18 | # While loop with short circuit condition 19 | while a && b 20 | body 21 | end 22 | #--------------------- 23 | 1 TestMod.a 24 | 2 (gotoifnot %₁ label₇) 25 | 3 TestMod.b 26 | 4 (gotoifnot %₃ label₇) 27 | 5 TestMod.body 28 | 6 (goto label₁) 29 | 7 (return core.nothing) 30 | 31 | ######################################## 32 | # While loop with with break and continue 33 | while cond 34 | body1 35 | break 36 | body2 37 | continue 38 | body3 39 | end 40 | #--------------------- 41 | 1 TestMod.cond 42 | 2 (gotoifnot %₁ label₉) 43 | 3 TestMod.body1 44 | 4 (goto label₉) 45 | 5 TestMod.body2 46 | 6 (goto label₈) 47 | 7 TestMod.body3 48 | 8 (goto label₁) 49 | 9 (return core.nothing) 50 | 51 | ######################################## 52 | # Basic for loop 53 | for x in xs 54 | body 55 | end 56 | #--------------------- 57 | 1 TestMod.xs 58 | 2 (= slot₁/next (call top.iterate %₁)) 59 | 3 slot₁/next 60 | 4 (call core.=== %₃ core.nothing) 61 | 5 (call top.not_int %₄) 62 | 6 (gotoifnot %₅ label₁₇) 63 | 7 slot₁/next 64 | 8 (= slot₂/x (call core.getfield %₇ 1)) 65 | 9 (call core.getfield %₇ 2) 66 | 10 TestMod.body 67 | 11 (= slot₁/next (call top.iterate %₁ %₉)) 68 | 12 slot₁/next 69 | 13 (call core.=== %₁₂ core.nothing) 70 | 14 (call top.not_int %₁₃) 71 | 15 (gotoifnot %₁₄ label₁₇) 72 | 16 (goto label₇) 73 | 17 (return core.nothing) 74 | 75 | ######################################## 76 | # Syntax sugar for nested for loop 77 | for x in xs, y in ys 78 | x = 10 # Copy of x; does not overwrite x iteration var 79 | end 80 | #--------------------- 81 | 1 TestMod.xs 82 | 2 (= slot₂/next (call top.iterate %₁)) 83 | 3 slot₂/next 84 | 4 (call core.=== %₃ core.nothing) 85 | 5 (call top.not_int %₄) 86 | 6 (gotoifnot %₅ label₃₄) 87 | 7 slot₂/next 88 | 8 (= slot₃/x (call core.getfield %₇ 1)) 89 | 9 (call core.getfield %₇ 2) 90 | 10 TestMod.ys 91 | 11 (= slot₁/next (call top.iterate %₁₀)) 92 | 12 slot₁/next 93 | 13 (call core.=== %₁₂ core.nothing) 94 | 14 (call top.not_int %₁₃) 95 | 15 (gotoifnot %₁₄ label₂₈) 96 | 16 slot₃/x 97 | 17 (= slot₄/x %₁₆) 98 | 18 slot₁/next 99 | 19 (= slot₅/y (call core.getfield %₁₈ 1)) 100 | 20 (call core.getfield %₁₈ 2) 101 | 21 (= slot₄/x 10) 102 | 22 (= slot₁/next (call top.iterate %₁₀ %₂₀)) 103 | 23 slot₁/next 104 | 24 (call core.=== %₂₃ core.nothing) 105 | 25 (call top.not_int %₂₄) 106 | 26 (gotoifnot %₂₅ label₂₈) 107 | 27 (goto label₁₆) 108 | 28 (= slot₂/next (call top.iterate %₁ %₉)) 109 | 29 slot₂/next 110 | 30 (call core.=== %₂₉ core.nothing) 111 | 31 (call top.not_int %₃₀) 112 | 32 (gotoifnot %₃₁ label₃₄) 113 | 33 (goto label₇) 114 | 34 (return core.nothing) 115 | 116 | ######################################## 117 | # Error: break outside for/while 118 | break 119 | #--------------------- 120 | LoweringError: 121 | break 122 | └───┘ ── break must be used inside a `while` or `for` loop 123 | 124 | ######################################## 125 | # Error: continue outside for/while 126 | continue 127 | #--------------------- 128 | LoweringError: 129 | continue 130 | └──────┘ ── continue must be used inside a `while` or `for` loop 131 | 132 | ######################################## 133 | # Error: `outer` without outer local variable 134 | let 135 | for outer i = 1:2 136 | nothing 137 | end 138 | i 139 | end 140 | #--------------------- 141 | LoweringError: 142 | let 143 | for outer i = 1:2 144 | # ╙ ── `outer` annotations must match with a local variable in an outer scope but no such variable was found 145 | nothing 146 | end 147 | -------------------------------------------------------------------------------- /test/decls.jl: -------------------------------------------------------------------------------- 1 | @testset "Declarations" begin 2 | 3 | test_mod = Module() 4 | 5 | @test JuliaLowering.include_string(test_mod, """ 6 | begin 7 | local x::Int = 1.0 8 | x 9 | end 10 | """) === 1 11 | 12 | # In value position, yield the right hand side, not `x` 13 | @test JuliaLowering.include_string(test_mod, """ 14 | begin 15 | local x::Int = 1.0 16 | end 17 | """) === 1.0 18 | 19 | # Global decl in value position without assignment returns nothing 20 | @test JuliaLowering.include_string(test_mod, "global x_no_assign") === nothing 21 | 22 | # Unadorned declarations 23 | @test JuliaLowering.include_string(test_mod, """ 24 | let 25 | a = 0.0 26 | x::Int = a 27 | x 28 | end 29 | """) === 0 30 | 31 | @test JuliaLowering.include_string(test_mod, """ 32 | let 33 | local x::Int = 1 34 | x1 = x 35 | x = 20.0 36 | x2 = x 37 | (x1,x2) 38 | end 39 | """) === (1, 20) 40 | 41 | # Global const mixes 42 | @test JuliaLowering.include_string(test_mod, "global x_g = 1") === 1 43 | @test Base.isdefinedglobal(test_mod, :x_g) 44 | @test !Base.isconst(test_mod, :x_g) 45 | @test test_mod.x_g === 1 46 | 47 | @test JuliaLowering.include_string(test_mod, "const x_c = 1") === 1 48 | @test Base.isdefinedglobal(test_mod, :x_c) 49 | @test Base.isconst(test_mod, :x_c) 50 | @test test_mod.x_c === 1 51 | 52 | @test JuliaLowering.include_string(test_mod, "global const x_gc = 1") === 1 53 | @test Base.isdefinedglobal(test_mod, :x_gc) 54 | @test Base.isconst(test_mod, :x_gc) 55 | @test test_mod.x_gc === 1 56 | 57 | @test JuliaLowering.include_string(test_mod, "const global x_cg = 1") === 1 58 | @test Base.isdefinedglobal(test_mod, :x_cg) 59 | @test Base.isconst(test_mod, :x_cg) 60 | @test test_mod.x_cg === 1 61 | # Possibly worth testing excessive global/const keywords or invalid combinations 62 | # (local + global/const) once we decide whether that's a parse error or a 63 | # lowering error 64 | 65 | # Global decls with types 66 | @test JuliaLowering.include_string(test_mod, """ 67 | global a_typed_global::Int = 10.0 68 | """) === 10.0 69 | @test Core.get_binding_type(test_mod, :a_typed_global) === Int 70 | @test test_mod.a_typed_global === 10 71 | 72 | # Also allowed in nontrivial scopes in a top level thunk 73 | @test JuliaLowering.include_string(test_mod, """ 74 | let 75 | global a_typed_global_2::Int = 10.0 76 | end 77 | """) === 10.0 78 | @test Core.get_binding_type(test_mod, :a_typed_global_2) === Int 79 | @test test_mod.a_typed_global_2 === 10 80 | 81 | @test JuliaLowering.include_string(test_mod, "const x_c_T::Int = 9") === 9 82 | @test Base.isdefinedglobal(test_mod, :x_c_T) 83 | @test Base.isconst(test_mod, :x_c_T) 84 | 85 | @testset "typed const redeclaration" begin 86 | # redeclaration of the same value used to be allowed 87 | @test_throws ErrorException JuliaLowering.include_string(test_mod, "x_c_T = 9") 88 | @test_throws ErrorException JuliaLowering.include_string(test_mod, "x_c_T = 10") 89 | # redeclaration with const should be OK 90 | @test JuliaLowering.include_string(test_mod, "const x_c_T::Int = 0") === 0 91 | end 92 | 93 | # Tuple/destructuring assignments 94 | @test JuliaLowering.include_string(test_mod, "(a0, a1, a2) = [1,2,3]") == [1,2,3] 95 | 96 | @test JuliaLowering.include_string(test_mod, "const a,b,c = 1,2,3") === (1, 2, 3) 97 | 98 | test_mod_2 = Module() 99 | @testset "toplevel-preserving syntax" begin 100 | JuliaLowering.include_string(test_mod_2, "if true; global v1::Bool; else const v1 = 1; end") 101 | @test !isdefined(test_mod_2, :v1) 102 | @test Base.binding_kind(test_mod_2, :v1) == Base.PARTITION_KIND_GLOBAL 103 | @test Core.get_binding_type(test_mod_2, :v1) == Bool 104 | 105 | JuliaLowering.include_string(test_mod_2, "if false; global v2::Bool; else const v2 = 2; end") 106 | @test test_mod_2.v2 === 2 107 | @test Base.binding_kind(test_mod_2, :v2) == Base.PARTITION_KIND_CONST 108 | 109 | JuliaLowering.include_string(test_mod_2, "v3 = if true; global v4::Bool; 4 else const v4 = 5; 6; end") 110 | @test test_mod_2.v3 == 4 111 | @test !isdefined(test_mod_2, :v4) 112 | @test Base.binding_kind(test_mod_2, :v4) == Base.PARTITION_KIND_GLOBAL 113 | @test Core.get_binding_type(test_mod_2, :v4) == Bool 114 | 115 | JuliaLowering.include_string(test_mod_2, "v5 = if false; global v6::Bool; 4 else const v6 = 5; 6; end") 116 | @test test_mod_2.v5 === 6 117 | @test test_mod_2.v6 === 5 118 | @test Base.binding_kind(test_mod_2, :v6) == Base.PARTITION_KIND_CONST 119 | end 120 | 121 | end 122 | -------------------------------------------------------------------------------- /test/syntax_graph.jl: -------------------------------------------------------------------------------- 1 | @testset "SyntaxGraph attrs" begin 2 | st = parsestmt(SyntaxTree, "function foo end") 3 | g_init = JuliaLowering.unfreeze_attrs(st._graph) 4 | gf1 = JuliaLowering.freeze_attrs(g_init) 5 | gu1 = JuliaLowering.unfreeze_attrs(gf1) 6 | 7 | # Check that freeze/unfreeze do their jobs 8 | @test gf1.attributes isa NamedTuple 9 | @test gu1.attributes isa Dict 10 | @test Set(keys(gf1.attributes)) == Set(keys(gu1.attributes)) 11 | 12 | # ensure_attributes 13 | gf2 = JuliaLowering.ensure_attributes(gf1, test_attr=Symbol, foo=Type) 14 | gu2 = JuliaLowering.ensure_attributes(gu1, test_attr=Symbol, foo=Type) 15 | # returns a graph with the same attribute storage 16 | @test gf2.attributes isa NamedTuple 17 | @test gu2.attributes isa Dict 18 | # does its job 19 | @test (:test_attr=>Symbol) in JuliaLowering.attrdefs(gf2) 20 | @test (:foo=>Type) in JuliaLowering.attrdefs(gf2) 21 | @test Set(keys(gf2.attributes)) == Set(keys(gu2.attributes)) 22 | # no mutation 23 | @test !((:test_attr=>Symbol) in JuliaLowering.attrdefs(gf1)) 24 | @test !((:foo=>Type) in JuliaLowering.attrdefs(gf1)) 25 | @test Set(keys(gf1.attributes)) == Set(keys(gu1.attributes)) 26 | 27 | # delete_attributes 28 | gf3 = JuliaLowering.delete_attributes(gf2, :test_attr, :foo) 29 | gu3 = JuliaLowering.delete_attributes(gu2, :test_attr, :foo) 30 | # returns a graph with the same attribute storage 31 | @test gf3.attributes isa NamedTuple 32 | @test gu3.attributes isa Dict 33 | # does its job 34 | @test !((:test_attr=>Symbol) in JuliaLowering.attrdefs(gf3)) 35 | @test !((:foo=>Type) in JuliaLowering.attrdefs(gf3)) 36 | @test Set(keys(gf3.attributes)) == Set(keys(gu3.attributes)) 37 | # no mutation 38 | @test (:test_attr=>Symbol) in JuliaLowering.attrdefs(gf2) 39 | @test (:foo=>Type) in JuliaLowering.attrdefs(gf2) 40 | @test Set(keys(gf2.attributes)) == Set(keys(gu2.attributes)) 41 | end 42 | 43 | @testset "SyntaxTree" begin 44 | # Expr conversion 45 | @test Expr(parsestmt(SyntaxTree, "begin a + b ; c end", filename="none")) == 46 | Meta.parse("begin a + b ; c end") 47 | 48 | tree1 = JuliaLowering.@SyntaxTree :(some_unique_identifier) 49 | @test tree1 isa SyntaxTree 50 | @test kind(tree1) == K"Identifier" 51 | @test tree1.name_val == "some_unique_identifier" 52 | 53 | tree2 = JuliaLowering.@SyntaxTree quote 54 | x 55 | $tree1 56 | end 57 | @test tree2 isa SyntaxTree 58 | @test kind(tree2) == K"block" 59 | @test kind(tree2[1]) == K"Identifier" && tree2[1].name_val == "x" 60 | @test kind(tree2[2]) == K"Identifier" && tree2[2].name_val == "some_unique_identifier" 61 | 62 | "For filling required attrs in graphs created by hand" 63 | function testgraph(edge_ranges, edges, more_attrs...) 64 | kinds = Dict(map(i->(i=>K"block"), eachindex(edge_ranges))) 65 | sources = Dict(map(i->(i=>LineNumberNode(i)), eachindex(edge_ranges))) 66 | SyntaxGraph( 67 | edge_ranges, 68 | edges, 69 | Dict(:kind => kinds, :source => sources, more_attrs...)) 70 | end 71 | 72 | @testset "copy_ast" begin 73 | # 1 --> 2 --> 3 src(7-9) = line 7-9 74 | # 4 --> 5 --> 6 src(i) = i + 3 75 | # 7 --> 8 --> 9 76 | g = testgraph([1:1, 2:2, 0:-1, 3:3, 4:4, 0:-1, 5:5, 6:6, 0:-1], 77 | [2, 3, 5, 6, 8, 9], 78 | :source => Dict(enumerate([ 79 | map(i->i+3, 1:6)... 80 | map(LineNumberNode, 7:9)...]))) 81 | st = SyntaxTree(g, 1) 82 | stcopy = JuliaLowering.copy_ast(g, st) 83 | # Each node should be copied once 84 | @test length(g.edge_ranges) === 18 85 | @test st._id != stcopy._id 86 | @test st ≈ stcopy 87 | @test st.source !== stcopy.source 88 | @test st.source[1] !== stcopy.source[1] 89 | @test st.source[1][1] !== stcopy.source[1][1] 90 | 91 | stcopy2 = JuliaLowering.copy_ast(g, st; copy_source=false) 92 | # Only nodes 1-3 should be copied 93 | @test length(g.edge_ranges) === 21 94 | @test st._id != stcopy2._id 95 | @test st ≈ stcopy2 96 | @test st.source === stcopy2.source 97 | @test st.source[1] === stcopy2.source[1] 98 | @test st.source[1][1] === stcopy2.source[1][1] 99 | 100 | # Copy into a new graph 101 | new_g = ensure_attributes!(SyntaxGraph(); JuliaLowering.attrdefs(g)...) 102 | stcopy3 = JuliaLowering.copy_ast(new_g, st) 103 | @test length(new_g.edge_ranges) === 9 104 | @test st ≈ stcopy3 105 | 106 | new_g = ensure_attributes!(SyntaxGraph(); JuliaLowering.attrdefs(g)...) 107 | # Disallow for now, since we can't prevent dangling sourcerefs 108 | @test_throws ErrorException JuliaLowering.copy_ast(new_g, st; copy_source=false) 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /test/branching_ir.jl: -------------------------------------------------------------------------------- 1 | ######################################## 2 | # Basic branching tail && value 3 | begin 4 | local a, b 5 | if a 6 | b 7 | end 8 | end 9 | #--------------------- 10 | 1 (newvar slot₁/a) 11 | 2 (newvar slot₂/b) 12 | 3 slot₁/a 13 | 4 (gotoifnot %₃ label₇) 14 | 5 slot₂/b 15 | 6 (return %₅) 16 | 7 (return core.nothing) 17 | 18 | ######################################## 19 | # Branching, !tail && !value 20 | begin 21 | local a, b, c 22 | if a 23 | b 24 | end 25 | c 26 | end 27 | #--------------------- 28 | 1 (newvar slot₁/a) 29 | 2 (newvar slot₂/b) 30 | 3 (newvar slot₃/c) 31 | 4 slot₁/a 32 | 5 (gotoifnot %₄ label₇) 33 | 6 slot₂/b 34 | 7 slot₃/c 35 | 8 (return %₇) 36 | 37 | ######################################## 38 | # Branching with else 39 | begin 40 | local a, b, c 41 | if a 42 | b 43 | else 44 | c 45 | end 46 | end 47 | #--------------------- 48 | 1 (newvar slot₁/a) 49 | 2 (newvar slot₂/b) 50 | 3 (newvar slot₃/c) 51 | 4 slot₁/a 52 | 5 (gotoifnot %₄ label₈) 53 | 6 slot₂/b 54 | 7 (return %₆) 55 | 8 slot₃/c 56 | 9 (return %₈) 57 | 58 | ######################################## 59 | # Branching with else, !tail && !value 60 | begin 61 | local a, b, c, d 62 | if a 63 | b 64 | else 65 | c 66 | end 67 | d 68 | end 69 | #--------------------- 70 | 1 (newvar slot₁/a) 71 | 2 (newvar slot₂/b) 72 | 3 (newvar slot₃/c) 73 | 4 (newvar slot₄/d) 74 | 5 slot₁/a 75 | 6 (gotoifnot %₅ label₉) 76 | 7 slot₂/b 77 | 8 (goto label₁₀) 78 | 9 slot₃/c 79 | 10 slot₄/d 80 | 11 (return %₁₀) 81 | 82 | ######################################## 83 | # Blocks compile directly to branches 84 | begin 85 | local a, b, c, d 86 | if (a; b && c) 87 | d 88 | end 89 | end 90 | #--------------------- 91 | 1 (newvar slot₁/a) 92 | 2 (newvar slot₂/b) 93 | 3 (newvar slot₃/c) 94 | 4 (newvar slot₄/d) 95 | 5 slot₁/a 96 | 6 slot₂/b 97 | 7 (gotoifnot %₆ label₁₂) 98 | 8 slot₃/c 99 | 9 (gotoifnot %₈ label₁₂) 100 | 10 slot₄/d 101 | 11 (return %₁₀) 102 | 12 (return core.nothing) 103 | 104 | ######################################## 105 | # symbolic goto forward jump 106 | begin 107 | a 108 | @goto foo 109 | b 110 | @label foo 111 | end 112 | #--------------------- 113 | 1 TestMod.a 114 | 2 (goto label₄) 115 | 3 TestMod.b 116 | 4 (return core.nothing) 117 | 118 | ######################################## 119 | # symbolic goto backward jump 120 | begin 121 | a 122 | @label foo 123 | b 124 | @goto foo 125 | end 126 | #--------------------- 127 | 1 TestMod.a 128 | 2 TestMod.b 129 | 3 (goto label₂) 130 | 131 | ######################################## 132 | # Jumping out of try and catch blocks using @goto 133 | begin 134 | try 135 | a 136 | @goto lab 137 | b 138 | catch 139 | c 140 | @goto lab 141 | d 142 | end 143 | @label lab 144 | end 145 | #--------------------- 146 | 1 (enter label₈) 147 | 2 TestMod.a 148 | 3 (leave %₁) 149 | 4 (goto label₁₃) 150 | 5 TestMod.b 151 | 6 (leave %₁) 152 | 7 (goto label₁₃) 153 | 8 TestMod.c 154 | 9 (pop_exception %₁) 155 | 10 (goto label₁₃) 156 | 11 TestMod.d 157 | 12 (pop_exception %₁) 158 | 13 (return core.nothing) 159 | 160 | ######################################## 161 | # Jumping out of nested try/catch and catch/try 162 | begin 163 | try 164 | try 165 | a 166 | catch 167 | b 168 | @goto lab 169 | c 170 | end 171 | catch 172 | try 173 | d 174 | @goto lab 175 | e 176 | catch 177 | end 178 | end 179 | @label lab 180 | end 181 | #--------------------- 182 | 1 (enter label₁₄) 183 | 2 (enter label₆) 184 | 3 TestMod.a 185 | 4 (leave %₂) 186 | 5 (goto label₁₂) 187 | 6 TestMod.b 188 | 7 (pop_exception %₂) 189 | 8 (leave %₁) 190 | 9 (goto label₂₄) 191 | 10 TestMod.c 192 | 11 (pop_exception %₂) 193 | 12 (leave %₁) 194 | 13 (goto label₂₄) 195 | 14 (enter label₂₂) 196 | 15 TestMod.d 197 | 16 (pop_exception %₁) 198 | 17 (leave %₁₄) 199 | 18 (goto label₂₄) 200 | 19 TestMod.e 201 | 20 (leave %₁₄) 202 | 21 (goto label₂₃) 203 | 22 (pop_exception %₁₄) 204 | 23 (pop_exception %₁) 205 | 24 (return core.nothing) 206 | 207 | ######################################## 208 | # Error: no symbolic label 209 | begin 210 | @goto foo 211 | end 212 | #--------------------- 213 | LoweringError: 214 | begin 215 | @goto foo 216 | # └─┘ ── label `foo` referenced but not defined 217 | end 218 | 219 | ######################################## 220 | # Error: duplicate symbolic label 221 | begin 222 | @label foo 223 | @label foo 224 | end 225 | #--------------------- 226 | LoweringError: 227 | begin 228 | @label foo 229 | @label foo 230 | # └─┘ ── Label `foo` defined multiple times 231 | end 232 | 233 | ######################################## 234 | # Error: using value of symbolic label 235 | x = @label foo 236 | #--------------------- 237 | LoweringError: 238 | x = @label foo 239 | # └─┘ ── misplaced label in value position 240 | -------------------------------------------------------------------------------- /test/destructuring.jl: -------------------------------------------------------------------------------- 1 | @testset "Destructuring" begin 2 | 3 | test_mod = Module() 4 | 5 | @testset "Destructuring via iteration" begin 6 | 7 | @test JuliaLowering.include_string(test_mod, """ 8 | let 9 | as = [1,2,3] 10 | (x,y) = as 11 | (x,y) 12 | end 13 | """) == (1,2) 14 | 15 | @test JuliaLowering.include_string(test_mod, """ 16 | let 17 | as = [1,2,3] 18 | (x,ys...) = as 19 | (x,ys) 20 | end 21 | """) == (1, [2,3]) 22 | 23 | @test JuliaLowering.include_string(test_mod, """ 24 | let 25 | as = [1,2,3,4] 26 | (x,ys...,z) = as 27 | (x,ys,z) 28 | end 29 | """) == (1, [2, 3], 4) 30 | 31 | @test JuliaLowering.include_string(test_mod, """ 32 | let 33 | as = [1,2,3,4] 34 | (xs...,y) = as 35 | (xs,y) 36 | end 37 | """) == ([1, 2, 3], 4) 38 | 39 | # Case where indexed_iterate is just iteration 40 | @test JuliaLowering.include_string(test_mod, """ 41 | let 42 | (x,ys...,z) = "aβcδe" 43 | (x,ys,z) 44 | end 45 | """) == ('a', "βcδ", 'e') 46 | 47 | 48 | # Use in value position yields rhs 49 | @test JuliaLowering.include_string(test_mod, """ 50 | let 51 | as = [1,2] 52 | zs = begin 53 | (x,y) = as 54 | end 55 | (x,y, as === zs) 56 | end 57 | """) == (1, 2, true) 58 | 59 | # lhs variable name in rhs 60 | @test JuliaLowering.include_string(test_mod, """ 61 | let 62 | x = (1,2) 63 | (x,y) = x 64 | (x,y) 65 | end 66 | """) == (1, 2) 67 | 68 | @test JuliaLowering.include_string(test_mod, """ 69 | let 70 | x = (1,2) 71 | (x...,y) = x 72 | (x,y) 73 | end 74 | """) == ((1,), 2) 75 | 76 | @test JuliaLowering.include_string(test_mod, """ 77 | let 78 | zs = [(1,2), (3,(4,5))] 79 | ((a,b), (c,(d,e))) = zs 80 | (a,b,c,d,e) 81 | end 82 | """) == (1,2,3,4,5) 83 | 84 | @test JuliaLowering.include_string(test_mod, """ 85 | let 86 | zs = [[1,2,3], 4] 87 | ((a,bs...), c) = zs 88 | (a, bs, c) 89 | end 90 | """) == (1, [2,3], 4) 91 | 92 | end 93 | 94 | 95 | @testset "Tuple elimination with tuples on both sides" begin 96 | 97 | # Simple case 98 | @test JuliaLowering.include_string(test_mod, """ 99 | let a = 1, b = 2 100 | (x,y) = (a,b) 101 | (x,y) 102 | end 103 | """) == (1, 2) 104 | 105 | # lhs variable name in rhs 106 | @test JuliaLowering.include_string(test_mod, """ 107 | let x = 1, y = 2 108 | (x,y) = (y,x) 109 | (x,y) 110 | end 111 | """) == (2, 1) 112 | 113 | # Slurps and splats 114 | 115 | @test JuliaLowering.include_string(test_mod, """ 116 | let a = 1, b = 2, c = 3 117 | (x, ys..., z) = (a, b, c) 118 | (x, ys, z) 119 | end 120 | """) == (1, (2,), 3) 121 | 122 | @test JuliaLowering.include_string(test_mod, """ 123 | let a = 1, b = 2, cs = (3,4) 124 | (x, ys...) = (a, b, cs...) 125 | (x, ys) 126 | end 127 | """) == (1, (2,3,4)) 128 | 129 | @test JuliaLowering.include_string(test_mod, """ 130 | let a = 1, bs = (2,3), c = 4 131 | (x, ys...) = (a, bs..., c) 132 | (x, ys) 133 | end 134 | """) == (1, (2,3,4)) 135 | 136 | @test JuliaLowering.include_string(test_mod, """ 137 | let a = 1, b = 2, cs = (3,4) 138 | (x, ys..., z) = (a, b, cs...) 139 | (x, ys, z) 140 | end 141 | """) == (1, (2,3), 4) 142 | 143 | @test JuliaLowering.include_string(test_mod, """ 144 | let a = 1 145 | (x, ys...) = (a,) 146 | (x, ys) 147 | end 148 | """) == (1, ()) 149 | 150 | # dotted rhs in last place 151 | @test JuliaLowering.include_string(test_mod, """ 152 | let 153 | rh = (2, 3) 154 | (x,y,z) = (1,rh...) 155 | (x,y,z) 156 | end 157 | """) == (1, 2, 3) 158 | 159 | # in value position 160 | @test JuliaLowering.include_string(test_mod, """ 161 | let 162 | rh = (2, 3) 163 | (x,y) = (1,rh...) 164 | end 165 | """) == (1, 2, 3) 166 | 167 | # Side effects in the right hand tuple can affect the previous left hand side 168 | # bindings, for example, `x`, below. In this case we need to ensure `f()` is 169 | # called before `x` is assigned the value from the right hand side. 170 | # (the flisp implementation fails this test.) 171 | @test JuliaLowering.include_string(test_mod, """ 172 | let 173 | function f() 174 | x=100 175 | 2 176 | end 177 | (x,y) = (1,f()) 178 | x,y 179 | end 180 | """) == (1,2) 181 | 182 | # `x` is not assigned and no side effect from `f()` happens when the right hand 183 | # side throws an UndefVarError 184 | @test JuliaLowering.include_string(test_mod, """ 185 | let x=1, y=2, z=3, side_effect=false, a 186 | exc = try 187 | function f() 188 | side_effect=true 189 | end 190 | (x,y,z) = (100, a, f()) 191 | catch e 192 | e 193 | end 194 | (x, y, z, side_effect, exc.var) 195 | end 196 | """) == (1, 2, 3, false, :a) 197 | 198 | # Require that rhs is evaluated before any assignments, thus `x` is not defined 199 | # here because accessing `a` first throws an UndefVarError 200 | @test JuliaLowering.include_string(test_mod, """ 201 | let x, y, a 202 | try 203 | (x, y) = (1, a) 204 | catch 205 | end 206 | @isdefined(x) 207 | end 208 | """) == false 209 | 210 | end 211 | 212 | 213 | @testset "Property destructuring" begin 214 | 215 | @test JuliaLowering.include_string(test_mod, """ 216 | let 217 | ab = (a=1, b=2) 218 | (; a, b) = ab 219 | (a, b) 220 | end 221 | """) == (1, 2) 222 | 223 | end 224 | 225 | end 226 | -------------------------------------------------------------------------------- /test/loops.jl: -------------------------------------------------------------------------------- 1 | 2 | @testset "while loops" begin 3 | 4 | test_mod = Module() 5 | 6 | @test JuliaLowering.include_string(test_mod, """ 7 | let 8 | a = [] 9 | i = 0 10 | while i < 5 11 | i = i + 1 12 | push!(a, i) 13 | end 14 | a 15 | end 16 | """) == [1,2,3,4,5] 17 | 18 | @test JuliaLowering.include_string(test_mod, """ 19 | let 20 | a = [] 21 | i = 0 22 | while i < 5 23 | i = i + 1 24 | if i == 3 25 | break 26 | end 27 | push!(a, i) 28 | end 29 | a 30 | end 31 | """) == [1,2] 32 | 33 | @test JuliaLowering.include_string(test_mod, """ 34 | let 35 | a = [] 36 | i = 0 37 | while i < 5 38 | i = i + 1 39 | if isodd(i) 40 | continue 41 | end 42 | push!(a, i) 43 | end 44 | a 45 | end 46 | """) == [2,4] 47 | 48 | end 49 | 50 | @testset "for loops" begin 51 | 52 | test_mod = Module() 53 | 54 | # iteration 55 | @test JuliaLowering.include_string(test_mod, """ 56 | let 57 | a = [] 58 | for i = 1:3 59 | push!(a, i) 60 | end 61 | a 62 | end 63 | """) == [1,2,3] 64 | 65 | @test JuliaLowering.include_string(test_mod, """ 66 | let 67 | a = [] 68 | for i = 1:0 69 | push!(a, i) 70 | end 71 | a 72 | end 73 | """) == [] 74 | 75 | @test JuliaLowering.include_string(test_mod, """ 76 | let 77 | a = [] 78 | for _ = 1:3 79 | push!(a, 1) 80 | end 81 | a 82 | end 83 | """) == [1, 1, 1] 84 | 85 | # break 86 | @test JuliaLowering.include_string(test_mod, """ 87 | let 88 | a = [] 89 | for i = 1:6 90 | if i == 3 91 | break 92 | end 93 | push!(a, i) 94 | end 95 | a 96 | end 97 | """) == [1, 2] 98 | # Break from inner nested loop 99 | @test JuliaLowering.include_string(test_mod, """ 100 | let 101 | a = [] 102 | for i in 1:2 103 | for j in 3:4 104 | push!(a, (i, j)) 105 | j == 6 && break 106 | end 107 | end 108 | a 109 | end 110 | """) == [(1, 3), (1, 4), (2, 3), (2, 4)] 111 | 112 | # continue 113 | @test JuliaLowering.include_string(test_mod, """ 114 | let 115 | a = [] 116 | for i = 1:6 117 | if isodd(i) 118 | continue 119 | end 120 | push!(a, i) 121 | end 122 | a 123 | end 124 | """) == [2, 4, 6] 125 | 126 | # Loop variable scope 127 | @test JuliaLowering.include_string(test_mod, """ 128 | let 129 | a = [] 130 | for i = 1:3 131 | push!(a, i) 132 | i = 100 133 | end 134 | a 135 | end 136 | """) == [1,2,3] 137 | 138 | @test JuliaLowering.include_string(test_mod, """ 139 | let 140 | i = 100 141 | for i = 1:3 142 | end 143 | i 144 | end 145 | """) == 100 146 | 147 | @test JuliaLowering.include_string(test_mod, """ 148 | let 149 | i = 100 150 | for outer i = 1:2 151 | nothing 152 | end 153 | i 154 | end 155 | """) == 2 156 | 157 | # Fancy for loop left hand side - unpacking and scoping 158 | @test JuliaLowering.include_string(test_mod, """ 159 | let 160 | a = [] 161 | i = 100 162 | j = 200 163 | for (i,j) in [('a', 'b'), (1,2)] 164 | push!(a, (i,j)) 165 | end 166 | (a, i, j) 167 | end 168 | """) == ([('a', 'b'), (1,2)], 100, 200) 169 | 170 | end 171 | 172 | 173 | @testset "multidimensional for loops" begin 174 | 175 | test_mod = Module() 176 | 177 | @test JuliaLowering.include_string(test_mod, """ 178 | let 179 | a = [] 180 | for i = 1:2, j = 3:4 181 | push!(a, (i,j)) 182 | end 183 | a 184 | end 185 | """) == [(1,3), (1,4), (2,3), (2,4)] 186 | 187 | @testset "break/continue" begin 188 | 189 | @test JuliaLowering.include_string(test_mod, """ 190 | let 191 | a = [] 192 | for i = 1:2, j = 3:4 193 | push!(a, (i,j)) 194 | break 195 | end 196 | a 197 | end 198 | """) == [(1,3)] 199 | 200 | @test JuliaLowering.include_string(test_mod, """ 201 | let 202 | a = [] 203 | for i = 1:4, j = 3:4 204 | if isodd(i) 205 | continue 206 | end 207 | push!(a, (i,j)) 208 | end 209 | a 210 | end 211 | """) == [(2,3), (2,4), (4,3), (4,4)] 212 | 213 | @test JuliaLowering.include_string(test_mod, """ 214 | let 215 | a = [] 216 | for i = 1:2, j = 1:4 217 | if isodd(j) 218 | continue 219 | end 220 | push!(a, (i,j)) 221 | end 222 | a 223 | end 224 | """) == [(1,2), (1,4), (2,2), (2,4)] 225 | 226 | 227 | end 228 | 229 | 230 | @testset "Loop variable scope" begin 231 | 232 | # Test that `i` is copied in the inner loop 233 | @test JuliaLowering.include_string(test_mod, """ 234 | let 235 | a = [] 236 | for i = 1:2, j = 3:4 237 | push!(a, (i,j)) 238 | i = 100 239 | end 240 | a 241 | end 242 | """) == [(1,3), (1,4), (2,3), (2,4)] 243 | 244 | @test JuliaLowering.include_string(test_mod, """ 245 | let 246 | i = 100 247 | j = 200 248 | for i = 1:2, j = 3:4 249 | nothing 250 | end 251 | (i,j) 252 | end 253 | """) == (100,200) 254 | 255 | @test JuliaLowering.include_string(test_mod, """ 256 | let 257 | i = 100 258 | j = 200 259 | for outer i = 1:2, j = 3:4 260 | nothing 261 | end 262 | (i,j) 263 | end 264 | """) == (2,200) 265 | 266 | @test JuliaLowering.include_string(test_mod, """ 267 | let 268 | i = 100 269 | j = 200 270 | for i = 1:2, outer j = 3:4 271 | nothing 272 | end 273 | (i,j) 274 | end 275 | """) == (100,4) 276 | 277 | end 278 | 279 | end 280 | -------------------------------------------------------------------------------- /test/macros_ir.jl: -------------------------------------------------------------------------------- 1 | module MacroMethods 2 | macro some_macro() 3 | quote 4 | some_global 5 | end 6 | end 7 | 8 | module ExtraMacroMethods 9 | using ..MacroMethods 10 | macro MacroMethods.some_macro(ex) 11 | quote 12 | some_global 13 | end 14 | end 15 | end 16 | end 17 | 18 | macro strmac_str(ex, suff=nothing) 19 | s = "$(ex[1].value) from strmac" 20 | if !isnothing(suff) 21 | s = "$s with suffix $(suff.value)" 22 | end 23 | s 24 | end 25 | 26 | macro cmdmac_cmd(ex, suff=nothing) 27 | s = "$(ex[1].value) from cmdmac" 28 | if !isnothing(suff) 29 | s = "$s with suffix $(suff.value)" 30 | end 31 | s 32 | end 33 | 34 | #******************************************************************************* 35 | ######################################## 36 | # Simple macro 37 | macro add_one(ex) 38 | quote 39 | $ex + 1 40 | end 41 | end 42 | #--------------------- 43 | 1 (method TestMod.@add_one) 44 | 2 latestworld 45 | 3 TestMod.@add_one 46 | 4 (call core.Typeof %₃) 47 | 5 (call core.svec %₄ JuliaLowering.MacroContext core.Any) 48 | 6 (call core.svec) 49 | 7 SourceLocation::1:7 50 | 8 (call core.svec %₅ %₆ %₇) 51 | 9 --- method core.nothing %₈ 52 | slots: [slot₁/#self#(!read) slot₂/__context__(!read) slot₃/ex] 53 | 1 (call core.tuple slot₃/ex) 54 | 2 (call JuliaLowering.interpolate_ast SyntaxTree (inert (block (call-i ($ ex) + 1))) %₁) 55 | 3 (return %₂) 56 | 10 latestworld 57 | 11 TestMod.@add_one 58 | 12 (return %₁₁) 59 | 60 | ######################################## 61 | # Macro using `__context__` 62 | macro foo(ex) 63 | ctx = __context__ 64 | end 65 | #--------------------- 66 | 1 (method TestMod.@foo) 67 | 2 latestworld 68 | 3 TestMod.@foo 69 | 4 (call core.Typeof %₃) 70 | 5 (call core.svec %₄ JuliaLowering.MacroContext core.Any) 71 | 6 (call core.svec) 72 | 7 SourceLocation::1:7 73 | 8 (call core.svec %₅ %₆ %₇) 74 | 9 --- method core.nothing %₈ 75 | slots: [slot₁/#self#(!read) slot₂/__context__ slot₃/ex(!read) slot₄/ctx(!read)] 76 | 1 slot₂/__context__ 77 | 2 (= slot₄/ctx %₁) 78 | 3 (return %₁) 79 | 10 latestworld 80 | 11 TestMod.@foo 81 | 12 (return %₁₁) 82 | 83 | ######################################## 84 | # Scope for symbols emitted by macros is the module where the method was 85 | # defined, thus two different modules in this case, even though `@some_macro` 86 | # belongs to the MacroMethods module. 87 | (MacroMethods.@some_macro(), MacroMethods.@some_macro(unused)) 88 | #--------------------- 89 | 1 TestMod.MacroMethods.some_global 90 | 2 TestMod.MacroMethods.ExtraMacroMethods.some_global 91 | 3 (call core.tuple %₁ %₂) 92 | 4 (return %₃) 93 | 94 | ######################################## 95 | # Error: Macro with kw args 96 | macro mmm(a; b=2) 97 | end 98 | #--------------------- 99 | LoweringError: 100 | macro mmm(a; b=2) 101 | # └───┘ ── macros cannot accept keyword arguments 102 | end 103 | 104 | ######################################## 105 | # Error: Bad macro name 106 | macro mmm[](ex) 107 | end 108 | #--------------------- 109 | LoweringError: 110 | macro mmm[](ex) 111 | # └───┘ ── invalid macro name 112 | end 113 | 114 | ######################################## 115 | # Error: Macros not allowed in local scope 116 | let 117 | macro foo(ex) 118 | end 119 | end 120 | #--------------------- 121 | LoweringError: 122 | let 123 | # ┌──────────── 124 | macro foo(ex) 125 | end 126 | #─────┘ ── macro is only allowed in global scope 127 | end 128 | 129 | ######################################## 130 | # Error: Macros not allowed in local scope 131 | function f() 132 | macro foo() 133 | end 134 | end 135 | #--------------------- 136 | LoweringError: 137 | function f() 138 | # ┌────────── 139 | macro foo() 140 | end 141 | #─────┘ ── macro is only allowed in global scope 142 | end 143 | 144 | ######################################## 145 | # Error: Macros not found 146 | _never_exist = @m_not_exist 42 147 | #--------------------- 148 | MacroExpansionError while expanding @m_not_exist in module Main.TestMod: 149 | _never_exist = @m_not_exist 42 150 | # └──────────┘ ── Macro not found 151 | Caused by: 152 | UndefVarError: `@m_not_exist` not defined in `Main.TestMod` 153 | Suggestion: check for spelling errors or missing imports. 154 | 155 | ######################################## 156 | # Simple cmdstring 157 | `echo 1` 158 | #--------------------- 159 | 1 Base.cmd_gen 160 | 2 (call core.tuple "echo") 161 | 3 (call core.tuple "1") 162 | 4 (call core.tuple %₂ %₃) 163 | 5 (call %₁ %₄) 164 | 6 (return %₅) 165 | 166 | ######################################## 167 | # Simple string macro 168 | strmac"hello" 169 | #--------------------- 170 | 1 (return "hello from strmac") 171 | 172 | ######################################## 173 | # String macro with suffix 174 | strmac"hello"blah 175 | #--------------------- 176 | 1 (return "hello from strmac with suffix blah") 177 | 178 | ######################################## 179 | # Simple cmd macro 180 | cmdmac`hello` 181 | #--------------------- 182 | 1 (return "hello from cmdmac") 183 | 184 | ######################################## 185 | # Cmd macro with suffix 186 | cmdmac`hello`12345 187 | #--------------------- 188 | 1 (return "hello from cmdmac with suffix 12345") 189 | 190 | ######################################## 191 | # @nospecialize (zero args) 192 | function foo() 193 | @nospecialize 194 | end 195 | #--------------------- 196 | 1 (method TestMod.foo) 197 | 2 latestworld 198 | 3 TestMod.foo 199 | 4 (call core.Typeof %₃) 200 | 5 (call core.svec %₄) 201 | 6 (call core.svec) 202 | 7 SourceLocation::1:10 203 | 8 (call core.svec %₅ %₆ %₇) 204 | 9 --- method core.nothing %₈ 205 | slots: [slot₁/#self#(!read)] 206 | 1 (meta :nospecialize) 207 | 2 (return core.nothing) 208 | 10 latestworld 209 | 11 TestMod.foo 210 | 12 (return %₁₁) 211 | -------------------------------------------------------------------------------- /test/branching.jl: -------------------------------------------------------------------------------- 1 | # Branching 2 | 3 | @testset "branching" begin 4 | 5 | test_mod = Module() 6 | 7 | #------------------------------------------------------------------------------- 8 | @testset "Tail position" begin 9 | 10 | @test JuliaLowering.include_string(test_mod, """ 11 | let a = true 12 | if a 13 | 1 14 | end 15 | end 16 | """) === 1 17 | 18 | @test JuliaLowering.include_string(test_mod, """ 19 | let a = false 20 | if a 21 | 1 22 | end 23 | end 24 | """) === nothing 25 | 26 | @test JuliaLowering.include_string(test_mod, """ 27 | let a = true 28 | if a 29 | 1 30 | else 31 | 2 32 | end 33 | end 34 | """) === 1 35 | 36 | @test JuliaLowering.include_string(test_mod, """ 37 | let a = false 38 | if a 39 | 1 40 | else 41 | 2 42 | end 43 | end 44 | """) === 2 45 | 46 | @test JuliaLowering.include_string(test_mod, """ 47 | let a = false, b = true 48 | if a 49 | 1 50 | elseif b 51 | 2 52 | else 53 | 3 54 | end 55 | end 56 | """) === 2 57 | 58 | @test JuliaLowering.include_string(test_mod, """ 59 | let a = false, b = false 60 | if a 61 | 1 62 | elseif b 63 | 2 64 | else 65 | 3 66 | end 67 | end 68 | """) === 3 69 | 70 | end 71 | 72 | #------------------------------------------------------------------------------- 73 | @testset "Value required but not tail position" begin 74 | 75 | @test JuliaLowering.include_string(test_mod, """ 76 | let a = true 77 | x = if a 78 | 1 79 | end 80 | x 81 | end 82 | """) === 1 83 | 84 | @test JuliaLowering.include_string(test_mod, """ 85 | let a = false 86 | x = if a 87 | 1 88 | end 89 | x 90 | end 91 | """) === nothing 92 | 93 | @test JuliaLowering.include_string(test_mod, """ 94 | let a = true 95 | x = if a 96 | 1 97 | else 98 | 2 99 | end 100 | x 101 | end 102 | """) === 1 103 | 104 | @test JuliaLowering.include_string(test_mod, """ 105 | let a = false 106 | x = if a 107 | 1 108 | else 109 | 2 110 | end 111 | x 112 | end 113 | """) === 2 114 | 115 | @test JuliaLowering.include_string(test_mod, """ 116 | let a = false, b = true 117 | x = if a 118 | 1 119 | elseif b 120 | 2 121 | else 122 | 3 123 | end 124 | x 125 | end 126 | """) === 2 127 | 128 | @test JuliaLowering.include_string(test_mod, """ 129 | let a = false, b = false 130 | x = if a 131 | 1 132 | elseif b 133 | 2 134 | else 135 | 3 136 | end 137 | x 138 | end 139 | """) === 3 140 | 141 | end 142 | 143 | #------------------------------------------------------------------------------- 144 | @testset "Side effects (not value or tail position)" begin 145 | 146 | @test JuliaLowering.include_string(test_mod, """ 147 | let a = true 148 | x = nothing 149 | if a 150 | x = 1 151 | end 152 | x 153 | end 154 | """) === 1 155 | 156 | @test JuliaLowering.include_string(test_mod, """ 157 | let a = false 158 | x = nothing 159 | if a 160 | x = 1 161 | end 162 | x 163 | end 164 | """) === nothing 165 | 166 | @test JuliaLowering.include_string(test_mod, """ 167 | let a = true 168 | x = nothing 169 | if a 170 | x = 1 171 | else 172 | x = 2 173 | end 174 | x 175 | end 176 | """) === 1 177 | 178 | @test JuliaLowering.include_string(test_mod, """ 179 | let a = false 180 | x = nothing 181 | if a 182 | x = 1 183 | else 184 | x = 2 185 | end 186 | x 187 | end 188 | """) === 2 189 | 190 | @test JuliaLowering.include_string(test_mod, """ 191 | let a = false, b = true 192 | x = nothing 193 | if a 194 | x = 1 195 | elseif b 196 | x = 2 197 | else 198 | x = 3 199 | end 200 | x 201 | end 202 | """) === 2 203 | 204 | @test JuliaLowering.include_string(test_mod, """ 205 | let a = false, b = false 206 | x = nothing 207 | if a 208 | x = 1 209 | elseif b 210 | x = 2 211 | else 212 | x = 3 213 | end 214 | x 215 | end 216 | """) === 3 217 | 218 | end 219 | #------------------------------------------------------------------------------- 220 | # Block condition 221 | @test JuliaLowering.include_string(test_mod, """ 222 | let a = true 223 | if begin; x = 2; a; end 224 | x 225 | end 226 | end 227 | """) === 2 228 | 229 | #------------------------------------------------------------------------------- 230 | @testset "`&&` and `||` chains" begin 231 | 232 | @test JuliaLowering.include_string(test_mod, """ 233 | true && "hi" 234 | """) == "hi" 235 | 236 | @test JuliaLowering.include_string(test_mod, """ 237 | true && true && "hi" 238 | """) == "hi" 239 | 240 | @test JuliaLowering.include_string(test_mod, """ 241 | false && "hi" 242 | """) == false 243 | 244 | @test JuliaLowering.include_string(test_mod, """ 245 | true && false && "hi" 246 | """) == false 247 | 248 | @test JuliaLowering.include_string(test_mod, """ 249 | begin 250 | z = true && "hi" 251 | z 252 | end 253 | """) == "hi" 254 | 255 | @test JuliaLowering.include_string(test_mod, """ 256 | begin 257 | z = false && "hi" 258 | z 259 | end 260 | """) == false 261 | 262 | 263 | @test JuliaLowering.include_string(test_mod, """ 264 | true || "hi" 265 | """) == true 266 | 267 | @test JuliaLowering.include_string(test_mod, """ 268 | true || true || "hi" 269 | """) == true 270 | 271 | @test JuliaLowering.include_string(test_mod, """ 272 | false || "hi" 273 | """) == "hi" 274 | 275 | @test JuliaLowering.include_string(test_mod, """ 276 | false || true || "hi" 277 | """) == true 278 | 279 | @test JuliaLowering.include_string(test_mod, """ 280 | false || false || "hi" 281 | """) == "hi" 282 | 283 | @test JuliaLowering.include_string(test_mod, """ 284 | begin 285 | z = false || "hi" 286 | z 287 | end 288 | """) == "hi" 289 | 290 | @test JuliaLowering.include_string(test_mod, """ 291 | begin 292 | z = true || "hi" 293 | z 294 | end 295 | """) == true 296 | 297 | end 298 | 299 | @testset "symbolic goto/label" begin 300 | 301 | JuliaLowering.include_string(test_mod, """ 302 | let 303 | a = [] 304 | i = 1 305 | @label foo 306 | push!(a, i) 307 | i = i + 1 308 | if i <= 2 309 | @goto foo 310 | end 311 | a 312 | end 313 | """) == [1,2] 314 | 315 | end 316 | 317 | end 318 | -------------------------------------------------------------------------------- /test/closures.jl: -------------------------------------------------------------------------------- 1 | @testset "Closures" begin 2 | 3 | test_mod = Module() 4 | 5 | # Capture assigned before closure 6 | @test JuliaLowering.include_string(test_mod, """ 7 | let 8 | x = 1 9 | f(y) = x+y 10 | f(2), f(3) 11 | end 12 | """) == (3,4) 13 | 14 | # Capture assigned after closure 15 | @test JuliaLowering.include_string(test_mod, """ 16 | let 17 | f(y) = x+y 18 | x = 1 19 | f(2) 20 | end 21 | """) == 3 22 | 23 | # Capture assigned inside closure 24 | @test JuliaLowering.include_string(test_mod, """ 25 | let 26 | x = 1 27 | function f(y) 28 | x = y 29 | end 30 | f(100) 31 | x 32 | end 33 | """) == 100 34 | 35 | Base.eval(test_mod, :(call_it(f, args...) = f(args...))) 36 | 37 | # Closure where a local `x` is captured but not boxed 38 | @test JuliaLowering.include_string(test_mod, """ 39 | begin 40 | function f_unboxed_test(x) 41 | z = 0 42 | function g() 43 | y = x # x will not be boxed 44 | (y + 1, z) 45 | end 46 | z = 2 # will be boxed 47 | (x, g()) 48 | end 49 | f_unboxed_test(10) 50 | end 51 | """) == (10,(11,2)) 52 | 53 | # Use of isdefined 54 | @test JuliaLowering.include_string(test_mod, """ 55 | begin 56 | function f_isdefined(x) 57 | local w 58 | function g() 59 | z = 3 60 | (@isdefined(x), # unboxed, always defined capture 61 | @isdefined(y), # boxed capture 62 | @isdefined(z), # normal local var 63 | @isdefined(w)) # boxed undefined var 64 | end 65 | y = 2 66 | (@isdefined(y), @isdefined(w), g()) 67 | end 68 | f_isdefined(1) 69 | end 70 | """) == (true, false, (true, true, true, false)) 71 | 72 | # Mutually recursive closures (closure capturing a closure) 73 | @test JuliaLowering.include_string(test_mod, """ 74 | let 75 | function recursive_a(n) 76 | here = (:a, n) 77 | n <= 0 ? here : (here, recursive_b(n-1)) 78 | end 79 | function recursive_b(n) 80 | ((:b, n), recursive_a(n-1)) 81 | end 82 | recursive_a(2) 83 | end 84 | """) == ((:a, 2), ((:b, 1), (:a, 0))) 85 | 86 | # Global method capturing local variables 87 | JuliaLowering.include_string(test_mod, """ 88 | begin 89 | local x = 1 90 | function f_global_method_capturing_local() 91 | x = x + 1 92 | end 93 | end 94 | """) 95 | @test test_mod.f_global_method_capturing_local() == 2 96 | @test test_mod.f_global_method_capturing_local() == 3 97 | 98 | # Closure with multiple methods depending on local variables 99 | f_closure_local_var_types = JuliaLowering.include_string(test_mod, """ 100 | let T=Int, S=Float64 101 | function f_closure_local_var_types(::T) 102 | 1 103 | end 104 | function f_closure_local_var_types(::S) 105 | 1.0 106 | end 107 | end 108 | """) 109 | @test f_closure_local_var_types(2) == 1 110 | @test f_closure_local_var_types(2.0) == 1.0 111 | @test_throws MethodError f_closure_local_var_types("hi") 112 | 113 | # Multiply nested closures. In this case g_nest needs to capture `x` in order 114 | # to construct an instance of `h_nest()` inside it. 115 | @test JuliaLowering.include_string(test_mod, """ 116 | begin 117 | function f_nest(x) 118 | function g_nest(y) 119 | function h_nest(z) 120 | (x,y,z) 121 | end 122 | end 123 | end 124 | 125 | f_nest(1)(2)(3) 126 | end 127 | """) === (1,2,3) 128 | 129 | # Closure with return type must capture the return type 130 | @test JuliaLowering.include_string(test_mod, """ 131 | let T = Int 132 | function f_captured_return_type()::T 133 | 2.0 134 | end 135 | f_captured_return_type() 136 | end 137 | """) === 2 138 | 139 | # Capturing a typed local 140 | @test JuliaLowering.include_string(test_mod, """ 141 | let T = Int 142 | x::T = 1.0 143 | function f_captured_typed_local() 144 | x = 2.0 145 | end 146 | f_captured_typed_local() 147 | x 148 | end 149 | """) === 2 150 | 151 | # Capturing a typed local where the type is a nontrivial expression 152 | @test begin 153 | res = JuliaLowering.include_string(test_mod, """ 154 | let T = Int, V=Vector 155 | x::V{T} = [1,2] 156 | function f_captured_typed_local_composite() 157 | x = [100.0, 200.0] 158 | end 159 | f_captured_typed_local_composite() 160 | x 161 | end 162 | """) 163 | res == [100, 200] && eltype(res) == Int 164 | end 165 | 166 | # Evil case where we mutate `T` which is the type of `x`, such that x is 167 | # eventually set to a Float64. 168 | # 169 | # Completely dynamic types for variables should be disallowed somehow?? For 170 | # example, by emitting the expression computing the type of `x` alongside the 171 | # newvar node. However, for now we verify that this potentially evil behavior 172 | # is compatible with the existing implementation :) 173 | @test JuliaLowering.include_string(test_mod, """ 174 | let T = Int 175 | x::T = 1.0 176 | function f_captured_mutating_typed_local() 177 | x = 2 178 | end 179 | T = Float64 180 | f_captured_mutating_typed_local() 181 | x 182 | end 183 | """) === 2.0 184 | 185 | # Anon function syntax 186 | @test JuliaLowering.include_string(test_mod, """ 187 | begin 188 | local y = 2 189 | call_it(x->x+y, 3) 190 | end 191 | """) == 5 192 | 193 | # Anon function syntax with `where` 194 | @test JuliaLowering.include_string(test_mod, """ 195 | begin 196 | local y = 2 197 | call_it((x::T where {T<:Integer})->x+y, 3) 198 | end 199 | """) == 5 200 | 201 | # Do block syntax 202 | @test JuliaLowering.include_string(test_mod, """ 203 | begin 204 | local y = 2 205 | call_it(3) do x 206 | x + y 207 | end 208 | end 209 | """) == 5 210 | 211 | # Attempt to reference capture which is not assigned 212 | @test_throws UndefVarError(:x, :local) JuliaLowering.include_string(test_mod, """ 213 | let 214 | function f() 215 | x 216 | end 217 | f() 218 | x = 1 219 | end 220 | """) 221 | 222 | # Opaque closure 223 | @test JuliaLowering.include_string(test_mod, """ 224 | let y = 1 225 | oc = Base.Experimental.@opaque x->2x + y 226 | oc(3) 227 | end 228 | """) == 7 229 | 230 | # Opaque closure with `...` 231 | @test JuliaLowering.include_string(test_mod, """ 232 | let 233 | oc = Base.Experimental.@opaque (xs...)->xs 234 | oc(3,4,5) 235 | end 236 | """) == (3,4,5) 237 | 238 | # opaque_closure_method internals 239 | method_ex = lower_str(test_mod, "Base.Experimental.@opaque x -> 2x").args[1].code[3] 240 | @test method_ex.head === :opaque_closure_method 241 | @test method_ex.args[1] === nothing 242 | @test method_ex.args[4] isa LineNumberNode 243 | 244 | end 245 | -------------------------------------------------------------------------------- /src/utils.jl: -------------------------------------------------------------------------------- 1 | # Error handling 2 | 3 | TODO(msg::AbstractString) = throw(ErrorException("Lowering TODO: $msg")) 4 | TODO(ex::SyntaxTree, msg="") = throw(LoweringError(ex, "Lowering TODO: $msg")) 5 | 6 | # Errors found during lowering will result in LoweringError being thrown to 7 | # indicate the syntax causing the error. 8 | struct LoweringError <: Exception 9 | ex::SyntaxTree 10 | msg::String 11 | end 12 | 13 | function Base.showerror(io::IO, exc::LoweringError; show_detail=true) 14 | print(io, "LoweringError:\n") 15 | src = sourceref(exc.ex) 16 | highlight(io, src; note=exc.msg) 17 | 18 | if show_detail 19 | print(io, "\n\nDetailed provenance:\n") 20 | showprov(io, exc.ex, tree=true) 21 | end 22 | end 23 | 24 | #------------------------------------------------------------------------------- 25 | function _show_provtree(io::IO, ex::SyntaxTree, indent) 26 | print(io, ex, "\n") 27 | prov = provenance(ex) 28 | for (i, e) in enumerate(prov) 29 | islast = i == length(prov) 30 | printstyled(io, "$indent$(islast ? "└─ " : "├─ ")", color=:light_black) 31 | inner_indent = indent * (islast ? " " : "│ ") 32 | _show_provtree(io, e, inner_indent) 33 | end 34 | end 35 | 36 | function _show_provtree(io::IO, prov, indent) 37 | fn = filename(prov) 38 | line, _ = source_location(prov) 39 | printstyled(io, "@ $fn:$line\n", color=:light_black) 40 | end 41 | 42 | function showprov(io::IO, exs::AbstractVector; 43 | note=nothing, include_location::Bool=true, highlight_kwargs...) 44 | for (i,ex) in enumerate(Iterators.reverse(exs)) 45 | sr = sourceref(ex) 46 | if i > 1 47 | print(io, "\n\n") 48 | end 49 | k = kind(ex) 50 | ex_note = !isnothing(note) ? note : 51 | i > 1 && k == K"macrocall" ? "in macro expansion" : 52 | i > 1 && k == K"$" ? "interpolated here" : 53 | "in source" 54 | highlight(io, sr; note=ex_note, highlight_kwargs...) 55 | 56 | if include_location 57 | line, _ = source_location(sr) 58 | locstr = "$(filename(sr)):$line" 59 | JuliaSyntax._printstyled(io, "\n# @ $locstr", fgcolor=:light_black) 60 | end 61 | end 62 | end 63 | 64 | function showprov(io::IO, ex::SyntaxTree; tree::Bool=false, showprov_kwargs...) 65 | if tree 66 | _show_provtree(io, ex, "") 67 | else 68 | showprov(io, flattened_provenance(ex); showprov_kwargs...) 69 | end 70 | end 71 | 72 | function showprov(x; kws...) 73 | showprov(stdout, x; kws...) 74 | end 75 | 76 | function subscript_str(i) 77 | replace(string(i), 78 | "0"=>"₀", "1"=>"₁", "2"=>"₂", "3"=>"₃", "4"=>"₄", 79 | "5"=>"₅", "6"=>"₆", "7"=>"₇", "8"=>"₈", "9"=>"₉") 80 | end 81 | 82 | function _deref_ssa(stmts, ex) 83 | while kind(ex) == K"SSAValue" 84 | ex = stmts[ex.var_id] 85 | end 86 | ex 87 | end 88 | 89 | function _find_method_lambda(ex, name) 90 | @assert kind(ex) == K"code_info" 91 | # Heuristic search through outer thunk for the method in question. 92 | method_found = false 93 | stmts = children(ex[1]) 94 | for e in stmts 95 | if kind(e) == K"method" && numchildren(e) >= 2 96 | sig = _deref_ssa(stmts, e[2]) 97 | @assert kind(sig) == K"call" 98 | arg_types = _deref_ssa(stmts, sig[2]) 99 | @assert kind(arg_types) == K"call" 100 | self_type = _deref_ssa(stmts, arg_types[2]) 101 | if kind(self_type) == K"globalref" && occursin(name, self_type.name_val) 102 | return e[3] 103 | end 104 | end 105 | end 106 | end 107 | 108 | function print_ir(io::IO, ex, method_filter=nothing) 109 | @assert kind(ex) == K"code_info" 110 | if !isnothing(method_filter) 111 | filtered = _find_method_lambda(ex, method_filter) 112 | if isnothing(filtered) 113 | @warn "Method not found with method filter $method_filter" 114 | else 115 | ex = filtered 116 | end 117 | end 118 | _print_ir(io, ex, "") 119 | end 120 | 121 | function _print_ir(io::IO, ex, indent) 122 | added_indent = " " 123 | @assert (kind(ex) == K"lambda" || kind(ex) == K"code_info") && kind(ex[1]) == K"block" 124 | if !ex.is_toplevel_thunk && kind(ex) == K"code_info" 125 | slots = ex.slots 126 | print(io, indent, "slots: [") 127 | for (i,slot) in enumerate(slots) 128 | print(io, "slot$(subscript_str(i))/$(slot.name)") 129 | flags = String[] 130 | slot.is_nospecialize && push!(flags, "nospecialize") 131 | !slot.is_read && push!(flags, "!read") 132 | slot.is_single_assign && push!(flags, "single_assign") 133 | slot.is_maybe_undef && push!(flags, "maybe_undef") 134 | slot.is_called && push!(flags, "called") 135 | if !isempty(flags) 136 | print(io, "($(join(flags, ",")))") 137 | end 138 | if i < length(slots) 139 | print(io, " ") 140 | end 141 | end 142 | println(io, "]") 143 | end 144 | stmts = children(ex[1]) 145 | for (i, e) in enumerate(stmts) 146 | lno = rpad(i, 3) 147 | if kind(e) == K"method" && numchildren(e) == 3 148 | print(io, indent, lno, " --- method ", string(e[1]), " ", string(e[2])) 149 | if kind(e[3]) == K"lambda" || kind(e[3]) == K"code_info" 150 | println(io) 151 | _print_ir(io, e[3], indent*added_indent) 152 | else 153 | println(io, " ", string(e[3])) 154 | end 155 | elseif kind(e) == K"opaque_closure_method" 156 | @assert numchildren(e) == 5 157 | print(io, indent, lno, " --- opaque_closure_method ") 158 | for i=1:4 159 | print(io, " ", e[i]) 160 | end 161 | println(io) 162 | _print_ir(io, e[5], indent*added_indent) 163 | elseif kind(e) == K"code_info" 164 | println(io, indent, lno, " --- ", e.is_toplevel_thunk ? "thunk" : "code_info") 165 | _print_ir(io, e, indent*added_indent) 166 | else 167 | code = string(e) 168 | println(io, indent, lno, " ", code) 169 | end 170 | end 171 | end 172 | 173 | # Wrap a function body in Base.Compiler.@zone for profiling 174 | if isdefined(Base.Compiler, Symbol("@zone")) 175 | macro fzone(str, f) 176 | @assert f isa Expr && f.head === :function && length(f.args) === 2 && str isa String 177 | esc(Expr(:function, f.args[1], 178 | # Use source of our caller, not of this macro. 179 | Expr(:macrocall, :(Base.Compiler.var"@zone"), __source__, str, f.args[2]))) 180 | end 181 | else 182 | macro fzone(str, f) 183 | esc(f) 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /test/exceptions.jl: -------------------------------------------------------------------------------- 1 | @testset "try/catch" begin 2 | 3 | test_mod = Module() 4 | 5 | @test isempty(current_exceptions()) 6 | 7 | @testset "tail position" begin 8 | 9 | @test JuliaLowering.include_string(test_mod, """ 10 | try 11 | 1 12 | catch 13 | 2 14 | end 15 | """) == 1 16 | 17 | @test JuliaLowering.include_string(test_mod, """ 18 | try 19 | error("hi") 20 | 1 21 | catch 22 | 2 23 | end 24 | """) == 2 25 | 26 | @test JuliaLowering.include_string(test_mod, """ 27 | try 28 | error("hi") 29 | catch exc 30 | exc 31 | end 32 | """) == ErrorException("hi") 33 | 34 | 35 | @test JuliaLowering.include_string(test_mod, """ 36 | try 37 | 1 38 | catch 39 | 2 40 | else 41 | 3 42 | end 43 | """) == 3 44 | 45 | @test JuliaLowering.include_string(test_mod, """ 46 | try 47 | error("hi") 48 | 1 49 | catch 50 | 2 51 | else 52 | 3 53 | end 54 | """) == 2 55 | 56 | @test JuliaLowering.include_string(test_mod, """ 57 | begin 58 | function f() 59 | try 60 | return 1 61 | catch 62 | end 63 | return 2 64 | end 65 | f() 66 | end 67 | """) == 1 68 | 69 | @test JuliaLowering.include_string(test_mod, """ 70 | begin 71 | function g() 72 | try 73 | return 1 74 | catch 75 | end 76 | end 77 | g() 78 | end 79 | """) == 1 80 | 81 | @test JuliaLowering.include_string(test_mod, """ 82 | let x = -1 83 | while true 84 | try 85 | error("hi") 86 | catch 87 | x = 2 88 | break 89 | end 90 | end 91 | x 92 | end 93 | """) == 2 94 | 95 | @test JuliaLowering.include_string(test_mod, """ 96 | let x = -1 97 | while true 98 | try 99 | x = 2 100 | break 101 | catch 102 | end 103 | end 104 | x 105 | end 106 | """) == 2 107 | end 108 | 109 | @testset "value position" begin 110 | 111 | @test JuliaLowering.include_string(test_mod, """ 112 | let 113 | x = try 114 | 1 115 | catch 116 | 2 117 | end 118 | x 119 | end 120 | """) == 1 121 | 122 | @test JuliaLowering.include_string(test_mod, """ 123 | let 124 | x = try 125 | error("hi") 126 | 1 127 | catch 128 | 2 129 | end 130 | x 131 | end 132 | """) == 2 133 | 134 | @test JuliaLowering.include_string(test_mod, """ 135 | let 136 | x = try 137 | error("hi") 138 | catch exc 139 | exc 140 | end 141 | x 142 | end 143 | """) == ErrorException("hi") 144 | 145 | 146 | @test JuliaLowering.include_string(test_mod, """ 147 | let 148 | x = try 149 | 1 150 | catch 151 | 2 152 | else 153 | 3 154 | end 155 | x 156 | end 157 | """) == 3 158 | 159 | @test JuliaLowering.include_string(test_mod, """ 160 | let 161 | x = try 162 | error("hi") 163 | 1 164 | catch 165 | 2 166 | else 167 | 3 168 | end 169 | x 170 | end 171 | """) == 2 172 | 173 | end 174 | 175 | @testset "not value/tail position" begin 176 | 177 | @test JuliaLowering.include_string(test_mod, """ 178 | let x = -1 179 | try 180 | x = 1 181 | catch 182 | x = 2 183 | end 184 | x 185 | end 186 | """) == 1 187 | 188 | @test JuliaLowering.include_string(test_mod, """ 189 | let x = -1 190 | try 191 | error("hi") 192 | x = 1 193 | catch 194 | x = 2 195 | end 196 | x 197 | end 198 | """) == 2 199 | 200 | @test JuliaLowering.include_string(test_mod, """ 201 | let x = -1 202 | try 203 | x = error("hi") 204 | catch exc 205 | x = exc 206 | end 207 | x 208 | end 209 | """) == ErrorException("hi") 210 | 211 | 212 | @test JuliaLowering.include_string(test_mod, """ 213 | let x = -1 214 | try 215 | x = 1 216 | catch 217 | x = 2 218 | else 219 | x = 3 220 | end 221 | x 222 | end 223 | """) == 3 224 | 225 | @test JuliaLowering.include_string(test_mod, """ 226 | let x = -1 227 | try 228 | error("hi") 229 | x = 1 230 | catch 231 | x = 2 232 | else 233 | x = 3 234 | end 235 | x 236 | end 237 | """) == 2 238 | 239 | end 240 | 241 | @testset "exception stack" begin 242 | 243 | @test JuliaLowering.include_string(test_mod, """ 244 | try 245 | try 246 | error("hi") 247 | catch 248 | error("ho") 249 | end 250 | catch 251 | a = [] 252 | for x in current_exceptions() 253 | push!(a, x.exception) 254 | end 255 | a 256 | end 257 | """) == [ErrorException("hi"), ErrorException("ho")] 258 | 259 | end 260 | 261 | @test isempty(current_exceptions()) 262 | 263 | end 264 | 265 | #------------------------------------------------------------------------------- 266 | @testset "try/finally" begin 267 | 268 | test_mod = Module() 269 | 270 | @test JuliaLowering.include_string(test_mod, """ 271 | let x = -1 272 | try 273 | x = 1 274 | finally 275 | x = 2 276 | end 277 | x 278 | end 279 | """) == 2 280 | 281 | @test JuliaLowering.include_string(test_mod, """ 282 | let x = -1 283 | try 284 | try 285 | error("hi") 286 | x = 1 287 | finally 288 | x = 2 289 | end 290 | catch 291 | end 292 | x 293 | end 294 | """) == 2 295 | 296 | JuliaLowering.include_string(test_mod, """ 297 | begin 298 | function nested_finally(a, x, b, c) 299 | try 300 | try 301 | if x 302 | return b 303 | end 304 | c 305 | finally 306 | push!(a, 1) 307 | end 308 | finally 309 | push!(a, 2) 310 | end 311 | end 312 | end 313 | """) 314 | @test (a = []; res = test_mod.nested_finally(a, true, 100, 200); (a, res)) == ([1,2], 100) 315 | @test (a = []; res = test_mod.nested_finally(a, false, 100, 200); (a, res)) == ([1,2], 200) 316 | 317 | @test JuliaLowering.include_string(test_mod, """ 318 | try 319 | 1 320 | catch 321 | 2 322 | finally 323 | 3 324 | end 325 | """) == 1 326 | 327 | @test JuliaLowering.include_string(test_mod, """ 328 | try 329 | error("hi") 330 | 1 331 | catch 332 | 2 333 | finally 334 | 3 335 | end 336 | """) == 2 337 | 338 | end 339 | -------------------------------------------------------------------------------- /test/quoting.jl: -------------------------------------------------------------------------------- 1 | @testset "Syntax quoting & interpolation" begin 2 | 3 | test_mod = Module() 4 | 5 | ex = JuliaLowering.include_string(test_mod, """ 6 | begin 7 | x = 10 8 | y = :(g(z)) 9 | quote 10 | f(\$(x+1), \$y) 11 | end 12 | end 13 | """) 14 | @test ex ≈ @ast_ [K"block" 15 | [K"call" 16 | "f"::K"Identifier" 17 | 11::K"Value" 18 | [K"call" 19 | "g"::K"Identifier" 20 | "z"::K"Identifier" 21 | ] 22 | ] 23 | ] 24 | @test sourcetext(ex[1]) == "f(\$(x+1), \$y)" 25 | @test sourcetext(ex[1][2]) == "\$(x+1)" 26 | @test sourcetext.(flattened_provenance(ex[1][3])) == ["\$y", "g(z)"] 27 | @test sprint(io->showprov(io, ex[1][3], tree=true)) == raw""" 28 | (call g z) 29 | ├─ (call g z) 30 | │ └─ (call g z) 31 | │ └─ @ string:3 32 | └─ ($ y) 33 | └─ @ string:5 34 | """ 35 | @test sprint(io->showprov(io, ex[1][3])) == raw""" 36 | begin 37 | x = 10 38 | y = :(g(z)) 39 | # └──┘ ── in source 40 | quote 41 | f($(x+1), $y) 42 | # @ string:3 43 | 44 | y = :(g(z)) 45 | quote 46 | f($(x+1), $y) 47 | # └┘ ── interpolated here 48 | end 49 | end 50 | # @ string:5""" 51 | @test sprint(io->showprov(io, ex[1][3]; note="foo")) == raw""" 52 | begin 53 | x = 10 54 | y = :(g(z)) 55 | # └──┘ ── foo 56 | quote 57 | f($(x+1), $y) 58 | # @ string:3 59 | 60 | y = :(g(z)) 61 | quote 62 | f($(x+1), $y) 63 | # └┘ ── foo 64 | end 65 | end 66 | # @ string:5""" 67 | 68 | 69 | # Test expression flags are preserved during interpolation 70 | @test JuliaSyntax.is_infix_op_call(JuliaLowering.include_string(test_mod, """ 71 | let 72 | x = 1 73 | :(\$x + \$x) 74 | end 75 | """)) 76 | 77 | # Test that trivial interpolation without any nesting works. 78 | ex = JuliaLowering.include_string(test_mod, """ 79 | let 80 | x = 123 81 | :(\$x) 82 | end 83 | """) 84 | @test kind(ex) == K"Value" 85 | @test ex.value == 123 86 | 87 | # Test that interpolation with field access works 88 | # (the field name can be interpolated into 89 | ex = JuliaLowering.include_string(test_mod, """ 90 | let 91 | field_name = :(a) 92 | :(x.\$field_name) 93 | end 94 | """) 95 | @test kind(ex[2]) == K"Identifier" 96 | @test ex[2].name_val == "a" 97 | 98 | # Test quoted property access syntax like `Core.:(foo)` and `Core.:(!==)` 99 | @test JuliaLowering.include_string(test_mod, """ 100 | x = (a=1, b=2) 101 | x.:(a) 102 | """) == 1 103 | @test JuliaLowering.include_string(test_mod, """ 104 | Core.:(!==) 105 | """) === (!==) 106 | 107 | # Test quoted operator function definitions (issue #20) 108 | @test JuliaLowering.include_string(test_mod, """ 109 | begin 110 | struct Issue20 111 | x::Int 112 | end 113 | Base.:(==)(a::Issue20, b::Issue20) = a.x == b.x 114 | Issue20(1) == Issue20(1) 115 | end 116 | """) === true 117 | 118 | @test JuliaLowering.include_string(test_mod, """ 119 | begin 120 | Base.:(<)(a::Issue20, b::Issue20) = a.x < b.x 121 | Issue20(1) < Issue20(2) 122 | end 123 | """) === true 124 | 125 | # interpolations at multiple depths 126 | ex = JuliaLowering.include_string(test_mod, raw""" 127 | let 128 | args = (:(x),:(y)) 129 | quote 130 | x = 1 131 | y = 2 132 | quote 133 | f($$(args...)) 134 | end 135 | end 136 | end 137 | """) 138 | @test ex ≈ @ast_ [K"block" 139 | [K"=" 140 | "x"::K"Identifier" 141 | 1::K"Integer" 142 | ] 143 | [K"=" 144 | "y"::K"Identifier" 145 | 2::K"Integer" 146 | ] 147 | [K"quote" 148 | [K"block" 149 | [K"call" 150 | "f"::K"Identifier" 151 | [K"$" 152 | "x"::K"Identifier" 153 | "y"::K"Identifier" 154 | ] 155 | ] 156 | ] 157 | ] 158 | ] 159 | @test sourcetext(ex[3][1][1][2]) == "\$\$(args...)" 160 | @test sourcetext(ex[3][1][1][2][1]) == "x" 161 | @test sourcetext(ex[3][1][1][2][2]) == "y" 162 | 163 | ex2 = JuliaLowering.eval(test_mod, ex) 164 | @test sourcetext(ex2[1][2]) == "x" 165 | @test sourcetext(ex2[1][3]) == "y" 166 | 167 | @test JuliaLowering.include_string(test_mod, ":x") isa Symbol 168 | @test JuliaLowering.include_string(test_mod, ":(x)") isa SyntaxTree 169 | 170 | # Double interpolation 171 | double_interp_ex = JuliaLowering.include_string(test_mod, raw""" 172 | let 173 | args = (:(xxx),) 174 | :(:($$(args...))) 175 | end 176 | """) 177 | Base.eval(test_mod, :(xxx = 111)) 178 | dinterp_eval = JuliaLowering.eval(test_mod, double_interp_ex) 179 | @test kind(dinterp_eval) == K"Value" 180 | @test dinterp_eval.value == 111 181 | 182 | multi_interp_ex = JuliaLowering.include_string(test_mod, raw""" 183 | let 184 | args = (:(x), :(y)) 185 | :(:($$(args...))) 186 | end 187 | """) 188 | @test try 189 | JuliaLowering.eval(test_mod, multi_interp_ex) 190 | nothing 191 | catch exc 192 | @test exc isa LoweringError 193 | sprint(io->Base.showerror(io, exc, show_detail=false)) 194 | end == raw""" 195 | LoweringError: 196 | let 197 | args = (:(x), :(y)) 198 | :(:($$(args...))) 199 | # └─────────┘ ── More than one value in bare `$` expression 200 | end""" 201 | 202 | @test try 203 | JuliaLowering.eval(test_mod, multi_interp_ex, expr_compat_mode=true) 204 | nothing 205 | catch exc 206 | @test exc isa LoweringError 207 | sprint(io->Base.showerror(io, exc, show_detail=false)) 208 | end == raw""" 209 | LoweringError: 210 | No source for expression 211 | └ ── More than one value in bare `$` expression""" 212 | # ^ TODO: Improve error messages involving expr_to_syntaxtree! 213 | 214 | # Interpolation of SyntaxTree Identifier vs plain Symbol 215 | symbol_interp = JuliaLowering.include_string(test_mod, raw""" 216 | let 217 | x = :xx # Plain Symbol 218 | y = :(yy) # SyntaxTree K"Identifier" 219 | :(f($x, $y, z)) 220 | end 221 | """) 222 | @test symbol_interp ≈ @ast_ [K"call" 223 | "f"::K"Identifier" 224 | "xx"::K"Identifier" 225 | "yy"::K"Identifier" 226 | "z"::K"Identifier" 227 | ] 228 | @test sourcetext(symbol_interp[2]) == "\$x" # No provenance for plain Symbol 229 | @test sourcetext(symbol_interp[3]) == "yy" 230 | 231 | # Mixing Expr into a SyntaxTree doesn't graft it onto the SyntaxTree AST but 232 | # treats it as a plain old value. (This is the conservative API choice and also 233 | # encourages ASTs to be written in the new form. However we may choose to 234 | # change this if necessary for compatibility.) 235 | expr_interp_is_value = JuliaLowering.include_string(test_mod, raw""" 236 | let 237 | x = Expr(:call, :f, :x) 238 | :(g($x)) 239 | end 240 | """) 241 | @test expr_interp_is_value ≈ @ast_ [K"call" 242 | "g"::K"Identifier" 243 | Expr(:call, :f, :x)::K"Value" 244 | # ^^ NB not [K"call" "f"::K"Identifier" "x"::K"Identifier"] 245 | ] 246 | @test Expr(expr_interp_is_value) == Expr(:call, :g, QuoteNode(Expr(:call, :f, :x))) 247 | 248 | @testset "Interpolation in Expr compat mode" begin 249 | expr_interp = JuliaLowering.include_string(test_mod, raw""" 250 | let 251 | x = :xx 252 | :(f($x, z)) 253 | end 254 | """, expr_compat_mode=true) 255 | @test expr_interp == Expr(:call, :f, :xx, :z) 256 | 257 | double_interp_expr = JuliaLowering.include_string(test_mod, raw""" 258 | let 259 | x = :xx 260 | :(:(f($$x, $y))) 261 | end 262 | """, expr_compat_mode=true) 263 | @test double_interp_expr == Expr(:quote, Expr(:call, :f, Expr(:$, :xx), Expr(:$, :y))) 264 | 265 | # Test that ASTs are copied before they're seen by the user 266 | @test JuliaLowering.include_string(test_mod, raw""" 267 | exs = [] 268 | for i = 1:2 269 | push!(exs, :(f(x,y))) 270 | push!(exs[end].args, :z) 271 | end 272 | exs 273 | """, expr_compat_mode=true) == Any[Expr(:call, :f, :x, :y, :z), Expr(:call, :f, :x, :y, :z)] 274 | 275 | # Test interpolation into QuoteNode 276 | @test JuliaLowering.include_string(test_mod, raw""" 277 | let x = :push! 278 | @eval Base.$x 279 | end 280 | """; expr_compat_mode=true) == Base.push! 281 | end 282 | 283 | end 284 | -------------------------------------------------------------------------------- /test/exceptions_ir.jl: -------------------------------------------------------------------------------- 1 | ######################################## 2 | # Return from inside try/catch 3 | try 4 | f 5 | return x 6 | catch 7 | g 8 | return y 9 | end 10 | #--------------------- 11 | 1 (enter label₆) 12 | 2 TestMod.f 13 | 3 TestMod.x 14 | 4 (leave %₁) 15 | 5 (return %₃) 16 | 6 TestMod.g 17 | 7 TestMod.y 18 | 8 (pop_exception %₁) 19 | 9 (return %₇) 20 | 21 | ######################################## 22 | # Return from inside try/catch with simple return vals 23 | try 24 | f 25 | return 10 26 | catch 27 | g 28 | return 20 29 | end 30 | #--------------------- 31 | 1 (enter label₅) 32 | 2 TestMod.f 33 | 3 (leave %₁) 34 | 4 (return 10) 35 | 5 TestMod.g 36 | 6 (pop_exception %₁) 37 | 7 (return 20) 38 | 39 | ######################################## 40 | # Return from multiple try + try/catch 41 | try 42 | try 43 | return 10 44 | catch 45 | return 20 46 | end 47 | catch 48 | end 49 | #--------------------- 50 | 1 (enter label₁₄) 51 | 2 (enter label₇) 52 | 3 (leave %₁ %₂) 53 | 4 (return 10) 54 | 5 (leave %₂) 55 | 6 (goto label₁₁) 56 | 7 (leave %₁) 57 | 8 (pop_exception %₂) 58 | 9 (return 20) 59 | 10 (pop_exception %₂) 60 | 11 slot₁/try_result 61 | 12 (leave %₁) 62 | 13 (return %₁₁) 63 | 14 (pop_exception %₁) 64 | 15 (return core.nothing) 65 | 66 | ######################################## 67 | # Return from multiple catch + try/catch 68 | try 69 | catch 70 | try 71 | return 10 72 | catch 73 | return 20 74 | end 75 | end 76 | #--------------------- 77 | 1 (enter label₄) 78 | 2 (leave %₁) 79 | 3 (return core.nothing) 80 | 4 (enter label₈) 81 | 5 (leave %₄) 82 | 6 (pop_exception %₁) 83 | 7 (return 10) 84 | 8 (pop_exception %₁) 85 | 9 (return 20) 86 | 87 | ######################################## 88 | # try/catch/else, tail position 89 | try 90 | a 91 | catch 92 | b 93 | else 94 | c 95 | end 96 | #--------------------- 97 | 1 (enter label₆) 98 | 2 TestMod.a 99 | 3 (leave %₁) 100 | 4 TestMod.c 101 | 5 (return %₄) 102 | 6 TestMod.b 103 | 7 (pop_exception %₁) 104 | 8 (return %₆) 105 | 106 | ######################################## 107 | # try/catch/else, value position 108 | let 109 | z = try 110 | a 111 | catch 112 | b 113 | else 114 | c 115 | end 116 | end 117 | #--------------------- 118 | 1 (newvar slot₁/z) 119 | 2 (enter label₈) 120 | 3 TestMod.a 121 | 4 (leave %₂) 122 | 5 TestMod.c 123 | 6 (= slot₂/try_result %₅) 124 | 7 (goto label₁₁) 125 | 8 TestMod.b 126 | 9 (= slot₂/try_result %₈) 127 | 10 (pop_exception %₂) 128 | 11 slot₂/try_result 129 | 12 (= slot₁/z %₁₁) 130 | 13 (return %₁₁) 131 | 132 | ######################################## 133 | # try/catch/else, not value/tail 134 | begin 135 | try 136 | a 137 | catch 138 | b 139 | else 140 | c 141 | end 142 | z 143 | end 144 | #--------------------- 145 | 1 (enter label₆) 146 | 2 TestMod.a 147 | 3 (leave %₁) 148 | 4 TestMod.c 149 | 5 (goto label₈) 150 | 6 TestMod.b 151 | 7 (pop_exception %₁) 152 | 8 TestMod.z 153 | 9 (return %₈) 154 | 155 | ######################################## 156 | # basic try/finally, tail position 157 | try 158 | a 159 | finally 160 | b 161 | end 162 | #--------------------- 163 | 1 (enter label₇) 164 | 2 (= slot₁/finally_tag -1) 165 | 3 (= slot₂/returnval_via_finally TestMod.a) 166 | 4 (= slot₁/finally_tag 1) 167 | 5 (leave %₁) 168 | 6 (goto label₈) 169 | 7 (= slot₁/finally_tag 2) 170 | 8 TestMod.b 171 | 9 (call core.=== slot₁/finally_tag 2) 172 | 10 (gotoifnot %₉ label₁₂) 173 | 11 (call top.rethrow) 174 | 12 slot₂/returnval_via_finally 175 | 13 (return %₁₂) 176 | 177 | ######################################## 178 | # basic try/finally, value position 179 | let 180 | z = try 181 | a 182 | finally 183 | b 184 | end 185 | end 186 | #--------------------- 187 | 1 (newvar slot₁/z) 188 | 2 (enter label₈) 189 | 3 (= slot₃/finally_tag -1) 190 | 4 TestMod.a 191 | 5 (= slot₂/try_result %₄) 192 | 6 (leave %₂) 193 | 7 (goto label₉) 194 | 8 (= slot₃/finally_tag 1) 195 | 9 TestMod.b 196 | 10 (call core.=== slot₃/finally_tag 1) 197 | 11 (gotoifnot %₁₀ label₁₃) 198 | 12 (call top.rethrow) 199 | 13 slot₂/try_result 200 | 14 (= slot₁/z %₁₃) 201 | 15 (return %₁₃) 202 | 203 | ######################################## 204 | # basic try/finally, not value/tail 205 | begin 206 | try 207 | a 208 | finally 209 | b 210 | end 211 | z 212 | end 213 | #--------------------- 214 | 1 (enter label₆) 215 | 2 (= slot₁/finally_tag -1) 216 | 3 TestMod.a 217 | 4 (leave %₁) 218 | 5 (goto label₇) 219 | 6 (= slot₁/finally_tag 1) 220 | 7 TestMod.b 221 | 8 (call core.=== slot₁/finally_tag 1) 222 | 9 (gotoifnot %₈ label₁₁) 223 | 10 (call top.rethrow) 224 | 11 TestMod.z 225 | 12 (return %₁₁) 226 | 227 | ######################################## 228 | # try/finally + break 229 | while true 230 | try 231 | a 232 | break 233 | finally 234 | b 235 | end 236 | end 237 | #--------------------- 238 | 1 (gotoifnot true label₁₅) 239 | 2 (enter label₉) 240 | 3 (= slot₁/finally_tag -1) 241 | 4 TestMod.a 242 | 5 (leave %₂) 243 | 6 (goto label₁₅) 244 | 7 (leave %₂) 245 | 8 (goto label₁₀) 246 | 9 (= slot₁/finally_tag 1) 247 | 10 TestMod.b 248 | 11 (call core.=== slot₁/finally_tag 1) 249 | 12 (gotoifnot %₁₁ label₁₄) 250 | 13 (call top.rethrow) 251 | 14 (goto label₁) 252 | 15 (return core.nothing) 253 | 254 | ######################################## 255 | # try/catch/finally 256 | try 257 | a 258 | catch 259 | b 260 | finally 261 | c 262 | end 263 | #--------------------- 264 | 1 (enter label₁₅) 265 | 2 (= slot₁/finally_tag -1) 266 | 3 (enter label₈) 267 | 4 TestMod.a 268 | 5 (= slot₂/try_result %₄) 269 | 6 (leave %₃) 270 | 7 (goto label₁₁) 271 | 8 TestMod.b 272 | 9 (= slot₂/try_result %₈) 273 | 10 (pop_exception %₃) 274 | 11 (= slot₃/returnval_via_finally slot₂/try_result) 275 | 12 (= slot₁/finally_tag 1) 276 | 13 (leave %₁) 277 | 14 (goto label₁₆) 278 | 15 (= slot₁/finally_tag 2) 279 | 16 TestMod.c 280 | 17 (call core.=== slot₁/finally_tag 2) 281 | 18 (gotoifnot %₁₇ label₂₀) 282 | 19 (call top.rethrow) 283 | 20 slot₃/returnval_via_finally 284 | 21 (return %₂₀) 285 | 286 | ######################################## 287 | # Nested finally blocks 288 | try 289 | try 290 | if x 291 | return a 292 | end 293 | b 294 | finally 295 | c 296 | end 297 | finally 298 | d 299 | end 300 | #--------------------- 301 | 1 (enter label₃₀) 302 | 2 (= slot₁/finally_tag -1) 303 | 3 (enter label₁₅) 304 | 4 (= slot₃/finally_tag -1) 305 | 5 TestMod.x 306 | 6 (gotoifnot %₅ label₁₁) 307 | 7 (= slot₄/returnval_via_finally TestMod.a) 308 | 8 (= slot₃/finally_tag 1) 309 | 9 (leave %₃) 310 | 10 (goto label₁₆) 311 | 11 TestMod.b 312 | 12 (= slot₂/try_result %₁₁) 313 | 13 (leave %₃) 314 | 14 (goto label₁₆) 315 | 15 (= slot₃/finally_tag 2) 316 | 16 TestMod.c 317 | 17 (call core.=== slot₃/finally_tag 2) 318 | 18 (gotoifnot %₁₇ label₂₀) 319 | 19 (call top.rethrow) 320 | 20 (call core.=== slot₃/finally_tag 1) 321 | 21 (gotoifnot %₂₀ label₂₆) 322 | 22 (= slot₅/returnval_via_finally slot₄/returnval_via_finally) 323 | 23 (= slot₁/finally_tag 1) 324 | 24 (leave %₁) 325 | 25 (goto label₃₁) 326 | 26 (= slot₆/returnval_via_finally slot₂/try_result) 327 | 27 (= slot₁/finally_tag 2) 328 | 28 (leave %₁) 329 | 29 (goto label₃₁) 330 | 30 (= slot₁/finally_tag 3) 331 | 31 TestMod.d 332 | 32 (call core.=== slot₁/finally_tag 3) 333 | 33 (gotoifnot %₃₂ label₃₅) 334 | 34 (call top.rethrow) 335 | 35 (call core.=== slot₁/finally_tag 2) 336 | 36 (gotoifnot %₃₅ label₃₉) 337 | 37 slot₆/returnval_via_finally 338 | 38 (return %₃₇) 339 | 39 slot₅/returnval_via_finally 340 | 40 (return %₃₉) 341 | 342 | ######################################## 343 | # Access to the exception object 344 | try 345 | a 346 | catch exc 347 | b 348 | end 349 | #--------------------- 350 | 1 (enter label₅) 351 | 2 TestMod.a 352 | 3 (leave %₁) 353 | 4 (return %₂) 354 | 5 (= slot₁/exc (call JuliaLowering.current_exception)) 355 | 6 TestMod.b 356 | 7 (pop_exception %₁) 357 | 8 (return %₆) 358 | -------------------------------------------------------------------------------- /src/kinds.jl: -------------------------------------------------------------------------------- 1 | # The following kinds are used in intermediate forms by lowering but are not 2 | # part of the surface syntax 3 | function _register_kinds() 4 | JuliaSyntax.register_kinds!(JuliaLowering, 1, [ 5 | # "Syntax extensions" - expression kinds emitted by macros or macro 6 | # expansion, and known to lowering. These are part of the AST API but 7 | # without having surface syntax. 8 | "BEGIN_EXTENSION_KINDS" 9 | # atomic fields or accesses (see `@atomic`) 10 | "atomic" 11 | # Flag for @generated parts of a function 12 | "generated" 13 | # Temporary rooting of identifiers (GC.@preserve) 14 | "gc_preserve" 15 | "gc_preserve_begin" 16 | "gc_preserve_end" 17 | # A literal Julia value of any kind, as might be inserted into the 18 | # AST during macro expansion 19 | "Value" 20 | # A (quoted) `Symbol` 21 | "Symbol" 22 | # QuoteNode; not quasiquote 23 | "inert" 24 | # Compiler metadata hints 25 | "meta" 26 | # TODO: Use `meta` for inbounds and loopinfo etc? 27 | "inbounds" 28 | "boundscheck" 29 | "inline" 30 | "noinline" 31 | "loopinfo" 32 | # Call into foreign code. Emitted by `@ccall` 33 | "foreigncall" 34 | # Special form for constructing a function callable from C 35 | "cfunction" 36 | # Special form emitted by `Base.Experimental.@opaque` 37 | "opaque_closure" 38 | # Test whether a variable is defined 39 | "isdefined" 40 | # [K"throw_undef_if_not" var cond] 41 | # This form is used internally in Core.Compiler but might be 42 | # emitted by packages such as Diffractor. In principle it needs to 43 | # be passed through lowering in a similar way to `isdefined` 44 | "throw_undef_if_not" 45 | # named labels for `@label` and `@goto` 46 | "symbolic_label" 47 | # Goto named label 48 | "symbolic_goto" 49 | # Internal initializer for struct types, for inner constructors/functions 50 | "new" 51 | "splatnew" 52 | # Used for converting `esc()`'d expressions arising from old macro 53 | # invocations during macro expansion (gone after macro expansion) 54 | "escape" 55 | # Used for converting the old-style macro hygienic-scope form (gone 56 | # after macro expansion) 57 | "hygienic_scope" 58 | # An expression which will eventually be evaluated "statically" in 59 | # the context of a CodeInfo and thus allows access only to globals 60 | # and static parameters. Used for ccall, cfunction, cglobal 61 | # TODO: Use this for GeneratedFunctionStub also? 62 | "static_eval" 63 | # Catch-all for additional syntax extensions without the need to 64 | # extend `Kind`. Known extensions include: 65 | # locals, islocal, isglobal 66 | # The content of an assertion is not considered to be quoted, so 67 | # use K"Symbol" or K"inert" inside where necessary. 68 | "extension" 69 | "END_EXTENSION_KINDS" 70 | 71 | # The following kinds are internal to lowering 72 | "BEGIN_LOWERING_KINDS" 73 | # Semantic assertions used by lowering. The content of an assertion 74 | # is not considered to be quoted, so use K"Symbol" etc inside where necessary. 75 | "assert" 76 | # Unique identifying integer for bindings (of variables, constants, etc) 77 | "BindingId" 78 | # Various heads harvested from flisp lowering. 79 | # (TODO: May or may not need all these - assess later) 80 | "break_block" 81 | # Like block, but introduces a lexical scope; used during scope resolution. 82 | "scope_block" 83 | # [K"always_defined" x] is an assertion that variable `x` is assigned before use 84 | # ('local-def in flisp implementation is K"local" plus K"always_defined" 85 | "always_defined" 86 | "_while" 87 | "_do_while" 88 | "_typevars" # used for supplying already-allocated `TypeVar`s to `where` 89 | "with_static_parameters" 90 | "top" 91 | "core" 92 | "lambda" 93 | # "A source location literal" - a node which exists only to record 94 | # a sourceref 95 | "SourceLocation" 96 | # [K"function_decl" name] 97 | # Declare a zero-method generic function with global `name` or 98 | # creates a closure object and assigns it to the local `name`. 99 | "function_decl" 100 | # [K"function_type name] 101 | # Evaluates to the type of the function or closure with given `name` 102 | "function_type" 103 | # [K"method_defs" name block] 104 | # The code in `block` defines methods for generic function `name` 105 | "method_defs" 106 | # The code in `block` defines methods for generic function `name` 107 | "_opaque_closure" 108 | # The enclosed statements must be executed at top level 109 | "toplevel_butfirst" 110 | "assign_or_constdecl_if_global" 111 | "moved_local" 112 | "label" 113 | "trycatchelse" 114 | "tryfinally" 115 | # The contained block of code causes no side effects and can be 116 | # removed by a later lowering pass if its value isn't used. 117 | # (That is, it's removable in the same sense as 118 | # `@assume_effects :removable`.) 119 | "removable" 120 | # Variable type declaration; `x::T = rhs` will be temporarily 121 | # desugared to include `(decl x T)` 122 | "decl" 123 | # [K"captured_local" index] 124 | # A local variable captured into a global method. Contains the 125 | # `index` of the associated `Box` in the rewrite list. 126 | "captured_local" 127 | # Causes the linearization pass to conditionally emit a world age increment 128 | "latestworld_if_toplevel" 129 | # This has two forms: 130 | # [K"constdecl" var val] => declare and assign constant 131 | # [K"constdecl" var] => declare undefined constant 132 | # var is GlobalRef Value or Identifier 133 | "constdecl" 134 | # Returned from statements that should error if the result is used. 135 | "unused_only" 136 | "END_LOWERING_KINDS" 137 | 138 | # The following kinds are emitted by lowering and used in Julia's untyped IR 139 | "BEGIN_IR_KINDS" 140 | # Identifier for a value which is only assigned once 141 | "SSAValue" 142 | # Local variable in a `CodeInfo` code object (including lambda arguments) 143 | "slot" 144 | # Static parameter to a `CodeInfo` code object ("type parameters" to methods) 145 | "static_parameter" 146 | # References/declares a global variable within a module 147 | "globalref" 148 | # Unconditional goto 149 | "goto" 150 | # Conditional goto 151 | "gotoifnot" 152 | # Exception handling 153 | "enter" 154 | "leave" 155 | "pop_exception" 156 | # Lowering targets for method definitions arising from `function` etc 157 | "method" 158 | # (re-)initialize a slot to undef 159 | # See Core.NewvarNode 160 | "newvar" 161 | # Result of lowering a `K"lambda"` after bindings have been 162 | # converted to slot/globalref/SSAValue. 163 | "code_info" 164 | # Internal initializer for opaque closures 165 | "new_opaque_closure" 166 | # Wrapper for the lambda of around opaque closure methods 167 | "opaque_closure_method" 168 | # World age increment (TODO: use top level assertion and only one latestworld kind) 169 | "latestworld" 170 | "END_IR_KINDS" 171 | ]) 172 | end 173 | -------------------------------------------------------------------------------- /test/decls_ir.jl: -------------------------------------------------------------------------------- 1 | ######################################## 2 | # Local declaration with type 3 | begin 4 | local x::T = 1 5 | end 6 | #--------------------- 7 | 1 (newvar slot₁/x) 8 | 2 1 9 | 3 TestMod.T 10 | 4 (= slot₂/tmp %₂) 11 | 5 slot₂/tmp 12 | 6 (call core.isa %₅ %₃) 13 | 7 (gotoifnot %₆ label₉) 14 | 8 (goto label₁₂) 15 | 9 slot₂/tmp 16 | 10 (call top.convert %₃ %₉) 17 | 11 (= slot₂/tmp (call core.typeassert %₁₀ %₃)) 18 | 12 slot₂/tmp 19 | 13 (= slot₁/x %₁₂) 20 | 14 (return %₂) 21 | 22 | ######################################## 23 | # Error: Local declarations outside a scope are disallowed 24 | # See https://github.com/JuliaLang/julia/issues/57483 25 | local x 26 | #--------------------- 27 | LoweringError: 28 | local x 29 | └─────┘ ── local declarations have no effect outside a scope 30 | 31 | ######################################## 32 | # Local declaration allowed in tail position 33 | begin 34 | local x 35 | end 36 | #--------------------- 37 | 1 (newvar slot₁/x) 38 | 2 (return core.nothing) 39 | 40 | ######################################## 41 | # Local declaration allowed in value position 42 | # TODO: This may be a bug in flisp lowering - should we reconsider this? 43 | let 44 | y = local x 45 | end 46 | #--------------------- 47 | 1 (newvar slot₁/x) 48 | 2 core.nothing 49 | 3 (= slot₂/y %₂) 50 | 4 (return %₂) 51 | 52 | ######################################## 53 | # Global declaration allowed in tail position 54 | global x 55 | #--------------------- 56 | 1 (call core.declare_global TestMod :x false) 57 | 2 latestworld 58 | 3 (return core.nothing) 59 | 60 | ######################################## 61 | # Global declaration allowed in tail position, nested 62 | begin 63 | global x 64 | end 65 | #--------------------- 66 | 1 (call core.declare_global TestMod :x false) 67 | 2 latestworld 68 | 3 (return core.nothing) 69 | 70 | ######################################## 71 | # Error: Global declaration not allowed in tail position in functions 72 | function f() 73 | global x 74 | end 75 | #--------------------- 76 | LoweringError: 77 | function f() 78 | global x 79 | # ╙ ── global declaration doesn't read the variable and can't return a value 80 | end 81 | 82 | ######################################## 83 | # Error: Global declaration not allowed in value position 84 | y = global x 85 | #--------------------- 86 | LoweringError: 87 | y = global x 88 | # ╙ ── global declaration doesn't read the variable and can't return a value 89 | 90 | ######################################## 91 | # const 92 | const xx = 10 93 | #--------------------- 94 | 1 10 95 | 2 (call core.declare_const TestMod :xx %₁) 96 | 3 latestworld 97 | 4 (return %₁) 98 | 99 | ######################################## 100 | # Typed const 101 | const xx::T = 10 102 | #--------------------- 103 | 1 TestMod.T 104 | 2 (= slot₁/tmp 10) 105 | 3 slot₁/tmp 106 | 4 (call core.isa %₃ %₁) 107 | 5 (gotoifnot %₄ label₇) 108 | 6 (goto label₁₀) 109 | 7 slot₁/tmp 110 | 8 (call top.convert %₁ %₇) 111 | 9 (= slot₁/tmp (call core.typeassert %₈ %₁)) 112 | 10 slot₁/tmp 113 | 11 (call core.declare_const TestMod :xx %₁₀) 114 | 12 latestworld 115 | 13 (return %₁₀) 116 | 117 | ######################################## 118 | # Const tuple 119 | const xxx,xxxx,xxxxx = 10,20,30 120 | #--------------------- 121 | 1 10 122 | 2 (call core.declare_const TestMod :xxx %₁) 123 | 3 latestworld 124 | 4 20 125 | 5 (call core.declare_const TestMod :xxxx %₄) 126 | 6 latestworld 127 | 7 30 128 | 8 (call core.declare_const TestMod :xxxxx %₇) 129 | 9 latestworld 130 | 10 (call core.tuple 10 20 30) 131 | 11 (return %₁₀) 132 | 133 | ######################################## 134 | # Const in chain: only first is const 135 | const c0 = v0 = v1 = 123 136 | #--------------------- 137 | 1 123 138 | 2 (call core.declare_const TestMod :c0 %₁) 139 | 3 latestworld 140 | 4 (call core.declare_global TestMod :v0 true) 141 | 5 latestworld 142 | 6 (call core.get_binding_type TestMod :v0) 143 | 7 (= slot₁/tmp %₁) 144 | 8 slot₁/tmp 145 | 9 (call core.isa %₈ %₆) 146 | 10 (gotoifnot %₉ label₁₂) 147 | 11 (goto label₁₄) 148 | 12 slot₁/tmp 149 | 13 (= slot₁/tmp (call top.convert %₆ %₁₂)) 150 | 14 slot₁/tmp 151 | 15 (call core.setglobal! TestMod :v0 %₁₄) 152 | 16 (call core.declare_global TestMod :v1 true) 153 | 17 latestworld 154 | 18 (call core.get_binding_type TestMod :v1) 155 | 19 (= slot₂/tmp %₁) 156 | 20 slot₂/tmp 157 | 21 (call core.isa %₂₀ %₁₈) 158 | 22 (gotoifnot %₂₁ label₂₄) 159 | 23 (goto label₂₆) 160 | 24 slot₂/tmp 161 | 25 (= slot₂/tmp (call top.convert %₁₈ %₂₄)) 162 | 26 slot₂/tmp 163 | 27 (call core.setglobal! TestMod :v1 %₂₆) 164 | 28 (return %₁) 165 | 166 | ######################################## 167 | # Global assignment 168 | xx = 10 169 | #--------------------- 170 | 1 (call core.declare_global TestMod :xx true) 171 | 2 latestworld 172 | 3 (call core.get_binding_type TestMod :xx) 173 | 4 (= slot₁/tmp 10) 174 | 5 slot₁/tmp 175 | 6 (call core.isa %₅ %₃) 176 | 7 (gotoifnot %₆ label₉) 177 | 8 (goto label₁₁) 178 | 9 slot₁/tmp 179 | 10 (= slot₁/tmp (call top.convert %₃ %₉)) 180 | 11 slot₁/tmp 181 | 12 (call core.setglobal! TestMod :xx %₁₁) 182 | 13 (return 10) 183 | 184 | ######################################## 185 | # Typed global assignment 186 | global xx::T = 10 187 | #--------------------- 188 | 1 (call core.declare_global TestMod :xx false) 189 | 2 latestworld 190 | 3 TestMod.T 191 | 4 (call core.declare_global TestMod :xx true %₃) 192 | 5 latestworld 193 | 6 (call core.declare_global TestMod :xx true) 194 | 7 latestworld 195 | 8 (call core.get_binding_type TestMod :xx) 196 | 9 (= slot₁/tmp 10) 197 | 10 slot₁/tmp 198 | 11 (call core.isa %₁₀ %₈) 199 | 12 (gotoifnot %₁₁ label₁₄) 200 | 13 (goto label₁₆) 201 | 14 slot₁/tmp 202 | 15 (= slot₁/tmp (call top.convert %₈ %₁₄)) 203 | 16 slot₁/tmp 204 | 17 (call core.setglobal! TestMod :xx %₁₆) 205 | 18 (return 10) 206 | 207 | ######################################## 208 | # Error: x declared twice 209 | begin 210 | local x::T = 1 211 | local x::S = 1 212 | end 213 | #--------------------- 214 | LoweringError: 215 | begin 216 | local x::T = 1 217 | local x::S = 1 218 | # └───────┘ ── multiple type declarations found for `x` 219 | end 220 | 221 | ######################################## 222 | # Error: Const not supported on locals 223 | const local x = 1 224 | #--------------------- 225 | LoweringError: 226 | const local x = 1 227 | └───────────────┘ ── unsupported `const local` declaration 228 | 229 | ######################################## 230 | # Error: Const not supported on locals 231 | let 232 | const x = 1 233 | end 234 | #--------------------- 235 | LoweringError: 236 | let 237 | const x = 1 238 | # └────┘ ── unsupported `const` declaration on local variable 239 | end 240 | 241 | ######################################## 242 | # Type decl on function argument 243 | function f(x) 244 | x::Int = 1 245 | x = 2.0 246 | x 247 | end 248 | #--------------------- 249 | 1 (method TestMod.f) 250 | 2 latestworld 251 | 3 TestMod.f 252 | 4 (call core.Typeof %₃) 253 | 5 (call core.svec %₄ core.Any) 254 | 6 (call core.svec) 255 | 7 SourceLocation::1:10 256 | 8 (call core.svec %₅ %₆ %₇) 257 | 9 --- method core.nothing %₈ 258 | slots: [slot₁/#self#(!read) slot₂/x slot₃/tmp(!read) slot₄/tmp(!read)] 259 | 1 1 260 | 2 TestMod.Int 261 | 3 (= slot₃/tmp %₁) 262 | 4 slot₃/tmp 263 | 5 (call core.isa %₄ %₂) 264 | 6 (gotoifnot %₅ label₈) 265 | 7 (goto label₁₁) 266 | 8 slot₃/tmp 267 | 9 (call top.convert %₂ %₈) 268 | 10 (= slot₃/tmp (call core.typeassert %₉ %₂)) 269 | 11 slot₃/tmp 270 | 12 (= slot₂/x %₁₁) 271 | 13 2.0 272 | 14 TestMod.Int 273 | 15 (= slot₄/tmp %₁₃) 274 | 16 slot₄/tmp 275 | 17 (call core.isa %₁₆ %₁₄) 276 | 18 (gotoifnot %₁₇ label₂₀) 277 | 19 (goto label₂₃) 278 | 20 slot₄/tmp 279 | 21 (call top.convert %₁₄ %₂₀) 280 | 22 (= slot₄/tmp (call core.typeassert %₂₁ %₁₄)) 281 | 23 slot₄/tmp 282 | 24 (= slot₂/x %₂₃) 283 | 25 slot₂/x 284 | 26 (return %₂₅) 285 | 10 latestworld 286 | 11 TestMod.f 287 | 12 (return %₁₁) 288 | 289 | ######################################## 290 | # Error: global type decls only allowed at top level 291 | function f() 292 | global x::Int = 1 293 | end 294 | #--------------------- 295 | LoweringError: 296 | function f() 297 | global x::Int = 1 298 | # └─────────┘ ── type declarations for global variables must be at top level, not inside a function 299 | end 300 | -------------------------------------------------------------------------------- /test/misc.jl: -------------------------------------------------------------------------------- 1 | @testset "Miscellaneous" begin 2 | 3 | test_mod = Module() 4 | 5 | # Blocks 6 | @test JuliaLowering.include_string(test_mod, """ 7 | begin 8 | end 9 | """) == nothing 10 | 11 | # Placeholders 12 | @test JuliaLowering.include_string(test_mod, """_ = 10""") == 10 13 | 14 | # GC.@preserve 15 | @test JuliaLowering.include_string(test_mod, """ 16 | let x = [1,2] 17 | GC.@preserve x begin 18 | x 19 | end 20 | end 21 | """) == [1,2] 22 | 23 | @test JuliaLowering.include_string(test_mod, raw""" 24 | let 25 | x = 10 26 | @eval $x + 2 27 | end 28 | """) == 12 29 | 30 | @test JuliaLowering.include_string(test_mod, raw""" 31 | module EvalTest 32 | _some_var = 2 33 | end 34 | let 35 | x = 10 36 | @eval EvalTest $x + _some_var 37 | end 38 | """) == 12 39 | 40 | @test JuliaLowering.include_string(test_mod, """ 41 | let x=11 42 | 20x 43 | end 44 | """) == 220 45 | 46 | # ccall 47 | @test JuliaLowering.include_string(test_mod, """ 48 | ccall(:strlen, Csize_t, (Cstring,), "asdfg") 49 | """) == 5 50 | @test JuliaLowering.include_string(test_mod, """ 51 | function cvarargs_0() 52 | strp = Ref{Ptr{Cchar}}(0) 53 | fmt = "hi" 54 | len = ccall(:asprintf, Cint, (Ptr{Ptr{Cchar}}, Cstring, Cfloat...), strp, fmt) 55 | str = unsafe_string(strp[], len) 56 | Libc.free(strp[]) 57 | return str 58 | end 59 | """) isa Function 60 | @test test_mod.cvarargs_0() == "hi" 61 | @test JuliaLowering.include_string(test_mod, """ 62 | function cvarargs_2(arg1::Float64, arg2::Float64) 63 | strp = Ref{Ptr{Cchar}}(0) 64 | fmt = "%3.1f %3.1f" 65 | len = ccall(:asprintf, Cint, (Ptr{Ptr{Cchar}}, Cstring, Cfloat...), strp, fmt, arg1, arg2) 66 | str = unsafe_string(strp[], len) 67 | Libc.free(strp[]) 68 | return str 69 | end 70 | """) isa Function 71 | @test test_mod.cvarargs_2(1.1, 2.2) == "1.1 2.2" 72 | 73 | # cfunction 74 | JuliaLowering.include_string(test_mod, """ 75 | function f_ccallable(x, y) 76 | x + y * 10 77 | end 78 | """) 79 | cf_int = JuliaLowering.include_string(test_mod, """ 80 | @cfunction(f_ccallable, Int, (Int,Int)) 81 | """) 82 | @test @ccall($cf_int(2::Int, 3::Int)::Int) == 32 83 | cf_float = JuliaLowering.include_string(test_mod, """ 84 | @cfunction(f_ccallable, Float64, (Float64,Float64)) 85 | """) 86 | @test @ccall($cf_float(2::Float64, 3::Float64)::Float64) == 32.0 87 | 88 | # Test that hygiene works with @ccallable function names (this is broken in 89 | # Base) 90 | JuliaLowering.include_string(test_mod, raw""" 91 | f_ccallable_hygiene() = 1 92 | 93 | module Nested 94 | f_ccallable_hygiene() = 2 95 | macro cfunction_hygiene() 96 | :(@cfunction(f_ccallable_hygiene, Int, ())) 97 | end 98 | end 99 | """) 100 | cf_hygiene = JuliaLowering.include_string(test_mod, """ 101 | Nested.@cfunction_hygiene 102 | """) 103 | @test @ccall($cf_hygiene()::Int) == 2 104 | 105 | # Test that ccall can be passed static parameters in type signatures. 106 | # 107 | # Note that the cases where this works are extremely limited and tend to look 108 | # like `Ptr{T}` or `Ref{T}` (`T` doesn't work!?) because of the compilation 109 | # order in which the runtime inspects the arguments to ccall (`Ptr{T}` has a 110 | # well defined C ABI even when `T` is not yet determined). See also 111 | # https://github.com/JuliaLang/julia/issues/29400 112 | # https://github.com/JuliaLang/julia/pull/40947 113 | JuliaLowering.include_string(test_mod, raw""" 114 | function sparam_ccallable(x::Ptr{T}) where {T} 115 | unsafe_store!(x, one(T)) 116 | nothing 117 | end 118 | 119 | function ccall_with_sparams(::Type{T}) where {T} 120 | x = T[zero(T)] 121 | cf = @cfunction(sparam_ccallable, Cvoid, (Ptr{T},)) 122 | @ccall $cf(x::Ptr{T})::Cvoid 123 | x[1] 124 | end 125 | """) 126 | @test test_mod.ccall_with_sparams(Int) === 1 127 | @test test_mod.ccall_with_sparams(Float64) === 1.0 128 | 129 | # FIXME Currently JL cannot handle `@generated` functions, so the following test cases are commented out. 130 | # # Test that ccall can be passed static parameters in the function name 131 | # # Note that this only works with `@generated` functions from 1.13 onwards, 132 | # # where the function name can be evaluated at code generation time. 133 | # JuliaLowering.include_string(test_mod, raw""" 134 | # # In principle, may add other strlen-like functions here for different string 135 | # # types 136 | # ccallable_sptest_name(::Type{String}) = :strlen 137 | # 138 | # @generated function ccall_with_sparams_in_name(s::T) where {T} 139 | # name = QuoteNode(ccallable_sptest_name(T)) 140 | # :(ccall($name, Csize_t, (Cstring,), s)) 141 | # end 142 | # """) 143 | # @test test_mod.ccall_with_sparams_in_name("hii") == 3 144 | 145 | @testset "CodeInfo: has_image_globalref" begin 146 | @test lower_str(test_mod, "x + y").args[1].has_image_globalref === false 147 | @test lower_str(Main, "x + y").args[1].has_image_globalref === true 148 | end 149 | 150 | @testset "docstrings: doc-only expressions" begin 151 | local jeval(mod, str) = JuliaLowering.include_string(mod, str; expr_compat_mode=true) 152 | jeval(test_mod, "function fun_exists(x); x; end") 153 | jeval(test_mod, "module M end; module M2 end") 154 | # TODO: return values are to be determined, currently Base.Docs.Binding for 155 | # both lowering implementations. We can't return the value of the 156 | # expression in these special cases. 157 | jeval(test_mod, "\"docstr1\" sym_noexist") 158 | jeval(test_mod, "\"docstr2\" fun_noexist()") 159 | jeval(test_mod, "\"docstr3\" fun_exists(sym_noexist)") 160 | jeval(test_mod, "\"docstr4\" M.sym_noexist") 161 | jeval(test_mod, "\"docstr5\" M.fun_noexist()") 162 | jeval(test_mod, "\"docstr6\" M.fun_exists(sym_noexist)") 163 | @test jeval(test_mod, "@doc sym_noexist") |> string === "docstr1\n" 164 | @test jeval(test_mod, "@doc fun_noexist()") |> string === "docstr2\n" 165 | @test jeval(test_mod, "@doc fun_exists(sym_noexist)") |> string === "docstr3\n" 166 | @test jeval(test_mod, "@doc M.sym_noexist") |> string === "docstr4\n" 167 | @test jeval(test_mod, "@doc M.fun_noexist()") |> string === "docstr5\n" 168 | @test jeval(test_mod, "@doc M.fun_exists(sym_noexist)") |> string === "docstr6\n" 169 | @test jeval(test_mod.M, "@doc M.sym_noexist") |> string === "docstr4\n" 170 | @test jeval(test_mod.M, "@doc M.fun_noexist()") |> string === "docstr5\n" 171 | @test jeval(test_mod.M, "@doc M.fun_exists(sym_noexist)") |> string === "docstr6\n" 172 | 173 | jeval(test_mod.M2, "\"docstr7\" M2.M2.sym_noexist") 174 | jeval(test_mod.M2, "\"docstr8\" M2.M2.fun_noexist()") 175 | jeval(test_mod.M2, "\"docstr9\" M2.M2.fun_exists(sym_noexist)") 176 | @test jeval(test_mod, "@doc M2.M2.sym_noexist") |> string === "docstr7\n" 177 | @test jeval(test_mod, "@doc M2.M2.fun_noexist()") |> string === "docstr8\n" 178 | @test jeval(test_mod, "@doc M2.M2.fun_exists(sym_noexist)") |> string === "docstr9\n" 179 | @test jeval(test_mod.M2, "@doc M2.M2.sym_noexist") |> string === "docstr7\n" 180 | @test jeval(test_mod.M2, "@doc M2.M2.fun_noexist()") |> string === "docstr8\n" 181 | @test jeval(test_mod.M2, "@doc M2.M2.fun_exists(sym_noexist)") |> string === "docstr9\n" 182 | 183 | # Try with signatures and type variables 184 | jeval(test_mod, "abstract type T_exists end") 185 | 186 | jeval(test_mod, "\"docstr10\" f10(x::Int, y, z::T_exists)") 187 | d = jeval(test_mod, "@doc f10") 188 | @test d |> string === "docstr10\n" 189 | # TODO: Is there a better way of accessing this? Feel free to change tests 190 | # if docsystem storage changes. 191 | @test d.meta[:results][1].data[:typesig] === Tuple{Int, Any, test_mod.T_exists} 192 | 193 | jeval(test_mod, "\"docstr11\" f11(x::T_exists, y::U, z::T) where {T, U<:Number}") 194 | d = jeval(test_mod, "@doc f11") 195 | @test d |> string === "docstr11\n" 196 | @test d.meta[:results][1].data[:typesig] === Tuple{test_mod.T_exists, U, T} where {T, U<:Number} 197 | 198 | jeval(test_mod, "\"docstr12\" f12(x::Int, y::U, z::T=1) where {T, U<:Number}") 199 | d = jeval(test_mod, "@doc f12") 200 | @test d |> string === "docstr12\n" 201 | @test d.meta[:results][1].data[:typesig] === Union{Tuple{Int, U, T}, Tuple{Int, U}} where {T, U<:Number} 202 | 203 | end 204 | 205 | # SyntaxTree @eval should pass along expr_compat_mode 206 | @test JuliaLowering.include_string(test_mod, "@eval quote x end"; 207 | expr_compat_mode=false) isa SyntaxTree 208 | @test JuliaLowering.include_string(test_mod, "@eval quote x end"; 209 | expr_compat_mode=true) isa Expr 210 | @test JuliaLowering.include_string(test_mod, raw""" 211 | let T = :foo 212 | @eval @doc $"This is a $T" $T = 1 213 | end 214 | """; expr_compat_mode=true) === 1 215 | 216 | end 217 | -------------------------------------------------------------------------------- /test/assignments_ir.jl: -------------------------------------------------------------------------------- 1 | ######################################## 2 | # chain of assignments 3 | let 4 | a = b = c = 1 5 | end 6 | #--------------------- 7 | 1 1 8 | 2 (= slot₁/a %₁) 9 | 3 (= slot₂/b %₁) 10 | 4 (= slot₃/c %₁) 11 | 5 (return %₁) 12 | 13 | ######################################## 14 | # chain of assignments with nontrivial rhs 15 | let 16 | a = b = c = f() 17 | end 18 | #--------------------- 19 | 1 TestMod.f 20 | 2 (call %₁) 21 | 3 (= slot₁/a %₂) 22 | 4 (= slot₂/b %₂) 23 | 5 (= slot₃/c %₂) 24 | 6 (return %₂) 25 | 26 | ######################################## 27 | # Assignment in value but not tail position 28 | let 29 | x = begin 30 | y = 42 31 | end 32 | x 33 | end 34 | #--------------------- 35 | 1 42 36 | 2 (= slot₂/y %₁) 37 | 3 (= slot₁/x %₁) 38 | 4 slot₁/x 39 | 5 (return %₄) 40 | 41 | ######################################## 42 | # short form function def, not chain of assignments 43 | begin 44 | local a 45 | a = b() = c = d 46 | end 47 | #--------------------- 48 | 1 (method TestMod.b) 49 | 2 latestworld 50 | 3 TestMod.b 51 | 4 (call core.Typeof %₃) 52 | 5 (call core.svec %₄) 53 | 6 (call core.svec) 54 | 7 SourceLocation::3:9 55 | 8 (call core.svec %₅ %₆ %₇) 56 | 9 --- method core.nothing %₈ 57 | slots: [slot₁/#self#(!read) slot₂/c(!read)] 58 | 1 TestMod.d 59 | 2 (= slot₂/c %₁) 60 | 3 (return %₁) 61 | 10 latestworld 62 | 11 TestMod.b 63 | 12 (= slot₁/a %₁₁) 64 | 13 (return %₁₁) 65 | 66 | ######################################## 67 | # a.b = ... => setproperty! assignment 68 | let 69 | a.b = c 70 | end 71 | #--------------------- 72 | 1 TestMod.a 73 | 2 TestMod.c 74 | 3 (call top.setproperty! %₁ :b %₂) 75 | 4 TestMod.c 76 | 5 (return %₄) 77 | 78 | ######################################## 79 | # a.b.c = f() => setproperty! assignment, complex case 80 | let 81 | a.b.c = f() 82 | end 83 | #--------------------- 84 | 1 TestMod.a 85 | 2 (call top.getproperty %₁ :b) 86 | 3 TestMod.f 87 | 4 (call %₃) 88 | 5 (call top.setproperty! %₂ :c %₄) 89 | 6 (return %₄) 90 | 91 | ######################################## 92 | # declarations of typed locals 93 | let 94 | x::T = f() 95 | x 96 | end 97 | #--------------------- 98 | 1 (newvar slot₁/x) 99 | 2 TestMod.f 100 | 3 (call %₂) 101 | 4 TestMod.T 102 | 5 (= slot₂/tmp %₃) 103 | 6 slot₂/tmp 104 | 7 (call core.isa %₆ %₄) 105 | 8 (gotoifnot %₇ label₁₀) 106 | 9 (goto label₁₃) 107 | 10 slot₂/tmp 108 | 11 (call top.convert %₄ %₁₀) 109 | 12 (= slot₂/tmp (call core.typeassert %₁₁ %₄)) 110 | 13 slot₂/tmp 111 | 14 (= slot₁/x %₁₃) 112 | 15 slot₁/x 113 | 16 (return %₁₅) 114 | 115 | ######################################## 116 | # "complex lhs" of `::T` => type-assert, not decl 117 | let 118 | a.b::T = f() 119 | x 120 | end 121 | #--------------------- 122 | 1 TestMod.a 123 | 2 (call top.getproperty %₁ :b) 124 | 3 TestMod.T 125 | 4 (call core.typeassert %₂ %₃) 126 | 5 TestMod.f 127 | 6 (call %₅) 128 | 7 TestMod.a 129 | 8 (call top.setproperty! %₇ :b %₆) 130 | 9 TestMod.x 131 | 10 (return %₉) 132 | 133 | ######################################## 134 | # UnionAll expansion at global scope results in const decl 135 | X{T} = Y{T,T} 136 | #--------------------- 137 | 1 (call core.TypeVar :T) 138 | 2 (= slot₁/T %₁) 139 | 3 slot₁/T 140 | 4 TestMod.Y 141 | 5 slot₁/T 142 | 6 slot₁/T 143 | 7 (call core.apply_type %₄ %₅ %₆) 144 | 8 (call core.UnionAll %₃ %₇) 145 | 9 (call core.declare_const TestMod :X %₈) 146 | 10 latestworld 147 | 11 (return %₈) 148 | 149 | ######################################## 150 | # UnionAll expansion in local scope 151 | let 152 | X{T} = Y{T,T} 153 | end 154 | #--------------------- 155 | 1 (call core.TypeVar :T) 156 | 2 (= slot₂/T %₁) 157 | 3 slot₂/T 158 | 4 TestMod.Y 159 | 5 slot₂/T 160 | 6 slot₂/T 161 | 7 (call core.apply_type %₄ %₅ %₆) 162 | 8 (call core.UnionAll %₃ %₇) 163 | 9 (= slot₁/X %₈) 164 | 10 (return %₈) 165 | 166 | ######################################## 167 | # Error: Invalid lhs in `=` 168 | a.(b) = rhs 169 | #--------------------- 170 | LoweringError: 171 | a.(b) = rhs 172 | └───┘ ── invalid dot call syntax on left hand side of assignment 173 | 174 | ######################################## 175 | # Error: Invalid lhs in `=` 176 | T[x y] = rhs 177 | #--------------------- 178 | LoweringError: 179 | T[x y] = rhs 180 | └────┘ ── invalid spacing in left side of indexed assignment 181 | 182 | ######################################## 183 | # Error: Invalid lhs in `=` 184 | T[x; y] = rhs 185 | #--------------------- 186 | LoweringError: 187 | T[x; y] = rhs 188 | └─────┘ ── unexpected `;` in left side of indexed assignment 189 | 190 | ######################################## 191 | # Error: Invalid lhs in `=` 192 | T[x ;;; y] = rhs 193 | #--------------------- 194 | LoweringError: 195 | T[x ;;; y] = rhs 196 | └────────┘ ── unexpected `;` in left side of indexed assignment 197 | 198 | ######################################## 199 | # Error: Invalid lhs in `=` 200 | [x, y] = rhs 201 | #--------------------- 202 | LoweringError: 203 | [x, y] = rhs 204 | └────┘ ── use `(a, b) = ...` to assign multiple values 205 | 206 | ######################################## 207 | # Error: Invalid lhs in `=` 208 | [x y] = rhs 209 | #--------------------- 210 | LoweringError: 211 | [x y] = rhs 212 | └───┘ ── use `(a, b) = ...` to assign multiple values 213 | 214 | ######################################## 215 | # Error: Invalid lhs in `=` 216 | [x; y] = rhs 217 | #--------------------- 218 | LoweringError: 219 | [x; y] = rhs 220 | └────┘ ── use `(a, b) = ...` to assign multiple values 221 | 222 | ######################################## 223 | # Error: Invalid lhs in `=` 224 | [x ;;; y] = rhs 225 | #--------------------- 226 | LoweringError: 227 | [x ;;; y] = rhs 228 | └───────┘ ── use `(a, b) = ...` to assign multiple values 229 | 230 | ######################################## 231 | # Error: Invalid lhs in `=` 232 | 1 = rhs 233 | #--------------------- 234 | LoweringError: 235 | 1 = rhs 236 | ╙ ── invalid assignment location 237 | 238 | ######################################## 239 | # Basic updating assignment 240 | begin 241 | local x 242 | x += y 243 | end 244 | #--------------------- 245 | 1 TestMod.+ 246 | 2 slot₁/x 247 | 3 TestMod.y 248 | 4 (call %₁ %₂ %₃) 249 | 5 (= slot₁/x %₄) 250 | 6 (return %₄) 251 | 252 | ######################################## 253 | # Broadcasted updating assignment 254 | begin 255 | local x 256 | x .+= y 257 | end 258 | #--------------------- 259 | 1 (newvar slot₁/x) 260 | 2 slot₁/x 261 | 3 TestMod.+ 262 | 4 TestMod.y 263 | 5 (call top.broadcasted %₃ %₂ %₄) 264 | 6 (call top.materialize! %₂ %₅) 265 | 7 (return %₆) 266 | 267 | ######################################## 268 | # Broadcasted updating assignment with general left hand side permitted 269 | f() .+= y 270 | #--------------------- 271 | 1 TestMod.f 272 | 2 (call %₁) 273 | 3 TestMod.+ 274 | 4 TestMod.y 275 | 5 (call top.broadcasted %₃ %₂ %₄) 276 | 6 (call top.materialize! %₂ %₅) 277 | 7 (return %₆) 278 | 279 | ######################################## 280 | # Updating assignment with basic ref as left hand side 281 | x[i] += y 282 | #--------------------- 283 | 1 TestMod.+ 284 | 2 TestMod.x 285 | 3 TestMod.i 286 | 4 (call top.getindex %₂ %₃) 287 | 5 TestMod.y 288 | 6 (call %₁ %₄ %₅) 289 | 7 TestMod.x 290 | 8 TestMod.i 291 | 9 (call top.setindex! %₇ %₆ %₈) 292 | 10 (return %₆) 293 | 294 | ######################################## 295 | # Updating assignment with complex ref as left hand side 296 | g()[f(), end] += y 297 | #--------------------- 298 | 1 TestMod.g 299 | 2 (call %₁) 300 | 3 TestMod.f 301 | 4 (call %₃) 302 | 5 (call top.lastindex %₂ 2) 303 | 6 TestMod.+ 304 | 7 (call top.getindex %₂ %₄ %₅) 305 | 8 TestMod.y 306 | 9 (call %₆ %₇ %₈) 307 | 10 (call top.setindex! %₂ %₉ %₄ %₅) 308 | 11 (return %₉) 309 | 310 | ######################################## 311 | # Updating assignment with type assert on left hand side 312 | begin 313 | local x 314 | x::T += y 315 | end 316 | #--------------------- 317 | 1 TestMod.+ 318 | 2 slot₁/x 319 | 3 TestMod.T 320 | 4 (call core.typeassert %₂ %₃) 321 | 5 TestMod.y 322 | 6 (call %₁ %₄ %₅) 323 | 7 (= slot₁/x %₆) 324 | 8 (return %₆) 325 | 326 | ######################################## 327 | # Updating assignment with ref and type assert on left hand side 328 | begin 329 | local x 330 | x[f()]::T += y 331 | end 332 | #--------------------- 333 | 1 (newvar slot₁/x) 334 | 2 TestMod.f 335 | 3 (call %₂) 336 | 4 TestMod.+ 337 | 5 slot₁/x 338 | 6 (call top.getindex %₅ %₃) 339 | 7 TestMod.T 340 | 8 (call core.typeassert %₆ %₇) 341 | 9 TestMod.y 342 | 10 (call %₄ %₈ %₉) 343 | 11 slot₁/x 344 | 12 (call top.setindex! %₁₁ %₁₀ %₃) 345 | 13 (return %₁₀) 346 | 347 | ######################################## 348 | # Error: Updating assignment with invalid left hand side 349 | f() += y 350 | #--------------------- 351 | LoweringError: 352 | f() += y 353 | └─┘ ── invalid assignment location 354 | 355 | ######################################## 356 | # Error: Updating assignment with invalid tuple destructuring on left hand side 357 | (if false end, b) += 2 358 | #--------------------- 359 | LoweringError: 360 | (if false end, b) += 2 361 | └───────────────┘ ── invalid multiple assignment location 362 | -------------------------------------------------------------------------------- /test/typedefs.jl: -------------------------------------------------------------------------------- 1 | @testset "Type definitions" begin 2 | 3 | test_mod = Module(:TestMod) 4 | 5 | Base.eval(test_mod, :(struct XX{S,T,U,W} end)) 6 | 7 | @test JuliaLowering.include_string(test_mod, """ 8 | XX{Int, <:Integer, Float64, >:AbstractChar} 9 | """) == (test_mod.XX{Int, T, Float64, S} where {T <: Integer, S >: AbstractChar}) 10 | 11 | @test JuliaLowering.include_string(test_mod, """ 12 | abstract type A end 13 | """) === nothing 14 | @test supertype(test_mod.A) === Any 15 | @test isabstracttype(test_mod.A) 16 | 17 | @test JuliaLowering.include_string(test_mod, """ 18 | abstract type B <: A end 19 | """) === nothing 20 | @test supertype(test_mod.B) === test_mod.A 21 | 22 | @test JuliaLowering.include_string(test_mod, """ 23 | abstract type C{X} end 24 | """) === nothing 25 | 26 | @test JuliaLowering.include_string(test_mod, """ 27 | abstract type D{X<:A} end 28 | """) === nothing 29 | @test test_mod.D{test_mod.B} isa Type 30 | @test_throws Exception test_mod.D{Int} 31 | 32 | @test JuliaLowering.include_string(test_mod, """ 33 | abstract type E <: C{E} end 34 | """) === nothing 35 | @test test_mod.E isa Type 36 | 37 | @test JuliaLowering.include_string(test_mod, """ 38 | primitive type P <: A 16 end 39 | """) === nothing 40 | @test isconcretetype(test_mod.P) 41 | @test supertype(test_mod.P) === test_mod.A 42 | @test reinterpret(test_mod.P, 0x0001) isa test_mod.P 43 | @test reinterpret(UInt16, reinterpret(test_mod.P, 0x1337)) === 0x1337 44 | 45 | @test JuliaLowering.include_string(test_mod, """ 46 | struct S1{X,Y} <: A 47 | x::X 48 | y::Y 49 | z 50 | end 51 | """) === nothing 52 | @test !isconcretetype(test_mod.S1) 53 | @test fieldnames(test_mod.S1) == (:x, :y, :z) 54 | @test fieldtypes(test_mod.S1) == (Any, Any, Any) 55 | @test isconcretetype(test_mod.S1{Int,String}) 56 | @test fieldtypes(test_mod.S1{Int,String}) == (Int, String, Any) 57 | @test supertype(test_mod.S1) == test_mod.A 58 | 59 | # Inner constructors: one field non-Any 60 | @test JuliaLowering.include_string(test_mod, """ 61 | struct S2 62 | x::Int 63 | y 64 | end 65 | """) === nothing 66 | @test length(methods(test_mod.S2)) == 2 67 | let s = test_mod.S2(42, "hi") 68 | # exact types 69 | @test s.x === 42 70 | @test s.y == "hi" 71 | end 72 | let s = test_mod.S2(42.0, "hi") 73 | # converted types 74 | @test s.x === 42 75 | @test s.y == "hi" 76 | end 77 | 78 | # Constructors: All fields Any 79 | @test JuliaLowering.include_string(test_mod, """ 80 | struct S3 81 | x 82 | y 83 | end 84 | """) === nothing 85 | @test length(methods(test_mod.S3)) == 1 86 | let s = test_mod.S3(42, "hi") 87 | @test s.x === 42 88 | @test s.y == "hi" 89 | end 90 | 91 | # Inner constructors: All fields Any; dynamically tested against whatever 92 | # S4_Field resolves to 93 | @test JuliaLowering.include_string(test_mod, """ 94 | S4_Field = Any # actually Any! 95 | 96 | struct S4 97 | x::S4_Field 98 | y 99 | end 100 | """) === nothing 101 | @test length(methods(test_mod.S4)) == 1 102 | let s = test_mod.S4(42, "hi") 103 | @test s.x === 42 104 | @test s.y == "hi" 105 | end 106 | 107 | # Inner & outer constructors; parameterized types 108 | @test JuliaLowering.include_string(test_mod, """ 109 | struct S5{U} 110 | x::U 111 | y 112 | end 113 | """) === nothing 114 | @test length(methods(test_mod.S5)) == 1 115 | let s = test_mod.S5(42, "hi") 116 | @test s isa test_mod.S5{Int} 117 | @test s.x === 42 118 | @test s.y == "hi" 119 | end 120 | @test length(methods(test_mod.S5{Int})) == 1 121 | let s = test_mod.S5{Int}(42.0, "hi") 122 | @test s isa test_mod.S5{Int} 123 | @test s.x === 42 124 | @test s.y == "hi" 125 | end 126 | let s = test_mod.S5{Any}(42.0, "hi") 127 | @test s isa test_mod.S5{Any} 128 | @test s.x === 42.0 129 | @test s.y == "hi" 130 | end 131 | @test JuliaLowering.include_string(test_mod, """ 132 | function S5{Int}(x::Int) 133 | S5(x, x) 134 | end 135 | """) === nothing 136 | let s = test_mod.S5{Int}(1) 137 | @test s.x === 1 138 | @test s.y === 1 139 | @test s isa test_mod.S5{Int} 140 | end 141 | @test_throws MethodError test_mod.S5{Int}(1.1) 142 | @test JuliaLowering.include_string(test_mod, """ 143 | function S5{T}(x, y, z) where {T<:AbstractFloat} 144 | S5(x, x) 145 | end 146 | """) === nothing 147 | let s = test_mod.S5{Float64}(Float64(1.1), 0, 0) 148 | @test s.x === 1.1 149 | @test s.y === 1.1 150 | @test s isa test_mod.S5{Float64} 151 | end 152 | @test JuliaLowering.include_string(test_mod, """ 153 | S5{<:AbstractFloat}(x) = S5(x, x) 154 | """) === nothing 155 | let s = test_mod.S5{<:AbstractFloat}(Float64(1.1)) 156 | @test s.x === 1.1 157 | @test s.y === 1.1 158 | @test s isa test_mod.S5{Float64} 159 | end 160 | @test JuliaLowering.include_string(test_mod, """ 161 | S5{T}(x::T) where {T<:Real} = S5(x, x) 162 | """) === nothing 163 | let s = test_mod.S5{Real}(pi) 164 | @test s.x === pi 165 | @test s.y === pi 166 | @test s isa test_mod.S5{<:Real} 167 | end 168 | outer_mod = Module() 169 | @test JuliaLowering.include_string(test_mod, """ 170 | Base.Vector{T}(x::T) where {S5<:T<:S5} = T[x] 171 | """) === nothing 172 | let v = Base.Vector{test_mod.S5}(test_mod.S5(1,1)) 173 | @test v isa Vector{test_mod.S5} 174 | @test v[1] === test_mod.S5(1,1) 175 | end 176 | 177 | # User defined inner constructors and helper functions for structs without type params 178 | @test JuliaLowering.include_string(test_mod, """ 179 | struct S6 180 | x 181 | S6_f() = new(42) 182 | 183 | "some docs" 184 | S6() = S6_f() 185 | S6(x) = new(x) 186 | end 187 | """) === nothing 188 | let s = test_mod.S6() 189 | @test s isa test_mod.S6 190 | @test s.x === 42 191 | end 192 | let s = test_mod.S6(2) 193 | @test s isa test_mod.S6 194 | @test s.x === 2 195 | end 196 | @test docstrings_equal(@doc(test_mod.S6), Markdown.doc"some docs") 197 | 198 | # User defined inner constructors and helper functions for structs with type params 199 | @test JuliaLowering.include_string(test_mod, """ 200 | struct S7{S,T} 201 | x::S 202 | y 203 | 204 | # Cases where full struct type may be deduced and used in body 205 | S7{Int,String}() = new(10.0, "y1") 206 | S7{S,T}() where {S,T} = new(10.0, "y2") 207 | S7{Int,T}() where {T} = new(10.0, "y3") 208 | (::Type{S7{Int,UInt8}})() = new{Int,UInt8}(10.0, "y4") 209 | 210 | # Cases where new{...} is called 211 | S7() = new{Int,Int}(10.0, "y5") 212 | S7{UInt8}() = S7_f() 213 | S7_f() = new{UInt8,UInt8}(10.0, "y6") 214 | end 215 | """) === nothing 216 | let s = test_mod.S7{Int,String}() 217 | @test s isa test_mod.S7{Int,String} 218 | @test s.x === 10 219 | @test s.y === "y1" 220 | end 221 | let s = test_mod.S7{UInt16,UInt16}() 222 | @test s isa test_mod.S7{UInt16,UInt16} 223 | @test s.x === UInt16(10) 224 | @test s.y === "y2" 225 | end 226 | let s = test_mod.S7{Int,UInt16}() 227 | @test s isa test_mod.S7{Int,UInt16} 228 | @test s.x === 10 229 | @test s.y === "y3" 230 | end 231 | let s = test_mod.S7{Int,UInt8}() 232 | @test s isa test_mod.S7{Int,UInt8} 233 | @test s.x === 10 234 | @test s.y === "y4" 235 | end 236 | let s = test_mod.S7() 237 | @test s isa test_mod.S7{Int,Int} 238 | @test s.x === 10 239 | @test s.y === "y5" 240 | end 241 | let s = test_mod.S7{UInt8}() 242 | @test s isa test_mod.S7{UInt8,UInt8} 243 | @test s.x === UInt8(10) 244 | @test s.y === "y6" 245 | end 246 | 247 | # new() with splats and typed fields 248 | @test JuliaLowering.include_string(test_mod, """ 249 | struct S8 250 | x::Int 251 | y::Float64 252 | 253 | S8(xs, ys) = new(xs..., ys...) 254 | end 255 | """) === nothing 256 | let s = test_mod.S8((10.0,), (20,)) 257 | @test s isa test_mod.S8 258 | @test s.x === 10 259 | @test s.y === 20.0 260 | end 261 | # Wrong number of args checked by lowering 262 | @test_throws ArgumentError test_mod.S8((1,), ()) 263 | @test_throws ArgumentError test_mod.S8((1,2,3), ()) 264 | 265 | # new() with splats and untyped fields 266 | @test JuliaLowering.include_string(test_mod, """ 267 | struct S9 268 | x 269 | y 270 | 271 | S9(xs) = new(xs...) 272 | end 273 | """) === nothing 274 | let s = test_mod.S9((10.0,20)) 275 | @test s isa test_mod.S9 276 | @test s.x === 10.0 277 | @test s.y === 20 278 | end 279 | # Wrong number of args checked by the runtime 280 | @test_throws ArgumentError test_mod.S9((1,)) 281 | @test_throws ArgumentError test_mod.S9((1,2,3)) 282 | 283 | # Test cases from 284 | # https://github.com/JuliaLang/julia/issues/36104 285 | # https://github.com/JuliaLang/julia/pull/36121 286 | JuliaLowering.include_string(test_mod, """ 287 | # issue #36104 288 | module M36104 289 | struct T36104 290 | v::Vector{M36104.T36104} 291 | end 292 | struct T36104 # check that redefining it works, issue #21816 293 | v::Vector{T36104} 294 | end 295 | end 296 | """) 297 | @test fieldtypes(test_mod.M36104.T36104) == (Vector{test_mod.M36104.T36104},) 298 | @test_throws ErrorException("expected") JuliaLowering.include_string(test_mod, """struct X36104; x::error("expected"); end""") 299 | @test !isdefined(test_mod, :X36104) 300 | JuliaLowering.include_string(test_mod, "struct X36104; x::Int; end") 301 | @test fieldtypes(test_mod.X36104) == (Int,) 302 | JuliaLowering.include_string(test_mod, "primitive type P36104 8 end") 303 | JuliaLowering.include_string(test_mod, "const orig_P36104 = P36104") 304 | JuliaLowering.include_string(test_mod, "primitive type P36104 16 end") 305 | @test test_mod.P36104 !== test_mod.orig_P36104 306 | 307 | # Struct with outer constructor where one typevar is constrained by the other 308 | # See https://github.com/JuliaLang/julia/issues/27269) 309 | @test JuliaLowering.include_string(test_mod, """ 310 | struct X27269{T, S <: Vector{T}} 311 | v::Vector{S} 312 | end 313 | """) === nothing 314 | @test test_mod.X27269([[1,2]]) isa test_mod.X27269{Int, Vector{Int}} 315 | 316 | end 317 | -------------------------------------------------------------------------------- /test/generators_ir.jl: -------------------------------------------------------------------------------- 1 | ######################################## 2 | # Simple 1D generator 3 | (x+1 for x in xs) 4 | #--------------------- 5 | 1 (call core.svec) 6 | 2 (call core.svec) 7 | 3 (call JuliaLowering.eval_closure_type TestMod :#->##0 %₁ %₂) 8 | 4 latestworld 9 | 5 TestMod.#->##0 10 | 6 (call core.svec %₅ core.Any) 11 | 7 (call core.svec) 12 | 8 SourceLocation::1:2 13 | 9 (call core.svec %₆ %₇ %₈) 14 | 10 --- method core.nothing %₉ 15 | slots: [slot₁/#self#(!read) slot₂/x] 16 | 1 TestMod.+ 17 | 2 (call %₁ slot₂/x 1) 18 | 3 (return %₂) 19 | 11 latestworld 20 | 12 TestMod.#->##0 21 | 13 (new %₁₂) 22 | 14 TestMod.xs 23 | 15 (call top.Generator %₁₃ %₁₄) 24 | 16 (return %₁₅) 25 | 26 | ######################################## 27 | # Product iteration 28 | (x+y for x in xs, y in ys) 29 | #--------------------- 30 | 1 (call core.svec) 31 | 2 (call core.svec) 32 | 3 (call JuliaLowering.eval_closure_type TestMod :#->##1 %₁ %₂) 33 | 4 latestworld 34 | 5 TestMod.#->##1 35 | 6 (call core.svec %₅ core.Any) 36 | 7 (call core.svec) 37 | 8 SourceLocation::1:2 38 | 9 (call core.svec %₆ %₇ %₈) 39 | 10 --- method core.nothing %₉ 40 | slots: [slot₁/#self#(!read) slot₂/destructured_arg slot₃/iterstate slot₄/x slot₅/y] 41 | 1 (call top.indexed_iterate slot₂/destructured_arg 1) 42 | 2 (= slot₄/x (call core.getfield %₁ 1)) 43 | 3 (= slot₃/iterstate (call core.getfield %₁ 2)) 44 | 4 slot₃/iterstate 45 | 5 (call top.indexed_iterate slot₂/destructured_arg 2 %₄) 46 | 6 (= slot₅/y (call core.getfield %₅ 1)) 47 | 7 TestMod.+ 48 | 8 slot₄/x 49 | 9 slot₅/y 50 | 10 (call %₇ %₈ %₉) 51 | 11 (return %₁₀) 52 | 11 latestworld 53 | 12 TestMod.#->##1 54 | 13 (new %₁₂) 55 | 14 TestMod.xs 56 | 15 TestMod.ys 57 | 16 (call top.product %₁₄ %₁₅) 58 | 17 (call top.Generator %₁₃ %₁₆) 59 | 18 (return %₁₇) 60 | 61 | ######################################## 62 | # Use `identity` as the Generator function when possible eg in filters 63 | ((x,y) for (x,y) in iter if f(x)) 64 | #--------------------- 65 | 1 (call core.svec) 66 | 2 (call core.svec) 67 | 3 (call JuliaLowering.eval_closure_type TestMod :#->##2 %₁ %₂) 68 | 4 latestworld 69 | 5 TestMod.#->##2 70 | 6 (call core.svec %₅ core.Any) 71 | 7 (call core.svec) 72 | 8 SourceLocation::1:29 73 | 9 (call core.svec %₆ %₇ %₈) 74 | 10 --- method core.nothing %₉ 75 | slots: [slot₁/#self#(!read) slot₂/destructured_arg slot₃/iterstate slot₄/x slot₅/y(!read)] 76 | 1 (call top.indexed_iterate slot₂/destructured_arg 1) 77 | 2 (= slot₄/x (call core.getfield %₁ 1)) 78 | 3 (= slot₃/iterstate (call core.getfield %₁ 2)) 79 | 4 slot₃/iterstate 80 | 5 (call top.indexed_iterate slot₂/destructured_arg 2 %₄) 81 | 6 (= slot₅/y (call core.getfield %₅ 1)) 82 | 7 TestMod.f 83 | 8 slot₄/x 84 | 9 (call %₇ %₈) 85 | 10 (return %₉) 86 | 11 latestworld 87 | 12 TestMod.#->##2 88 | 13 (new %₁₂) 89 | 14 TestMod.iter 90 | 15 (call top.Filter %₁₃ %₁₄) 91 | 16 (call top.Generator top.identity %₁₅) 92 | 17 (return %₁₆) 93 | 94 | ######################################## 95 | # Use of placeholders in iteration vars 96 | (1 for _ in xs) 97 | #--------------------- 98 | 1 (call core.svec) 99 | 2 (call core.svec) 100 | 3 (call JuliaLowering.eval_closure_type TestMod :#->##3 %₁ %₂) 101 | 4 latestworld 102 | 5 TestMod.#->##3 103 | 6 (call core.svec %₅ core.Any) 104 | 7 (call core.svec) 105 | 8 SourceLocation::1:2 106 | 9 (call core.svec %₆ %₇ %₈) 107 | 10 --- method core.nothing %₉ 108 | slots: [slot₁/#self#(!read) slot₂/_(!read)] 109 | 1 (return 1) 110 | 11 latestworld 111 | 12 TestMod.#->##3 112 | 13 (new %₁₂) 113 | 14 TestMod.xs 114 | 15 (call top.Generator %₁₃ %₁₄) 115 | 16 (return %₁₅) 116 | 117 | ######################################## 118 | # Error: Use of placeholders in body 119 | (_ for _ in xs) 120 | #--------------------- 121 | LoweringError: 122 | (_ for _ in xs) 123 | #╙ ── all-underscore identifiers are write-only and their values cannot be used in expressions 124 | 125 | ######################################## 126 | # 1D generator with destructuring 127 | (body for (x,_,y) in iter) 128 | #--------------------- 129 | 1 (call core.svec) 130 | 2 (call core.svec) 131 | 3 (call JuliaLowering.eval_closure_type TestMod :#->##5 %₁ %₂) 132 | 4 latestworld 133 | 5 TestMod.#->##5 134 | 6 (call core.svec %₅ core.Any) 135 | 7 (call core.svec) 136 | 8 SourceLocation::1:2 137 | 9 (call core.svec %₆ %₇ %₈) 138 | 10 --- method core.nothing %₉ 139 | slots: [slot₁/#self#(!read) slot₂/destructured_arg slot₃/iterstate slot₄/x(!read) slot₅/y(!read)] 140 | 1 (call top.indexed_iterate slot₂/destructured_arg 1) 141 | 2 (= slot₄/x (call core.getfield %₁ 1)) 142 | 3 (= slot₃/iterstate (call core.getfield %₁ 2)) 143 | 4 slot₃/iterstate 144 | 5 (call top.indexed_iterate slot₂/destructured_arg 2 %₄) 145 | 6 (call core.getfield %₅ 1) 146 | 7 (= slot₃/iterstate (call core.getfield %₅ 2)) 147 | 8 slot₃/iterstate 148 | 9 (call top.indexed_iterate slot₂/destructured_arg 3 %₈) 149 | 10 (= slot₅/y (call core.getfield %₉ 1)) 150 | 11 TestMod.body 151 | 12 (return %₁₁) 152 | 11 latestworld 153 | 12 TestMod.#->##5 154 | 13 (new %₁₂) 155 | 14 TestMod.iter 156 | 15 (call top.Generator %₁₃ %₁₄) 157 | 16 (return %₁₅) 158 | 159 | ######################################## 160 | # return permitted in quoted syntax in generator 161 | (:(return x) for _ in iter) 162 | #--------------------- 163 | 1 (call core.svec) 164 | 2 (call core.svec) 165 | 3 (call JuliaLowering.eval_closure_type TestMod :#->##6 %₁ %₂) 166 | 4 latestworld 167 | 5 TestMod.#->##6 168 | 6 (call core.svec %₅ core.Any) 169 | 7 (call core.svec) 170 | 8 SourceLocation::1:4 171 | 9 (call core.svec %₆ %₇ %₈) 172 | 10 --- method core.nothing %₉ 173 | slots: [slot₁/#self#(!read) slot₂/_(!read)] 174 | 1 (call JuliaLowering.interpolate_ast SyntaxTree (inert (return x))) 175 | 2 (return %₁) 176 | 11 latestworld 177 | 12 TestMod.#->##6 178 | 13 (new %₁₂) 179 | 14 TestMod.iter 180 | 15 (call top.Generator %₁₃ %₁₄) 181 | 16 (return %₁₅) 182 | 183 | ######################################## 184 | # Error: `return` not permitted in generator body 185 | ((return x) + y for x in iter) 186 | #--------------------- 187 | LoweringError: 188 | ((return x) + y for x in iter) 189 | # └──────┘ ── `return` not allowed inside comprehension or generator 190 | 191 | ######################################## 192 | # Nested case with duplicate iteration variables 193 | (x for x in 1:3 for x in 1:2) 194 | #--------------------- 195 | 1 (call core.svec) 196 | 2 (call core.svec) 197 | 3 (call JuliaLowering.eval_closure_type TestMod :#->##7 %₁ %₂) 198 | 4 latestworld 199 | 5 (call core.svec) 200 | 6 (call core.svec) 201 | 7 (call JuliaLowering.eval_closure_type TestMod :#->#->##0 %₅ %₆) 202 | 8 latestworld 203 | 9 TestMod.#->#->##0 204 | 10 (call core.svec %₉ core.Any) 205 | 11 (call core.svec) 206 | 12 SourceLocation::1:2 207 | 13 (call core.svec %₁₀ %₁₁ %₁₂) 208 | 14 --- method core.nothing %₁₃ 209 | slots: [slot₁/#self#(!read) slot₂/x slot₃/x] 210 | 1 slot₂/x 211 | 2 (= slot₃/x %₁) 212 | 3 slot₃/x 213 | 4 (return %₃) 214 | 15 latestworld 215 | 16 TestMod.#->##7 216 | 17 (call core.svec %₁₆ core.Any) 217 | 18 (call core.svec) 218 | 19 SourceLocation::1:2 219 | 20 (call core.svec %₁₇ %₁₈ %₁₉) 220 | 21 --- method core.nothing %₂₀ 221 | slots: [slot₁/#self#(!read) slot₂/x(!read)] 222 | 1 TestMod.#->#->##0 223 | 2 (new %₁) 224 | 3 TestMod.: 225 | 4 (call %₃ 1 2) 226 | 5 (call top.Generator %₂ %₄) 227 | 6 (return %₅) 228 | 22 latestworld 229 | 23 TestMod.#->##7 230 | 24 (new %₂₃) 231 | 25 TestMod.: 232 | 26 (call %₂₅ 1 3) 233 | 27 (call top.Generator %₂₄ %₂₆) 234 | 28 (call top.Flatten %₂₇) 235 | 29 (return %₂₈) 236 | 237 | ######################################## 238 | # Comprehension lowers to generator with collect 239 | [x for x in xs] 240 | #--------------------- 241 | 1 TestMod.xs 242 | 2 (call top.Generator top.identity %₁) 243 | 3 (call top.collect %₂) 244 | 4 (return %₃) 245 | 246 | ######################################## 247 | # Simple typed comprehension lowers to for loop 248 | T[(x,y) for x in xs, y in ys] 249 | #--------------------- 250 | 1 TestMod.xs 251 | 2 TestMod.ys 252 | 3 (call top.product %₁ %₂) 253 | 4 (call top.IteratorSize %₃) 254 | 5 (call core.isa %₄ top.SizeUnknown) 255 | 6 TestMod.T 256 | 7 (call top._array_for %₆ %₃ %₄) 257 | 8 (call top.LinearIndices %₇) 258 | 9 (= slot₁/idx (call top.first %₈)) 259 | 10 (= slot₃/next (call top.iterate %₂)) 260 | 11 slot₃/next 261 | 12 (call core.=== %₁₁ core.nothing) 262 | 13 (call top.not_int %₁₂) 263 | 14 (gotoifnot %₁₃ label₅₀) 264 | 15 slot₃/next 265 | 16 (= slot₄/y (call core.getfield %₁₅ 1)) 266 | 17 (call core.getfield %₁₅ 2) 267 | 18 (= slot₂/next (call top.iterate %₁)) 268 | 19 slot₂/next 269 | 20 (call core.=== %₁₉ core.nothing) 270 | 21 (call top.not_int %₂₀) 271 | 22 (gotoifnot %₂₁ label₄₄) 272 | 23 slot₄/y 273 | 24 (= slot₆/y %₂₃) 274 | 25 slot₂/next 275 | 26 (= slot₅/x (call core.getfield %₂₅ 1)) 276 | 27 (call core.getfield %₂₅ 2) 277 | 28 slot₅/x 278 | 29 slot₆/y 279 | 30 (call core.tuple %₂₈ %₂₉) 280 | 31 (gotoifnot %₅ label₃₄) 281 | 32 (call top.push! %₇ %₃₀) 282 | 33 (goto label₃₆) 283 | 34 slot₁/idx 284 | 35 (call top.setindex! %₇ %₃₀ %₃₄) 285 | 36 slot₁/idx 286 | 37 (= slot₁/idx (call top.add_int %₃₆ 1)) 287 | 38 (= slot₂/next (call top.iterate %₁ %₂₇)) 288 | 39 slot₂/next 289 | 40 (call core.=== %₃₉ core.nothing) 290 | 41 (call top.not_int %₄₀) 291 | 42 (gotoifnot %₄₁ label₄₄) 292 | 43 (goto label₂₃) 293 | 44 (= slot₃/next (call top.iterate %₂ %₁₇)) 294 | 45 slot₃/next 295 | 46 (call core.=== %₄₅ core.nothing) 296 | 47 (call top.not_int %₄₆) 297 | 48 (gotoifnot %₄₇ label₅₀) 298 | 49 (goto label₁₅) 299 | 50 (return %₇) 300 | -------------------------------------------------------------------------------- /src/bindings.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Metadata about a binding 3 | """ 4 | struct BindingInfo 5 | id::IdTag # Unique integer identifying this binding 6 | name::String 7 | kind::Symbol # :local :global :argument :static_parameter 8 | node_id::Int # ID of associated K"BindingId" node in the syntax graph 9 | mod::Union{Nothing,Module} # Set when `kind === :global` 10 | type::Union{Nothing,SyntaxTree} # Type, for bindings declared like x::T = 10 11 | n_assigned::Int32 # Number of times variable is assigned to 12 | is_const::Bool # Constant, cannot be reassigned 13 | is_ssa::Bool # Single assignment, defined before use 14 | is_captured::Bool # Variable is captured by some lambda 15 | is_always_defined::Bool # A local that we know has an assignment that dominates all usages (is never undef) 16 | is_internal::Bool # True for internal bindings generated by the compiler 17 | is_ambiguous_local::Bool # Local, but would be global in soft scope (ie, the REPL) 18 | is_nospecialize::Bool # @nospecialize on this argument (only valid for kind == :argument) 19 | end 20 | 21 | function BindingInfo(id::IdTag, name::AbstractString, kind::Symbol, node_id::Integer; 22 | mod::Union{Nothing,Module} = nothing, 23 | type::Union{Nothing,SyntaxTree} = nothing, 24 | n_assigned::Integer = 0, 25 | is_const::Bool = false, 26 | is_ssa::Bool = false, 27 | is_captured::Bool = false, 28 | is_always_defined::Bool = is_ssa, 29 | is_internal::Bool = false, 30 | is_ambiguous_local::Bool = false, 31 | is_nospecialize::Bool = false) 32 | BindingInfo(id, name, kind, node_id, mod, type, n_assigned, is_const, 33 | is_ssa, is_captured, is_always_defined, 34 | is_internal, is_ambiguous_local, is_nospecialize) 35 | end 36 | 37 | function Base.show(io::IO, binfo::BindingInfo) 38 | print(io, "BindingInfo(", binfo.id, ", ", 39 | repr(binfo.name), ", ", 40 | repr(binfo.kind), ", ", 41 | binfo.node_id) 42 | if !isnothing(binfo.mod) 43 | print(io, ", mod=", binfo.mod) 44 | end 45 | if !isnothing(binfo.type) 46 | print(io, ", type=", binfo.type) 47 | end 48 | if binfo.n_assigned != 0 49 | print(io, ", n_assigned=", binfo.n_assigned) 50 | end 51 | if binfo.is_const 52 | print(io, ", is_const=", binfo.is_const) 53 | end 54 | if binfo.is_ssa 55 | print(io, ", is_ssa=", binfo.is_ssa) 56 | end 57 | if binfo.is_captured 58 | print(io, ", is_captured=", binfo.is_captured) 59 | end 60 | if binfo.is_always_defined != binfo.is_ssa 61 | print(io, ", is_always_defined=", binfo.is_always_defined) 62 | end 63 | if binfo.is_internal 64 | print(io, ", is_internal=", binfo.is_internal) 65 | end 66 | if binfo.is_ambiguous_local 67 | print(io, ", is_ambiguous_local=", binfo.is_ambiguous_local) 68 | end 69 | if binfo.is_nospecialize 70 | print(io, ", is_nospecialize=", binfo.is_nospecialize) 71 | end 72 | print(io, ")") 73 | end 74 | 75 | """ 76 | Metadata about "entities" (variables, constants, etc) in the program. Each 77 | entity is associated to a unique integer id, the BindingId. A binding will be 78 | inferred for each *name* in the user's source program by symbolic analysis of 79 | the source. 80 | 81 | However, bindings can also be introduced programmatically during lowering or 82 | macro expansion: the primary key for bindings is the `BindingId` integer, not 83 | a name. 84 | """ 85 | struct Bindings 86 | info::Vector{BindingInfo} 87 | end 88 | 89 | Bindings() = Bindings(Vector{BindingInfo}()) 90 | 91 | next_binding_id(bindings::Bindings) = length(bindings.info) + 1 92 | 93 | function add_binding(bindings::Bindings, binding) 94 | if next_binding_id(bindings) != binding.id 95 | error("Use next_binding_id() to create a valid binding id") 96 | end 97 | push!(bindings.info, binding) 98 | end 99 | 100 | function _binding_id(id::Integer) 101 | id 102 | end 103 | 104 | function _binding_id(ex::SyntaxTree) 105 | @chk kind(ex) == K"BindingId" 106 | ex.var_id 107 | end 108 | 109 | function update_binding!(bindings::Bindings, x; 110 | type=nothing, is_const=nothing, add_assigned=0, 111 | is_always_defined=nothing, is_captured=nothing) 112 | id = _binding_id(x) 113 | b = lookup_binding(bindings, id) 114 | bindings.info[id] = BindingInfo( 115 | b.id, 116 | b.name, 117 | b.kind, 118 | b.node_id, 119 | b.mod, 120 | isnothing(type) ? b.type : type, 121 | b.n_assigned + add_assigned, 122 | isnothing(is_const) ? b.is_const : is_const, 123 | b.is_ssa, 124 | isnothing(is_captured) ? b.is_captured : is_captured, 125 | isnothing(is_always_defined) ? b.is_always_defined : is_always_defined, 126 | b.is_internal, 127 | b.is_ambiguous_local, 128 | b.is_nospecialize 129 | ) 130 | end 131 | 132 | function lookup_binding(bindings::Bindings, x) 133 | bindings.info[_binding_id(x)] 134 | end 135 | 136 | function lookup_binding(ctx::AbstractLoweringContext, x) 137 | lookup_binding(ctx.bindings, x) 138 | end 139 | 140 | function update_binding!(ctx::AbstractLoweringContext, x; kws...) 141 | update_binding!(ctx.bindings, x; kws...) 142 | end 143 | 144 | function new_binding(ctx::AbstractLoweringContext, srcref::SyntaxTree, 145 | name::AbstractString, kind::Symbol; kws...) 146 | binding_id = next_binding_id(ctx.bindings) 147 | ex = @ast ctx srcref binding_id::K"BindingId" 148 | add_binding(ctx.bindings, BindingInfo(binding_id, name, kind, ex._id; kws...)) 149 | ex 150 | end 151 | 152 | # Create a new SSA binding 153 | function ssavar(ctx::AbstractLoweringContext, srcref, name="tmp") 154 | nameref = makeleaf(ctx, srcref, K"Identifier", name_val=name) 155 | new_binding(ctx, nameref, name, :local; is_ssa=true, is_internal=true) 156 | end 157 | 158 | # Create a new local mutable binding or lambda argument 159 | function new_local_binding(ctx::AbstractLoweringContext, srcref, name; kind=:local, kws...) 160 | @assert kind === :local || kind === :argument 161 | nameref = makeleaf(ctx, srcref, K"Identifier", name_val=name) 162 | ex = new_binding(ctx, nameref, name, kind; is_internal=true, kws...) 163 | lbindings = current_lambda_bindings(ctx) 164 | if !isnothing(lbindings) 165 | init_lambda_binding(lbindings, ex.var_id) 166 | end 167 | ex 168 | end 169 | 170 | function new_global_binding(ctx::AbstractLoweringContext, srcref, name, mod; kws...) 171 | nameref = makeleaf(ctx, srcref, K"Identifier", name_val=name) 172 | new_binding(ctx, nameref, name, :global; is_internal=true, mod=mod, kws...) 173 | end 174 | 175 | function binding_ex(ctx::AbstractLoweringContext, id::IdTag) 176 | # Reconstruct the SyntaxTree for this binding. We keep only the node_id 177 | # here, because that's got a concrete type. Whereas if we stored SyntaxTree 178 | # that would contain the type of the graph used in the pass where the 179 | # bindings were created and we'd need to call reparent(), etc. 180 | SyntaxTree(syntax_graph(ctx), lookup_binding(ctx, id).node_id) 181 | end 182 | 183 | 184 | #------------------------------------------------------------------------------- 185 | """ 186 | Metadata about how a binding is used within some enclosing lambda 187 | """ 188 | struct LambdaBindingInfo 189 | is_captured::Bool 190 | is_read::Bool 191 | is_assigned::Bool 192 | # Binding was the function name in a call. Used for specialization 193 | # heuristics in the optimizer. 194 | is_called::Bool 195 | end 196 | 197 | LambdaBindingInfo() = LambdaBindingInfo(false, false, false, false) 198 | 199 | function LambdaBindingInfo(parent::LambdaBindingInfo; 200 | is_captured = nothing, 201 | is_read = nothing, 202 | is_assigned = nothing, 203 | is_called = nothing) 204 | LambdaBindingInfo( 205 | isnothing(is_captured) ? parent.is_captured : is_captured, 206 | isnothing(is_read) ? parent.is_read : is_read, 207 | isnothing(is_assigned) ? parent.is_assigned : is_assigned, 208 | isnothing(is_called) ? parent.is_called : is_called, 209 | ) 210 | end 211 | 212 | struct LambdaBindings 213 | # Bindings used within the lambda 214 | self::IdTag 215 | bindings::Dict{IdTag,LambdaBindingInfo} 216 | end 217 | 218 | LambdaBindings(self::IdTag = 0) = LambdaBindings(self, Dict{IdTag,LambdaBindings}()) 219 | 220 | function init_lambda_binding(bindings::LambdaBindings, id; kws...) 221 | @assert !haskey(bindings.bindings, id) 222 | bindings.bindings[id] = LambdaBindingInfo(LambdaBindingInfo(); kws...) 223 | end 224 | 225 | function update_lambda_binding!(bindings::LambdaBindings, x; kws...) 226 | id = _binding_id(x) 227 | binfo = bindings.bindings[id] 228 | bindings.bindings[id] = LambdaBindingInfo(binfo; kws...) 229 | end 230 | 231 | function update_lambda_binding!(ctx::AbstractLoweringContext, x; kws...) 232 | update_lambda_binding!(current_lambda_bindings(ctx), x; kws...) 233 | end 234 | 235 | function lookup_lambda_binding(bindings::LambdaBindings, x) 236 | get(bindings.bindings, _binding_id(x), nothing) 237 | end 238 | 239 | function lookup_lambda_binding(ctx::AbstractLoweringContext, x) 240 | lookup_lambda_binding(current_lambda_bindings(ctx), x) 241 | end 242 | 243 | function has_lambda_binding(bindings::LambdaBindings, x) 244 | haskey(bindings.bindings, _binding_id(x)) 245 | end 246 | 247 | function has_lambda_binding(ctx::AbstractLoweringContext, x) 248 | has_lambda_binding(current_lambda_bindings(ctx), x) 249 | end 250 | -------------------------------------------------------------------------------- /test/destructuring_ir.jl: -------------------------------------------------------------------------------- 1 | ######################################## 2 | # Simple destructuring 3 | let 4 | (x,y) = as 5 | end 6 | #--------------------- 7 | 1 TestMod.as 8 | 2 (call top.indexed_iterate %₁ 1) 9 | 3 (= slot₂/x (call core.getfield %₂ 1)) 10 | 4 (= slot₁/iterstate (call core.getfield %₂ 2)) 11 | 5 TestMod.as 12 | 6 slot₁/iterstate 13 | 7 (call top.indexed_iterate %₅ 2 %₆) 14 | 8 (= slot₃/y (call core.getfield %₇ 1)) 15 | 9 TestMod.as 16 | 10 (return %₉) 17 | 18 | ######################################## 19 | # Trivial slurping 20 | let 21 | (xs...,) = as 22 | end 23 | #--------------------- 24 | 1 TestMod.as 25 | 2 (= slot₁/xs (call top.rest %₁)) 26 | 3 TestMod.as 27 | 4 (return %₃) 28 | 29 | ######################################## 30 | # Slurping last arg 31 | let 32 | (x, ys...) = as 33 | end 34 | #--------------------- 35 | 1 TestMod.as 36 | 2 (call top.indexed_iterate %₁ 1) 37 | 3 (= slot₂/x (call core.getfield %₂ 1)) 38 | 4 (= slot₁/iterstate (call core.getfield %₂ 2)) 39 | 5 TestMod.as 40 | 6 slot₁/iterstate 41 | 7 (= slot₃/ys (call top.rest %₅ %₆)) 42 | 8 TestMod.as 43 | 9 (return %₈) 44 | 45 | ######################################## 46 | # Slurping, first arg 47 | let 48 | (xs..., y, z) = as 49 | end 50 | #--------------------- 51 | 1 TestMod.as 52 | 2 (call top.split_rest %₁ 2) 53 | 3 (= slot₂/xs (call core.getfield %₂ 1)) 54 | 4 (call core.getfield %₂ 2) 55 | 5 (call top.indexed_iterate %₄ 1) 56 | 6 (= slot₃/y (call core.getfield %₅ 1)) 57 | 7 (= slot₁/iterstate (call core.getfield %₅ 2)) 58 | 8 slot₁/iterstate 59 | 9 (call top.indexed_iterate %₄ 2 %₈) 60 | 10 (= slot₄/z (call core.getfield %₉ 1)) 61 | 11 TestMod.as 62 | 12 (return %₁₁) 63 | 64 | ######################################## 65 | # Slurping, middle arg 66 | let 67 | (x, ys..., z) = as 68 | end 69 | #--------------------- 70 | 1 TestMod.as 71 | 2 (call top.indexed_iterate %₁ 1) 72 | 3 (= slot₂/x (call core.getfield %₂ 1)) 73 | 4 (= slot₁/iterstate (call core.getfield %₂ 2)) 74 | 5 TestMod.as 75 | 6 slot₁/iterstate 76 | 7 (call top.split_rest %₅ 1 %₆) 77 | 8 (= slot₃/ys (call core.getfield %₇ 1)) 78 | 9 (call core.getfield %₇ 2) 79 | 10 (call top.indexed_iterate %₉ 1) 80 | 11 (= slot₄/z (call core.getfield %₁₀ 1)) 81 | 12 TestMod.as 82 | 13 (return %₁₂) 83 | 84 | ######################################## 85 | # Error: Slurping multiple args 86 | (xs..., ys...) = x 87 | #--------------------- 88 | LoweringError: 89 | (xs..., ys...) = x 90 | # └────┘ ── multiple `...` in destructuring assignment are ambiguous 91 | 92 | ######################################## 93 | # Recursive destructuring 94 | let 95 | ((x,y), (z,w)) = as 96 | end 97 | #--------------------- 98 | 1 TestMod.as 99 | 2 (call top.indexed_iterate %₁ 1) 100 | 3 (call core.getfield %₂ 1) 101 | 4 (= slot₁/iterstate (call core.getfield %₂ 2)) 102 | 5 TestMod.as 103 | 6 slot₁/iterstate 104 | 7 (call top.indexed_iterate %₅ 2 %₆) 105 | 8 (call core.getfield %₇ 1) 106 | 9 (call top.indexed_iterate %₃ 1) 107 | 10 (= slot₅/x (call core.getfield %₉ 1)) 108 | 11 (= slot₂/iterstate (call core.getfield %₉ 2)) 109 | 12 slot₂/iterstate 110 | 13 (call top.indexed_iterate %₃ 2 %₁₂) 111 | 14 (= slot₆/y (call core.getfield %₁₃ 1)) 112 | 15 (call top.indexed_iterate %₈ 1) 113 | 16 (= slot₇/z (call core.getfield %₁₅ 1)) 114 | 17 (= slot₃/iterstate (call core.getfield %₁₅ 2)) 115 | 18 slot₃/iterstate 116 | 19 (call top.indexed_iterate %₈ 2 %₁₈) 117 | 20 (= slot₄/w (call core.getfield %₁₉ 1)) 118 | 21 TestMod.as 119 | 22 (return %₂₁) 120 | 121 | ######################################## 122 | # Recursive destructuring with slurping 123 | let 124 | ((x,ys...), z) = as 125 | end 126 | #--------------------- 127 | 1 TestMod.as 128 | 2 (call top.indexed_iterate %₁ 1) 129 | 3 (call core.getfield %₂ 1) 130 | 4 (= slot₁/iterstate (call core.getfield %₂ 2)) 131 | 5 TestMod.as 132 | 6 slot₁/iterstate 133 | 7 (call top.indexed_iterate %₅ 2 %₆) 134 | 8 (= slot₅/z (call core.getfield %₇ 1)) 135 | 9 (call top.indexed_iterate %₃ 1) 136 | 10 (= slot₃/x (call core.getfield %₉ 1)) 137 | 11 (= slot₂/iterstate (call core.getfield %₉ 2)) 138 | 12 slot₂/iterstate 139 | 13 (= slot₄/ys (call top.rest %₃ %₁₂)) 140 | 14 TestMod.as 141 | 15 (return %₁₄) 142 | 143 | ######################################## 144 | # Destructuring with simple tuple elimination 145 | let 146 | (x, y) = (a, b) 147 | end 148 | #--------------------- 149 | 1 TestMod.a 150 | 2 TestMod.b 151 | 3 (= slot₁/x %₁) 152 | 4 (= slot₂/y %₂) 153 | 5 (call core.tuple %₁ %₂) 154 | 6 (return %₅) 155 | 156 | ######################################## 157 | # Destructuring with tuple elimination where variables are repeated 158 | let 159 | (x, y, z) = (y, a, x) 160 | end 161 | #--------------------- 162 | 1 slot₂/y 163 | 2 TestMod.a 164 | 3 slot₁/x 165 | 4 (= slot₁/x %₁) 166 | 5 (= slot₂/y %₂) 167 | 6 (= slot₃/z %₃) 168 | 7 (call core.tuple %₁ %₂ %₃) 169 | 8 (return %₇) 170 | 171 | ######################################## 172 | # Destructuring with simple tuple elimination and rhs with side effects 173 | let 174 | (x, y) = (f(), b) 175 | end 176 | #--------------------- 177 | 1 TestMod.f 178 | 2 (call %₁) 179 | 3 TestMod.b 180 | 4 (= slot₁/x %₂) 181 | 5 (= slot₂/y %₃) 182 | 6 (call core.tuple %₂ %₃) 183 | 7 (return %₆) 184 | 185 | ######################################## 186 | # Destructuring with simple tuple elimination and lhs with side effects 187 | let 188 | (x[10], y[20]) = (1,2) 189 | end 190 | #--------------------- 191 | 1 1 192 | 2 TestMod.x 193 | 3 (call top.setindex! %₂ %₁ 10) 194 | 4 2 195 | 5 TestMod.y 196 | 6 (call top.setindex! %₅ %₄ 20) 197 | 7 (call core.tuple 1 2) 198 | 8 (return %₇) 199 | 200 | ######################################## 201 | # Destructuring with tuple elimination and trailing rhs ... 202 | let 203 | (x, y) = (a, rhs...) 204 | end 205 | #--------------------- 206 | 1 TestMod.a 207 | 2 TestMod.rhs 208 | 3 (= slot₁/x %₁) 209 | 4 (call top.indexed_iterate %₂ 1) 210 | 5 (= slot₂/y (call core.getfield %₄ 1)) 211 | 6 (call core.tuple %₁) 212 | 7 (call core._apply_iterate top.iterate core.tuple %₆ %₂) 213 | 8 (return %₇) 214 | 215 | ######################################## 216 | # Destructuring with with non-trailing rhs `...` does not use tuple elimination 217 | # (though we could do it for the `x = a` part here) 218 | let 219 | (x, y, z) = (a, rhs..., b) 220 | end 221 | #--------------------- 222 | 1 TestMod.a 223 | 2 (call core.tuple %₁) 224 | 3 TestMod.rhs 225 | 4 TestMod.b 226 | 5 (call core.tuple %₄) 227 | 6 (call core._apply_iterate top.iterate core.tuple %₂ %₃ %₅) 228 | 7 (call top.indexed_iterate %₆ 1) 229 | 8 (= slot₂/x (call core.getfield %₇ 1)) 230 | 9 (= slot₁/iterstate (call core.getfield %₇ 2)) 231 | 10 slot₁/iterstate 232 | 11 (call top.indexed_iterate %₆ 2 %₁₀) 233 | 12 (= slot₃/y (call core.getfield %₁₁ 1)) 234 | 13 (= slot₁/iterstate (call core.getfield %₁₁ 2)) 235 | 14 slot₁/iterstate 236 | 15 (call top.indexed_iterate %₆ 3 %₁₄) 237 | 16 (= slot₄/z (call core.getfield %₁₅ 1)) 238 | 17 (return %₆) 239 | 240 | ######################################## 241 | # Destructuring with tuple elimination and final ... on lhs 242 | let 243 | (x, ys...) = (a,b,c) 244 | end 245 | #--------------------- 246 | 1 TestMod.a 247 | 2 TestMod.b 248 | 3 TestMod.c 249 | 4 (= slot₁/x %₁) 250 | 5 (call core.tuple %₂ %₃) 251 | 6 (= slot₂/ys %₅) 252 | 7 (call core.tuple %₁ %₂ %₃) 253 | 8 (return %₇) 254 | 255 | ######################################## 256 | # Destructuring with tuple elimination, slurping, and completely effect free right hand sides 257 | let 258 | (x, ys...) = (1,2,3) 259 | end 260 | #--------------------- 261 | 1 (= slot₁/x 1) 262 | 2 (call core.tuple 2 3) 263 | 3 (= slot₂/ys %₂) 264 | 4 (call core.tuple 1 2 3) 265 | 5 (return %₄) 266 | 267 | ######################################## 268 | # Destructuring with tuple elimination and non-final ... on lhs 269 | let 270 | (x, ys..., z) = (a,b,c) 271 | end 272 | #--------------------- 273 | 1 TestMod.a 274 | 2 TestMod.b 275 | 3 TestMod.c 276 | 4 (= slot₁/x %₁) 277 | 5 (call core.tuple %₂) 278 | 6 (= slot₂/ys %₅) 279 | 7 (= slot₃/z %₃) 280 | 8 (call core.tuple %₁ %₂ %₃) 281 | 9 (return %₈) 282 | 283 | ######################################## 284 | # Error: Destructuring with tuple elimination and too few RHS elements 285 | (x,) = () 286 | #--------------------- 287 | LoweringError: 288 | (x,) = () 289 | └───────┘ ── More variables on left hand side than right hand in tuple assignment 290 | 291 | ######################################## 292 | # Error: Destructuring with tuple elimination, slurping, and too few RHS elements 293 | (x,y,ys...) = (1,) 294 | #--------------------- 295 | LoweringError: 296 | (x,y,ys...) = (1,) 297 | └────────────────┘ ── More variables on left hand side than right hand in tuple assignment 298 | 299 | ######################################## 300 | # Destructuring with tuple elimination but not in value position never creates 301 | # the tuple 302 | let 303 | (x, ys...) = (a,b,c) 304 | nothing 305 | end 306 | #--------------------- 307 | 1 TestMod.a 308 | 2 TestMod.b 309 | 3 TestMod.c 310 | 4 (= slot₁/x %₁) 311 | 5 (call core.tuple %₂ %₃) 312 | 6 (= slot₂/ys %₅) 313 | 7 TestMod.nothing 314 | 8 (return %₇) 315 | 316 | ######################################## 317 | # Property destructuring 318 | let 319 | (; x, y) = rhs 320 | end 321 | #--------------------- 322 | 1 TestMod.rhs 323 | 2 (= slot₁/x (call top.getproperty %₁ :x)) 324 | 3 (= slot₂/y (call top.getproperty %₁ :y)) 325 | 4 (return %₁) 326 | 327 | ######################################## 328 | # Property destructuring with colliding symbolic lhs/rhs 329 | let 330 | local x 331 | (; x, y) = x 332 | end 333 | #--------------------- 334 | 1 slot₁/x 335 | 2 (= slot₁/x (call top.getproperty %₁ :x)) 336 | 3 (= slot₂/y (call top.getproperty %₁ :y)) 337 | 4 (return %₁) 338 | 339 | ######################################## 340 | # Property destructuring with nontrivial rhs 341 | let 342 | (; x, y) = f() 343 | end 344 | #--------------------- 345 | 1 TestMod.f 346 | 2 (call %₁) 347 | 3 (= slot₁/x (call top.getproperty %₂ :x)) 348 | 4 (= slot₂/y (call top.getproperty %₂ :y)) 349 | 5 (return %₂) 350 | 351 | ######################################## 352 | # Property destructuring with type decl 353 | let 354 | (; x::T) = rhs 355 | end 356 | #--------------------- 357 | 1 (newvar slot₁/x) 358 | 2 TestMod.rhs 359 | 3 (call top.getproperty %₂ :x) 360 | 4 TestMod.T 361 | 5 (= slot₂/tmp %₃) 362 | 6 slot₂/tmp 363 | 7 (call core.isa %₆ %₄) 364 | 8 (gotoifnot %₇ label₁₀) 365 | 9 (goto label₁₃) 366 | 10 slot₂/tmp 367 | 11 (call top.convert %₄ %₁₀) 368 | 12 (= slot₂/tmp (call core.typeassert %₁₁ %₄)) 369 | 13 slot₂/tmp 370 | 14 (= slot₁/x %₁₃) 371 | 15 (return %₂) 372 | 373 | ######################################## 374 | # Error: Property destructuring with frankentuple 375 | (x ; a, b) = rhs 376 | #--------------------- 377 | LoweringError: 378 | (x ; a, b) = rhs 379 | └────────┘ ── Property destructuring must use a single `;` before the property names, eg `(; a, b) = rhs` 380 | 381 | ######################################## 382 | # Error: Property destructuring with values for properties 383 | (; a=1, b) = rhs 384 | #--------------------- 385 | LoweringError: 386 | (; a=1, b) = rhs 387 | # └─┘ ── invalid assignment location 388 | -------------------------------------------------------------------------------- /src/syntax_macros.jl: -------------------------------------------------------------------------------- 1 | # The following are versions of macros from Base which act as "standard syntax 2 | # extensions": 3 | # 4 | # * They emit syntactic forms with special `Kind`s and semantics known to 5 | # lowering 6 | # * There is no other Julia surface syntax for these `Kind`s. 7 | 8 | # In order to implement these here without getting into bootstrapping problems, 9 | # we just write them as plain old macro-named functions and add the required 10 | # __context__ argument ourselves. 11 | # 12 | # TODO: @inline, @noinline, @inbounds, @simd, @ccall, @assume_effects 13 | # 14 | # TODO: Eventually move these to proper `macro` definitions and use 15 | # `JuliaLowering.include()` or something. Then we'll be in the fun little world 16 | # of bootstrapping but it shouldn't be too painful :) 17 | 18 | function _apply_nospecialize(ctx, ex) 19 | k = kind(ex) 20 | if k == K"Identifier" || k == K"Placeholder" || k == K"tuple" 21 | setmeta(ex; nospecialize=true) 22 | elseif k == K"..." || k == K"::" || k == K"=" 23 | if k == K"::" && numchildren(ex) == 1 24 | ex = @ast ctx ex [K"::" "_"::K"Placeholder" ex[1]] 25 | end 26 | mapchildren(c->_apply_nospecialize(ctx, c), ctx, ex, 1:1) 27 | else 28 | throw(LoweringError(ex, "Invalid function argument")) 29 | end 30 | end 31 | 32 | function Base.var"@nospecialize"(__context__::MacroContext, ex, exs...) 33 | # TODO support multi-arg version properly 34 | _apply_nospecialize(__context__, ex) 35 | end 36 | 37 | function Base.var"@atomic"(__context__::MacroContext, ex) 38 | @chk kind(ex) == K"Identifier" || kind(ex) == K"::" (ex, "Expected identifier or declaration") 39 | @ast __context__ __context__.macrocall [K"atomic" ex] 40 | end 41 | 42 | function Base.var"@label"(__context__::MacroContext, ex) 43 | @chk kind(ex) == K"Identifier" 44 | @ast __context__ ex ex=>K"symbolic_label" 45 | end 46 | 47 | function Base.var"@goto"(__context__::MacroContext, ex) 48 | @chk kind(ex) == K"Identifier" 49 | @ast __context__ ex ex=>K"symbolic_goto" 50 | end 51 | 52 | function Base.var"@locals"(__context__::MacroContext) 53 | @ast __context__ __context__.macrocall [K"extension" "locals"::K"Symbol"] 54 | end 55 | 56 | function Base.var"@isdefined"(__context__::MacroContext, ex) 57 | @ast __context__ __context__.macrocall [K"isdefined" ex] 58 | end 59 | 60 | function Base.var"@generated"(__context__::MacroContext) 61 | @ast __context__ __context__.macrocall [K"generated"] 62 | end 63 | function Base.var"@generated"(__context__::MacroContext, ex) 64 | if kind(ex) != K"function" 65 | throw(LoweringError(ex, "Expected a function argument to `@generated`")) 66 | end 67 | @ast __context__ __context__.macrocall [K"function" 68 | ex[1] 69 | [K"if" [K"generated"] 70 | ex[2] 71 | [K"block" 72 | [K"meta" "generated_only"::K"Symbol"] 73 | [K"return"] 74 | ] 75 | ] 76 | ] 77 | end 78 | 79 | function Base.var"@cfunction"(__context__::MacroContext, callable, return_type, arg_types) 80 | if kind(arg_types) != K"tuple" 81 | throw(MacroExpansionError(arg_types, "@cfunction argument types must be a literal tuple")) 82 | end 83 | arg_types_svec = @ast __context__ arg_types [K"call" 84 | "svec"::K"core" 85 | children(arg_types)... 86 | ] 87 | if kind(callable) == K"$" 88 | fptr = callable[1] 89 | typ = Base.CFunction 90 | else 91 | # Kinda weird semantics here - without `$`, the callable is a top level 92 | # expression which will be evaluated by `jl_resolve_globals_in_ir`, 93 | # implicitly within the module where the `@cfunction` is expanded into. 94 | fptr = @ast __context__ callable [K"static_eval"( 95 | meta=name_hint("cfunction function name")) 96 | callable 97 | ] 98 | typ = Ptr{Cvoid} 99 | end 100 | @ast __context__ __context__.macrocall [K"cfunction" 101 | typ::K"Value" 102 | fptr 103 | [K"static_eval"(meta=name_hint("cfunction return type")) 104 | return_type 105 | ] 106 | [K"static_eval"(meta=name_hint("cfunction argument type")) 107 | arg_types_svec 108 | ] 109 | "ccall"::K"Symbol" 110 | ] 111 | end 112 | 113 | function ccall_macro_parse(ctx, ex, opts) 114 | gc_safe=false 115 | for opt in opts 116 | if kind(opt) != K"=" || numchildren(opt) != 2 || 117 | kind(opt[1]) != K"Identifier" 118 | throw(MacroExpansionError(opt, "Bad option to ccall")) 119 | else 120 | optname = opt[1].name_val 121 | if optname == "gc_safe" 122 | if kind(opt[2]) == K"Bool" 123 | gc_safe = opt[2].value::Bool 124 | else 125 | throw(MacroExpansionError(opt[2], "gc_safe must be true or false")) 126 | end 127 | else 128 | throw(MacroExpansionError(opt[1], "Unknown option name for ccall")) 129 | end 130 | end 131 | end 132 | 133 | if kind(ex) != K"::" 134 | throw(MacroExpansionError(ex, "Expected a return type annotation `::SomeType`", position=:end)) 135 | end 136 | 137 | rettype = ex[2] 138 | call = ex[1] 139 | if kind(call) != K"call" 140 | throw(MacroExpansionError(call, "Expected function call syntax `f()`")) 141 | end 142 | 143 | func = call[1] 144 | varargs = numchildren(call) > 1 && kind(call[end]) == K"parameters" ? 145 | children(call[end]) : nothing 146 | 147 | # collect args and types 148 | args = SyntaxList(ctx) 149 | types = SyntaxList(ctx) 150 | function pusharg!(arg) 151 | if kind(arg) != K"::" 152 | throw(MacroExpansionError(arg, "argument needs a type annotation")) 153 | end 154 | push!(args, arg[1]) 155 | push!(types, arg[2]) 156 | end 157 | 158 | for e in call[2:(isnothing(varargs) ? end : end-1)] 159 | kind(e) != K"parameters" || throw(MacroExpansionError(call[end], "Multiple parameter blocks not allowed")) 160 | pusharg!(e) 161 | end 162 | 163 | if !isnothing(varargs) 164 | num_required_args = length(args) 165 | if num_required_args == 0 166 | throw(MacroExpansionError(call[end], "C ABI prohibits varargs without one required argument")) 167 | end 168 | for e in varargs 169 | pusharg!(e) 170 | end 171 | else 172 | num_required_args = 0 # Non-vararg call 173 | end 174 | 175 | return func, rettype, types, args, gc_safe, num_required_args 176 | end 177 | 178 | function ccall_macro_lower(ctx, ex, convention, func, rettype, types, args, gc_safe, num_required_args) 179 | statements = SyntaxTree[] 180 | kf = kind(func) 181 | if kf == K"Identifier" 182 | lowered_func = @ast ctx func func=>K"Symbol" 183 | elseif kf == K"." 184 | lowered_func = @ast ctx func [K"tuple" 185 | func[2]=>K"Symbol" 186 | [K"static_eval"(meta=name_hint("@ccall library name")) 187 | func[1] 188 | ] 189 | ] 190 | elseif kf == K"$" 191 | check = @SyntaxTree quote 192 | func = $(func[1]) 193 | if !isa(func, Ptr{Cvoid}) 194 | name = :($(func[1])) 195 | throw(ArgumentError("interpolated function `$name` was not a `Ptr{Cvoid}`, but $(typeof(func))")) 196 | end 197 | end 198 | push!(statements, check) 199 | lowered_func = check[1][1] 200 | else 201 | throw(MacroExpansionError(func, 202 | "Function name must be a symbol like `foo`, a library and function name like `libc.printf` or an interpolated function pointer like `\$ptr`")) 203 | end 204 | 205 | roots = SyntaxTree[] 206 | cargs = SyntaxTree[] 207 | for (i, (type, arg)) in enumerate(zip(types, args)) 208 | argi = @ast ctx arg "arg$i"::K"Identifier" 209 | # TODO: Does it help to emit ssavar() here for the `argi`? 210 | push!(statements, @SyntaxTree :(local $argi = Base.cconvert($type, $arg))) 211 | push!(roots, argi) 212 | push!(cargs, @SyntaxTree :(Base.unsafe_convert($type, $argi))) 213 | end 214 | effect_flags = UInt16(0) 215 | push!(statements, @ast ctx ex [K"foreigncall" 216 | lowered_func 217 | [K"static_eval"(meta=name_hint("@ccall return type")) 218 | rettype 219 | ] 220 | [K"static_eval"(meta=name_hint("@ccall argument type")) 221 | [K"call" 222 | "svec"::K"core" 223 | types... 224 | ] 225 | ] 226 | num_required_args::K"Integer" 227 | QuoteNode((convention, effect_flags, gc_safe))::K"Value" 228 | cargs... 229 | roots... 230 | ]) 231 | 232 | @ast ctx ex [K"block" 233 | statements... 234 | ] 235 | end 236 | 237 | function Base.var"@ccall"(ctx::MacroContext, ex, opts...) 238 | ccall_macro_lower(ctx, ex, :ccall, ccall_macro_parse(ctx, ex, opts)...) 239 | end 240 | 241 | function Base.GC.var"@preserve"(__context__::MacroContext, exs...) 242 | idents = exs[1:end-1] 243 | for e in idents 244 | if kind(e) != K"Identifier" 245 | throw(MacroExpansionError(e, "Preserved variable must be a symbol")) 246 | end 247 | end 248 | @ast __context__ __context__.macrocall [K"block" 249 | [K"=" 250 | "s"::K"Identifier" 251 | [K"gc_preserve_begin" 252 | idents... 253 | ] 254 | ] 255 | [K"=" 256 | "r"::K"Identifier" 257 | exs[end] 258 | ] 259 | [K"gc_preserve_end" "s"::K"Identifier"] 260 | "r"::K"Identifier" 261 | ] 262 | end 263 | 264 | function Base.Experimental.var"@opaque"(__context__::MacroContext, ex) 265 | @chk kind(ex) == K"->" 266 | @ast __context__ __context__.macrocall [K"opaque_closure" 267 | "nothing"::K"core" 268 | "nothing"::K"core" 269 | "nothing"::K"core" 270 | true::K"Bool" 271 | ex 272 | ] 273 | end 274 | 275 | function _at_eval_code(ctx, srcref, mod, ex) 276 | @ast ctx srcref [K"block" 277 | [K"local" 278 | [K"=" 279 | "eval_result"::K"Identifier" 280 | [K"call" 281 | # TODO: Call "eval"::K"core" here 282 | JuliaLowering.eval::K"Value" 283 | mod 284 | [K"quote" ex] 285 | [K"parameters" 286 | [K"=" 287 | "expr_compat_mode"::K"Identifier" 288 | ctx.expr_compat_mode::K"Bool" 289 | ] 290 | ] 291 | ] 292 | ] 293 | ] 294 | (::K"latestworld_if_toplevel") 295 | "eval_result"::K"Identifier" 296 | ] 297 | end 298 | 299 | function Base.var"@eval"(__context__::MacroContext, ex) 300 | mod = @ast __context__ __context__.macrocall __context__.scope_layer.mod::K"Value" 301 | _at_eval_code(__context__, __context__.macrocall, mod, ex) 302 | end 303 | 304 | function Base.var"@eval"(__context__::MacroContext, mod, ex) 305 | _at_eval_code(__context__, __context__.macrocall, mod, ex) 306 | end 307 | 308 | #-------------------------------------------------------------------------------- 309 | # The following `@islocal` and `@inert` are macros for special syntax known to 310 | # lowering which don't exist in Base but arguably should. 311 | # 312 | # For now we have our own versions 313 | function var"@islocal"(__context__::MacroContext, ex) 314 | @chk kind(ex) == K"Identifier" 315 | @ast __context__ __context__.macrocall [K"extension" 316 | "islocal"::K"Symbol" 317 | ex 318 | ] 319 | end 320 | 321 | """ 322 | A non-interpolating quoted expression. 323 | 324 | For example, 325 | 326 | ```julia 327 | @inert quote 328 | \$x 329 | end 330 | ``` 331 | 332 | does not take `x` from the surrounding scope - instead it leaves the 333 | interpolation `\$x` intact as part of the expression tree. 334 | 335 | TODO: What is the correct way for `@inert` to work? ie which of the following 336 | should work? 337 | 338 | ```julia 339 | @inert quote 340 | body 341 | end 342 | 343 | @inert begin 344 | body 345 | end 346 | 347 | @inert x 348 | 349 | @inert \$x 350 | ``` 351 | 352 | The especially tricky cases involve nested interpolation ... 353 | ```julia 354 | quote 355 | @inert \$x 356 | end 357 | 358 | @inert quote 359 | quote 360 | \$x 361 | end 362 | end 363 | 364 | @inert quote 365 | quote 366 | \$\$x 367 | end 368 | end 369 | ``` 370 | 371 | etc. Needs careful thought - we should probably just copy what lisp does with 372 | quote+quasiquote 😅 373 | """ 374 | function var"@inert"(__context__::MacroContext, ex) 375 | @chk kind(ex) == K"quote" 376 | @ast __context__ __context__.macrocall [K"inert" ex] 377 | end 378 | -------------------------------------------------------------------------------- /test/utils.jl: -------------------------------------------------------------------------------- 1 | # Shared testing code which should be included before running individual test files. 2 | using Test 3 | 4 | using JuliaLowering 5 | using JuliaSyntax 6 | 7 | import FileWatching 8 | 9 | # The following are for docstrings testing. We need to load the REPL module 10 | # here for `Base.@doc` lookup to work at all. Yes this does seem really, 11 | # really, REALLY messed up. 12 | using Markdown 13 | import REPL 14 | 15 | using .JuliaSyntax: sourcetext, set_numeric_flags 16 | 17 | using .JuliaLowering: 18 | SyntaxGraph, newnode!, ensure_attributes!, 19 | Kind, SourceRef, SyntaxTree, NodeId, 20 | makenode, makeleaf, setattr!, sethead!, 21 | is_leaf, numchildren, children, 22 | @ast, flattened_provenance, showprov, LoweringError, MacroExpansionError, 23 | syntax_graph, Bindings, ScopeLayer, mapchildren 24 | 25 | function _ast_test_graph() 26 | graph = SyntaxGraph() 27 | ensure_attributes!(graph, 28 | kind=Kind, syntax_flags=UInt16, 29 | source=Union{SourceRef,NodeId,Tuple,LineNumberNode}, 30 | var_id=Int, value=Any, name_val=String, is_toplevel_thunk=Bool, 31 | toplevel_pure=Bool) 32 | end 33 | 34 | function _source_node(graph, src) 35 | id = newnode!(graph) 36 | sethead!(graph, id, K"None") 37 | setattr!(graph, id, source=src) 38 | SyntaxTree(graph, id) 39 | end 40 | 41 | macro ast_(tree) 42 | # TODO: Implement this in terms of new-style macros. 43 | quote 44 | graph = _ast_test_graph() 45 | srcref = _source_node(graph, $(QuoteNode(__source__))) 46 | @ast graph srcref $tree 47 | end 48 | end 49 | 50 | function ≈(ex1, ex2) 51 | if kind(ex1) != kind(ex2) || is_leaf(ex1) != is_leaf(ex2) 52 | return false 53 | end 54 | if is_leaf(ex1) 55 | return get(ex1, :value, nothing) == get(ex2, :value, nothing) && 56 | get(ex1, :name_val, nothing) == get(ex2, :name_val, nothing) 57 | else 58 | if numchildren(ex1) != numchildren(ex2) 59 | return false 60 | end 61 | return all(c1 ≈ c2 for (c1,c2) in zip(children(ex1), children(ex2))) 62 | end 63 | end 64 | 65 | 66 | #------------------------------------------------------------------------------- 67 | function _format_as_ast_macro(io, ex, indent) 68 | k = kind(ex) 69 | kind_str = repr(k) 70 | if !is_leaf(ex) 71 | println(io, indent, "[", kind_str) 72 | ind2 = indent*" " 73 | for c in children(ex) 74 | _format_as_ast_macro(io, c, ind2) 75 | end 76 | println(io, indent, "]") 77 | else 78 | val_str = if k == K"Identifier" || k == K"core" || k == K"top" 79 | repr(ex.name_val) 80 | elseif k == K"BindingId" 81 | repr(ex.var_id) 82 | else 83 | repr(get(ex, :value, nothing)) 84 | end 85 | println(io, indent, val_str, "::", kind_str) 86 | end 87 | end 88 | 89 | function format_as_ast_macro(io::IO, ex) 90 | print(io, "@ast_ ") 91 | _format_as_ast_macro(io, ex, "") 92 | end 93 | 94 | """ 95 | format_as_ast_macro(ex) 96 | 97 | Format AST `ex` as a Juila source code call to the `@ast_` macro for generating 98 | test case comparisons with the `≈` function. 99 | """ 100 | format_as_ast_macro(ex) = format_as_ast_macro(stdout, ex) 101 | 102 | #------------------------------------------------------------------------------- 103 | 104 | # Test tools 105 | 106 | function desugar(mod::Module, src::String) 107 | ex = parsestmt(SyntaxTree, src, filename="foo.jl") 108 | ctx = JuliaLowering.DesugaringContext(syntax_graph(ex), Bindings(), ScopeLayer[], mod) 109 | JuliaLowering.expand_forms_2(ctx, ex) 110 | end 111 | 112 | function uncomment_description(desc) 113 | replace(desc, r"^# ?"m=>"") 114 | end 115 | 116 | function comment_description(desc) 117 | lines = replace(split(desc, '\n')) do line 118 | strip("# " * line) 119 | end 120 | join(lines, '\n') 121 | end 122 | 123 | function match_ir_test_case(case_str) 124 | m = match(r"(^#(?:.|\n)*?)^([^#](?:.|\n)*)"m, strip(case_str)) 125 | if isnothing(m) 126 | error("Malformatted IR test case:\n$(repr(case_str))") 127 | end 128 | description = uncomment_description(m[1]) 129 | inout = split(m[2], r"#----*") 130 | input, output = length(inout) == 2 ? inout : 131 | length(inout) == 1 ? (inout[1], "") : 132 | error("Too many sections in IR test case") 133 | expect_error = startswith(description, "Error") 134 | is_broken = startswith(description, "FIXME") 135 | method_filter = begin 136 | mf = match(r"\[method_filter: *(.*)\]", description) 137 | isnothing(mf) ? nothing : strip(mf[1]) 138 | end 139 | (; expect_error=expect_error, is_broken=is_broken, 140 | description=strip(description), 141 | method_filter=method_filter, 142 | input=strip(input), output=strip(output)) 143 | end 144 | 145 | function read_ir_test_cases(filename) 146 | str = read(filename, String) 147 | parts = split(str, r"#\*+") 148 | if length(parts) == 2 149 | preamble_str = strip(parts[1]) 150 | cases_str = parts[2] 151 | else 152 | preamble_str = "" 153 | cases_str = only(parts) 154 | end 155 | (preamble_str, 156 | [match_ir_test_case(s) for s in split(cases_str, r"######*") if strip(s) != ""]) 157 | end 158 | 159 | function setup_ir_test_module(preamble) 160 | test_mod = Module(:TestMod) 161 | Base.eval(test_mod, :(const JuliaLowering = $JuliaLowering)) 162 | Base.eval(test_mod, :(const var"@ast_" = $(var"@ast_"))) 163 | JuliaLowering.include_string(test_mod, preamble) 164 | test_mod 165 | end 166 | 167 | function format_ir_for_test(mod, case) 168 | ex = parsestmt(SyntaxTree, case.input) 169 | try 170 | if kind(ex) == K"macrocall" && kind(ex[1]) == K"macro_name" && ex[1][1].name_val == "ast_" 171 | # Total hack, until @ast_ can be implemented in terms of new-style 172 | # macros. 173 | ex = Base.eval(mod, Expr(ex)) 174 | end 175 | x = JuliaLowering.lower(mod, ex) 176 | if case.expect_error 177 | error("Expected a lowering error in test case \"$(case.description)\"") 178 | end 179 | ir = strip(sprint(JuliaLowering.print_ir, x, case.method_filter)) 180 | return replace(ir, string(mod)=>"TestMod") 181 | catch exc 182 | if exc isa InterruptException 183 | rethrow() 184 | elseif case.expect_error && (exc isa LoweringError) 185 | return sprint(io->Base.showerror(io, exc, show_detail=false)) 186 | elseif case.expect_error && (exc isa MacroExpansionError) 187 | return sprint(io->Base.showerror(io, exc)) 188 | elseif case.is_broken 189 | return sprint(io->Base.showerror(io, exc)) 190 | else 191 | throw("Error in test case \"$(case.description)\"") 192 | end 193 | end 194 | end 195 | 196 | function test_ir_cases(filename::AbstractString) 197 | preamble, cases = read_ir_test_cases(filename) 198 | test_mod = setup_ir_test_module(preamble) 199 | for case in cases 200 | if case.is_broken 201 | continue 202 | end 203 | output = format_ir_for_test(test_mod, case) 204 | @testset "$(case.description)" begin 205 | if output != case.output 206 | # Do additional error dumping, as @test will not format errors in a nice way 207 | @error "Test \"$(case.description)\" failed" output=Text(output) ref=Text(case.output) 208 | end 209 | @test output == case.output 210 | end 211 | end 212 | end 213 | 214 | """ 215 | Update all IR test cases in `filename` when the IR format has changed. 216 | 217 | When `pattern` is supplied, update only those tests where 218 | `occursin(pattern, description)` is true. 219 | """ 220 | function refresh_ir_test_cases(filename, pattern=nothing) 221 | preamble, cases = read_ir_test_cases(filename) 222 | test_mod = setup_ir_test_module(preamble) 223 | io = IOBuffer() 224 | if !isempty(preamble) 225 | println(io, preamble, "\n") 226 | println(io, "#*******************************************************************************") 227 | end 228 | for case in cases 229 | if isnothing(pattern) || occursin(pattern, case.description) 230 | ir = format_ir_for_test(test_mod, case) 231 | if rstrip(ir) != case.output 232 | @info "Refreshing test case $(repr(case.description)) in $filename" 233 | end 234 | else 235 | ir = case.output 236 | end 237 | (case == cases[end] ? print : println)(io, 238 | """ 239 | ######################################## 240 | $(comment_description(case.description)) 241 | $(strip(case.input)) 242 | #--------------------- 243 | $ir 244 | """ 245 | ) 246 | end 247 | # Write only at the end to ensure we don't write rubbish if we crash! 248 | write(filename, take!(io)) 249 | nothing 250 | end 251 | 252 | function refresh_all_ir_test_cases(test_dir=".") 253 | foreach(refresh_ir_test_cases, filter(fn->endswith(fn, "ir.jl"), readdir(test_dir, join=true))) 254 | end 255 | 256 | function watch_ir_tests(dir, delay=0.5) 257 | dir = abspath(dir) 258 | while true 259 | (name, event) = FileWatching.watch_folder(dir) 260 | if endswith(name, "_ir.jl") && (event.changed || event.renamed) 261 | FileWatching.unwatch_folder(dir) 262 | sleep(delay) 263 | try 264 | refresh_ir_test_cases(joinpath(dir, name)) 265 | catch 266 | @error "Error refreshing test case" exception=current_exceptions() 267 | end 268 | end 269 | end 270 | end 271 | 272 | function lower_str(mod::Module, s::AbstractString) 273 | ex = parsestmt(JuliaLowering.SyntaxTree, s) 274 | return JuliaLowering.to_lowered_expr(JuliaLowering.lower(mod, ex)) 275 | end 276 | 277 | # See Julia Base tests in "test/docs.jl" 278 | function docstrings_equal(d1, d2; debug=true) 279 | io1 = IOBuffer() 280 | io2 = IOBuffer() 281 | show(io1, MIME"text/markdown"(), d1) 282 | show(io2, MIME"text/markdown"(), d2) 283 | s1 = String(take!(io1)) 284 | s2 = String(take!(io2)) 285 | if debug && s1 != s2 286 | print(s1) 287 | println("--------------------------------------------------------------------------------") 288 | print(s2) 289 | println("================================================================================") 290 | end 291 | return s1 == s2 292 | end 293 | docstrings_equal(d1::Docs.DocStr, d2) = docstrings_equal(Docs.parsedoc(d1), d2) 294 | 295 | #------------------------------------------------------------------------------- 296 | # Tools for test case reduction 297 | 298 | function block_reduction_1(is_lowering_error::Function, orig_ex::ST, ex::ST, 299 | curr_path = Int[]) where {ST <: SyntaxTree} 300 | if !is_leaf(ex) 301 | if kind(ex) == K"block" 302 | for i in 1:numchildren(ex) 303 | trial_ex = delete_block_child(orig_ex, orig_ex, curr_path, i) 304 | if is_lowering_error(trial_ex) 305 | # @info "Reduced expression" curr_path i 306 | return trial_ex 307 | end 308 | end 309 | end 310 | for (i,e) in enumerate(children(ex)) 311 | push!(curr_path, i) 312 | res = block_reduction_1(is_lowering_error, orig_ex, e, curr_path) 313 | if !isnothing(res) 314 | return res 315 | end 316 | pop!(curr_path) 317 | end 318 | end 319 | return nothing 320 | end 321 | 322 | # Find children of all `K"block"`s in an expression and try deleting them while 323 | # preserving the invariant `is_lowering_error(reduced) == true`. 324 | function block_reduction(is_lowering_error, ex) 325 | reduced = ex 326 | was_reduced = false 327 | while true 328 | r = block_reduction_1(is_lowering_error, reduced, reduced) 329 | if isnothing(r) 330 | return (reduced, was_reduced) 331 | end 332 | reduced = r 333 | was_reduced = true 334 | end 335 | end 336 | 337 | function delete_block_child(ctx, ex, block_path, child_idx, depth=1) 338 | if depth > length(block_path) 339 | cs = copy(children(ex)) 340 | deleteat!(cs, child_idx) 341 | @ast ctx ex [ex cs...] 342 | else 343 | j = block_path[depth] 344 | mapchildren(ctx, ex, j:j) do e 345 | delete_block_child(ctx, e, block_path, child_idx, depth+1) 346 | end 347 | end 348 | end 349 | 350 | function throws_lowering_exc(mod, ex) 351 | try 352 | debug_lower(mod, ex) 353 | return false 354 | catch exc 355 | if exc isa LoweringError 356 | return true 357 | else 358 | rethrow() 359 | end 360 | end 361 | end 362 | 363 | # Parse a file and lower the top level expression one child at a time, finding 364 | # any top level statement that fails lowering and producing a partially reduced 365 | # test case. 366 | function reduce_any_failing_toplevel(mod::Module, filename::AbstractString; do_eval::Bool=false) 367 | text = read(filename, String) 368 | ex0 = parseall(SyntaxTree, text; filename) 369 | for ex in children(ex0) 370 | try 371 | ex_compiled = JuliaLowering.lower(mod, ex) 372 | ex_expr = JuliaLowering.to_lowered_expr(ex_compiled) 373 | if do_eval 374 | Base.eval(mod, ex_expr) 375 | end 376 | catch exc 377 | @error "Failure lowering code" ex 378 | if !(exc isa LoweringError) 379 | rethrow() 380 | end 381 | (reduced,was_reduced) = block_reduction(e->throws_lowering_exc(mod,e), ex) 382 | if !was_reduced 383 | @info "No reduction possible" 384 | return ex 385 | else 386 | @info "Reduced code" reduced 387 | return reduced 388 | end 389 | end 390 | end 391 | nothing 392 | end 393 | --------------------------------------------------------------------------------