From 04d2b14a9bf4b290dbfb076334e8c1e56a93b700 Mon Sep 17 00:00:00 2001
From: Venkateshprasad <32921645+ven-k@users.noreply.github.com>
Date: Tue, 9 Jan 2024 14:10:53 +0530
Subject: [PATCH 01/17] feat: allow registering units externally

- By interpolating `Units.UNIT_SYMBOLS` while registering units and updating vectors-and-maps-of-units, new units can be registered even from outside the `Units` module
- Internally, units are registered lazily with `_lazy_register_unit` aka
`UNIT_MAPPING` is updated after all units are defined.
---
 src/DynamicQuantities.jl   |  4 +-
 src/register_units.jl      | 37 ++++++++++++++++
 src/symbolic_dimensions.jl | 43 +++++++++---------
 src/units.jl               | 91 +++++++++++++++++++-------------------
 test/runtests.jl           |  3 --
 test/unittests.jl          | 40 +++++++++++++++--
 6 files changed, 144 insertions(+), 74 deletions(-)
 create mode 100644 src/register_units.jl

diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl
index 46e83662..bf81cd6d 100644
--- a/src/DynamicQuantities.jl
+++ b/src/DynamicQuantities.jl
@@ -23,9 +23,10 @@ include("uparse.jl")
 include("symbolic_dimensions.jl")
 include("complex.jl")
 include("disambiguities.jl")
-
+include("register_units.jl")
 include("deprecated.jl")
 export expand_units
+export @register_unit
 
 import PackageExtensionCompat: @require_extensions
 import .Units
@@ -43,7 +44,6 @@ let _units_import_expr = :(using .Units: m, g)
     eval(_units_import_expr)
 end
 
-
 function __init__()
     @require_extensions
 end
diff --git a/src/register_units.jl b/src/register_units.jl
new file mode 100644
index 00000000..3c6d5b86
--- /dev/null
+++ b/src/register_units.jl
@@ -0,0 +1,37 @@
+
+import .Units: UNIT_MAPPING, UNIT_SYMBOLS, UNIT_VALUES, _lazy_register_unit
+import .SymbolicUnits:
+    SymbolicDimensionsSingleton, SYMBOLIC_UNIT_VALUES, update_symbolic_unit_values!
+
+# Update the unit collections
+function update_unit_mapping(name, value, unit_mapping::Dict{Symbol,Int} = UNIT_MAPPING)
+    unit_mapping[name] = length(unit_mapping) + 1
+end
+
+function update_all_values(name_symbol, unit)
+    push!(ALL_SYMBOLS, name_symbol)
+    push!(ALL_VALUES, unit)
+    ALL_MAPPING[name_symbol] = INDEX_TYPE(length(ALL_MAPPING) + 1)
+end
+
+
+# Register
+macro register_unit(name, value)
+    return esc(_register_unit(name, value))
+end
+
+function _register_unit(name::Symbol, value)
+    name_symbol = Meta.quot(name)
+    reg_expr = _lazy_register_unit(name, value)
+    push!(reg_expr.args,
+        quote
+            $update_unit_mapping($name_symbol, $value)
+            $update_all_values($name_symbol, $value)
+            $update_symbolic_unit_values!($name_symbol)
+            # suppress the print of `SYMBOLIC_UNIT_VALUES`
+            nothing
+        end
+    )
+    return reg_expr
+end
+
diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl
index 42f56286..2b0d00ea 100644
--- a/src/symbolic_dimensions.jl
+++ b/src/symbolic_dimensions.jl
@@ -1,21 +1,20 @@
 import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES
 import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES
 
-
 const SYMBOL_CONFLICTS = intersect(UNIT_SYMBOLS, CONSTANT_SYMBOLS)
 
 disambiguate_symbol(s) = s in SYMBOL_CONFLICTS ? Symbol(s, :_constant) : s
 
-const INDEX_TYPE = UInt8
 # Prefer units over constants:
 # For example, this means we can't have a symbolic Planck's constant,
 # as it is just "hours" (h), which is more common.
-const ALL_SYMBOLS = (
-    UNIT_SYMBOLS...,
-    disambiguate_symbol.(CONSTANT_SYMBOLS)...
-)
-const ALL_VALUES = (UNIT_VALUES..., CONSTANT_VALUES...)
-const ALL_MAPPING = NamedTuple{ALL_SYMBOLS}(INDEX_TYPE(1):INDEX_TYPE(length(ALL_SYMBOLS)))
+const INDEX_TYPE = UInt16
+# Prefer units over constants:
+# For example, this means we can't have a symbolic Planck's constant,
+# as it is just "hours" (h), which is more common.
+const ALL_SYMBOLS = [UNIT_SYMBOLS..., disambiguate_symbol.(CONSTANT_SYMBOLS)...]
+const ALL_VALUES = [UNIT_VALUES..., CONSTANT_VALUES...]
+const ALL_MAPPING = Dict(ALL_SYMBOLS .=> (INDEX_TYPE(1):INDEX_TYPE(length(ALL_SYMBOLS))))
 
 """
     AbstractSymbolicDimensions{R} <: AbstractDimensions{R}
@@ -91,7 +90,7 @@ end
 function SymbolicDimensionsSingleton{R}(s::Symbol) where {R}
     i = get(ALL_MAPPING, s, INDEX_TYPE(0))
     iszero(i) && error("$s is not available as a symbol in `SymbolicDimensionsSingleton`. Symbols available: $(ALL_SYMBOLS).")
-    return SymbolicDimensionsSingleton{R}(i)
+    SymbolicDimensionsSingleton{R}(i)
 end
 
 # Traits:
@@ -169,7 +168,7 @@ uexpand(q::QuantityArray) = uexpand.(q)
     uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions})
 
 Convert a quantity `q` with base SI units to the symbolic units of `qout`, for `q` and `qout` with compatible units.
-Mathematically, the result has value `q / uexpand(qout)` and units `dimension(qout)`. 
+Mathematically, the result has value `q / uexpand(qout)` and units `dimension(qout)`.
 """
 function uconvert(qout::UnionAbstractQuantity{<:Any, <:SymbolicDimensions}, q::UnionAbstractQuantity{<:Any, <:Dimensions})
     @assert isone(ustrip(qout)) "You passed a quantity with a non-unit value to uconvert."
@@ -224,7 +223,7 @@ end
 """
     uconvert(qout::UnionAbstractQuantity{<:Any, <:AbstractSymbolicDimensions})
 
-Create a function that converts an input quantity `q` with base SI units to the symbolic units of `qout`, i.e 
+Create a function that converts an input quantity `q` with base SI units to the symbolic units of `qout`, i.e
 a function equivalent to `q -> uconvert(qout, q)`.
 """
 uconvert(qout::UnionAbstractQuantity{<:Any,<:AbstractSymbolicDimensions}) = Base.Fix1(uconvert, qout)
@@ -368,6 +367,7 @@ to enable pretty-printing of units.
 """
 module SymbolicUnits
 
+
     import ..UNIT_SYMBOLS
     import ..CONSTANT_SYMBOLS
     import ..SymbolicDimensionsSingleton
@@ -383,9 +383,9 @@ module SymbolicUnits
         import ...SymbolicDimensionsSingleton
         import ...constructorof
         import ...disambiguate_symbol
-        import ....DEFAULT_SYMBOLIC_QUANTITY_TYPE
-        import ....DEFAULT_VALUE_TYPE
-        import ....DEFAULT_DIM_BASE_TYPE
+        import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE
+        import ...DEFAULT_VALUE_TYPE
+        import ...DEFAULT_DIM_BASE_TYPE
 
         const _SYMBOLIC_CONSTANT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[]
 
@@ -393,7 +393,9 @@ module SymbolicUnits
             @eval begin
                 const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(
                     DEFAULT_VALUE_TYPE(1.0),
-                    SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit))))
+                    SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}(
+                        $(QuoteNode(disambiguate_symbol(unit))),
+                    ),
                 )
                 push!(_SYMBOLIC_CONSTANT_VALUES, $unit)
             end
@@ -404,18 +406,19 @@ module SymbolicUnits
     import .Constants as SymbolicConstants
     import .Constants: SYMBOLIC_CONSTANT_VALUES
 
-    const _SYMBOLIC_UNIT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[]
-    for unit in UNIT_SYMBOLS
+    const SYMBOLIC_UNIT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[]
+
+    function update_symbolic_unit_values!(unit, symbolic_unit_values = SYMBOLIC_UNIT_VALUES)
         @eval begin
             const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(
                 DEFAULT_VALUE_TYPE(1.0),
-                SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit)))
+                SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit))),
             )
-            push!(_SYMBOLIC_UNIT_VALUES, $unit)
+            push!($symbolic_unit_values, $unit)
         end
     end
-    const SYMBOLIC_UNIT_VALUES = Tuple(_SYMBOLIC_UNIT_VALUES)
 
+    update_symbolic_unit_values!.(UNIT_SYMBOLS)
 
     """
         sym_uparse(raw_string::AbstractString)
