From 4cf054a30d770d8ef75c305d775602a7d567a3f7 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Wed, 22 Apr 2026 15:29:08 +0000 Subject: [PATCH 01/15] fix: preserve symbolic external units across precompile Co-authored-by: Miles Cranmer --- src/symbolic_dimensions.jl | 60 ++++++++++++------- .../ExternalUnitRegistration.jl | 10 +++- test/unittests.jl | 5 +- 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 6513c49e..17dc1f5c 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -409,13 +409,26 @@ module SymbolicUnits import ..UNIT_SYMBOLS import ..CONSTANT_SYMBOLS + import ..ALL_MAPPING import ..SymbolicDimensionsSingleton import ..constructorof import ..DEFAULT_SYMBOLIC_QUANTITY_TYPE import ..DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE import ..DEFAULT_VALUE_TYPE import ..DEFAULT_DIM_BASE_TYPE + import ..INDEX_TYPE + import ..UnionAbstractQuantity import ..WriteOnceReadMany + import ..disambiguate_constant_symbol + + symbolic_unit_from_symbol(unit::Symbol) = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( + DEFAULT_VALUE_TYPE(1.0), + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}(unit) + ) + symbolic_constant_from_symbol(unit::Symbol) = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( + DEFAULT_VALUE_TYPE(1.0), + SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}(disambiguate_constant_symbol(unit)) + ) # Lazily create unit symbols (since there are so many) module Constants @@ -462,11 +475,7 @@ module SymbolicUnits # Non-eval version of `update_symbolic_unit_values!` for registering units in # an external module. function update_external_symbolic_unit_value(unit) - unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)( - DEFAULT_VALUE_TYPE(1.0), - SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}(unit) - ) - push!(SYMBOLIC_UNIT_VALUES, unit) + push!(SYMBOLIC_UNIT_VALUES, symbolic_unit_from_symbol(unit)) end """ @@ -495,39 +504,50 @@ module SymbolicUnits as_quantity(x::Number) = convert(DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE, x) as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") - @unstable function map_to_scope(ex::Expr) + @unstable map_to_scope(ex::Expr) = map_to_scope(@__MODULE__, ex) + @unstable function map_to_scope(mod::Module, ex::Expr) if !(ex.head == :call) && !(ex.head == :. && ex.args[1] == :Constants) throw(ArgumentError("Unexpected expression: $ex. Only `:call` and `:.` (for `SymbolicConstants`) are expected.")) end if ex.head == :call - ex.args[2:end] = map(map_to_scope, ex.args[2:end]) + ex.args[2:end] = map(arg -> map_to_scope(mod, arg), ex.args[2:end]) return ex else # if ex.head == :. && ex.args[1] == :Constants @assert ex.args[2] isa QuoteNode - return lookup_constant(ex.args[2].value) + return Expr(:call, GlobalRef(@__MODULE__, :lookup_constant), QuoteNode(ex.args[2].value)) end end - function map_to_scope(sym::Symbol) - if sym in UNIT_SYMBOLS + map_to_scope(sym::Symbol) = map_to_scope(@__MODULE__, sym) + function map_to_scope(mod::Module, sym::Symbol) + if sym in UNIT_SYMBOLS || _has_quantity_binding(mod, sym) # return at end elseif sym in CONSTANT_SYMBOLS throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `us\"Constants.$sym\"` instead.")) else throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) end - return lookup_unit(sym) + return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), mod, QuoteNode(sym)) end - function map_to_scope(ex) - return ex + map_to_scope(ex) = ex + map_to_scope(::Module, ex) = ex + + function _has_quantity_binding(mod::Module, sym::Symbol) + return isdefined(mod, sym) && Base.invokelatest(getproperty, mod, sym) isa UnionAbstractQuantity end - function lookup_unit(ex::Symbol) - i = findfirst(==(ex), UNIT_SYMBOLS)::Int - return as_quantity(SYMBOLIC_UNIT_VALUES[i]) + function _ensure_registered(mod::Module, sym::Symbol) + if iszero(get(ALL_MAPPING, sym, INDEX_TYPE(0))) && _has_quantity_binding(mod, sym) + update_all_values = getfield(parentmodule(@__MODULE__), :update_all_values) + unit = Base.invokelatest(getproperty, mod, sym) + Base.invokelatest(update_all_values, sym, unit) + end + return nothing end - function lookup_constant(ex::Symbol) - i = findfirst(==(ex), CONSTANT_SYMBOLS)::Int - return as_quantity(SYMBOLIC_CONSTANT_VALUES[i]) + function lookup_unit(mod::Module, ex::Symbol) + _ensure_registered(mod, ex) + return as_quantity(symbolic_unit_from_symbol(ex)) end + lookup_unit(ex::Symbol) = lookup_unit(@__MODULE__, ex) + lookup_constant(ex::Symbol) = as_quantity(symbolic_constant_from_symbol(ex)) end import .SymbolicUnits: as_quantity, sym_uparse, SymbolicConstants, map_to_scope @@ -548,7 +568,7 @@ module. So, for example, `us"Constants.c^2 * Hz^2"` would evaluate to namespace collisions, a few physical constants are automatically converted. """ macro us_str(s) - ex = map_to_scope(Meta.parse(s)) + ex = map_to_scope(__module__, Meta.parse(s)) ex = :($as_quantity($ex)) return esc(ex) end diff --git a/test/precompile_test/ExternalUnitRegistration.jl b/test/precompile_test/ExternalUnitRegistration.jl index 6395b9d2..0357fad8 100644 --- a/test/precompile_test/ExternalUnitRegistration.jl +++ b/test/precompile_test/ExternalUnitRegistration.jl @@ -7,15 +7,21 @@ using Test @register_unit MyWb u"m^2*kg*s^-2*A^-1" +expanded_mywb() = 1u"MyWb" +symbolic_mywb() = 1us"MyWb" + @testset "Register Unit Inside a Module" begin for collection in (UNIT_SYMBOLS, ALL_SYMBOLS, keys(ALL_MAPPING._raw_data), keys(UNIT_MAPPING._raw_data)) @test :MyWb ∈ collection end - w = u"MyWb" - ws = us"MyWb" + w = expanded_mywb() + ws = symbolic_mywb() @test w isa DEFAULT_QUANTITY_TYPE @test ws isa DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE + @test w == u"MyWb" + @test ws == us"MyWb" + @test string(ws) == "1.0 MyWb" end end diff --git a/test/unittests.jl b/test/unittests.jl index 76553a6b..a8f02a82 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -2334,10 +2334,13 @@ end push!(LOAD_PATH, joinpath(@__DIR__, "precompile_test")) -using ExternalUnitRegistration: MyWb +using ExternalUnitRegistration: MyWb, expanded_mywb, symbolic_mywb @testset "Type of External Unit" begin @test MyWb isa DEFAULT_QUANTITY_TYPE @test MyWb/u"m^2*kg*s^-2*A^-1" == 1.0 + @test expanded_mywb() == MyWb + @test uexpand(symbolic_mywb()) == MyWb + @test string(symbolic_mywb()) == "1.0 MyWb" end pop!(LOAD_PATH) From 37b3bd9540d7f045e5b5e476f5e42b544c802398 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Wed, 22 Apr 2026 18:32:26 +0000 Subject: [PATCH 02/15] test: cover SymbolicUnits wrapper entrypoints Co-authored-by: Miles Cranmer --- test/unittests.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/unittests.jl b/test/unittests.jl index a8f02a82..99f278c9 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -975,6 +975,10 @@ end sym5 = dimension(us"km/s") @test_throws "my_special_symbol is not available as a symbol" sym5.my_special_symbol + # Exercise the no-module SymbolicUnits wrappers directly. + @test DynamicQuantities.SymbolicUnits.lookup_unit(:m) == us"m" + @test DynamicQuantities.SymbolicUnits.lookup_constant(:c) == us"Constants.c" + # Test deprecated method q = 1.5us"km/s" @test expand_units(q) == uexpand(q) From 45aa0f76d2755d70831e47f2e12aaeac1e6c0a08 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Wed, 22 Apr 2026 21:25:03 +0000 Subject: [PATCH 03/15] test: restore precompile coverage shape Co-authored-by: Miles Cranmer --- test/precompile_test/ExternalUnitRegistration.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/precompile_test/ExternalUnitRegistration.jl b/test/precompile_test/ExternalUnitRegistration.jl index 0357fad8..2b431eea 100644 --- a/test/precompile_test/ExternalUnitRegistration.jl +++ b/test/precompile_test/ExternalUnitRegistration.jl @@ -15,8 +15,8 @@ symbolic_mywb() = 1us"MyWb" @test :MyWb ∈ collection end - w = expanded_mywb() - ws = symbolic_mywb() + w = u"MyWb" + ws = us"MyWb" @test w isa DEFAULT_QUANTITY_TYPE @test ws isa DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE @test w == u"MyWb" From 7645686b00ec80691d89fa2f02e4fd56d47258be Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Thu, 23 Apr 2026 08:39:46 +0000 Subject: [PATCH 04/15] test: cover package-style external unit registration Co-authored-by: Miles Cranmer --- .../ExternalUnitRegistration.jl | 27 ------------------- .../src/ExternalUnitRegistration.jl | 17 ++++++++++++ test/unittests.jl | 13 +++++---- 3 files changed, 25 insertions(+), 32 deletions(-) delete mode 100644 test/precompile_test/ExternalUnitRegistration.jl create mode 100644 test/precompile_test/ExternalUnitRegistration/src/ExternalUnitRegistration.jl diff --git a/test/precompile_test/ExternalUnitRegistration.jl b/test/precompile_test/ExternalUnitRegistration.jl deleted file mode 100644 index 2b431eea..00000000 --- a/test/precompile_test/ExternalUnitRegistration.jl +++ /dev/null @@ -1,27 +0,0 @@ -module ExternalUnitRegistration - -using DynamicQuantities: @register_unit, @u_str, @us_str -using DynamicQuantities: ALL_MAPPING, ALL_SYMBOLS, DEFAULT_QUANTITY_TYPE -using DynamicQuantities: DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE, UNIT_SYMBOLS, UNIT_MAPPING -using Test - -@register_unit MyWb u"m^2*kg*s^-2*A^-1" - -expanded_mywb() = 1u"MyWb" -symbolic_mywb() = 1us"MyWb" - -@testset "Register Unit Inside a Module" begin - for collection in (UNIT_SYMBOLS, ALL_SYMBOLS, keys(ALL_MAPPING._raw_data), keys(UNIT_MAPPING._raw_data)) - @test :MyWb ∈ collection - end - - w = u"MyWb" - ws = us"MyWb" - @test w isa DEFAULT_QUANTITY_TYPE - @test ws isa DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE - @test w == u"MyWb" - @test ws == us"MyWb" - @test string(ws) == "1.0 MyWb" -end - -end diff --git a/test/precompile_test/ExternalUnitRegistration/src/ExternalUnitRegistration.jl b/test/precompile_test/ExternalUnitRegistration/src/ExternalUnitRegistration.jl new file mode 100644 index 00000000..6e72f583 --- /dev/null +++ b/test/precompile_test/ExternalUnitRegistration/src/ExternalUnitRegistration.jl @@ -0,0 +1,17 @@ +module ExternalUnitRegistration + +using DynamicQuantities: @register_unit, @u_str, @us_str +using DynamicQuantities: DEFAULT_QUANTITY_TYPE, DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE + +@register_unit MyWb u"m^2*kg*s^-2*A^-1" + +const MYWB_EXPANDED = u"MyWb" + +expanded_mywb() = 1u"MyWb" +symbolic_mywb() = 1us"MyWb" +symbolic_mywb_from_helper() = one(symbolic_mywb()) * us"MyWb" +expanded_constant_mywb() = MYWB_EXPANDED + +export MYWB_EXPANDED, expanded_constant_mywb, expanded_mywb, symbolic_mywb, symbolic_mywb_from_helper + +end diff --git a/test/unittests.jl b/test/unittests.jl index 99f278c9..28527a59 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -2338,12 +2338,15 @@ end push!(LOAD_PATH, joinpath(@__DIR__, "precompile_test")) -using ExternalUnitRegistration: MyWb, expanded_mywb, symbolic_mywb +using ExternalUnitRegistration: MYWB_EXPANDED, expanded_constant_mywb, expanded_mywb +using ExternalUnitRegistration: symbolic_mywb, symbolic_mywb_from_helper @testset "Type of External Unit" begin - @test MyWb isa DEFAULT_QUANTITY_TYPE - @test MyWb/u"m^2*kg*s^-2*A^-1" == 1.0 - @test expanded_mywb() == MyWb - @test uexpand(symbolic_mywb()) == MyWb + @test MYWB_EXPANDED isa DEFAULT_QUANTITY_TYPE + @test MYWB_EXPANDED / u"m^2*kg*s^-2*A^-1" == 1.0 + @test expanded_constant_mywb() == MYWB_EXPANDED + @test expanded_mywb() == MYWB_EXPANDED + @test uexpand(symbolic_mywb()) == MYWB_EXPANDED + @test symbolic_mywb_from_helper() == symbolic_mywb() @test string(symbolic_mywb()) == "1.0 MyWb" end From 524cb15a0c66590b65910ce3f4ea3d8ca8dba50e Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Thu, 23 Apr 2026 09:22:10 +0000 Subject: [PATCH 05/15] fix: address symbolic unit review feedback Co-authored-by: Miles Cranmer --- src/register_units.jl | 16 ++++++++++------ src/symbolic_dimensions.jl | 15 ++++++++++----- test/unittests.jl | 24 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/register_units.jl b/src/register_units.jl index 1bbfd129..55ddef62 100644 --- a/src/register_units.jl +++ b/src/register_units.jl @@ -4,14 +4,18 @@ import .SymbolicUnits: update_external_symbolic_unit_value # Update the unit collections const UNIT_UPDATE_LOCK = Threads.SpinLock() +function update_all_values_unlocked(name_symbol, unit) + push!(ALL_SYMBOLS, name_symbol) + push!(ALL_VALUES, unit) + i = lastindex(ALL_VALUES) + ALL_MAPPING[name_symbol] = i + UNIT_MAPPING[name_symbol] = i + update_external_symbolic_unit_value(name_symbol) +end + function update_all_values(name_symbol, unit) lock(UNIT_UPDATE_LOCK) do - push!(ALL_SYMBOLS, name_symbol) - push!(ALL_VALUES, unit) - i = lastindex(ALL_VALUES) - ALL_MAPPING[name_symbol] = i - UNIT_MAPPING[name_symbol] = i - update_external_symbolic_unit_value(name_symbol) + update_all_values_unlocked(name_symbol, unit) end end diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 17dc1f5c..742e7006 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -519,10 +519,12 @@ module SymbolicUnits end map_to_scope(sym::Symbol) = map_to_scope(@__MODULE__, sym) function map_to_scope(mod::Module, sym::Symbol) - if sym in UNIT_SYMBOLS || _has_quantity_binding(mod, sym) + if sym in UNIT_SYMBOLS # return at end elseif sym in CONSTANT_SYMBOLS throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `us\"Constants.$sym\"` instead.")) + elseif _has_quantity_binding(mod, sym) + # return at end else throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) end @@ -535,10 +537,13 @@ module SymbolicUnits return isdefined(mod, sym) && Base.invokelatest(getproperty, mod, sym) isa UnionAbstractQuantity end function _ensure_registered(mod::Module, sym::Symbol) - if iszero(get(ALL_MAPPING, sym, INDEX_TYPE(0))) && _has_quantity_binding(mod, sym) - update_all_values = getfield(parentmodule(@__MODULE__), :update_all_values) - unit = Base.invokelatest(getproperty, mod, sym) - Base.invokelatest(update_all_values, sym, unit) + unit_update_lock = getfield(parentmodule(@__MODULE__), :UNIT_UPDATE_LOCK) + update_all_values_unlocked = getfield(parentmodule(@__MODULE__), :update_all_values_unlocked) + lock(unit_update_lock) do + if iszero(get(ALL_MAPPING, sym, INDEX_TYPE(0))) && _has_quantity_binding(mod, sym) + unit = Base.invokelatest(getproperty, mod, sym) + Base.invokelatest(update_all_values_unlocked, sym, unit) + end end return nothing end diff --git a/test/unittests.jl b/test/unittests.jl index 28527a59..351ffda9 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -710,6 +710,13 @@ end @test_throws "Symbol c found in `Constants` but not `Units`" sym_uparse("c") @test_throws "Unexpected expression" sym_uparse("import ..Units") @test_throws "Unexpected expression" sym_uparse("(m, m)") + + @eval module SymbolicUnitShadowingTest + using DynamicQuantities + const c = 1u"m" + end + @test_throws "Symbol c found in `Constants` but not `Units`" Core.eval(SymbolicUnitShadowingTest, :(us"c")) + @test Core.eval(SymbolicUnitShadowingTest, :(us"Constants.c")) == us"Constants.c" end @testset "Constants" begin @@ -2350,5 +2357,22 @@ using ExternalUnitRegistration: symbolic_mywb, symbolic_mywb_from_helper @test string(symbolic_mywb()) == "1.0 MyWb" end +@testset "Concurrent first-use registration" begin + if Threads.nthreads() > 1 + @eval module SymbolicUnitConcurrentRegistrationTest + using DynamicQuantities + const ConcurrentFooUnitForLazyRegistration = 1u"m" + parse_concurrent_symbol() = us"ConcurrentFooUnitForLazyRegistration" + end + + results = Vector{Any}(undef, Threads.nthreads()) + Threads.@threads for i in eachindex(results) + results[i] = SymbolicUnitConcurrentRegistrationTest.parse_concurrent_symbol() + end + + @test all(x -> uexpand(x) == 1u"m", results) + end +end + pop!(LOAD_PATH) From dde297580cd2a57b4599d4c6f7b7ed2eff8f1699 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Thu, 23 Apr 2026 12:43:49 +0000 Subject: [PATCH 06/15] style: simplify symbolic unit lookup branches Co-authored-by: Miles Cranmer --- src/symbolic_dimensions.jl | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 742e7006..f88f8d6e 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -519,14 +519,12 @@ module SymbolicUnits end map_to_scope(sym::Symbol) = map_to_scope(@__MODULE__, sym) function map_to_scope(mod::Module, sym::Symbol) - if sym in UNIT_SYMBOLS - # return at end - elseif sym in CONSTANT_SYMBOLS - throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `us\"Constants.$sym\"` instead.")) - elseif _has_quantity_binding(mod, sym) - # return at end - else - throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) + if !(sym in UNIT_SYMBOLS) + if sym in CONSTANT_SYMBOLS + throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `us\"Constants.$sym\"` instead.")) + elseif !_has_quantity_binding(mod, sym) + throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) + end end return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), mod, QuoteNode(sym)) end From 982937deb639fce5bf7d6050a98743777c0dceae Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Thu, 23 Apr 2026 13:42:37 +0000 Subject: [PATCH 07/15] refactor: simplify symbolic unit registration lookup Co-authored-by: Miles Cranmer --- src/symbolic_dimensions.jl | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index f88f8d6e..edff94eb 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -532,15 +532,17 @@ module SymbolicUnits map_to_scope(::Module, ex) = ex function _has_quantity_binding(mod::Module, sym::Symbol) - return isdefined(mod, sym) && Base.invokelatest(getproperty, mod, sym) isa UnionAbstractQuantity + isdefined(mod, sym) || return false + return Core.getglobal(mod, sym) isa UnionAbstractQuantity end + _unit_update_lock() = getfield(parentmodule(@__MODULE__), :UNIT_UPDATE_LOCK) + _update_all_values_unlocked(sym, unit) = getfield(parentmodule(@__MODULE__), :update_all_values_unlocked)(sym, unit) + function _ensure_registered(mod::Module, sym::Symbol) - unit_update_lock = getfield(parentmodule(@__MODULE__), :UNIT_UPDATE_LOCK) - update_all_values_unlocked = getfield(parentmodule(@__MODULE__), :update_all_values_unlocked) - lock(unit_update_lock) do + lock(_unit_update_lock()) do if iszero(get(ALL_MAPPING, sym, INDEX_TYPE(0))) && _has_quantity_binding(mod, sym) - unit = Base.invokelatest(getproperty, mod, sym) - Base.invokelatest(update_all_values_unlocked, sym, unit) + unit = Core.getglobal(mod, sym)::UnionAbstractQuantity + _update_all_values_unlocked(sym, unit) end end return nothing From b06c809d95ebbdcccaa5d4ed2f25b95eb772a63f Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Thu, 23 Apr 2026 14:46:35 +0000 Subject: [PATCH 08/15] fix: simplify symbolic external unit lookup Co-authored-by: Miles Cranmer --- src/symbolic_dimensions.jl | 45 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index edff94eb..62127037 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -471,6 +471,7 @@ module SymbolicUnits update_symbolic_unit_values!(w::WriteOnceReadMany) = update_symbolic_unit_values!.(w._raw_data) update_symbolic_unit_values!(UNIT_SYMBOLS) + const BUILTIN_UNIT_SYMBOLS = Tuple(UNIT_SYMBOLS._raw_data) # Non-eval version of `update_symbolic_unit_values!` for registering units in # an external module. @@ -519,39 +520,39 @@ module SymbolicUnits end map_to_scope(sym::Symbol) = map_to_scope(@__MODULE__, sym) function map_to_scope(mod::Module, sym::Symbol) - if !(sym in UNIT_SYMBOLS) - if sym in CONSTANT_SYMBOLS - throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `us\"Constants.$sym\"` instead.")) - elseif !_has_quantity_binding(mod, sym) - throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) - end + if sym in BUILTIN_UNIT_SYMBOLS + return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), QuoteNode(sym)) + elseif sym in CONSTANT_SYMBOLS + throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `us\"Constants.$sym\"` instead.")) + elseif !(mod === @__MODULE__) && _external_quantity_binding(mod, sym) + return Expr(:call, GlobalRef(@__MODULE__, :lookup_external_unit), QuoteNode(sym), GlobalRef(mod, sym)) + elseif sym in UNIT_SYMBOLS + return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), QuoteNode(sym)) + else + throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) end - return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), mod, QuoteNode(sym)) end map_to_scope(ex) = ex map_to_scope(::Module, ex) = ex - function _has_quantity_binding(mod::Module, sym::Symbol) - isdefined(mod, sym) || return false - return Core.getglobal(mod, sym) isa UnionAbstractQuantity + function _external_quantity_binding(mod::Module, sym::Symbol) + return isdefined(mod, sym) && getfield(mod, sym) isa UnionAbstractQuantity end - _unit_update_lock() = getfield(parentmodule(@__MODULE__), :UNIT_UPDATE_LOCK) - _update_all_values_unlocked(sym, unit) = getfield(parentmodule(@__MODULE__), :update_all_values_unlocked)(sym, unit) - - function _ensure_registered(mod::Module, sym::Symbol) - lock(_unit_update_lock()) do - if iszero(get(ALL_MAPPING, sym, INDEX_TYPE(0))) && _has_quantity_binding(mod, sym) - unit = Core.getglobal(mod, sym)::UnionAbstractQuantity - _update_all_values_unlocked(sym, unit) + function _ensure_registered_external_unit(sym::Symbol, unit::UnionAbstractQuantity) + unit_update_lock = getfield(parentmodule(@__MODULE__), :UNIT_UPDATE_LOCK) + update_all_values_unlocked = getfield(parentmodule(@__MODULE__), :update_all_values_unlocked) + lock(unit_update_lock) do + if iszero(get(ALL_MAPPING, sym, INDEX_TYPE(0))) + update_all_values_unlocked(sym, unit) end end return nothing end - function lookup_unit(mod::Module, ex::Symbol) - _ensure_registered(mod, ex) - return as_quantity(symbolic_unit_from_symbol(ex)) + lookup_unit(ex::Symbol) = as_quantity(symbolic_unit_from_symbol(ex)) + function lookup_external_unit(sym::Symbol, unit::UnionAbstractQuantity) + _ensure_registered_external_unit(sym, unit) + return lookup_unit(sym) end - lookup_unit(ex::Symbol) = lookup_unit(@__MODULE__, ex) lookup_constant(ex::Symbol) = as_quantity(symbolic_constant_from_symbol(ex)) end From 39abd36d1965ccb250b7470c1cf36f6da2d0cfff Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Thu, 23 Apr 2026 17:21:47 +0000 Subject: [PATCH 09/15] fix: make external unit parsing symmetric Co-authored-by: Miles Cranmer --- src/symbolic_dimensions.jl | 14 +----- src/uparse.jl | 43 +++++++++++++++---- .../src/ExternalUnitRegistration.jl | 6 ++- test/unittests.jl | 9 +++- 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 62127037..8df9e134 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -417,6 +417,7 @@ module SymbolicUnits import ..DEFAULT_VALUE_TYPE import ..DEFAULT_DIM_BASE_TYPE import ..INDEX_TYPE + import ..UnitsParse: _ensure_registered_external_unit, _external_quantity_binding import ..UnionAbstractQuantity import ..WriteOnceReadMany import ..disambiguate_constant_symbol @@ -535,19 +536,6 @@ module SymbolicUnits map_to_scope(ex) = ex map_to_scope(::Module, ex) = ex - function _external_quantity_binding(mod::Module, sym::Symbol) - return isdefined(mod, sym) && getfield(mod, sym) isa UnionAbstractQuantity - end - function _ensure_registered_external_unit(sym::Symbol, unit::UnionAbstractQuantity) - unit_update_lock = getfield(parentmodule(@__MODULE__), :UNIT_UPDATE_LOCK) - update_all_values_unlocked = getfield(parentmodule(@__MODULE__), :update_all_values_unlocked) - lock(unit_update_lock) do - if iszero(get(ALL_MAPPING, sym, INDEX_TYPE(0))) - update_all_values_unlocked(sym, unit) - end - end - return nothing - end lookup_unit(ex::Symbol) = as_quantity(symbolic_unit_from_symbol(ex)) function lookup_external_unit(sym::Symbol, unit::UnionAbstractQuantity) _ensure_registered_external_unit(sym, unit) diff --git a/src/uparse.jl b/src/uparse.jl index 27f0ce60..aa529b99 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -6,7 +6,8 @@ import ..constructorof import ..DEFAULT_QUANTITY_TYPE import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE -import ..Units: UNIT_SYMBOLS, UNIT_VALUES +import ..UnionAbstractQuantity +import ..Units: UNIT_SYMBOLS, UNIT_MAPPING import ..Constants: CONSTANT_SYMBOLS, CONSTANT_VALUES import ..Constants @@ -59,28 +60,32 @@ the quantity corresponding to the speed of light multiplied by Hertz, squared. """ macro u_str(s) - ex = map_to_scope(Meta.parse(s)) + ex = map_to_scope(__module__, Meta.parse(s)) ex = :($as_quantity($ex)) return esc(ex) end -@unstable function map_to_scope(ex::Expr) +@unstable map_to_scope(ex::Expr) = map_to_scope(@__MODULE__, ex) +@unstable function map_to_scope(mod::Module, ex::Expr) if !(ex.head == :call) && !(ex.head == :. && ex.args[1] == :Constants) throw(ArgumentError("Unexpected expression: $ex. Only `:call` and `:.` (for `Constants`) are expected.")) end if ex.head == :call - ex.args[2:end] = map(map_to_scope, ex.args[2:end]) + ex.args[2:end] = map(arg -> map_to_scope(mod, arg), ex.args[2:end]) return ex else # if ex.head == :. && ex.args[1] == :Constants @assert ex.args[2] isa QuoteNode - return lookup_constant(ex.args[2].value) + return Expr(:call, GlobalRef(@__MODULE__, :lookup_constant), QuoteNode(ex.args[2].value)) end end -function map_to_scope(sym::Symbol) +map_to_scope(sym::Symbol) = map_to_scope(@__MODULE__, sym) +function map_to_scope(mod::Module, sym::Symbol) if sym in UNIT_SYMBOLS - return lookup_unit(sym) + return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), QuoteNode(sym)) elseif sym in CONSTANT_SYMBOLS throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `u\"Constants.$sym\"` instead.")) + elseif !(mod === @__MODULE__) && _external_quantity_binding(mod, sym) + return Expr(:call, GlobalRef(@__MODULE__, :lookup_external_unit), QuoteNode(sym), GlobalRef(mod, sym)) else throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) end @@ -88,9 +93,29 @@ end function map_to_scope(ex) return ex end +map_to_scope(::Module, ex) = ex + +function _external_quantity_binding(mod::Module, sym::Symbol) + return isdefined(mod, sym) && getfield(mod, sym) isa UnionAbstractQuantity +end +function _ensure_registered_external_unit(sym::Symbol, unit::UnionAbstractQuantity) + unit_update_lock = getfield(parentmodule(@__MODULE__), :UNIT_UPDATE_LOCK) + update_all_values_unlocked = getfield(parentmodule(@__MODULE__), :update_all_values_unlocked) + lock(unit_update_lock) do + if iszero(get(UNIT_MAPPING, sym, 0)) + update_all_values_unlocked(sym, unit) + end + end + return nothing +end function lookup_unit(ex::Symbol) - i = findfirst(==(ex), UNIT_SYMBOLS)::Int - return UNIT_VALUES[i] + i = get(UNIT_MAPPING, ex, 0) + iszero(i) && throw(ArgumentError("Symbol $ex not found in `Units`.")) + return getfield(parentmodule(@__MODULE__), :ALL_VALUES)[i] +end +function lookup_external_unit(sym::Symbol, unit::UnionAbstractQuantity) + _ensure_registered_external_unit(sym, unit) + return lookup_unit(sym) end function lookup_constant(ex::Symbol) i = findfirst(==(ex), CONSTANT_SYMBOLS)::Int diff --git a/test/precompile_test/ExternalUnitRegistration/src/ExternalUnitRegistration.jl b/test/precompile_test/ExternalUnitRegistration/src/ExternalUnitRegistration.jl index 6e72f583..5fe334f6 100644 --- a/test/precompile_test/ExternalUnitRegistration/src/ExternalUnitRegistration.jl +++ b/test/precompile_test/ExternalUnitRegistration/src/ExternalUnitRegistration.jl @@ -9,9 +9,13 @@ const MYWB_EXPANDED = u"MyWb" expanded_mywb() = 1u"MyWb" symbolic_mywb() = 1us"MyWb" +expanded_mywb_from_helper() = one(expanded_mywb()) * u"MyWb" symbolic_mywb_from_helper() = one(symbolic_mywb()) * us"MyWb" expanded_constant_mywb() = MYWB_EXPANDED -export MYWB_EXPANDED, expanded_constant_mywb, expanded_mywb, symbolic_mywb, symbolic_mywb_from_helper +export MyWb +export MYWB_EXPANDED +export expanded_constant_mywb, expanded_mywb, expanded_mywb_from_helper +export symbolic_mywb, symbolic_mywb_from_helper end diff --git a/test/unittests.jl b/test/unittests.jl index 351ffda9..defed8b6 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -2345,13 +2345,19 @@ end push!(LOAD_PATH, joinpath(@__DIR__, "precompile_test")) -using ExternalUnitRegistration: MYWB_EXPANDED, expanded_constant_mywb, expanded_mywb +using ExternalUnitRegistration: MYWB_EXPANDED, MyWb +using ExternalUnitRegistration: expanded_constant_mywb, expanded_mywb, expanded_mywb_from_helper using ExternalUnitRegistration: symbolic_mywb, symbolic_mywb_from_helper @testset "Type of External Unit" begin @test MYWB_EXPANDED isa DEFAULT_QUANTITY_TYPE @test MYWB_EXPANDED / u"m^2*kg*s^-2*A^-1" == 1.0 + @test u"MyWb" == MYWB_EXPANDED + @test uexpand(us"MyWb") == MYWB_EXPANDED + @test string(us"MyWb") == "1.0 MyWb" + @test MyWb == MYWB_EXPANDED @test expanded_constant_mywb() == MYWB_EXPANDED @test expanded_mywb() == MYWB_EXPANDED + @test expanded_mywb_from_helper() == expanded_mywb() @test uexpand(symbolic_mywb()) == MYWB_EXPANDED @test symbolic_mywb_from_helper() == symbolic_mywb() @test string(symbolic_mywb()) == "1.0 MyWb" @@ -2375,4 +2381,3 @@ end end pop!(LOAD_PATH) - From 97a29834ccc2c2f5b1fcc9acd211e5bad3ebb257 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Thu, 23 Apr 2026 17:56:54 +0000 Subject: [PATCH 10/15] fix: handle external unit lookup in precompile --- src/uparse.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/uparse.jl b/src/uparse.jl index aa529b99..e884f28f 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -24,6 +24,7 @@ macro generate_units_import() end @generate_units_import +const BUILTIN_UNIT_SYMBOLS = Tuple(UNIT_SYMBOLS._raw_data) """ uparse(s::AbstractString) @@ -80,12 +81,14 @@ end end map_to_scope(sym::Symbol) = map_to_scope(@__MODULE__, sym) function map_to_scope(mod::Module, sym::Symbol) - if sym in UNIT_SYMBOLS + if sym in BUILTIN_UNIT_SYMBOLS return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), QuoteNode(sym)) elseif sym in CONSTANT_SYMBOLS throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `u\"Constants.$sym\"` instead.")) elseif !(mod === @__MODULE__) && _external_quantity_binding(mod, sym) return Expr(:call, GlobalRef(@__MODULE__, :lookup_external_unit), QuoteNode(sym), GlobalRef(mod, sym)) + elseif sym in UNIT_SYMBOLS + return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), QuoteNode(sym)) else throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) end @@ -108,12 +111,12 @@ function _ensure_registered_external_unit(sym::Symbol, unit::UnionAbstractQuanti end return nothing end -function lookup_unit(ex::Symbol) +@unstable function lookup_unit(ex::Symbol) i = get(UNIT_MAPPING, ex, 0) iszero(i) && throw(ArgumentError("Symbol $ex not found in `Units`.")) return getfield(parentmodule(@__MODULE__), :ALL_VALUES)[i] end -function lookup_external_unit(sym::Symbol, unit::UnionAbstractQuantity) +@unstable function lookup_external_unit(sym::Symbol, unit::UnionAbstractQuantity) _ensure_registered_external_unit(sym, unit) return lookup_unit(sym) end From a8912ae4364623692cc2e7cc72882f57edbec3d0 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Thu, 23 Apr 2026 19:01:27 +0000 Subject: [PATCH 11/15] fix: remove unreachable unit lookup fallbacks Co-authored-by: Miles Cranmer --- src/symbolic_dimensions.jl | 2 -- src/uparse.jl | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 8df9e134..fbc36e80 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -527,8 +527,6 @@ module SymbolicUnits throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `us\"Constants.$sym\"` instead.")) elseif !(mod === @__MODULE__) && _external_quantity_binding(mod, sym) return Expr(:call, GlobalRef(@__MODULE__, :lookup_external_unit), QuoteNode(sym), GlobalRef(mod, sym)) - elseif sym in UNIT_SYMBOLS - return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), QuoteNode(sym)) else throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) end diff --git a/src/uparse.jl b/src/uparse.jl index e884f28f..2ac4abd1 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -87,8 +87,6 @@ function map_to_scope(mod::Module, sym::Symbol) throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `u\"Constants.$sym\"` instead.")) elseif !(mod === @__MODULE__) && _external_quantity_binding(mod, sym) return Expr(:call, GlobalRef(@__MODULE__, :lookup_external_unit), QuoteNode(sym), GlobalRef(mod, sym)) - elseif sym in UNIT_SYMBOLS - return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), QuoteNode(sym)) else throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) end From c5c186801e069f099938ff7d9b9751961948fa4d Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Thu, 23 Apr 2026 22:48:56 +0000 Subject: [PATCH 12/15] refactor: centralize builtin unit symbol snapshot Co-authored-by: Miles Cranmer --- src/symbolic_dimensions.jl | 2 +- src/units.jl | 2 ++ src/uparse.jl | 3 +-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index fbc36e80..34569e5e 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -408,6 +408,7 @@ module SymbolicUnits using DispatchDoctor: @unstable import ..UNIT_SYMBOLS + import ..Units: BUILTIN_UNIT_SYMBOLS import ..CONSTANT_SYMBOLS import ..ALL_MAPPING import ..SymbolicDimensionsSingleton @@ -472,7 +473,6 @@ module SymbolicUnits update_symbolic_unit_values!(w::WriteOnceReadMany) = update_symbolic_unit_values!.(w._raw_data) update_symbolic_unit_values!(UNIT_SYMBOLS) - const BUILTIN_UNIT_SYMBOLS = Tuple(UNIT_SYMBOLS._raw_data) # Non-eval version of `update_symbolic_unit_values!` for registering units in # an external module. diff --git a/src/units.jl b/src/units.jl index 2ae7608e..8f7018e5 100644 --- a/src/units.jl +++ b/src/units.jl @@ -302,6 +302,8 @@ end # Do not wish to define physical constants, as the number of symbols might lead to ambiguity. # The user should define these instead. +const BUILTIN_UNIT_SYMBOLS = Tuple(UNIT_SYMBOLS._raw_data) + # Update `UNIT_MAPPING` with all internally defined unit symbols. const UNIT_MAPPING = WriteOnceReadMany(Dict(s => i for (i, s) in enumerate(UNIT_SYMBOLS))) diff --git a/src/uparse.jl b/src/uparse.jl index 2ac4abd1..3c894a07 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -7,7 +7,7 @@ import ..DEFAULT_QUANTITY_TYPE import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE import ..UnionAbstractQuantity -import ..Units: UNIT_SYMBOLS, UNIT_MAPPING +import ..Units: BUILTIN_UNIT_SYMBOLS, UNIT_SYMBOLS, UNIT_MAPPING import ..Constants: CONSTANT_SYMBOLS, CONSTANT_VALUES import ..Constants @@ -24,7 +24,6 @@ macro generate_units_import() end @generate_units_import -const BUILTIN_UNIT_SYMBOLS = Tuple(UNIT_SYMBOLS._raw_data) """ uparse(s::AbstractString) From 6efcab19a92b848081c6b71087d3deaff0a0d7a9 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Thu, 23 Apr 2026 23:05:57 +0000 Subject: [PATCH 13/15] refactor: clean up external unit lookup Co-authored-by: Miles Cranmer --- src/DynamicQuantities.jl | 1 + src/external_unit_lookup.jl | 18 ++++++++++++++ src/symbolic_dimensions.jl | 25 +++++++++++-------- src/uparse.jl | 48 +++++++++++++++---------------------- 4 files changed, 53 insertions(+), 39 deletions(-) create mode 100644 src/external_unit_lookup.jl diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index 6c8a2382..1a4fc94f 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -27,6 +27,7 @@ using DispatchDoctor: @stable include("arrays.jl") include("units.jl") include("constants.jl") + include("external_unit_lookup.jl") include("uparse.jl") include("symbolic_dimensions.jl") include("affine_dimensions.jl") diff --git a/src/external_unit_lookup.jl b/src/external_unit_lookup.jl new file mode 100644 index 00000000..a788d28f --- /dev/null +++ b/src/external_unit_lookup.jl @@ -0,0 +1,18 @@ +function external_quantity_binding(mod::Module, sym::Symbol) + return isdefined(mod, sym) && getfield(mod, sym) isa UnionAbstractQuantity +end + +function ensure_registered_external_unit(sym::Symbol, unit::UnionAbstractQuantity) + lock(UNIT_UPDATE_LOCK) do + if iszero(get(UNIT_MAPPING, sym, 0)) + update_all_values_unlocked(sym, unit) + end + end + return nothing +end + +function lookup_registered_unit(sym::Symbol) + i = get(UNIT_MAPPING, sym, 0) + iszero(i) && throw(ArgumentError("Symbol $sym not found in `Units`.")) + return ALL_VALUES[i] +end diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 34569e5e..49021b89 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -418,8 +418,8 @@ module SymbolicUnits import ..DEFAULT_VALUE_TYPE import ..DEFAULT_DIM_BASE_TYPE import ..INDEX_TYPE - import ..UnitsParse: _ensure_registered_external_unit, _external_quantity_binding - import ..UnionAbstractQuantity + import ..ensure_registered_external_unit + import ..external_quantity_binding import ..WriteOnceReadMany import ..disambiguate_constant_symbol @@ -521,22 +521,27 @@ module SymbolicUnits end map_to_scope(sym::Symbol) = map_to_scope(@__MODULE__, sym) function map_to_scope(mod::Module, sym::Symbol) - if sym in BUILTIN_UNIT_SYMBOLS - return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), QuoteNode(sym)) - elseif sym in CONSTANT_SYMBOLS + has_builtin_binding = sym in BUILTIN_UNIT_SYMBOLS + has_external_binding = !(mod === @__MODULE__) && external_quantity_binding(mod, sym) + + if !has_builtin_binding && sym in CONSTANT_SYMBOLS throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `us\"Constants.$sym\"` instead.")) - elseif !(mod === @__MODULE__) && _external_quantity_binding(mod, sym) - return Expr(:call, GlobalRef(@__MODULE__, :lookup_external_unit), QuoteNode(sym), GlobalRef(mod, sym)) - else + elseif !has_builtin_binding && !has_external_binding throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) end + + if has_builtin_binding + return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), QuoteNode(sym)) + else + return Expr(:call, GlobalRef(@__MODULE__, :lookup_external_unit), QuoteNode(sym), GlobalRef(mod, sym)) + end end map_to_scope(ex) = ex map_to_scope(::Module, ex) = ex lookup_unit(ex::Symbol) = as_quantity(symbolic_unit_from_symbol(ex)) - function lookup_external_unit(sym::Symbol, unit::UnionAbstractQuantity) - _ensure_registered_external_unit(sym, unit) + function lookup_external_unit(sym::Symbol, unit) + ensure_registered_external_unit(sym, unit) return lookup_unit(sym) end lookup_constant(ex::Symbol) = as_quantity(symbolic_constant_from_symbol(ex)) diff --git a/src/uparse.jl b/src/uparse.jl index 3c894a07..c223d98e 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -6,8 +6,10 @@ import ..constructorof import ..DEFAULT_QUANTITY_TYPE import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE -import ..UnionAbstractQuantity -import ..Units: BUILTIN_UNIT_SYMBOLS, UNIT_SYMBOLS, UNIT_MAPPING +import ..external_quantity_binding +import ..ensure_registered_external_unit +import ..lookup_registered_unit +import ..Units: BUILTIN_UNIT_SYMBOLS, UNIT_SYMBOLS import ..Constants: CONSTANT_SYMBOLS, CONSTANT_VALUES import ..Constants @@ -80,42 +82,30 @@ end end map_to_scope(sym::Symbol) = map_to_scope(@__MODULE__, sym) function map_to_scope(mod::Module, sym::Symbol) - if sym in BUILTIN_UNIT_SYMBOLS - return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), QuoteNode(sym)) - elseif sym in CONSTANT_SYMBOLS + has_builtin_binding = sym in BUILTIN_UNIT_SYMBOLS + has_external_binding = !(mod === @__MODULE__) && external_quantity_binding(mod, sym) + + if !has_builtin_binding && sym in CONSTANT_SYMBOLS throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `u\"Constants.$sym\"` instead.")) - elseif !(mod === @__MODULE__) && _external_quantity_binding(mod, sym) - return Expr(:call, GlobalRef(@__MODULE__, :lookup_external_unit), QuoteNode(sym), GlobalRef(mod, sym)) - else + elseif !has_builtin_binding && !has_external_binding throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) end + + if has_builtin_binding + return Expr(:call, GlobalRef(parentmodule(@__MODULE__), :lookup_registered_unit), QuoteNode(sym)) + else + return Expr(:call, GlobalRef(@__MODULE__, :lookup_external_unit), QuoteNode(sym), GlobalRef(mod, sym)) + end end function map_to_scope(ex) return ex end map_to_scope(::Module, ex) = ex -function _external_quantity_binding(mod::Module, sym::Symbol) - return isdefined(mod, sym) && getfield(mod, sym) isa UnionAbstractQuantity -end -function _ensure_registered_external_unit(sym::Symbol, unit::UnionAbstractQuantity) - unit_update_lock = getfield(parentmodule(@__MODULE__), :UNIT_UPDATE_LOCK) - update_all_values_unlocked = getfield(parentmodule(@__MODULE__), :update_all_values_unlocked) - lock(unit_update_lock) do - if iszero(get(UNIT_MAPPING, sym, 0)) - update_all_values_unlocked(sym, unit) - end - end - return nothing -end -@unstable function lookup_unit(ex::Symbol) - i = get(UNIT_MAPPING, ex, 0) - iszero(i) && throw(ArgumentError("Symbol $ex not found in `Units`.")) - return getfield(parentmodule(@__MODULE__), :ALL_VALUES)[i] -end -@unstable function lookup_external_unit(sym::Symbol, unit::UnionAbstractQuantity) - _ensure_registered_external_unit(sym, unit) - return lookup_unit(sym) +@unstable lookup_unit(ex::Symbol) = lookup_registered_unit(ex) +@unstable function lookup_external_unit(sym::Symbol, unit) + ensure_registered_external_unit(sym, unit) + return lookup_registered_unit(sym) end function lookup_constant(ex::Symbol) i = findfirst(==(ex), CONSTANT_SYMBOLS)::Int From 9f886d9c552d133178958177ee42c77da9a34ba1 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Fri, 24 Apr 2026 21:57:27 +0000 Subject: [PATCH 14/15] fix: restore registered unit parsing paths Co-authored-by: Miles Cranmer --- src/DynamicQuantities.jl | 1 - src/external_unit_lookup.jl | 18 ----------------- src/symbolic_dimensions.jl | 20 ++++++++----------- src/uparse.jl | 39 ++++++++++++++++++++++++++----------- test/unittests.jl | 7 +++++++ 5 files changed, 43 insertions(+), 42 deletions(-) delete mode 100644 src/external_unit_lookup.jl diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index 1a4fc94f..6c8a2382 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -27,7 +27,6 @@ using DispatchDoctor: @stable include("arrays.jl") include("units.jl") include("constants.jl") - include("external_unit_lookup.jl") include("uparse.jl") include("symbolic_dimensions.jl") include("affine_dimensions.jl") diff --git a/src/external_unit_lookup.jl b/src/external_unit_lookup.jl deleted file mode 100644 index a788d28f..00000000 --- a/src/external_unit_lookup.jl +++ /dev/null @@ -1,18 +0,0 @@ -function external_quantity_binding(mod::Module, sym::Symbol) - return isdefined(mod, sym) && getfield(mod, sym) isa UnionAbstractQuantity -end - -function ensure_registered_external_unit(sym::Symbol, unit::UnionAbstractQuantity) - lock(UNIT_UPDATE_LOCK) do - if iszero(get(UNIT_MAPPING, sym, 0)) - update_all_values_unlocked(sym, unit) - end - end - return nothing -end - -function lookup_registered_unit(sym::Symbol) - i = get(UNIT_MAPPING, sym, 0) - iszero(i) && throw(ArgumentError("Symbol $sym not found in `Units`.")) - return ALL_VALUES[i] -end diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index 49021b89..caf55f59 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -408,9 +408,7 @@ module SymbolicUnits using DispatchDoctor: @unstable import ..UNIT_SYMBOLS - import ..Units: BUILTIN_UNIT_SYMBOLS import ..CONSTANT_SYMBOLS - import ..ALL_MAPPING import ..SymbolicDimensionsSingleton import ..constructorof import ..DEFAULT_SYMBOLIC_QUANTITY_TYPE @@ -521,27 +519,25 @@ module SymbolicUnits end map_to_scope(sym::Symbol) = map_to_scope(@__MODULE__, sym) function map_to_scope(mod::Module, sym::Symbol) - has_builtin_binding = sym in BUILTIN_UNIT_SYMBOLS + has_registered_binding = sym in UNIT_SYMBOLS has_external_binding = !(mod === @__MODULE__) && external_quantity_binding(mod, sym) - if !has_builtin_binding && sym in CONSTANT_SYMBOLS + if !has_registered_binding && sym in CONSTANT_SYMBOLS throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `us\"Constants.$sym\"` instead.")) - elseif !has_builtin_binding && !has_external_binding + elseif !has_registered_binding && !has_external_binding throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) + elseif has_external_binding + return Expr(:call, GlobalRef(@__MODULE__, :lookup_external_unit), QuoteNode(mod), QuoteNode(sym)) end - if has_builtin_binding - return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), QuoteNode(sym)) - else - return Expr(:call, GlobalRef(@__MODULE__, :lookup_external_unit), QuoteNode(sym), GlobalRef(mod, sym)) - end + return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), QuoteNode(sym)) end map_to_scope(ex) = ex map_to_scope(::Module, ex) = ex lookup_unit(ex::Symbol) = as_quantity(symbolic_unit_from_symbol(ex)) - function lookup_external_unit(sym::Symbol, unit) - ensure_registered_external_unit(sym, unit) + function lookup_external_unit(mod::Module, sym::Symbol) + ensure_registered_external_unit(sym, getfield(mod, sym)) return lookup_unit(sym) end lookup_constant(ex::Symbol) = as_quantity(symbolic_constant_from_symbol(ex)) diff --git a/src/uparse.jl b/src/uparse.jl index c223d98e..a8e85048 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -1,3 +1,22 @@ +function external_quantity_binding(mod::Module, sym::Symbol) + return isdefined(mod, sym) && getfield(mod, sym) isa UnionAbstractQuantity +end + +function ensure_registered_external_unit(sym::Symbol, unit::UnionAbstractQuantity) + lock(UNIT_UPDATE_LOCK) do + if iszero(get(UNIT_MAPPING, sym, 0)) + update_all_values_unlocked(sym, unit) + end + end + return nothing +end + +function lookup_registered_unit(sym::Symbol) + i = get(UNIT_MAPPING, sym, 0) + iszero(i) && throw(ArgumentError("Symbol $sym not found in `Units`.")) + return ALL_VALUES[i] +end + module UnitsParse using DispatchDoctor: @unstable @@ -9,7 +28,7 @@ import ..DEFAULT_VALUE_TYPE import ..external_quantity_binding import ..ensure_registered_external_unit import ..lookup_registered_unit -import ..Units: BUILTIN_UNIT_SYMBOLS, UNIT_SYMBOLS +import ..Units: UNIT_SYMBOLS import ..Constants: CONSTANT_SYMBOLS, CONSTANT_VALUES import ..Constants @@ -82,20 +101,18 @@ end end map_to_scope(sym::Symbol) = map_to_scope(@__MODULE__, sym) function map_to_scope(mod::Module, sym::Symbol) - has_builtin_binding = sym in BUILTIN_UNIT_SYMBOLS + has_registered_binding = sym in UNIT_SYMBOLS has_external_binding = !(mod === @__MODULE__) && external_quantity_binding(mod, sym) - if !has_builtin_binding && sym in CONSTANT_SYMBOLS + if !has_registered_binding && sym in CONSTANT_SYMBOLS throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `u\"Constants.$sym\"` instead.")) - elseif !has_builtin_binding && !has_external_binding + elseif !has_registered_binding && !has_external_binding throw(ArgumentError("Symbol $sym not found in `Units` or `Constants`.")) + elseif has_external_binding + return Expr(:call, GlobalRef(@__MODULE__, :lookup_external_unit), QuoteNode(mod), QuoteNode(sym)) end - if has_builtin_binding - return Expr(:call, GlobalRef(parentmodule(@__MODULE__), :lookup_registered_unit), QuoteNode(sym)) - else - return Expr(:call, GlobalRef(@__MODULE__, :lookup_external_unit), QuoteNode(sym), GlobalRef(mod, sym)) - end + return Expr(:call, GlobalRef(@__MODULE__, :lookup_unit), QuoteNode(sym)) end function map_to_scope(ex) return ex @@ -103,8 +120,8 @@ end map_to_scope(::Module, ex) = ex @unstable lookup_unit(ex::Symbol) = lookup_registered_unit(ex) -@unstable function lookup_external_unit(sym::Symbol, unit) - ensure_registered_external_unit(sym, unit) +@unstable function lookup_external_unit(mod::Module, sym::Symbol) + ensure_registered_external_unit(sym, getfield(mod, sym)) return lookup_registered_unit(sym) end function lookup_constant(ex::Symbol) diff --git a/test/unittests.jl b/test/unittests.jl index defed8b6..fb1b536c 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -2322,6 +2322,13 @@ end MySV = u"MySV" MySV2 = u"MySV2" + @test uparse("MyV") == u"V" + @test uparse("MySV") == u"V" + @test uparse("MySV2") == u"km/h" + @test sym_uparse("MyV") == us"MyV" + @test sym_uparse("MySV") == us"MySV" + @test sym_uparse("MySV2") == us"MySV2" + @test MyV === u"V" @test MyV == us"V" @test MySV == us"V" From 7ec2b6cf7212b43b73efdd5b67a8e225697937c7 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Mon, 8 Jun 2026 19:46:43 +0000 Subject: [PATCH 15/15] fix: support registering units during init --- src/register_units.jl | 38 ++++++++++++------- src/symbolic_dimensions.jl | 5 ++- src/uparse.jl | 22 ++++++++++- .../src/ExternalUnitRegistration.jl | 7 ++++ test/unittests.jl | 11 +++--- 5 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/register_units.jl b/src/register_units.jl index 55ddef62..387bbfe7 100644 --- a/src/register_units.jl +++ b/src/register_units.jl @@ -1,10 +1,12 @@ -import .Units: UNIT_MAPPING, UNIT_SYMBOLS, UNIT_VALUES, _lazy_register_unit +import .Units: UNIT_MAPPING, UNIT_SYMBOLS, UNIT_VALUES import .SymbolicUnits: update_external_symbolic_unit_value # Update the unit collections const UNIT_UPDATE_LOCK = Threads.SpinLock() function update_all_values_unlocked(name_symbol, unit) + push!(UNIT_SYMBOLS, name_symbol) + push!(UNIT_VALUES, unit) push!(ALL_SYMBOLS, name_symbol) push!(ALL_VALUES, unit) i = lastindex(ALL_VALUES) @@ -15,8 +17,20 @@ end function update_all_values(name_symbol, unit) lock(UNIT_UPDATE_LOCK) do - update_all_values_unlocked(name_symbol, unit) + index = get(ALL_MAPPING, name_symbol, INDEX_TYPE(0)) + if iszero(index) + update_all_values_unlocked(name_symbol, unit) + elseif ALL_VALUES[index] != unit + error("Unit `$name_symbol` is already defined as `$(ALL_VALUES[index])`") + end + end +end + +function define_unit_binding(mod::Module, name::Symbol, unit) + if !isdefined(mod, name) + Core.eval(mod, Expr(:const, Expr(:(=), name, QuoteNode(unit)))) end + return unit end """ @@ -50,10 +64,11 @@ julia> x * y^2 |> us"W^2" |> sqrt |> uexpand """ macro register_unit(symbol, value) - return esc(_register_unit(symbol, value)) + declare_external_unit(__module__, symbol) + return esc(_register_unit(__module__, symbol, value)) end -function _register_unit(name::Symbol, value) +function _register_unit(mod::Module, name::Symbol, value) name_symbol = Meta.quot(name) index = get(ALL_MAPPING, name, INDEX_TYPE(0)) if !iszero(index) @@ -64,13 +79,10 @@ function _register_unit(name::Symbol, value) # unit.value != value && throw("Unit $name is already defined as $unit") error("Unit `$name` is already defined as `$unit`") end - reg_expr = _lazy_register_unit(name, value) - push!( - reg_expr.args, - quote - $update_all_values($name_symbol, $value) - nothing - end - ) - return reg_expr + return quote + local unit = $value + $define_unit_binding($(QuoteNode(mod)), $name_symbol, unit) + $update_all_values($name_symbol, unit) + nothing + end end diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl index caf55f59..89743d99 100644 --- a/src/symbolic_dimensions.jl +++ b/src/symbolic_dimensions.jl @@ -418,6 +418,7 @@ module SymbolicUnits import ..INDEX_TYPE import ..ensure_registered_external_unit import ..external_quantity_binding + import ..external_unit_declaration import ..WriteOnceReadMany import ..disambiguate_constant_symbol @@ -520,7 +521,9 @@ module SymbolicUnits map_to_scope(sym::Symbol) = map_to_scope(@__MODULE__, sym) function map_to_scope(mod::Module, sym::Symbol) has_registered_binding = sym in UNIT_SYMBOLS - has_external_binding = !(mod === @__MODULE__) && external_quantity_binding(mod, sym) + has_external_binding = !(mod === @__MODULE__) && ( + external_quantity_binding(mod, sym) || external_unit_declaration(mod, sym) + ) if !has_registered_binding && sym in CONSTANT_SYMBOLS throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `us\"Constants.$sym\"` instead.")) diff --git a/src/uparse.jl b/src/uparse.jl index a8e85048..53a4ec64 100644 --- a/src/uparse.jl +++ b/src/uparse.jl @@ -1,3 +1,20 @@ +const EXTERNAL_UNIT_DECLARATION_LOCK = Threads.SpinLock() +const EXTERNAL_UNIT_DECLARATIONS = IdDict{Module,Set{Symbol}}() + +function declare_external_unit(mod::Module, name::Symbol) + lock(EXTERNAL_UNIT_DECLARATION_LOCK) do + push!(get!(() -> Set{Symbol}(), EXTERNAL_UNIT_DECLARATIONS, mod), name) + end + return nothing +end + +function external_unit_declaration(mod::Module, name::Symbol) + lock(EXTERNAL_UNIT_DECLARATION_LOCK) do + declarations = get(EXTERNAL_UNIT_DECLARATIONS, mod, nothing) + return declarations !== nothing && name in declarations + end +end + function external_quantity_binding(mod::Module, sym::Symbol) return isdefined(mod, sym) && getfield(mod, sym) isa UnionAbstractQuantity end @@ -26,6 +43,7 @@ import ..DEFAULT_QUANTITY_TYPE import ..DEFAULT_DIM_TYPE import ..DEFAULT_VALUE_TYPE import ..external_quantity_binding +import ..external_unit_declaration import ..ensure_registered_external_unit import ..lookup_registered_unit import ..Units: UNIT_SYMBOLS @@ -102,7 +120,9 @@ end map_to_scope(sym::Symbol) = map_to_scope(@__MODULE__, sym) function map_to_scope(mod::Module, sym::Symbol) has_registered_binding = sym in UNIT_SYMBOLS - has_external_binding = !(mod === @__MODULE__) && external_quantity_binding(mod, sym) + has_external_binding = !(mod === @__MODULE__) && ( + external_quantity_binding(mod, sym) || external_unit_declaration(mod, sym) + ) if !has_registered_binding && sym in CONSTANT_SYMBOLS throw(ArgumentError("Symbol $sym found in `Constants` but not `Units`. Please use `u\"Constants.$sym\"` instead.")) diff --git a/test/precompile_test/ExternalUnitRegistration/src/ExternalUnitRegistration.jl b/test/precompile_test/ExternalUnitRegistration/src/ExternalUnitRegistration.jl index 5fe334f6..721f6305 100644 --- a/test/precompile_test/ExternalUnitRegistration/src/ExternalUnitRegistration.jl +++ b/test/precompile_test/ExternalUnitRegistration/src/ExternalUnitRegistration.jl @@ -5,6 +5,10 @@ using DynamicQuantities: DEFAULT_QUANTITY_TYPE, DEFAULT_SYMBOLIC_QUANTITY_OUTPUT @register_unit MyWb u"m^2*kg*s^-2*A^-1" +function __init__() + @register_unit MyInitWb u"m^2*kg*s^-2*A^-1" +end + const MYWB_EXPANDED = u"MyWb" expanded_mywb() = 1u"MyWb" @@ -12,10 +16,13 @@ symbolic_mywb() = 1us"MyWb" expanded_mywb_from_helper() = one(expanded_mywb()) * u"MyWb" symbolic_mywb_from_helper() = one(symbolic_mywb()) * us"MyWb" expanded_constant_mywb() = MYWB_EXPANDED +init_expanded_mywb() = 1u"MyInitWb" +init_symbolic_mywb() = 1us"MyInitWb" export MyWb export MYWB_EXPANDED export expanded_constant_mywb, expanded_mywb, expanded_mywb_from_helper export symbolic_mywb, symbolic_mywb_from_helper +export init_expanded_mywb, init_symbolic_mywb end diff --git a/test/unittests.jl b/test/unittests.jl index fb1b536c..3e518517 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -2293,9 +2293,6 @@ end @test_throws DimensionError x^y end -# `@testset` rewrites the test block with a `let...end`, resulting in an invalid -# local `const` (ref: src/units.jl:26). To avoid it, register units outside the -# test block. map_count_before_registering = length(UNIT_MAPPING) all_map_count_before_registering = length(ALL_MAPPING) @@ -2312,10 +2309,10 @@ if :MySV2 ∉ UNIT_SYMBOLS @eval @register_unit MySV2 us"km/h" end -@test_throws "Unit `m` is already defined as `1.0 m`" esc(_register_unit(:m, u"s")) +@test_throws "Unit `m` is already defined as `1.0 m`" esc(_register_unit(@__MODULE__, :m, u"s")) # Constants as well: -@test_throws "Unit `Ryd` is already defined" esc(_register_unit(:Ryd, u"Constants.Ryd")) +@test_throws "Unit `Ryd` is already defined" esc(_register_unit(@__MODULE__, :Ryd, u"Constants.Ryd")) @testset "Register Unit" begin MyV = u"MyV" @@ -2355,6 +2352,7 @@ push!(LOAD_PATH, joinpath(@__DIR__, "precompile_test")) using ExternalUnitRegistration: MYWB_EXPANDED, MyWb using ExternalUnitRegistration: expanded_constant_mywb, expanded_mywb, expanded_mywb_from_helper using ExternalUnitRegistration: symbolic_mywb, symbolic_mywb_from_helper +using ExternalUnitRegistration: init_expanded_mywb, init_symbolic_mywb @testset "Type of External Unit" begin @test MYWB_EXPANDED isa DEFAULT_QUANTITY_TYPE @test MYWB_EXPANDED / u"m^2*kg*s^-2*A^-1" == 1.0 @@ -2368,6 +2366,9 @@ using ExternalUnitRegistration: symbolic_mywb, symbolic_mywb_from_helper @test uexpand(symbolic_mywb()) == MYWB_EXPANDED @test symbolic_mywb_from_helper() == symbolic_mywb() @test string(symbolic_mywb()) == "1.0 MyWb" + @test init_expanded_mywb() == MYWB_EXPANDED + @test uexpand(init_symbolic_mywb()) == MYWB_EXPANDED + @test string(init_symbolic_mywb()) == "1.0 MyInitWb" end @testset "Concurrent first-use registration" begin