From ed15bb48d4b69304222cad2781af57f5fa2a4cea Mon Sep 17 00:00:00 2001 From: William Moses Date: Sun, 31 Mar 2024 22:34:18 -0400 Subject: [PATCH 1/7] Mark randn! as inactive (#1378) --- src/internal_rules.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/internal_rules.jl b/src/internal_rules.jl index 9bcce5925c..8afcaa0a20 100644 --- a/src/internal_rules.jl +++ b/src/internal_rules.jl @@ -75,6 +75,9 @@ end function EnzymeRules.inactive(::typeof(Random.randn), args...) return nothing end +function EnzymeRules.inactive(::typeof(Random.randn!), args...) + return nothing +end function EnzymeRules.inactive(::typeof(Random.default_rng), args...) return nothing end From 6ac1212928113fa3dbfef97c9e841903042eb31e Mon Sep 17 00:00:00 2001 From: William Moses Date: Sun, 31 Mar 2024 23:35:56 -0400 Subject: [PATCH 2/7] [EnzymeTestUtils] Mark 1.8 batch test as failing (#1379) --- lib/EnzymeTestUtils/test/test_forward.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/EnzymeTestUtils/test/test_forward.jl b/lib/EnzymeTestUtils/test/test_forward.jl index 8768d5324e..a2ab010042 100644 --- a/lib/EnzymeTestUtils/test/test_forward.jl +++ b/lib/EnzymeTestUtils/test/test_forward.jl @@ -85,7 +85,9 @@ end elseif TT <: NamedTuple x = (a=randn(T), b=randn(T)) else # TT <: TestStruct - VERSION ≤ v"1.8" && (@test_skip false; continue) + if VERSION <= v"1.8" && Tx == BatchDuplicated + continue + end x = TestStruct(randn(T, 5), randn(T)) end atol = rtol = sqrt(eps(real(T))) From edfb21fdc85949c78fcad095527d9aefc0c40c15 Mon Sep 17 00:00:00 2001 From: William Moses Date: Sun, 31 Mar 2024 23:55:47 -0400 Subject: [PATCH 3/7] Fix undefined memory in faq (#1376) --- docs/src/faq.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/faq.md b/docs/src/faq.md index c8315464ac..72fa1f97d9 100644 --- a/docs/src/faq.md +++ b/docs/src/faq.md @@ -271,7 +271,7 @@ Enzyme.autodiff(Reverse, f, Active(1.2), Const(Vector{Float64}(undef, 1)), Const Passing in a dupliacted (e.g. differentiable) variable for `tmp` now leads to the correct answer. ```jldoctest storage -Enzyme.autodiff(Reverse, f, Active(1.2), Duplicated(Vector{Float64}(undef, 1), Vector{Float64}(undef, 1)), Const(1), Const(5)) # Correct (returns 10.367999999999999 == 1.2^4 * 5) +Enzyme.autodiff(Reverse, f, Active(1.2), Duplicated(Vector{Float64}(undef, 1), zeros(1)), Const(1), Const(5)) # Correct (returns 10.367999999999999 == 1.2^4 * 5) # output @@ -539,4 +539,4 @@ For `d/d conj(z)`, $\frac12 \left( [u_x + i v_x] + i [u_y + i v_y] \right) = \fr 3.1 + 2.7im ``` -Note: when writing rules for complex scalar functions, in reverse mode one needs to conjugate the differential return, and similarly the true result will be the conjugate of that value (in essence you can think of reverse-mode AD as working in the conjugate space). \ No newline at end of file +Note: when writing rules for complex scalar functions, in reverse mode one needs to conjugate the differential return, and similarly the true result will be the conjugate of that value (in essence you can think of reverse-mode AD as working in the conjugate space). From 55b75677685f3356aa5a818567edb443d3ae4e80 Mon Sep 17 00:00:00 2001 From: William Moses Date: Fri, 5 Apr 2024 13:55:37 -0700 Subject: [PATCH 4/7] Restore 1.9+ setfield (#1383) --- src/rules/llvmrules.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/llvmrules.jl b/src/rules/llvmrules.jl index f066910450..4c06290ce9 100644 --- a/src/rules/llvmrules.jl +++ b/src/rules/llvmrules.jl @@ -60,7 +60,7 @@ function jlcall_augfwd(B, orig, gutils, normalR, shadowR, tapeR) if in(name, ("ijl_f_getfield", "jl_f_getfield")) return common_jl_getfield_augfwd(2, B, orig, gutils, normalR, shadowR, tapeR) end - if in(name, ("ijl_s_getfield", "jl_s_getfield")) + if in(name, ("ijl_f_setfield", "jl_f_setfield")) return common_setfield_augfwd(2, B, orig, gutils, normalR, shadowR, tapeR) end if in(name, ("ijl_f__apply_iterate", "jl_f__apply_iterate")) From 724b9bc31c316744142b90e3b9c603a9d60270f5 Mon Sep 17 00:00:00 2001 From: William Moses Date: Sat, 6 Apr 2024 04:56:41 -0700 Subject: [PATCH 5/7] Update Project.toml (#1384) --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index dbc3b7478d..1032750266 100644 --- a/Project.toml +++ b/Project.toml @@ -25,7 +25,7 @@ EnzymeSpecialFunctionsExt = "SpecialFunctions" [compat] CEnum = "0.4, 0.5" EnzymeCore = "0.7" -Enzyme_jll = "0.0.103" +Enzyme_jll = "0.0.104" GPUCompiler = "0.21, 0.22, 0.23, 0.24, 0.25, 0.26" LLVM = "6.1" ObjectFile = "0.4" From 1bf16f8217f2f0e516666f5dff2deb27a653302d Mon Sep 17 00:00:00 2001 From: William Moses Date: Sat, 6 Apr 2024 08:55:50 -0700 Subject: [PATCH 6/7] add complex sqrt (#1324) * add complex sqrt * fixes --- src/compiler.jl | 208 ++++++++++++++++++++++++------------ src/compiler/interpreter.jl | 2 +- test/runtests.jl | 1 + 3 files changed, 139 insertions(+), 72 deletions(-) diff --git a/src/compiler.jl b/src/compiler.jl index 03a413c880..7fdcf86dbd 100644 --- a/src/compiler.jl +++ b/src/compiler.jl @@ -45,17 +45,6 @@ end import GPUCompiler: @safe_debug, @safe_info, @safe_warn, @safe_error -safe_println(head, tail) = ccall(:jl_safe_printf, Cvoid, (Cstring, Cstring...), "%s%s\n",head, tail) -macro safe_show(exs...) - blk = Expr(:block) - for ex in exs - push!(blk.args, :($safe_println($(sprint(Base.show_unquoted, ex)*" = "), - repr(begin local value = $(esc(ex)) end)))) - end - isempty(exs) || push!(blk.args, :value) - return blk -end - if LLVM.has_orc_v1() include("compiler/orcv1.jl") else @@ -70,6 +59,7 @@ include("compiler/utils.jl") const cmplx_known_ops = Dict{DataType, Tuple{Symbol, Int, Union{Nothing, Tuple{Symbol, DataType}}}}( typeof(Base.inv) => (:cmplx_inv, 1, nothing), + typeof(Base.sqrt) => (:cmplx_sqrt, 1, nothing), ) const known_ops = Dict{DataType, Tuple{Symbol, Int, Union{Nothing, Tuple{Symbol, DataType}}}}( @@ -4082,10 +4072,13 @@ function lower_convention(functy::Type, mod::LLVM.Module, entry_f::LLVM.Function end for e in toErase if !isempty(collect(uses(e))) - @safe_show mod - @safe_show entry_f - @safe_show e - throw(AssertionError("Use after deletion")) + msg = sprint() do io + println(io, string(mod)) + println(io, string(entry_f)) + println(io, string(e)) + println(io, "Use after deletion") + end + throw(AssertionError(msg)) end LLVM.API.LLVMInstructionEraseFromParent(e) end @@ -4144,6 +4137,9 @@ function lower_convention(functy::Type, mod::LLVM.Module, entry_f::LLVM.Function @assert eltype(ty) == value_type(wrapparm) store!(builder, wrapparm, ptr) push!(wrapper_args, ptr) + push!(parameter_attributes(wrapper_f, arg.codegen.i-sret-returnRoots), StringAttribute("enzyme_type", string(typetree(arg.typ, ctx, dl, seen)))) + push!(parameter_attributes(wrapper_f, arg.codegen.i-sret-returnRoots), StringAttribute("enzymejl_parmtype", string(convert(UInt, unsafe_to_pointer(arg.typ))))) + push!(parameter_attributes(wrapper_f, arg.codegen.i-sret-returnRoots), StringAttribute("enzymejl_parmtype_ref", string(UInt(GPUCompiler.BITS_REF)))) else push!(wrapper_args, wrapparm) for attr in collect(parameter_attributes(entry_f, arg.codegen.i)) @@ -4206,16 +4202,26 @@ function lower_convention(functy::Type, mod::LLVM.Module, entry_f::LLVM.Function position!(builder, def) ret!(builder, extract_value!(builder, res, 0)) + + push!(return_attributes(wrapper_f), StringAttribute("enzyme_type", string(typetree(actualRetType, ctx, dl, seen)))) + push!(return_attributes(wrapper_f), StringAttribute("enzymejl_parmtype", string(convert(UInt, unsafe_to_pointer(actualRetType))))) + push!(return_attributes(wrapper_f), StringAttribute("enzymejl_parmtype_ref", string(UInt(GPUCompiler.BITS_REF)))) end elseif sret if sretPtr === nothing ret!(builder) else + push!(return_attributes(wrapper_f), StringAttribute("enzyme_type", string(typetree(actualRetType, ctx, dl, seen)))) + push!(return_attributes(wrapper_f), StringAttribute("enzymejl_parmtype", string(convert(UInt, unsafe_to_pointer(actualRetType))))) + push!(return_attributes(wrapper_f), StringAttribute("enzymejl_parmtype_ref", string(UInt(GPUCompiler.BITS_REF)))) ret!(builder, load!(builder, RT, sretPtr)) end elseif LLVM.return_type(entry_ft) == LLVM.VoidType() ret!(builder) else + push!(return_attributes(wrapper_f), StringAttribute("enzyme_type", string(typetree(actualRetType, ctx, dl, seen)))) + push!(return_attributes(wrapper_f), StringAttribute("enzymejl_parmtype", string(convert(UInt, unsafe_to_pointer(actualRetType))))) + push!(return_attributes(wrapper_f), StringAttribute("enzymejl_parmtype_ref", string(UInt(GPUCompiler.BITS_REF)))) ret!(builder, res) end dispose(builder) @@ -4231,14 +4237,52 @@ function lower_convention(functy::Type, mod::LLVM.Module, entry_f::LLVM.Function attributes = function_attributes(wrapper_f) push!(attributes, StringAttribute("enzymejl_mi", string(convert(UInt, pointer_from_objref(mi))))) push!(attributes, StringAttribute("enzymejl_rt", string(convert(UInt, unsafe_to_pointer(rt))))) + + for prev in collect(function_attributes(entry_f)) + if kind(prev) == kind(StringAttribute("enzyme_ta_norecur")) + push!(attributes, prev) + end + if kind(prev) == kind(StringAttribute("enzyme_parmremove")) + push!(attributes, prev) + end + if kind(prev) == kind(StringAttribute("enzyme_math")) + push!(attributes, prev) + end + if kind(prev) == kind(StringAttribute("enzyme_shouldrecompute")) + push!(attributes, prev) + end + if kind(prev) == kind(EnumAttribute("readonly")) + push!(attributes, prev) + end + if kind(prev) == kind(EnumAttribute("readnone")) + push!(attributes, prev) + end + if kind(prev) == kind(EnumAttribute("argmemonly")) + push!(attributes, prev) + end + if kind(prev) == kind(EnumAttribute("inaccessiblememonly")) + push!(attributes, prev) + end + if kind(prev) == kind(EnumAttribute("speculatable")) + push!(attributes, prev) + end + if kind(prev) == kind(EnumAttribute("nofree")) + push!(attributes, prev) + end + if kind(prev) == kind(StringAttribute("enzyme_inactive")) + push!(attributes, prev) + end + end if LLVM.API.LLVMVerifyFunction(wrapper_f, LLVM.API.LLVMReturnStatusAction) != 0 - @safe_show mod - @safe_show LLVM.API.LLVMVerifyFunction(wrapper_f, LLVM.API.LLVMPrintMessageAction) - @safe_show wrapper_f - @safe_show parmsRemoved, retRemoved, prargs - flush(stdout) - throw(LLVM.LLVMException("broken function")) + msg = sprint() do io + println(io, string(mod)) + println(io, LVM.API.LLVMVerifyFunction(wrapper_f, LLVM.API.LLVMPrintMessageAction)) + println(io, string(wrapper_f)) + println(io, "parmsRemoved=", parmsRemoved, " retRemoved=", retRemoved, " prargs=", prargs) + println(io, "Broken function") + end + throw(LLVM.LLVMException(msg)) end ModulePassManager() do pm @@ -4333,19 +4377,17 @@ function lower_convention(functy::Type, mod::LLVM.Module, entry_f::LLVM.Function end if LLVM.API.LLVMVerifyFunction(wrapper_f, LLVM.API.LLVMReturnStatusAction) != 0 - @safe_show mod - @safe_show LLVM.API.LLVMVerifyFunction(wrapper_f, LLVM.API.LLVMPrintMessageAction) - @safe_show wrapper_f - flush(stdout) - throw(LLVM.LLVMException("broken function")) + msg = sprint() do io + println(io, string(mod)) + println(io, LVM.API.LLVMVerifyFunction(wrapper_f, LLVM.API.LLVMPrintMessageAction)) + println(io, string(wrapper_f)) + println(io, "Broken function") + end + throw(LLVM.LLVMException(msg)) end return wrapper_f, returnRoots, boxedArgs, loweredArgs end -function adim(::Array{T, N}) where {T, N} - return N -end - function GPUCompiler.codegen(output::Symbol, job::CompilerJob{<:EnzymeTarget}; libraries::Bool=true, deferred_codegen::Bool=true, optimize::Bool=true, toplevel::Bool=true, strip::Bool=false, validate::Bool=true, only_entry::Bool=false, parent_job::Union{Nothing, CompilerJob} = nothing) @@ -4628,7 +4670,7 @@ function GPUCompiler.codegen(output::Symbol, job::CompilerJob{<:EnzymeTarget}; name = meth.name jlmod = meth.module - function handleCustom(name, attrs=[], setlink=true, noinl=true) + function handleCustom(llvmfn, name, attrs=[], setlink=true, noinl=true) attributes = function_attributes(llvmfn) custom[k_name] = linkage(llvmfn) if setlink @@ -4647,7 +4689,7 @@ function GPUCompiler.codegen(output::Symbol, job::CompilerJob{<:EnzymeTarget}; julia_activity_rule(llvmfn) if has_custom_rule - handleCustom("enzyme_custom", [StringAttribute("enzyme_preserve_primal", "*")]) + handleCustom(llvmfn, "enzyme_custom", [StringAttribute("enzyme_preserve_primal", "*")]) continue end @@ -4655,7 +4697,7 @@ function GPUCompiler.codegen(output::Symbol, job::CompilerJob{<:EnzymeTarget}; sparam_vals = mi.specTypes.parameters[2:end] # mi.sparam_vals if func == typeof(Base.eps) || func == typeof(Base.nextfloat) || func == typeof(Base.prevfloat) - handleCustom("jl_inactive_inout", [StringAttribute("enzyme_inactive"), + handleCustom(llvmfn, "jl_inactive_inout", [StringAttribute("enzyme_inactive"), EnumAttribute("readnone", 0), EnumAttribute("speculatable", 0), StringAttribute("enzyme_shouldrecompute") @@ -4663,7 +4705,7 @@ function GPUCompiler.codegen(output::Symbol, job::CompilerJob{<:EnzymeTarget}; continue end if func == typeof(Base.to_tuple_type) - handleCustom("jl_to_tuple_type", + handleCustom(llvmfn, "jl_to_tuple_type", [EnumAttribute("readonly", 0), EnumAttribute("inaccessiblememonly", 0), EnumAttribute("speculatable", 0), @@ -4674,7 +4716,7 @@ function GPUCompiler.codegen(output::Symbol, job::CompilerJob{<:EnzymeTarget}; end if func == typeof(Base.Threads.threadid) || func == typeof(Base.Threads.nthreads) name = (func == typeof(Base.Threads.threadid)) ? "jl_threadid" : "jl_nthreads" - handleCustom(name, + handleCustom(llvmfn, name, [EnumAttribute("readonly", 0), EnumAttribute("inaccessiblememonly", 0), EnumAttribute("speculatable", 0), @@ -4689,15 +4731,15 @@ function GPUCompiler.codegen(output::Symbol, job::CompilerJob{<:EnzymeTarget}; # fn, but it doesn't presently so for now we will ensure this by hand if func == typeof(Base.Checked.throw_overflowerr_binaryop) llvmfn = functions(mod)[k.specfunc] - handleCustom("enz_noop", [StringAttribute("enzyme_inactive"), EnumAttribute("readonly")]) + handleCustom(llvmfn, "enz_noop", [StringAttribute("enzyme_inactive"), EnumAttribute("readonly")]) continue end if EnzymeRules.is_inactive_from_sig(mi.specTypes; world, method_table, caller) - handleCustom("enz_noop", [StringAttribute("enzyme_inactive"), EnumAttribute("nofree")]) + handleCustom(llvmfn, "enz_noop", [StringAttribute("enzyme_inactive"), EnumAttribute("nofree")]) continue end if EnzymeRules.is_inactive_noinl_from_sig(mi.specTypes; world, method_table, caller) - handleCustom("enz_noop", [StringAttribute("enzyme_inactive"), EnumAttribute("nofree")], false, false) + handleCustom(llvmfn, "enz_noop", [StringAttribute("enzyme_inactive"), EnumAttribute("nofree")], false, false) for bb in blocks(llvmfn) for inst in instructions(bb) if isa(inst, LLVM.CallInst) @@ -4709,54 +4751,78 @@ function GPUCompiler.codegen(output::Symbol, job::CompilerJob{<:EnzymeTarget}; continue end if func == typeof(Base.enq_work) && length(sparam_vals) == 1 && first(sparam_vals) <: Task - handleCustom("jl_enq_work") + handleCustom(llvmfn, "jl_enq_work") continue end if func == typeof(Base.wait) || func == typeof(Base._wait) if length(sparam_vals) == 1 && first(sparam_vals) <: Task - handleCustom("jl_wait") + handleCustom(llvmfn, "jl_wait") end continue end if func == typeof(Base.Threads.threading_run) if length(sparam_vals) == 1 || length(sparam_vals) == 2 - handleCustom("jl_threadsfor") + handleCustom(llvmfn, "jl_threadsfor") end continue end - name = nothing - arity = nothing - toinject = nothing - Tys = nothing + @inline function find_math_method() + if func ∈ keys(known_ops) + name, arity, toinject = known_ops[func] + Tys = (Float32, Float64) + + if length(sparam_vals) == arity + T = first(sparam_vals) + legal = T ∈ Tys + + if legal + if name == :ldexp + if !(sparam_vals[2] <: Integer) + legal = false + end + elseif name == :pow + if sparam_vals[2] <: Integer + name = :powi + elseif sparam_vals[2] != T + legal = false + end + elseif name == :jl_rem2pi + else + if !all(==(T), sparam_vals) + legal = false + end + end + end + if legal + return name, toinject, T + end + end + end - if func ∈ keys(known_ops) - name, arity, toinject = known_ops[func] - Tys = (Float32, Float64) - elseif func ∈ keys(cmplx_known_ops) - name, arity, toinject = cmplx_known_ops[func] - Tys = (Complex{Float32}, Complex{Float64}) - else - continue - end + if func ∈ keys(cmplx_known_ops) + name, arity, toinject = cmplx_known_ops[func] + Tys = (Complex{Float32}, Complex{Float64}) + if length(sparam_vals) == arity + T = first(sparam_vals) + legal = T ∈ Tys - length(sparam_vals) == arity || continue - T = first(sparam_vals) - isfloat = T ∈ Tys - if !isfloat - continue + if legal + if !all(==(T), sparam_vals) + legal = false + end + end + if legal + return name, toinject, T + end + end + end + return nothing, nothing, nothing end - if name == :ldexp - sparam_vals[2] <: Integer || continue - elseif name == :pow - if sparam_vals[2] <: Integer - name = :powi - elseif sparam_vals[2] != T - continue - end - elseif name == :jl_rem2pi - else - all(==(T), sparam_vals) || continue + + name, toinject, T = find_math_method() + if name === nothing + continue end if toinject !== nothing @@ -4778,7 +4844,7 @@ function GPUCompiler.codegen(output::Symbol, job::CompilerJob{<:EnzymeTarget}; name = string(name) name = T == Float32 ? name*"f" : name - handleCustom(name, [EnumAttribute("readnone", 0), + handleCustom(llvmfn, name, [EnumAttribute("readnone", 0), StringAttribute("enzyme_shouldrecompute")]) end diff --git a/src/compiler/interpreter.jl b/src/compiler/interpreter.jl index a2900b3356..5885679be5 100644 --- a/src/compiler/interpreter.jl +++ b/src/compiler/interpreter.jl @@ -94,7 +94,7 @@ function is_primitive_func(@nospecialize(TT)) end end - if ft == typeof(Base.inv) + if ft == typeof(Base.inv) || ft == typeof(Base.sqrt) if TT <: Tuple{ft, Complex{Float32}} || TT <: Tuple{ft, Complex{Float64}} return true end diff --git a/test/runtests.jl b/test/runtests.jl index 99aa3b208f..f6100bab81 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -277,6 +277,7 @@ make3() = (1.0, 2.0, 3.0) test_scalar(x->rem(x, 1), 0.7) test_scalar(x->rem2pi(x,RoundDown), 0.7) test_scalar(x->fma(x,x+1,x/3), 2.3) + test_scalar(sqrt, 1.7+2.1im) @test autodiff(Forward, sincos, Duplicated(1.0, 1.0))[1][1] ≈ cos(1.0) From 9d6b969b2c36f47d8acf31254498aa4a81edae31 Mon Sep 17 00:00:00 2001 From: Gaurav Arya Date: Mon, 15 Apr 2024 15:36:03 -0400 Subject: [PATCH 7/7] Try narrowing inactive rules for `rand` (#1388) * Restrict scope of inactive markers on Randon.rand * Simplify * Keep the randn rules * Add test * Rm newline * Fix typo * Add Random import for test --- src/internal_rules.jl | 4 ++-- test/internal_rules.jl | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/internal_rules.jl b/src/internal_rules.jl index 8afcaa0a20..b6dc8c75d6 100644 --- a/src/internal_rules.jl +++ b/src/internal_rules.jl @@ -66,10 +66,10 @@ end function EnzymeRules.inactive(::typeof(Core.kwfunc), args...) return nothing end -function EnzymeRules.inactive(::typeof(Random.rand), args...) +function EnzymeRules.inactive(::typeof(Random.rand), ::Random.AbstractRNG, ::Random.Sampler) return nothing end -function EnzymeRules.inactive(::typeof(Random.rand!), args...) +function EnzymeRules.inactive(::typeof(Random.rand!), ::Random.AbstractRNG, ::Random.Sampler, ::AbstractArray) return nothing end function EnzymeRules.inactive(::typeof(Random.randn), args...) diff --git a/test/internal_rules.jl b/test/internal_rules.jl index f9b2aca957..e325189dc1 100644 --- a/test/internal_rules.jl +++ b/test/internal_rules.jl @@ -7,6 +7,7 @@ using FiniteDifferences using LinearAlgebra using SparseArrays using Test +import Random struct TPair a::Float64 @@ -432,4 +433,18 @@ end end end end + +@testset "rand and randn rules" begin + # Distributed as x + unit normal + uniform + struct MyDistribution + x::Float64 + end + + Random.rand(rng::Random.AbstractRNG, d::MyDistribution) = d.x + randn() + rand() + Random.rand(d::MyDistribution) = rand(Random.default_rng(), d) + + # Outer rand should be differentiated through, and inner rand and randn should be ignored. + @test autodiff(Enzyme.Reverse, x -> rand(MyDistribution(x)), Active, Active(1.0)) == ((1.0,),) +end + end # InternalRules