diff --git a/src/units.jl b/src/units.jl
index 7c016d67..0bc9743d 100644
--- a/src/units.jl
+++ b/src/units.jl
@@ -6,27 +6,30 @@ import ..DEFAULT_QUANTITY_TYPE
 
 @assert DEFAULT_VALUE_TYPE == Float64 "`units.jl` must be updated to support a different default value type."
 
-const _UNIT_SYMBOLS = Symbol[]
-const _UNIT_VALUES = DEFAULT_QUANTITY_TYPE[]
+const UNIT_SYMBOLS = Symbol[]
+const UNIT_VALUES = DEFAULT_QUANTITY_TYPE[]
+const UNIT_MAPPING = Dict{Symbol,Int}()
 
-macro register_unit(name, value)
-    return esc(_register_unit(name, value))
+macro _lazy_register_unit(name, value)
+    return esc(_lazy_register_unit(name, value))
 end
 
 macro add_prefixes(base_unit, prefixes)
     @assert prefixes.head == :tuple
-    return esc(_add_prefixes(base_unit, prefixes.args, _register_unit))
+    return esc(_add_prefixes(base_unit, prefixes.args, _lazy_register_unit))
 end
 
-function _register_unit(name::Symbol, value)
-    s = string(name)
-    return quote
+function _lazy_register_unit(name::Symbol, value)
+    name_symbol = Meta.quot(name)
+    quote
+        haskey($UNIT_MAPPING, $name_symbol) && throw("Unit $($name_symbol) already exists.")
         const $name = $value
-        push!(_UNIT_SYMBOLS, Symbol($s))
-        push!(_UNIT_VALUES, $name)
+        push!($UNIT_SYMBOLS, $name_symbol)
+        push!($UNIT_VALUES, $name)
     end
 end
 
+
 function _add_prefixes(base_unit::Symbol, prefixes, register_function)
     all_prefixes = (
         f=1e-15, p=1e-12, n=1e-9, μ=1e-6, u=1e-6, m=1e-3, c=1e-2, d=1e-1,
@@ -42,13 +45,13 @@ function _add_prefixes(base_unit::Symbol, prefixes, register_function)
 end
 
 # SI base units
-@register_unit m DEFAULT_QUANTITY_TYPE(1.0, length=1)
-@register_unit g DEFAULT_QUANTITY_TYPE(1e-3, mass=1)
-@register_unit s DEFAULT_QUANTITY_TYPE(1.0, time=1)
-@register_unit A DEFAULT_QUANTITY_TYPE(1.0, current=1)
-@register_unit K DEFAULT_QUANTITY_TYPE(1.0, temperature=1)
-@register_unit cd DEFAULT_QUANTITY_TYPE(1.0, luminosity=1)
-@register_unit mol DEFAULT_QUANTITY_TYPE(1.0, amount=1)
+@_lazy_register_unit m DEFAULT_QUANTITY_TYPE(1.0, length = 1)
+@_lazy_register_unit g DEFAULT_QUANTITY_TYPE(1e-3, mass = 1)
+@_lazy_register_unit s DEFAULT_QUANTITY_TYPE(1.0, time = 1)
+@_lazy_register_unit A DEFAULT_QUANTITY_TYPE(1.0, current = 1)
+@_lazy_register_unit K DEFAULT_QUANTITY_TYPE(1.0, temperature = 1)
+@_lazy_register_unit cd DEFAULT_QUANTITY_TYPE(1.0, luminosity = 1)
+@_lazy_register_unit mol DEFAULT_QUANTITY_TYPE(1.0, amount = 1)
 
 @add_prefixes m (f, p, n, μ, u, c, d, m, k, M, G)
 @add_prefixes g (p, n, μ, u, m, k)
@@ -88,17 +91,17 @@ end
 )
 
 # SI derived units
-@register_unit Hz inv(s)
-@register_unit N kg * m / s^2
-@register_unit Pa N / m^2
-@register_unit J N * m
-@register_unit W J / s
-@register_unit C A * s
-@register_unit V W / A
-@register_unit F C / V
-@register_unit Ω V / A
-@register_unit ohm Ω
-@register_unit T N / (A * m)
+@_lazy_register_unit Hz inv(s)
+@_lazy_register_unit N kg * m / s^2
+@_lazy_register_unit Pa N / m^2
+@_lazy_register_unit J N * m
+@_lazy_register_unit W J / s
+@_lazy_register_unit C A * s
+@_lazy_register_unit V W / A
+@_lazy_register_unit F C / V
+@_lazy_register_unit Ω V / A
+@_lazy_register_unit ohm Ω
+@_lazy_register_unit T N / (A * m)
 
 @add_prefixes Hz (n, μ, u, m, k, M, G)
 @add_prefixes N ()
@@ -156,17 +159,17 @@ end
 
 # Common assorted units
 ## Time
-@register_unit min 60 * s
-@register_unit minute min
-@register_unit h 60 * min
-@register_unit hr h
-@register_unit day 24 * h
-@register_unit d day
-@register_unit wk 7 * day
-@register_unit yr 365.25 * day
-@register_unit inch 2.54 * cm
-@register_unit ft 12 * inch
-@register_unit mi 5280 * ft
+@_lazy_register_unit min 60 * s
+@_lazy_register_unit minute min
+@_lazy_register_unit h 60 * min
+@_lazy_register_unit hr h
+@_lazy_register_unit day 24 * h
+@_lazy_register_unit d day
+@_lazy_register_unit wk 7 * day
+@_lazy_register_unit yr 365.25 * day
+@_lazy_register_unit inch 2.54 * cm
+@_lazy_register_unit ft 12 * inch
+@_lazy_register_unit mi 5280 * ft
 
 @add_prefixes min ()
 @add_prefixes minute ()
@@ -178,7 +181,7 @@ end
 @add_prefixes yr (k, M, G)
 
 ## Volume
-@register_unit L dm^3
+@_lazy_register_unit L dm^3
 
 @add_prefixes L (μ, u, m, c, d)
 
@@ -188,7 +191,7 @@ end
 )
 
 ## Pressure
-@register_unit bar 100 * kPa
+@_lazy_register_unit bar 100 * kPa
 
 @add_prefixes bar (m,)
 
@@ -203,9 +206,7 @@ end
 # Do not wish to define physical constants, as the number of symbols might lead to ambiguity.
 # The user should define these instead.
 
-"""A tuple of all possible unit symbols."""
-const UNIT_SYMBOLS = Tuple(_UNIT_SYMBOLS)
-const UNIT_VALUES = Tuple(_UNIT_VALUES)
-const UNIT_MAPPING = NamedTuple([s => i for (i, s) in enumerate(UNIT_SYMBOLS)])
+# Update `UNIT_MAPPING` with all internally defined unit symbols.
+merge!(UNIT_MAPPING, Dict(UNIT_SYMBOLS .=> 1:lastindex(UNIT_SYMBOLS)))
 
 end
diff --git a/test/runtests.jl b/test/runtests.jl
index 70709e92..85c73883 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -19,9 +19,6 @@ else
     @safetestset "Measurements.jl integration tests" begin
         include("test_measurements.jl")
     end
-    @safetestset "Meshes.jl integration tests" begin
-        include("test_meshes.jl")
-    end
     @safetestset "Unit tests" begin
         include("unittests.jl")
     end
diff --git a/test/unittests.jl b/test/unittests.jl
index c7a9cf19..a896b2e6 100644
--- a/test/unittests.jl
+++ b/test/unittests.jl
@@ -1,9 +1,12 @@
 using DynamicQuantities
 using DynamicQuantities: FixedRational, NoDims, AbstractSymbolicDimensions
-using DynamicQuantities: DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE
+using DynamicQuantities:
+    DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE
 using DynamicQuantities: array_type, value_type, dim_type, quantity_type
 using DynamicQuantities: GenericQuantity, with_type_parameters, constructorof
 using DynamicQuantities: promote_quantity_on_quantity, promote_quantity_on_value
+using DynamicQuantities: UNIT_VALUES, UNIT_MAPPING, UNIT_SYMBOLS, ALL_MAPPING, ALL_SYMBOLS, ALL_VALUES
+using DynamicQuantities.SymbolicUnits: SYMBOLIC_UNIT_VALUES
 using DynamicQuantities: map_dimensions
 using Ratios: SimpleRatio
 using SaferIntegers: SafeInt16
@@ -686,8 +689,8 @@ end
         @test !iszero(sym)
     end
 
-    q = 1.5us"km/s"
-    @test q == 1.5 * us"km" / us"s"
+            q = 1.5us"km/s"
+            @test q == 1.5 * us"km" / us"s"
     @test typeof(q) <: with_type_parameters(DEFAULT_QUANTITY_TYPE, Float64, SymbolicDimensions{DEFAULT_DIM_BASE_TYPE})
     @test string(dimension(q)) == "s⁻¹ km"
     @test uexpand(q) == 1.5u"km/s"
@@ -1727,7 +1730,7 @@ end
         ) isa SymbolicDimensions{Int32}
 
     @test copy(km) == km
-    
+
     # Any operation should immediately convert it:
     @test km ^ -1 isa Quantity{T,DynamicQuantities.SymbolicDimensions{R}} where {T,R}
 
@@ -1848,3 +1851,32 @@ end
     y = Quantity(2.0im, mass=1)
     @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)
+@register_unit MyV u"V"
+@register_unit MySV us"V"
+@register_unit MySV2 us"km/h"
+
+@testset "Register Unit" begin
+    @test MyV === u"V"
+    @test MyV == us"V"
+    @test MySV == us"V"
+    @test MySV2 == us"km/h"
+
+    @test length(UNIT_MAPPING) == map_count_before_registering + 3
+    @test length(ALL_MAPPING) == all_map_count_before_registering + 3
+
+    for my_unit in (MySV, MyV)
+        @test my_unit in UNIT_VALUES
+        @test my_unit in ALL_VALUES
+        @test my_unit in SYMBOLIC_UNIT_VALUES
+    end
+    for my_unit in (:MySV, :MyV)
+        @test my_unit in UNIT_SYMBOLS
+        @test my_unit in ALL_SYMBOLS
+    end
+end

From 2c869e303ac46e5db6692ab198b8a39f507e564d Mon Sep 17 00:00:00 2001
From: Venkateshprasad <32921645+ven-k@users.noreply.github.com>
Date: Fri, 2 Feb 2024 17:34:20 +0530
Subject: [PATCH 02/17] fix: import of vars into SymbolicUnits sybmodule

---
 src/register_units.jl      |  2 --
 src/symbolic_dimensions.jl | 19 ++++++++-----------
 test/runtests.jl           |  3 +++
 test/unittests.jl          |  4 ++--
 4 files changed, 13 insertions(+), 15 deletions(-)

diff --git a/src/register_units.jl b/src/register_units.jl
index 3c6d5b86..d0722b5f 100644
--- a/src/register_units.jl
+++ b/src/register_units.jl
@@ -1,4 +1,3 @@
-
 import .Units: UNIT_MAPPING, UNIT_SYMBOLS, UNIT_VALUES, _lazy_register_unit
 import .SymbolicUnits:
     SymbolicDimensionsSingleton, SYMBOLIC_UNIT_VALUES, update_symbolic_unit_values!
@@ -14,7 +13,6 @@ function update_all_values(name_symbol, unit)
     ALL_MAPPING[name_symbol] = INDEX_TYPE(length(ALL_MAPPING) + 1)
 end
 
-
 # Register
 macro register_unit(name, value)
     return esc(_register_unit(name, value))
diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl
index 2b0d00ea..4f9c184d 100644
--- a/src/symbolic_dimensions.jl
+++ b/src/symbolic_dimensions.jl
@@ -90,7 +90,7 @@ end
 function SymbolicDimensionsSingleton{R}(s::Symbol) where {R}
     i = get(ALL_MAPPING, s, INDEX_TYPE(0))
     iszero(i) && error("$s is not available as a symbol in `SymbolicDimensionsSingleton`. Symbols available: $(ALL_SYMBOLS).")
-    SymbolicDimensionsSingleton{R}(i)
+    return SymbolicDimensionsSingleton{R}(i)
 end
 
 # Traits:
@@ -367,15 +367,14 @@ to enable pretty-printing of units.
 """
 module SymbolicUnits
 
-
     import ..UNIT_SYMBOLS
     import ..CONSTANT_SYMBOLS
     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 ..DEFAULT_SYMBOLIC_QUANTITY_TYPE
+    import ..constructorof
+    import ..DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE
+    import ..DEFAULT_VALUE_TYPE
+    import ..DEFAULT_DIM_BASE_TYPE
 
     # Lazily create unit symbols (since there are so many)
     module Constants
@@ -393,9 +392,7 @@ module SymbolicUnits
             @eval begin
                 const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(
                     DEFAULT_VALUE_TYPE(1.0),
-                    SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}(
-                        $(QuoteNode(disambiguate_symbol(unit))),
-                    ),
+                    SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit))))
                 )
                 push!(_SYMBOLIC_CONSTANT_VALUES, $unit)
             end
@@ -412,7 +409,7 @@ module SymbolicUnits
         @eval begin
             const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(
                 DEFAULT_VALUE_TYPE(1.0),
-                SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit))),
+                SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(unit)))
             )
             push!($symbolic_unit_values, $unit)
         end
diff --git a/test/runtests.jl b/test/runtests.jl
index 85c73883..70709e92 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -19,6 +19,9 @@ else
     @safetestset "Measurements.jl integration tests" begin
         include("test_measurements.jl")
     end
+    @safetestset "Meshes.jl integration tests" begin
+        include("test_meshes.jl")
+    end
     @safetestset "Unit tests" begin
         include("unittests.jl")
     end
diff --git a/test/unittests.jl b/test/unittests.jl
index a896b2e6..84b3751a 100644
--- a/test/unittests.jl
+++ b/test/unittests.jl
@@ -689,8 +689,8 @@ end
         @test !iszero(sym)
     end
 
-            q = 1.5us"km/s"
-            @test q == 1.5 * us"km" / us"s"
+    q = 1.5us"km/s"
+    @test q == 1.5 * us"km" / us"s"
     @test typeof(q) <: with_type_parameters(DEFAULT_QUANTITY_TYPE, Float64, SymbolicDimensions{DEFAULT_DIM_BASE_TYPE})
     @test string(dimension(q)) == "s⁻¹ km"
     @test uexpand(q) == 1.5u"km/s"

From 5c00b031ee32ae1820d094404a7277ac13a0b9b0 Mon Sep 17 00:00:00 2001
From: Venkateshprasad <32921645+ven-k@users.noreply.github.com>
Date: Sun, 4 Feb 2024 18:45:16 +0530
Subject: [PATCH 03/17] feat: add `WriteOnceReadMany` and utils

`UNIT_SYMBOLS`, `UNIT_VALUES`, `UNIT_MAPPING`, `ALL_VALUES`,`ALL_SYMBOLS`, ALL_MAPPING`, `SYMBOLIC_UNIT_VALUES` are instances of this collection type.
With this data type only a certain set of operations are permitted on these collections.
---
 src/DynamicQuantities.jl    |  5 ++++-
 src/register_units.jl       | 39 ++++++++++++++++++++--------------
 src/symbolic_dimensions.jl  | 18 ++++++++--------
 src/units.jl                |  9 ++++----
 src/write_once_read_many.jl | 42 +++++++++++++++++++++++++++++++++++++
 5 files changed, 82 insertions(+), 31 deletions(-)
 create mode 100644 src/write_once_read_many.jl

diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl
index bf81cd6d..bb5973d7 100644
--- a/src/DynamicQuantities.jl
+++ b/src/DynamicQuantities.jl
@@ -11,8 +11,11 @@ export ustrip, dimension
 export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount
 export uparse, @u_str, sym_uparse, @us_str, uexpand, uconvert
 
+const INDEX_TYPE = UInt16
+
 include("internal_utils.jl")
 include("fixed_rational.jl")
+include("write_once_read_many.jl")
 include("types.jl")
 include("utils.jl")
 include("math.jl")
@@ -39,7 +42,7 @@ using .Units: UNIT_SYMBOLS
 let _units_import_expr = :(using .Units: m, g)
     append!(
         _units_import_expr.args[1].args,
-        map(s -> Expr(:(.), s), filter(s -> s ∉ (:m, :g), UNIT_SYMBOLS))
+        Expr(:(.), s) for s in UNIT_SYMBOLS if s ∉ (:m, :g)
     )
     eval(_units_import_expr)
 end
diff --git a/src/register_units.jl b/src/register_units.jl
index d0722b5f..905726ae 100644
--- a/src/register_units.jl
+++ b/src/register_units.jl
@@ -3,14 +3,17 @@ import .SymbolicUnits:
     SymbolicDimensionsSingleton, SYMBOLIC_UNIT_VALUES, update_symbolic_unit_values!
 
 # Update the unit collections
-function update_unit_mapping(name, value, unit_mapping::Dict{Symbol,Int} = UNIT_MAPPING)
-    unit_mapping[name] = length(unit_mapping) + 1
-end
+const UNIT_UPDATE_LOCK = Threads.SpinLock()
 
 function update_all_values(name_symbol, unit)
-    push!(ALL_SYMBOLS, name_symbol)
-    push!(ALL_VALUES, unit)
-    ALL_MAPPING[name_symbol] = INDEX_TYPE(length(ALL_MAPPING) + 1)
+    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_symbolic_unit_values!(name_symbol)
+    end
 end
 
 # Register
@@ -20,16 +23,20 @@ end
 
 function _register_unit(name::Symbol, value)
     name_symbol = Meta.quot(name)
-    reg_expr = _lazy_register_unit(name, value)
-    push!(reg_expr.args,
-        quote
-            $update_unit_mapping($name_symbol, $value)
+    index = get(ALL_MAPPING, name, INDEX_TYPE(0))
+    if iszero(index)
+        reg_expr = _lazy_register_unit(name, value)
+        push!(reg_expr.args, quote
             $update_all_values($name_symbol, $value)
-            $update_symbolic_unit_values!($name_symbol)
-            # suppress the print of `SYMBOLIC_UNIT_VALUES`
             nothing
-        end
-    )
-    return reg_expr
+        end)
+        return reg_expr
+    else
+        unit = ALL_VALUES[index]
+        # When a utility function to expand `value` to its final form becomes
+        # available, enable the following check. This will avoid throwing an error
+        # if user is trying to register an existing unit with matching values.
+        # unit.value != value && throw("Unit $name is already defined as $unit")
+        throw("Unit `$name` is already defined as `$unit`")
+    end
 end
-
diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl
index 4f9c184d..84f2069c 100644
--- a/src/symbolic_dimensions.jl
+++ b/src/symbolic_dimensions.jl
@@ -1,3 +1,5 @@
+import ..WriteOnceReadMany
+import ..INDEX_TYPE
 import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES
 import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES
 
@@ -8,13 +10,9 @@ disambiguate_symbol(s) = s in SYMBOL_CONFLICTS ? Symbol(s, :_constant) : s
 # Prefer units over constants:
 # For example, this means we can't have a symbolic Planck's constant,
 # as it is just "hours" (h), which is more common.
-const INDEX_TYPE = UInt16
-# Prefer units over constants:
-# For example, this means we can't have a symbolic Planck's constant,
-# as it is just "hours" (h), which is more common.
-const ALL_SYMBOLS = [UNIT_SYMBOLS..., disambiguate_symbol.(CONSTANT_SYMBOLS)...]
-const ALL_VALUES = [UNIT_VALUES..., CONSTANT_VALUES...]
-const ALL_MAPPING = Dict(ALL_SYMBOLS .=> (INDEX_TYPE(1):INDEX_TYPE(length(ALL_SYMBOLS))))
+const ALL_SYMBOLS = WriteOnceReadMany([UNIT_SYMBOLS..., disambiguate_symbol.(CONSTANT_SYMBOLS)...])
+const ALL_VALUES = WriteOnceReadMany([UNIT_VALUES..., CONSTANT_VALUES...])
+const ALL_MAPPING = WriteOnceReadMany(Dict(s => INDEX_TYPE(i) for (i, s) in enumerate(ALL_SYMBOLS)))
 
 """
     AbstractSymbolicDimensions{R} <: AbstractDimensions{R}
@@ -375,6 +373,7 @@ module SymbolicUnits
     import ..DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE
     import ..DEFAULT_VALUE_TYPE
     import ..DEFAULT_DIM_BASE_TYPE
+    import ..WriteOnceReadMany
 
     # Lazily create unit symbols (since there are so many)
     module Constants
@@ -403,7 +402,7 @@ module SymbolicUnits
     import .Constants as SymbolicConstants
     import .Constants: SYMBOLIC_CONSTANT_VALUES
 
-    const SYMBOLIC_UNIT_VALUES = DEFAULT_SYMBOLIC_QUANTITY_TYPE[]
+    const SYMBOLIC_UNIT_VALUES = WriteOnceReadMany{Vector{DEFAULT_SYMBOLIC_QUANTITY_TYPE}}()
 
     function update_symbolic_unit_values!(unit, symbolic_unit_values = SYMBOLIC_UNIT_VALUES)
         @eval begin
@@ -415,7 +414,8 @@ module SymbolicUnits
         end
     end
 
-    update_symbolic_unit_values!.(UNIT_SYMBOLS)
+    update_symbolic_unit_values!(w::WriteOnceReadMany) = update_symbolic_unit_values!.(w._raw_data)
+    update_symbolic_unit_values!(UNIT_SYMBOLS)
 
     """
         sym_uparse(raw_string::AbstractString)
diff --git a/src/units.jl b/src/units.jl
index 0bc9743d..3cdba464 100644
--- a/src/units.jl
+++ b/src/units.jl
@@ -1,14 +1,14 @@
 module Units
 
+import ..WriteOnceReadMany
 import ..DEFAULT_DIM_TYPE
 import ..DEFAULT_VALUE_TYPE
 import ..DEFAULT_QUANTITY_TYPE
 
 @assert DEFAULT_VALUE_TYPE == Float64 "`units.jl` must be updated to support a different default value type."
 
-const UNIT_SYMBOLS = Symbol[]
-const UNIT_VALUES = DEFAULT_QUANTITY_TYPE[]
-const UNIT_MAPPING = Dict{Symbol,Int}()
+const UNIT_SYMBOLS = WriteOnceReadMany{Vector{Symbol}}()
+const UNIT_VALUES = WriteOnceReadMany{Vector{DEFAULT_QUANTITY_TYPE}}()
 
 macro _lazy_register_unit(name, value)
     return esc(_lazy_register_unit(name, value))
@@ -22,7 +22,6 @@ end
 function _lazy_register_unit(name::Symbol, value)
     name_symbol = Meta.quot(name)
     quote
-        haskey($UNIT_MAPPING, $name_symbol) && throw("Unit $($name_symbol) already exists.")
         const $name = $value
         push!($UNIT_SYMBOLS, $name_symbol)
         push!($UNIT_VALUES, $name)
@@ -207,6 +206,6 @@ end
 # The user should define these instead.
 
 # Update `UNIT_MAPPING` with all internally defined unit symbols.
-merge!(UNIT_MAPPING, Dict(UNIT_SYMBOLS .=> 1:lastindex(UNIT_SYMBOLS)))
+const UNIT_MAPPING = WriteOnceReadMany(Dict(s => i for (i, s) in enumerate(UNIT_SYMBOLS)))
 
 end
diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl
new file mode 100644
index 00000000..5245b968
--- /dev/null
+++ b/src/write_once_read_many.jl
@@ -0,0 +1,42 @@
+"""
+    WriteOnceReadMany()
+
+Used for storing units, values, symbolic-units.
+"""
+struct WriteOnceReadMany{V}
+    _raw_data::V
+
+    WriteOnceReadMany(_raw_data) = new{typeof(_raw_data)}(_raw_data)
+    WriteOnceReadMany{T}() where T = WriteOnceReadMany(T())
+end
+
+# Utility functions
+for f in (:enumerate, :length, :lastindex)
+    @eval begin
+        Base.$f(w::WriteOnceReadMany) = $f(w._raw_data)
+    end
+end
+
+Base.getindex(w::WriteOnceReadMany, i::Union{Int, INDEX_TYPE,  Symbol}) = getindex(w._raw_data, i)
+
+Base.iterate(w::WriteOnceReadMany) = iterate(w._raw_data)
+Base.iterate(w::WriteOnceReadMany, i::Int) = iterate(w._raw_data, i)
+
+Base.intersect(w::WriteOnceReadMany, v::AbstractSet) = intersect(w._raw_data, v)
+Base.intersect(v::AbstractSet, w::WriteOnceReadMany) = intersect(v, w._raw_data)
+
+Base.push!(w::WriteOnceReadMany, val...) = push!(w._raw_data, val...)
+
+for f in (:findfirst, :filter)
+    @eval begin
+        Base.$f(val::Function, w::WriteOnceReadMany) = $f(val, w._raw_data)
+    end
+end
+
+Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{Dict{Symbol, INDEX_TYPE}}, i::Int, s::Symbol) = setindex!(w, INDEX_TYPE(i), s)
+function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{Dict{Symbol, T}}, i::T, s::Symbol) where T <: Union{Int, INDEX_TYPE}
+    haskey(w._raw_data, s) && throw("Unit $s already exists at index $(w[s])")
+    setindex!(w._raw_data, i, s)
+end
+
+Base.get(w::WriteOnceReadMany{Dict{Symbol, INDEX_TYPE}}, a, b) = get(w._raw_data, a, b)

From cbd071c4c9efa1a71efa3608830ba61ce8c3cfba Mon Sep 17 00:00:00 2001
From: Venkateshprasad <32921645+ven-k@users.noreply.github.com>
Date: Sun, 11 Feb 2024 12:48:02 +0530
Subject: [PATCH 04/17] test: precompile `@register_unit` in an external
 module.

---
 src/register_units.jl                         |  5 ++---
 src/symbolic_dimensions.jl                    | 12 ++++++++++-
 .../ExternalUnitRegistration.jl               | 21 +++++++++++++++++++
 test/unittests.jl                             | 13 ++++++++++++
 4 files changed, 47 insertions(+), 4 deletions(-)
 create mode 100644 test/precompile_test/ExternalUnitRegistration.jl

diff --git a/src/register_units.jl b/src/register_units.jl
index 905726ae..bbd9e7c2 100644
--- a/src/register_units.jl
+++ b/src/register_units.jl
@@ -1,6 +1,5 @@
 import .Units: UNIT_MAPPING, UNIT_SYMBOLS, UNIT_VALUES, _lazy_register_unit
-import .SymbolicUnits:
-    SymbolicDimensionsSingleton, SYMBOLIC_UNIT_VALUES, update_symbolic_unit_values!
+import .SymbolicUnits: update_external_symbolic_unit_value
 
 # Update the unit collections
 const UNIT_UPDATE_LOCK = Threads.SpinLock()
@@ -12,7 +11,7 @@ function update_all_values(name_symbol, unit)
         i = lastindex(ALL_VALUES)
         ALL_MAPPING[name_symbol] = i
         UNIT_MAPPING[name_symbol] = i
-        update_symbolic_unit_values!(name_symbol)
+        update_external_symbolic_unit_value(name_symbol)
     end
 end
 
diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl
index 84f2069c..dcd19772 100644
--- a/src/symbolic_dimensions.jl
+++ b/src/symbolic_dimensions.jl
@@ -417,7 +417,17 @@ module SymbolicUnits
     update_symbolic_unit_values!(w::WriteOnceReadMany) = update_symbolic_unit_values!.(w._raw_data)
     update_symbolic_unit_values!(UNIT_SYMBOLS)
 
-    """
+    # 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)
+    end
+
+"""
         sym_uparse(raw_string::AbstractString)
 
     Parse a string containing an expression of units and return the
diff --git a/test/precompile_test/ExternalUnitRegistration.jl b/test/precompile_test/ExternalUnitRegistration.jl
new file mode 100644
index 00000000..48314590
--- /dev/null
+++ b/test/precompile_test/ExternalUnitRegistration.jl
@@ -0,0 +1,21 @@
+module ExternalUnitRegistration
+
+using DynamicQuantities: @register_unit, @u_str, @us_str,
+    ALL_MAPPING, ALL_SYMBOLS,  DEFAULT_QUANTITY_TYPE,
+    DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE, UNIT_SYMBOLS, UNIT_MAPPING
+using Test
+
+@register_unit Wb u"m^2*kg*s^-2*A^-1"
+
+@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 :Wb ∈ collection
+    end
+
+    w = u"Wb"
+    ws = us"Wb"
+    @test w isa DEFAULT_QUANTITY_TYPE
+    @test ws isa DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE
+end
+
+end
diff --git a/test/unittests.jl b/test/unittests.jl
index 84b3751a..150e6705 100644
--- a/test/unittests.jl
+++ b/test/unittests.jl
@@ -8,6 +8,7 @@ using DynamicQuantities: promote_quantity_on_quantity, promote_quantity_on_value
 using DynamicQuantities: UNIT_VALUES, UNIT_MAPPING, UNIT_SYMBOLS, ALL_MAPPING, ALL_SYMBOLS, ALL_VALUES
 using DynamicQuantities.SymbolicUnits: SYMBOLIC_UNIT_VALUES
 using DynamicQuantities: map_dimensions
+using DynamicQuantities: _register_unit
 using Ratios: SimpleRatio
 using SaferIntegers: SafeInt16
 using StaticArrays: SArray, MArray
@@ -1861,6 +1862,8 @@ all_map_count_before_registering = length(ALL_MAPPING)
 @register_unit MySV us"V"
 @register_unit MySV2 us"km/h"
 
+@test_throws "Unit `m` is already defined as `1.0 m`" esc(_register_unit(:m, u"s"))
+
 @testset "Register Unit" begin
     @test MyV === u"V"
     @test MyV == us"V"
@@ -1880,3 +1883,13 @@ all_map_count_before_registering = length(ALL_MAPPING)
         @test my_unit in ALL_SYMBOLS
     end
 end
+
+push!(LOAD_PATH, joinpath(@__DIR__, "precompile_test"))
+
+using ExternalUnitRegistration: Wb
+@testset "Type of Extenral Unit" begin
+    @test Wb isa DEFAULT_QUANTITY_TYPE
+    @test Wb/u"m^2*kg*s^-2*A^-1" == 1.0
+end
+
+pop!(LOAD_PATH)

From b44749f297a22e3f79f603229aa08220443942e6 Mon Sep 17 00:00:00 2001
From: MilesCranmer <miles.cranmer@gmail.com>
Date: Sun, 11 Feb 2024 23:48:26 +0000
Subject: [PATCH 05/17] Clean up `write_once_read_many.jl`

---
 src/write_once_read_many.jl | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl
index 5245b968..7e44fa59 100644
--- a/src/write_once_read_many.jl
+++ b/src/write_once_read_many.jl
@@ -17,7 +17,14 @@ for f in (:enumerate, :length, :lastindex)
     end
 end
 
-Base.getindex(w::WriteOnceReadMany, i::Union{Int, INDEX_TYPE,  Symbol}) = getindex(w._raw_data, i)
+Base.getindex(w::WriteOnceReadMany, i::Union{Integer,Symbol}) = getindex(w._raw_data, i)
+Base.get(w::WriteOnceReadMany{<:Dict}, a, b) = get(w._raw_data, a, b)
+
+# Only define setindex! for Dicts, and throw an error if the key already exists
+function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{<:Dict}, i, s::Symbol)
+    haskey(w._raw_data, s) && throw("Unit $s already exists at index $(w[s])")
+    setindex!(w._raw_data, i, s)
+end
 
 Base.iterate(w::WriteOnceReadMany) = iterate(w._raw_data)
 Base.iterate(w::WriteOnceReadMany, i::Int) = iterate(w._raw_data, i)
@@ -33,10 +40,3 @@ for f in (:findfirst, :filter)
     end
 end
 
-Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{Dict{Symbol, INDEX_TYPE}}, i::Int, s::Symbol) = setindex!(w, INDEX_TYPE(i), s)
-function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{Dict{Symbol, T}}, i::T, s::Symbol) where T <: Union{Int, INDEX_TYPE}
-    haskey(w._raw_data, s) && throw("Unit $s already exists at index $(w[s])")
-    setindex!(w._raw_data, i, s)
-end
-
-Base.get(w::WriteOnceReadMany{Dict{Symbol, INDEX_TYPE}}, a, b) = get(w._raw_data, a, b)

From 464d5a793892538fe9f5c033c8a4310071010c5a Mon Sep 17 00:00:00 2001
From: MilesCranmer <miles.cranmer@gmail.com>
Date: Sun, 11 Feb 2024 23:54:26 +0000
Subject: [PATCH 06/17] Clean up ambiguity calculation

---
 src/symbolic_dimensions.jl  | 10 ++++------
 src/write_once_read_many.jl |  3 ---
 2 files changed, 4 insertions(+), 9 deletions(-)

diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl
index dcd19772..d59c1ff1 100644
--- a/src/symbolic_dimensions.jl
+++ b/src/symbolic_dimensions.jl
@@ -3,14 +3,12 @@ import ..INDEX_TYPE
 import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES
 import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES
 
-const SYMBOL_CONFLICTS = intersect(UNIT_SYMBOLS, CONSTANT_SYMBOLS)
-
-disambiguate_symbol(s) = s in SYMBOL_CONFLICTS ? Symbol(s, :_constant) : s
+disambiguate_constant_symbol(s) = s in UNIT_SYMBOLS ? Symbol(s, :_constant) : s
 
 # Prefer units over constants:
 # For example, this means we can't have a symbolic Planck's constant,
 # as it is just "hours" (h), which is more common.
-const ALL_SYMBOLS = WriteOnceReadMany([UNIT_SYMBOLS..., disambiguate_symbol.(CONSTANT_SYMBOLS)...])
+const ALL_SYMBOLS = WriteOnceReadMany([UNIT_SYMBOLS..., disambiguate_constant_symbol.(CONSTANT_SYMBOLS)...])
 const ALL_VALUES = WriteOnceReadMany([UNIT_VALUES..., CONSTANT_VALUES...])
 const ALL_MAPPING = WriteOnceReadMany(Dict(s => INDEX_TYPE(i) for (i, s) in enumerate(ALL_SYMBOLS)))
 
@@ -380,7 +378,7 @@ module SymbolicUnits
         import ...CONSTANT_SYMBOLS
         import ...SymbolicDimensionsSingleton
         import ...constructorof
-        import ...disambiguate_symbol
+        import ...disambiguate_constant_symbol
         import ...DEFAULT_SYMBOLIC_QUANTITY_TYPE
         import ...DEFAULT_VALUE_TYPE
         import ...DEFAULT_DIM_BASE_TYPE
@@ -391,7 +389,7 @@ module SymbolicUnits
             @eval begin
                 const $unit = constructorof(DEFAULT_SYMBOLIC_QUANTITY_TYPE)(
                     DEFAULT_VALUE_TYPE(1.0),
-                    SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_symbol(unit))))
+                    SymbolicDimensionsSingleton{DEFAULT_DIM_BASE_TYPE}($(QuoteNode(disambiguate_constant_symbol(unit))))
                 )
                 push!(_SYMBOLIC_CONSTANT_VALUES, $unit)
             end
diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl
index 7e44fa59..943c16c4 100644
--- a/src/write_once_read_many.jl
+++ b/src/write_once_read_many.jl
@@ -29,9 +29,6 @@ end
 Base.iterate(w::WriteOnceReadMany) = iterate(w._raw_data)
 Base.iterate(w::WriteOnceReadMany, i::Int) = iterate(w._raw_data, i)
 
-Base.intersect(w::WriteOnceReadMany, v::AbstractSet) = intersect(w._raw_data, v)
-Base.intersect(v::AbstractSet, w::WriteOnceReadMany) = intersect(v, w._raw_data)
-
 Base.push!(w::WriteOnceReadMany, val...) = push!(w._raw_data, val...)
 
 for f in (:findfirst, :filter)

From f51b5547d3b6e385112fcfb75e48c1035c492f38 Mon Sep 17 00:00:00 2001
From: MilesCranmer <miles.cranmer@gmail.com>
Date: Sun, 11 Feb 2024 23:59:28 +0000
Subject: [PATCH 07/17] Simplify WriteOnceReadMany

---
 src/write_once_read_many.jl | 15 +++++----------
 1 file changed, 5 insertions(+), 10 deletions(-)

diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl
index 943c16c4..cf2ff604 100644
--- a/src/write_once_read_many.jl
+++ b/src/write_once_read_many.jl
@@ -12,9 +12,10 @@ end
 
 # Utility functions
 for f in (:enumerate, :length, :lastindex)
-    @eval begin
-        Base.$f(w::WriteOnceReadMany) = $f(w._raw_data)
-    end
+    @eval Base.$f(w::WriteOnceReadMany) = $f(w._raw_data)
+end
+for f in (:findfirst, :filter)
+    @eval Base.$f(val::Function, w::WriteOnceReadMany) = $f(val, w._raw_data)
 end
 
 Base.getindex(w::WriteOnceReadMany, i::Union{Integer,Symbol}) = getindex(w._raw_data, i)
@@ -27,13 +28,7 @@ function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{<:Dict}, i, s::Sy
 end
 
 Base.iterate(w::WriteOnceReadMany) = iterate(w._raw_data)
-Base.iterate(w::WriteOnceReadMany, i::Int) = iterate(w._raw_data, i)
+Base.iterate(w::WriteOnceReadMany, i) = iterate(w._raw_data, i)
 
 Base.push!(w::WriteOnceReadMany, val...) = push!(w._raw_data, val...)
 
-for f in (:findfirst, :filter)
-    @eval begin
-        Base.$f(val::Function, w::WriteOnceReadMany) = $f(val, w._raw_data)
-    end
-end
-

From 6a75e3ca27da479b93eade2664e3e70f3e21638f Mon Sep 17 00:00:00 2001
From: MilesCranmer <miles.cranmer@gmail.com>
Date: Mon, 12 Feb 2024 00:00:37 +0000
Subject: [PATCH 08/17] Fix return value from `Base.push!`

---
 src/write_once_read_many.jl | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl
index cf2ff604..dc801d84 100644
--- a/src/write_once_read_many.jl
+++ b/src/write_once_read_many.jl
@@ -30,5 +30,5 @@ end
 Base.iterate(w::WriteOnceReadMany) = iterate(w._raw_data)
 Base.iterate(w::WriteOnceReadMany, i) = iterate(w._raw_data, i)
 
-Base.push!(w::WriteOnceReadMany, val...) = push!(w._raw_data, val...)
+Base.push!(w::WriteOnceReadMany, val...) = (push!(w._raw_data, val...); w)
 

From 1fc82900444bb2f26c117f43b606815b9312276f Mon Sep 17 00:00:00 2001
From: MilesCranmer <miles.cranmer@gmail.com>
Date: Mon, 12 Feb 2024 00:02:57 +0000
Subject: [PATCH 09/17] Fix return value from `Base.setindex!`

---
 src/write_once_read_many.jl | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl
index dc801d84..d2f1c29c 100644
--- a/src/write_once_read_many.jl
+++ b/src/write_once_read_many.jl
@@ -25,6 +25,7 @@ Base.get(w::WriteOnceReadMany{<:Dict}, a, b) = get(w._raw_data, a, b)
 function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{<:Dict}, i, s::Symbol)
     haskey(w._raw_data, s) && throw("Unit $s already exists at index $(w[s])")
     setindex!(w._raw_data, i, s)
+    return w
 end
 
 Base.iterate(w::WriteOnceReadMany) = iterate(w._raw_data)

From 8f2cc240d1488589b65198f6e46b58b352fb1724 Mon Sep 17 00:00:00 2001
From: MilesCranmer <miles.cranmer@gmail.com>
Date: Mon, 12 Feb 2024 00:05:13 +0000
Subject: [PATCH 10/17] Move INDEX_TYPE back to symbolic_dimensions

---
 src/DynamicQuantities.jl   | 1 -
 src/symbolic_dimensions.jl | 3 ++-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl
index bb5973d7..45bf3788 100644
--- a/src/DynamicQuantities.jl
+++ b/src/DynamicQuantities.jl
@@ -11,7 +11,6 @@ export ustrip, dimension
 export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount
 export uparse, @u_str, sym_uparse, @us_str, uexpand, uconvert
 
-const INDEX_TYPE = UInt16
 
 include("internal_utils.jl")
 include("fixed_rational.jl")
diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl
index d59c1ff1..ef46ee78 100644
--- a/src/symbolic_dimensions.jl
+++ b/src/symbolic_dimensions.jl
@@ -1,10 +1,11 @@
 import ..WriteOnceReadMany
-import ..INDEX_TYPE
 import .Units: UNIT_SYMBOLS, UNIT_MAPPING, UNIT_VALUES
 import .Constants: CONSTANT_SYMBOLS, CONSTANT_MAPPING, CONSTANT_VALUES
 
+
 disambiguate_constant_symbol(s) = s in UNIT_SYMBOLS ? Symbol(s, :_constant) : s
 
+const INDEX_TYPE = UInt16
 # Prefer units over constants:
 # For example, this means we can't have a symbolic Planck's constant,
 # as it is just "hours" (h), which is more common.

From 9b1cba432fbca6658352948721081eabee6a7089 Mon Sep 17 00:00:00 2001
From: MilesCranmer <miles.cranmer@gmail.com>
Date: Mon, 12 Feb 2024 00:09:34 +0000
Subject: [PATCH 11/17] Add docstring for `@register_unit`

---
 src/register_units.jl | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/register_units.jl b/src/register_units.jl
index bbd9e7c2..225facac 100644
--- a/src/register_units.jl
+++ b/src/register_units.jl
@@ -15,9 +15,14 @@ function update_all_values(name_symbol, unit)
     end
 end
 
-# Register
-macro register_unit(name, value)
-    return esc(_register_unit(name, value))
+"""
+    @register_unit symbol value
+
+Register a new unit under the given symbol to have
+a particular value.
+"""
+macro register_unit(symbol, value)
+    return esc(_register_unit(symbol, value))
 end
 
 function _register_unit(name::Symbol, value)

From abbcdec2412aa969c28041a716d6b41da6532520 Mon Sep 17 00:00:00 2001
From: MilesCranmer <miles.cranmer@gmail.com>
Date: Mon, 12 Feb 2024 00:16:38 +0000
Subject: [PATCH 12/17] Improve @register_unit docstring and add to docs

---
 docs/src/units.md     |  8 ++++++++
 src/register_units.jl | 24 ++++++++++++++++++++++++
 2 files changed, 32 insertions(+)

diff --git a/docs/src/units.md b/docs/src/units.md
index 098bdbc5..218b94dd 100644
--- a/docs/src/units.md
+++ b/docs/src/units.md
@@ -42,3 +42,11 @@ Units.T
 Units.L
 Units.bar
 ```
+
+## Custom Units
+
+You can define custom units with the `@register_unit` macro:
+
+```@docs
+@register_unit
+```
diff --git a/src/register_units.jl b/src/register_units.jl
index 225facac..83d326d9 100644
--- a/src/register_units.jl
+++ b/src/register_units.jl
@@ -20,6 +20,30 @@ end
 
 Register a new unit under the given symbol to have
 a particular value.
+
+# Example
+
+```julia
+julia> @register_unit MyVolt 1.5u"V"
+```
+
+This will register a new unit `MyVolt` with a value of `1.5u"V"`.
+You can then use this unit in your calculations:
+
+```julia
+julia> x = 20us"MyVolt^2"
+20.0 MyVolt²
+
+julia> y = 2.5us"A"
+2.5 A
+
+julia> x * y^2 |> uconvert(us"W^2")
+281.25 W²
+
+julia> x * y^2 |> uconvert(us"W^2") |> sqrt |> uexpand
+16.77050983124842 m² kg s⁻³
+```
+
 """
 macro register_unit(symbol, value)
     return esc(_register_unit(symbol, value))

From ac1889aac1424ffdcb2ba4787d997483636c3d80 Mon Sep 17 00:00:00 2001
From: MilesCranmer <miles.cranmer@gmail.com>
Date: Mon, 12 Feb 2024 00:19:48 +0000
Subject: [PATCH 13/17] Type stability

---
 src/register_units.jl       | 20 +++++++++++---------
 src/write_once_read_many.jl |  2 +-
 2 files changed, 12 insertions(+), 10 deletions(-)

diff --git a/src/register_units.jl b/src/register_units.jl
index 83d326d9..80955fcd 100644
--- a/src/register_units.jl
+++ b/src/register_units.jl
@@ -52,19 +52,21 @@ end
 function _register_unit(name::Symbol, value)
     name_symbol = Meta.quot(name)
     index = get(ALL_MAPPING, name, INDEX_TYPE(0))
-    if iszero(index)
-        reg_expr = _lazy_register_unit(name, value)
-        push!(reg_expr.args, quote
-            $update_all_values($name_symbol, $value)
-            nothing
-        end)
-        return reg_expr
-    else
+    if !iszero(index)
         unit = ALL_VALUES[index]
         # When a utility function to expand `value` to its final form becomes
         # available, enable the following check. This will avoid throwing an error
         # if user is trying to register an existing unit with matching values.
         # unit.value != value && throw("Unit $name is already defined as $unit")
-        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
 end
diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl
index d2f1c29c..d4b54074 100644
--- a/src/write_once_read_many.jl
+++ b/src/write_once_read_many.jl
@@ -23,7 +23,7 @@ Base.get(w::WriteOnceReadMany{<:Dict}, a, b) = get(w._raw_data, a, b)
 
 # Only define setindex! for Dicts, and throw an error if the key already exists
 function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{<:Dict}, i, s::Symbol)
-    haskey(w._raw_data, s) && throw("Unit $s already exists at index $(w[s])")
+    haskey(w._raw_data, s) && error("Unit $s already exists at index $(w[s])")
     setindex!(w._raw_data, i, s)
     return w
 end

From d6f5cc11c91f6d7da957495850f807a01c8afc97 Mon Sep 17 00:00:00 2001
From: MilesCranmer <miles.cranmer@gmail.com>
Date: Mon, 12 Feb 2024 00:28:46 +0000
Subject: [PATCH 14/17] Clean up diff

---
 src/DynamicQuantities.jl   |  6 +++---
 src/symbolic_dimensions.jl |  4 ++--
 src/units.jl               | 15 +++++++--------
 test/unittests.jl          |  4 +---
 4 files changed, 13 insertions(+), 16 deletions(-)

diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl
index 45bf3788..b9ff34b1 100644
--- a/src/DynamicQuantities.jl
+++ b/src/DynamicQuantities.jl
@@ -9,7 +9,7 @@ export QuantityArray
 export DimensionError
 export ustrip, dimension
 export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount
-export uparse, @u_str, sym_uparse, @us_str, uexpand, uconvert
+export uparse, @u_str, sym_uparse, @us_str, uexpand, uconvert, @register_unit
 
 
 include("internal_utils.jl")
@@ -24,11 +24,11 @@ include("constants.jl")
 include("uparse.jl")
 include("symbolic_dimensions.jl")
 include("complex.jl")
-include("disambiguities.jl")
 include("register_units.jl")
+include("disambiguities.jl")
+
 include("deprecated.jl")
 export expand_units
-export @register_unit
 
 import PackageExtensionCompat: @require_extensions
 import .Units
diff --git a/src/symbolic_dimensions.jl b/src/symbolic_dimensions.jl
index ef46ee78..3b4ccf6f 100644
--- a/src/symbolic_dimensions.jl
+++ b/src/symbolic_dimensions.jl
@@ -367,8 +367,8 @@ module SymbolicUnits
     import ..UNIT_SYMBOLS
     import ..CONSTANT_SYMBOLS
     import ..SymbolicDimensionsSingleton
-    import ..DEFAULT_SYMBOLIC_QUANTITY_TYPE
     import ..constructorof
+    import ..DEFAULT_SYMBOLIC_QUANTITY_TYPE
     import ..DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE
     import ..DEFAULT_VALUE_TYPE
     import ..DEFAULT_DIM_BASE_TYPE
@@ -426,7 +426,7 @@ module SymbolicUnits
         push!(SYMBOLIC_UNIT_VALUES, unit)
     end
 
-"""
+    """
         sym_uparse(raw_string::AbstractString)
 
     Parse a string containing an expression of units and return the
diff --git a/src/units.jl b/src/units.jl
index 3cdba464..226ace39 100644
--- a/src/units.jl
+++ b/src/units.jl
@@ -28,7 +28,6 @@ function _lazy_register_unit(name::Symbol, value)
     end
 end
 
-
 function _add_prefixes(base_unit::Symbol, prefixes, register_function)
     all_prefixes = (
         f=1e-15, p=1e-12, n=1e-9, μ=1e-6, u=1e-6, m=1e-3, c=1e-2, d=1e-1,
@@ -44,13 +43,13 @@ function _add_prefixes(base_unit::Symbol, prefixes, register_function)
 end
 
 # SI base units
-@_lazy_register_unit m DEFAULT_QUANTITY_TYPE(1.0, length = 1)
-@_lazy_register_unit g DEFAULT_QUANTITY_TYPE(1e-3, mass = 1)
-@_lazy_register_unit s DEFAULT_QUANTITY_TYPE(1.0, time = 1)
-@_lazy_register_unit A DEFAULT_QUANTITY_TYPE(1.0, current = 1)
-@_lazy_register_unit K DEFAULT_QUANTITY_TYPE(1.0, temperature = 1)
-@_lazy_register_unit cd DEFAULT_QUANTITY_TYPE(1.0, luminosity = 1)
-@_lazy_register_unit mol DEFAULT_QUANTITY_TYPE(1.0, amount = 1)
+@_lazy_register_unit m DEFAULT_QUANTITY_TYPE(1.0, length=1)
+@_lazy_register_unit g DEFAULT_QUANTITY_TYPE(1e-3, mass=1)
+@_lazy_register_unit s DEFAULT_QUANTITY_TYPE(1.0, time=1)
+@_lazy_register_unit A DEFAULT_QUANTITY_TYPE(1.0, current=1)
+@_lazy_register_unit K DEFAULT_QUANTITY_TYPE(1.0, temperature=1)
+@_lazy_register_unit cd DEFAULT_QUANTITY_TYPE(1.0, luminosity=1)
+@_lazy_register_unit mol DEFAULT_QUANTITY_TYPE(1.0, amount=1)
 
 @add_prefixes m (f, p, n, μ, u, c, d, m, k, M, G)
 @add_prefixes g (p, n, μ, u, m, k)
diff --git a/test/unittests.jl b/test/unittests.jl
index 150e6705..eb4affd3 100644
--- a/test/unittests.jl
+++ b/test/unittests.jl
@@ -1,7 +1,6 @@
 using DynamicQuantities
 using DynamicQuantities: FixedRational, NoDims, AbstractSymbolicDimensions
-using DynamicQuantities:
-    DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE
+using DynamicQuantities: DEFAULT_QUANTITY_TYPE, DEFAULT_DIM_BASE_TYPE, DEFAULT_DIM_TYPE, DEFAULT_VALUE_TYPE
 using DynamicQuantities: array_type, value_type, dim_type, quantity_type
 using DynamicQuantities: GenericQuantity, with_type_parameters, constructorof
 using DynamicQuantities: promote_quantity_on_quantity, promote_quantity_on_value
@@ -1731,7 +1730,6 @@ end
         ) isa SymbolicDimensions{Int32}
 
     @test copy(km) == km
-
     # Any operation should immediately convert it:
     @test km ^ -1 isa Quantity{T,DynamicQuantities.SymbolicDimensions{R}} where {T,R}
 

From 81321a0f343d9da907ed135a338f5f24b11323dc Mon Sep 17 00:00:00 2001
From: MilesCranmer <miles.cranmer@gmail.com>
Date: Mon, 12 Feb 2024 00:38:45 +0000
Subject: [PATCH 15/17] Improve docstring of WriteOnceReadMany

---
 src/write_once_read_many.jl | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl
index d4b54074..12e956df 100644
--- a/src/write_once_read_many.jl
+++ b/src/write_once_read_many.jl
@@ -1,7 +1,12 @@
 """
-    WriteOnceReadMany()
+    WriteOnceReadMany{V}(container::V)
 
-Used for storing units, values, symbolic-units.
+A wrapper type for container that only defines methods
+for appending to and reading to, but not modifying the container.
+
+This is so that we can safely define a `@register_unit` interface
+without needing to worry about the user overwriting previously
+defined units and voiding the indexing of symbolic dimensions.
 """
 struct WriteOnceReadMany{V}
     _raw_data::V

From a1ba7e7d5faf3994bf38af58113ac6298f43a7a4 Mon Sep 17 00:00:00 2001
From: MilesCranmer <miles.cranmer@gmail.com>
Date: Mon, 12 Feb 2024 00:40:41 +0000
Subject: [PATCH 16/17] Stylistic tweaks

---
 src/write_once_read_many.jl                      | 4 ++--
 test/precompile_test/ExternalUnitRegistration.jl | 8 ++++----
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/write_once_read_many.jl b/src/write_once_read_many.jl
index 12e956df..e97f190d 100644
--- a/src/write_once_read_many.jl
+++ b/src/write_once_read_many.jl
@@ -24,10 +24,10 @@ for f in (:findfirst, :filter)
 end
 
 Base.getindex(w::WriteOnceReadMany, i::Union{Integer,Symbol}) = getindex(w._raw_data, i)
-Base.get(w::WriteOnceReadMany{<:Dict}, a, b) = get(w._raw_data, a, b)
+Base.get(w::WriteOnceReadMany{<:AbstractDict}, a, b) = get(w._raw_data, a, b)
 
 # Only define setindex! for Dicts, and throw an error if the key already exists
-function Base.setindex!(w::DynamicQuantities.WriteOnceReadMany{<:Dict}, i, s::Symbol)
+function Base.setindex!(w::WriteOnceReadMany{<:AbstractDict}, i, s::Symbol)
     haskey(w._raw_data, s) && error("Unit $s already exists at index $(w[s])")
     setindex!(w._raw_data, i, s)
     return w
diff --git a/test/precompile_test/ExternalUnitRegistration.jl b/test/precompile_test/ExternalUnitRegistration.jl
index 48314590..c9c0e6c4 100644
--- a/test/precompile_test/ExternalUnitRegistration.jl
+++ b/test/precompile_test/ExternalUnitRegistration.jl
@@ -1,13 +1,13 @@
 module ExternalUnitRegistration
 
-using DynamicQuantities: @register_unit, @u_str, @us_str,
-    ALL_MAPPING, ALL_SYMBOLS,  DEFAULT_QUANTITY_TYPE,
-    DEFAULT_SYMBOLIC_QUANTITY_OUTPUT_TYPE, UNIT_SYMBOLS, UNIT_MAPPING
+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 Wb u"m^2*kg*s^-2*A^-1"
 
-@testset " Register Unit Inside a Module" begin
+@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 :Wb ∈ collection
     end

From 92df2f428564fb3823f30a4390c1792b0effdb4a Mon Sep 17 00:00:00 2001
From: Miles Cranmer <miles.cranmer@gmail.com>
Date: Mon, 12 Feb 2024 00:48:51 +0000
Subject: [PATCH 17/17] Only test error string on 1.9+

---
 test/unittests.jl | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/test/unittests.jl b/test/unittests.jl
index eb4affd3..11b12bf8 100644
--- a/test/unittests.jl
+++ b/test/unittests.jl
@@ -1860,7 +1860,12 @@ all_map_count_before_registering = length(ALL_MAPPING)
 @register_unit MySV us"V"
 @register_unit MySV2 us"km/h"
 
-@test_throws "Unit `m` is already defined as `1.0 m`" esc(_register_unit(:m, u"s"))
+if VERSION >= v"1.9"
+    @test_throws "Unit `m` is already defined as `1.0 m`" esc(_register_unit(:m, u"s"))
+
+    # Constants as well:
+    @test_throws "Unit `Ryd` is already defined" esc(_register_unit(:Ryd, u"Constants.Ryd"))
+end
 
 @testset "Register Unit" begin
     @test MyV === u"V"