From 65cf598793afdfd855f47ae037447ee0a5c5f326 Mon Sep 17 00:00:00 2001 From: jClugstor Date: Tue, 25 Nov 2025 16:34:17 -0500 Subject: [PATCH 1/8] add a generator for verbosity specifiers --- Project.toml | 1 + src/SciMLLogging.jl | 2 + src/verbosity_specifier_generator.jl | 300 +++++++++++++++++++++ test/generation_test.jl | 381 +++++++++++++++++++++++++++ test/runtests.jl | 2 + 5 files changed, 686 insertions(+) create mode 100644 src/verbosity_specifier_generator.jl create mode 100644 test/generation_test.jl diff --git a/Project.toml b/Project.toml index 98b452e..57e9eb5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,5 +1,6 @@ name = "SciMLLogging" uuid = "a6db7da4-7206-11f0-1eab-35f2a5dbe1d1" +version = "1.5.0" authors = ["Jadon Clugston"] version = "1.6.0" diff --git a/src/SciMLLogging.jl b/src/SciMLLogging.jl index b3bd311..fe1a937 100644 --- a/src/SciMLLogging.jl +++ b/src/SciMLLogging.jl @@ -6,6 +6,7 @@ using Preferences include("verbosity.jl") include("utils.jl") +include("verbosity_specifier_generator.jl") # Export public API export AbstractVerbositySpecifier, AbstractVerbosityPreset, AbstractMessageLevel @@ -15,5 +16,6 @@ export verbosity_to_int, verbosity_to_bool export SciMLLogger export set_logging_backend, get_logging_backend export None, Minimal, Standard, Detailed, All +export generate_verbosity_specifier, eval_verbosity_specifier, define_verbosity_specifier end diff --git a/src/verbosity_specifier_generator.jl b/src/verbosity_specifier_generator.jl new file mode 100644 index 0000000..af18929 --- /dev/null +++ b/src/verbosity_specifier_generator.jl @@ -0,0 +1,300 @@ +function generate_verbosity_specifier(name::Symbol, toggles, preset_map, groups) + # Extract presets from preset_map keys + presets = collect(keys(preset_map)) + + # Standard presets that already exist + standard_presets = (:None, :Minimal, :Standard, :Detailed, :All) + + # Find custom presets that need type definitions + custom_presets = filter(p -> !(p in standard_presets), presets) + + # Generate singleton types for custom presets + custom_preset_types = [] + for custom_preset in custom_presets + push!(custom_preset_types, quote + struct $custom_preset <: AbstractVerbosityPreset end + end) + end + + # Validate that each preset has entries for all toggles + for preset in presets + preset_config = preset_map[preset] + preset_toggle_keys = keys(preset_config) + for toggle in toggles + if !(toggle in preset_toggle_keys) + error("Preset :$preset is missing toggle :$toggle. Available toggles in preset: $preset_toggle_keys") + end + end + end + + # Generate the verbosity specifier struct + # Create type parameters for each toggle + type_params = [Symbol("T$i") for i in eachindex(toggles)] + + # Build struct fields with type parameters + struct_fields = [] + for (i, toggle) in enumerate(toggles) + push!(struct_fields, :($(toggle)::$(type_params[i]))) + end + + # Create the parametric struct definition + struct_def = quote + struct $name{$(type_params...)} <: AbstractVerbositySpecifier + $(struct_fields...) + end + end + + # Generate constructor that gets typeof arguments + inner_constructor = quote + function $name($(toggles...)) + return $name{$([:(typeof($t)) for t in toggles]...)}($(toggles...)) + end + end + + # Generate constructors for each preset + constructors = [] + for preset in presets + preset_config = preset_map[preset] + + # Build the positional arguments for this preset + field_values = [] + for toggle in toggles + push!(field_values, preset_config[toggle]) + end + + # Create constructor function + constructor = quote + function $name(::$preset) + return $name($(field_values...)) + end + end + + push!(constructors, constructor) + end + + # Generate keyword argument constructor + # Build mapping from toggle to its containing group (if any) + toggle_to_group = Dict{Symbol, Union{Symbol, Nothing}}() + for toggle in toggles + toggle_to_group[toggle] = nothing + for group_name in keys(groups) + if toggle in groups[group_name] + toggle_to_group[toggle] = group_name + break + end + end + end + + # Default preset + default_preset = Standard() + + # Capture runtime data for the generated functions + _preset_map = preset_map + + # Build the runtime helper function name + runtime_helper_name = Symbol("_build_$(name)_runtime") + + # Generate validation expressions for groups + group_params = collect(keys(groups)) + group_validations = [] + for group_name in group_params + push!(group_validations, quote + if $(group_name) !== nothing && !($(group_name) isa AbstractMessageLevel) + throw(ArgumentError("$($(QuoteNode(group_name))) must be a SciMLLogging.AbstractMessageLevel, got $(typeof($(group_name)))")) + end + end) + end + + # Generate validation for individual kwargs + all_toggle_symbols = collect(toggles) + kwargs_validation = quote + for (key, value) in pairs(kwargs) + if !(key in $all_toggle_symbols) + throw(ArgumentError("Unknown verbosity option: \$key. Valid options are: $($all_toggle_symbols)")) + end + if !(value isa AbstractMessageLevel) + throw(ArgumentError("\$key must be a SciMLLogging.AbstractMessageLevel, got \$(typeof(value))")) + end + end + end + + # Build the group application logic with precedence: individual > group > preset + group_application_entries = [] + for toggle in toggles + group = toggle_to_group[toggle] + if group === nothing + # Not in any group: individual > preset + push!(group_application_entries, quote + $toggle = haskey(kwargs, $(QuoteNode(toggle))) ? kwargs[$(QuoteNode(toggle))] : preset_config[$(QuoteNode(toggle))] + end) + else + # In a group: individual > group > preset + push!(group_application_entries, quote + $toggle = if haskey(kwargs, $(QuoteNode(toggle))) + kwargs[$(QuoteNode(toggle))] + elseif $group !== nothing + $group + else + preset_config[$(QuoteNode(toggle))] + end + end) + end + end + + # Build the runtime helper + runtime_helper = quote + function $(runtime_helper_name)($(group_params...), preset, kwargs) + # Validate group arguments + $(group_validations...) + + # Validate preset + if preset !== nothing && !(preset isa AbstractVerbosityPreset) + throw(ArgumentError("preset must be a SciMLLogging.AbstractVerbosityPreset, got \$(typeof(preset))")) + end + + # Validate individual kwargs + $kwargs_validation + + # Get preset configuration + preset_to_use = preset === nothing ? $default_preset : preset + preset_config = $_preset_map[typeof(preset_to_use).name.name] + + # Apply precedence: individual kwargs > group > preset + $(group_application_entries...) + + return $name($([t for t in toggles]...)) + end + end + + # Build fast path values for Standard preset + standard_config = preset_map[:Standard] + fast_path_values = [standard_config[t] for t in toggles] + + # Build the @generated wrapper function + wrapper_name = Symbol("_build_$(name)") + + # Build the expression that the @generated function will return for the fast path + fast_path_expr = :($name($(fast_path_values...))) + + # Build the expression for the slow path + slow_path_expr = :($runtime_helper_name($(group_params...), preset, kwargs)) + + # Build chained && conditions for compile-time check + compile_time_checks = [:($(g) === Nothing) for g in group_params] + push!(compile_time_checks, :(preset === Nothing)) + push!(compile_time_checks, :(kwargs <: NamedTuple{()})) + # Chain them with binary && operators + compile_time_condition = compile_time_checks[1] + for check in compile_time_checks[2:end] + compile_time_condition = :($compile_time_condition && $check) + end + + # Build chained && conditions for runtime check + runtime_checks = [:($(g) === nothing) for g in group_params] + push!(runtime_checks, :(preset === nothing)) + push!(runtime_checks, :(isempty(kwargs))) + # Chain them with binary && operators + runtime_condition = runtime_checks[1] + for check in runtime_checks[2:end] + runtime_condition = :($runtime_condition && $check) + end + + # Quote the expressions for the generated function to return + quoted_fast_path = QuoteNode(fast_path_expr) + quoted_slow_path = QuoteNode(slow_path_expr) + + wrapper_function = quote + function $(wrapper_name)($(group_params...), preset, kwargs) + if @generated + # Check if all params are Nothing and kwargs is empty (fast default path) + if $compile_time_condition + # Return expression that constructs the default directly + return $quoted_fast_path + else + # Delegate to runtime logic + return $quoted_slow_path + end + else + # Runtime fallback + if $runtime_condition + # Fast default path + return $name($(fast_path_values...)) + else + # Complex path + return $runtime_helper_name($(group_params...), preset, kwargs) + end + end + end + end + + # Build the main keyword constructor + kwarg_params = [] + push!(kwarg_params, Expr(:kw, :preset, :nothing)) + for g in group_params + push!(kwarg_params, Expr(:kw, g, :nothing)) + end + + main_constructor = quote + function $name(; $(kwarg_params...), kwargs...) + $(wrapper_name)($(group_params...), preset, NamedTuple(kwargs)) + end + end + + return (custom_preset_types = custom_preset_types, + struct_def = struct_def, + inner_constructor = inner_constructor, + preset_constructors = constructors, + runtime_helper = runtime_helper, + wrapper_function = wrapper_function, + main_constructor = main_constructor) +end + +function eval_verbosity_specifier(generated_code) + # Evaluate custom preset types first + if !isempty(generated_code.custom_preset_types) + for preset_type in generated_code.custom_preset_types + Core.eval(@__MODULE__, preset_type) + end + end + # Then evaluate the main struct + Core.eval(@__MODULE__, generated_code.struct_def) + + # Evaluate the inner constructor (type-inferring constructor) + Core.eval(@__MODULE__, generated_code.inner_constructor) + + # Then the constructors and helper functions + for constructor in generated_code.preset_constructors + Core.eval(@__MODULE__, constructor) + end + Core.eval(@__MODULE__, generated_code.runtime_helper) + Core.eval(@__MODULE__, generated_code.wrapper_function) + Core.eval(@__MODULE__, generated_code.main_constructor) + + return nothing +end + +""" + define_verbosity_specifier(name::Symbol, toggles, preset_map, groups) + +Convenience function that generates and evaluates a verbosity specifier in one step. +Equivalent to calling `generate_verbosity_specifier` followed by `eval_verbosity_specifier`. + +# Arguments +- `name`: Symbol for the struct name +- `toggles`: Tuple of symbols representing individual toggle names +- `preset_map`: NamedTuple mapping preset names to their configurations +- `groups`: NamedTuple mapping group names to tuples of toggle symbols + +# Example +```julia +define_verbosity_specifier(:MyVerbosity, + (:toggle1, :toggle2), + (Standard = (toggle1 = InfoLevel(), toggle2 = WarnLevel()),), + (group1 = (:toggle1,),)) +``` +""" +function define_verbosity_specifier(name::Symbol, toggles, preset_map, groups) + generated = generate_verbosity_specifier(name, toggles, preset_map, groups) + eval_verbosity_specifier(generated) + return nothing +end \ No newline at end of file diff --git a/test/generation_test.jl b/test/generation_test.jl new file mode 100644 index 0000000..822f591 --- /dev/null +++ b/test/generation_test.jl @@ -0,0 +1,381 @@ +toggles = (:toggle1, :toggle2, :toggle3, :toggle4, :toggle5, + :toggle6, :toggle7, :toggle8, :toggle9, :toggle10) + +preset_map = ( + None = ( + toggle1 = Silent(), + toggle2 = Silent(), + toggle3 = Silent(), + toggle4 = Silent(), + toggle5 = Silent(), + toggle6 = Silent(), + toggle7 = Silent(), + toggle8 = Silent(), + toggle9 = Silent(), + toggle10 = Silent() + ), + Minimal = ( + toggle1 = WarnLevel(), + toggle2 = Silent(), + toggle3 = ErrorLevel(), + toggle4 = DebugLevel(), + toggle5 = InfoLevel(), + toggle6 = WarnLevel(), + toggle7 = Silent(), + toggle8 = InfoLevel(), + toggle9 = DebugLevel(), + toggle10 = ErrorLevel() + ), + Standard = ( + toggle1 = InfoLevel(), + toggle2 = WarnLevel(), + toggle3 = DebugLevel(), + toggle4 = ErrorLevel(), + toggle5 = Silent(), + toggle6 = InfoLevel(), + toggle7 = DebugLevel(), + toggle8 = WarnLevel(), + toggle9 = Silent(), + toggle10 = ErrorLevel() + ), + Detailed = ( + toggle1 = DebugLevel(), + toggle2 = InfoLevel(), + toggle3 = Silent(), + toggle4 = WarnLevel(), + toggle5 = ErrorLevel(), + toggle6 = DebugLevel(), + toggle7 = ErrorLevel(), + toggle8 = Silent(), + toggle9 = WarnLevel(), + toggle10 = InfoLevel() + ), + All = ( + toggle1 = ErrorLevel(), + toggle2 = DebugLevel(), + toggle3 = InfoLevel(), + toggle4 = Silent(), + toggle5 = WarnLevel(), + toggle6 = ErrorLevel(), + toggle7 = InfoLevel(), + toggle8 = DebugLevel(), + toggle9 = WarnLevel(), + toggle10 = Silent() + ) +) + +groups = ( + numerical = (:toggle1, :toggle2, :toggle3), + performance = (:toggle4, :toggle5, :toggle6, :toggle7), + error_control = (:toggle8, :toggle9, :toggle10) +) + + +generated = generate_verbosity_specifier(:SciMLLogging.VerbSpec, toggles, preset_map, groups) +eval_verbosity_specifier(generated) + +using Test + +@testset "SciMLLogging.VerbSpec Constructor Tests" begin + # Test 1: Default constructor (no arguments) - should use Standard preset + @testset "Default constructor" begin + v = SciMLLogging.VerbSpec() + @test v.toggle1 == InfoLevel() + @test v.toggle2 == WarnLevel() + @test v.toggle3 == DebugLevel() + @test v.toggle10 == ErrorLevel() + end + + # Test 2: Preset constructors + @testset "Preset constructors" begin + # None preset + v_none = SciMLLogging.SciMLLogging.VerbSpec(None()) + @test all(getfield(v_none, f) == Silent() for f in fieldnames(typeof(v_none))) + + # Minimal preset + v_min = SciMLLogging.SciMLLogging.VerbSpec(Minimal()) + @test v_min.toggle1 == WarnLevel() + @test v_min.toggle2 == Silent() + @test v_min.toggle3 == ErrorLevel() + + # Standard preset + v_std = SciMLLogging.SciMLLogging.VerbSpec(Standard()) + @test v_std.toggle1 == InfoLevel() + @test v_std.toggle2 == WarnLevel() + + # Detailed preset + v_det = SciMLLogging.SciMLLogging.VerbSpec(Detailed()) + @test v_det.toggle1 == DebugLevel() + @test v_det.toggle2 == InfoLevel() + + # All preset + v_all = SciMLLogging.SciMLLogging.VerbSpec(All()) + @test v_all.toggle1 == ErrorLevel() + @test v_all.toggle2 == DebugLevel() + end + + # Test 3: Keyword constructor with preset parameter + @testset "Keyword constructor with preset" begin + v = SciMLLogging.SciMLLogging.VerbSpec(preset=Minimal()) + @test v.toggle1 == WarnLevel() + @test v.toggle2 == Silent() + end + + # Test 4: Group parameters + @testset "Group parameters" begin + # Set all numerical toggles to ErrorLevel + v = SciMLLogging.VerbSpec(numerical=ErrorLevel()) + @test v.toggle1 == ErrorLevel() # numerical group + @test v.toggle2 == ErrorLevel() # numerical group + @test v.toggle3 == ErrorLevel() # numerical group + @test v.toggle4 == ErrorLevel() # performance group (from Standard preset) + + # Set all performance toggles + v2 = SciMLLogging.VerbSpec(performance=InfoLevel()) + @test v2.toggle4 == InfoLevel() # performance group + @test v2.toggle5 == InfoLevel() # performance group + @test v2.toggle6 == InfoLevel() # performance group + @test v2.toggle7 == InfoLevel() # performance group + + # Set all error_control toggles + v3 = SciMLLogging.VerbSpec(error_control=DebugLevel()) + @test v3.toggle8 == DebugLevel() # error_control group + @test v3.toggle9 == DebugLevel() # error_control group + @test v3.toggle10 == DebugLevel() # error_control group + end + + # Test 5: Individual toggle parameters + @testset "Individual toggle parameters" begin + v = SciMLLogging.VerbSpec(toggle1=ErrorLevel()) + @test v.toggle1 == ErrorLevel() + @test v.toggle2 == WarnLevel() # from Standard preset + + # Multiple individual toggles + v2 = SciMLLogging.VerbSpec(toggle1=ErrorLevel(), toggle5=WarnLevel()) + @test v2.toggle1 == ErrorLevel() + @test v2.toggle5 == WarnLevel() + end + + # Test 6: Precedence: individual > group > preset + @testset "Precedence tests" begin + # Individual overrides group + v = SciMLLogging.VerbSpec(numerical=WarnLevel(), toggle1=ErrorLevel()) + @test v.toggle1 == ErrorLevel() # individual wins + @test v.toggle2 == WarnLevel() # from group + @test v.toggle3 == WarnLevel() # from group + + # Group overrides preset + v2 = SciMLLogging.VerbSpec(preset=None(), numerical=InfoLevel()) + @test v2.toggle1 == InfoLevel() # from group + @test v2.toggle4 == Silent() # from preset (not in numerical group) + + # All three levels + v3 = SciMLLogging.VerbSpec(preset=None(), performance=WarnLevel(), toggle4=ErrorLevel()) + @test v3.toggle4 == ErrorLevel() # individual wins + @test v3.toggle5 == WarnLevel() # from group + @test v3.toggle1 == Silent() # from preset + end + + # Test 7: Combining preset with groups + @testset "Preset with groups" begin + v = SciMLLogging.VerbSpec(preset=Minimal(), numerical=DebugLevel()) + @test v.toggle1 == DebugLevel() # from group + @test v.toggle2 == DebugLevel() # from group + @test v.toggle3 == DebugLevel() # from group + @test v.toggle4 == DebugLevel() # from Minimal preset + end + + # Test 8: All three: preset, groups, and individual toggles + @testset "Preset + groups + individual toggles" begin + v = SciMLLogging.VerbSpec( + preset=Detailed(), + numerical=ErrorLevel(), + performance=WarnLevel(), + toggle1=InfoLevel(), + toggle4=DebugLevel() + ) + @test v.toggle1 == InfoLevel() # individual wins + @test v.toggle2 == ErrorLevel() # from numerical group + @test v.toggle3 == ErrorLevel() # from numerical group + @test v.toggle4 == DebugLevel() # individual wins + @test v.toggle5 == WarnLevel() # from performance group + @test v.toggle8 == Silent() # from Detailed preset + end + + # Test 9: Type stability - ensure parametric types work + @testset "Type stability" begin + v = SciMLLogging.VerbSpec() + @test typeof(v) <: SciMLLogging.VerbSpec + @test isconcretetype(typeof(v)) + end + + # Test 10: Error handling + @testset "Error handling" begin + # Invalid group argument type + @test_throws ArgumentError SciMLLogging.VerbSpec(numerical="invalid") + + # Invalid individual toggle type + @test_throws ArgumentError SciMLLogging.VerbSpec(toggle1="invalid") + + # Unknown toggle name + @test_throws ArgumentError SciMLLogging.VerbSpec(unknown_toggle=ErrorLevel()) + + # Invalid preset type + @test_throws ArgumentError SciMLLogging.VerbSpec(preset="invalid") + end +end + + + +# LinearVerbosity configuration +linear_toggles = ( + :default_lu_fallback, + :no_right_preconditioning, + :using_IterativeSolvers, + :IterativeSolvers_iterations, + :KrylovKit_verbosity, + :KrylovJL_verbosity, + :HYPRE_verbosity, + :pardiso_verbosity, + :blas_errors, + :blas_invalid_args, + :blas_info, + :blas_success, + :condition_number, + :convergence_failure, + :solver_failure, + :max_iters +) + +linear_preset_map = ( + None = ( + default_lu_fallback = Silent(), + no_right_preconditioning = Silent(), + using_IterativeSolvers = Silent(), + IterativeSolvers_iterations = Silent(), + KrylovKit_verbosity = Silent(), + KrylovJL_verbosity = Silent(), + HYPRE_verbosity = Silent(), + pardiso_verbosity = Silent(), + blas_errors = Silent(), + blas_invalid_args = Silent(), + blas_info = Silent(), + blas_success = Silent(), + condition_number = Silent(), + convergence_failure = Silent(), + solver_failure = Silent(), + max_iters = Silent() + ), + Minimal = ( + default_lu_fallback = Silent(), + no_right_preconditioning = Silent(), + using_IterativeSolvers = Silent(), + IterativeSolvers_iterations = Silent(), + KrylovKit_verbosity = Silent(), + KrylovJL_verbosity = Silent(), + HYPRE_verbosity = Silent(), + pardiso_verbosity = Silent(), + blas_errors = WarnLevel(), + blas_invalid_args = WarnLevel(), + blas_info = Silent(), + blas_success = Silent(), + condition_number = Silent(), + convergence_failure = Silent(), + solver_failure = Silent(), + max_iters = Silent() + ), + Standard = ( + default_lu_fallback = Silent(), + no_right_preconditioning = Silent(), + using_IterativeSolvers = Silent(), + IterativeSolvers_iterations = Silent(), + KrylovKit_verbosity = CustomLevel(1), + KrylovJL_verbosity = Silent(), + HYPRE_verbosity = InfoLevel(), + pardiso_verbosity = Silent(), + blas_errors = WarnLevel(), + blas_invalid_args = WarnLevel(), + blas_info = Silent(), + blas_success = Silent(), + condition_number = Silent(), + convergence_failure = WarnLevel(), + solver_failure = WarnLevel(), + max_iters = WarnLevel() + ), + Detailed = ( + default_lu_fallback = WarnLevel(), + no_right_preconditioning = InfoLevel(), + using_IterativeSolvers = InfoLevel(), + IterativeSolvers_iterations = Silent(), + KrylovKit_verbosity = CustomLevel(2), + KrylovJL_verbosity = CustomLevel(1), + HYPRE_verbosity = InfoLevel(), + pardiso_verbosity = CustomLevel(1), + blas_errors = WarnLevel(), + blas_invalid_args = WarnLevel(), + blas_info = InfoLevel(), + blas_success = InfoLevel(), + condition_number = Silent(), + convergence_failure = WarnLevel(), + solver_failure = WarnLevel(), + max_iters = WarnLevel() + ), + All = ( + default_lu_fallback = WarnLevel(), + no_right_preconditioning = InfoLevel(), + using_IterativeSolvers = InfoLevel(), + IterativeSolvers_iterations = InfoLevel(), + KrylovKit_verbosity = CustomLevel(3), + KrylovJL_verbosity = CustomLevel(1), + HYPRE_verbosity = InfoLevel(), + pardiso_verbosity = CustomLevel(1), + blas_errors = WarnLevel(), + blas_invalid_args = WarnLevel(), + blas_info = InfoLevel(), + blas_success = InfoLevel(), + condition_number = InfoLevel(), + convergence_failure = WarnLevel(), + solver_failure = WarnLevel(), + max_iters = WarnLevel() + ) +) + +linear_groups = ( + error_control = (:default_lu_fallback, :blas_errors, :blas_invalid_args), + performance = (:no_right_preconditioning,), + numerical = (:using_IterativeSolvers, :IterativeSolvers_iterations, + :KrylovKit_verbosity, :KrylovJL_verbosity, :HYPRE_verbosity, + :pardiso_verbosity, :blas_info, :blas_success, :condition_number, + :convergence_failure, :solver_failure, :max_iters) +) + +# Generate LinearVerbosity +linear_generated = generate_verbosity_specifier(:LinearVerbosity, linear_toggles, linear_preset_map, linear_groups) +eval_verbosity_specifier(linear_generated) + +# Test LinearVerbosity +@testset "LinearVerbosity Tests" begin + @testset "Default constructor" begin + v = LinearVerbosity() + @test v.blas_errors == WarnLevel() + @test v.default_lu_fallback == Silent() + @test v.KrylovKit_verbosity == CustomLevel(1) + end + + @testset "Preset constructors" begin + v_none = LinearVerbosity(None()) + @test all(getfield(v_none, f) == Silent() for f in fieldnames(typeof(v_none))) + + v_min = LinearVerbosity(Minimal()) + @test v_min.blas_errors == WarnLevel() + @test v_min.convergence_failure == Silent() + end + + @testset "Group parameters" begin + v = LinearVerbosity(error_control=ErrorLevel()) + @test v.default_lu_fallback == ErrorLevel() + @test v.blas_errors == ErrorLevel() + @test v.blas_invalid_args == ErrorLevel() + end +end + diff --git a/test/runtests.jl b/test/runtests.jl index 90b4d58..cca768f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,7 @@ using SciMLLogging +using SciMLLogging: None, Minimal, Standard, All, AbstractVerbosityPreset using Test using SafeTestsets @time @safetestset "Basic Tests" include("basics.jl") +@time @safetestset "Basic Tests" include("generation_test.jl") \ No newline at end of file From 7ff9e6ec2d4ee3f55fbc22c1bab2523f44e8b507 Mon Sep 17 00:00:00 2001 From: jClugstor Date: Wed, 26 Nov 2025 11:20:26 -0500 Subject: [PATCH 2/8] fix up files and imports --- src/SciMLLogging.jl | 4 +- src/verbosity_specifier_generator.jl | 300 --------------------------- src/verbspec_generation_macro.jl | 273 ++++++++++++++++++++++++ 3 files changed, 275 insertions(+), 302 deletions(-) delete mode 100644 src/verbosity_specifier_generator.jl create mode 100644 src/verbspec_generation_macro.jl diff --git a/src/SciMLLogging.jl b/src/SciMLLogging.jl index fe1a937..670a341 100644 --- a/src/SciMLLogging.jl +++ b/src/SciMLLogging.jl @@ -6,7 +6,7 @@ using Preferences include("verbosity.jl") include("utils.jl") -include("verbosity_specifier_generator.jl") +include("verbspec_generation_macro.jl") # Export public API export AbstractVerbositySpecifier, AbstractVerbosityPreset, AbstractMessageLevel @@ -16,6 +16,6 @@ export verbosity_to_int, verbosity_to_bool export SciMLLogger export set_logging_backend, get_logging_backend export None, Minimal, Standard, Detailed, All -export generate_verbosity_specifier, eval_verbosity_specifier, define_verbosity_specifier +export @define_verbosity_specifier end diff --git a/src/verbosity_specifier_generator.jl b/src/verbosity_specifier_generator.jl deleted file mode 100644 index af18929..0000000 --- a/src/verbosity_specifier_generator.jl +++ /dev/null @@ -1,300 +0,0 @@ -function generate_verbosity_specifier(name::Symbol, toggles, preset_map, groups) - # Extract presets from preset_map keys - presets = collect(keys(preset_map)) - - # Standard presets that already exist - standard_presets = (:None, :Minimal, :Standard, :Detailed, :All) - - # Find custom presets that need type definitions - custom_presets = filter(p -> !(p in standard_presets), presets) - - # Generate singleton types for custom presets - custom_preset_types = [] - for custom_preset in custom_presets - push!(custom_preset_types, quote - struct $custom_preset <: AbstractVerbosityPreset end - end) - end - - # Validate that each preset has entries for all toggles - for preset in presets - preset_config = preset_map[preset] - preset_toggle_keys = keys(preset_config) - for toggle in toggles - if !(toggle in preset_toggle_keys) - error("Preset :$preset is missing toggle :$toggle. Available toggles in preset: $preset_toggle_keys") - end - end - end - - # Generate the verbosity specifier struct - # Create type parameters for each toggle - type_params = [Symbol("T$i") for i in eachindex(toggles)] - - # Build struct fields with type parameters - struct_fields = [] - for (i, toggle) in enumerate(toggles) - push!(struct_fields, :($(toggle)::$(type_params[i]))) - end - - # Create the parametric struct definition - struct_def = quote - struct $name{$(type_params...)} <: AbstractVerbositySpecifier - $(struct_fields...) - end - end - - # Generate constructor that gets typeof arguments - inner_constructor = quote - function $name($(toggles...)) - return $name{$([:(typeof($t)) for t in toggles]...)}($(toggles...)) - end - end - - # Generate constructors for each preset - constructors = [] - for preset in presets - preset_config = preset_map[preset] - - # Build the positional arguments for this preset - field_values = [] - for toggle in toggles - push!(field_values, preset_config[toggle]) - end - - # Create constructor function - constructor = quote - function $name(::$preset) - return $name($(field_values...)) - end - end - - push!(constructors, constructor) - end - - # Generate keyword argument constructor - # Build mapping from toggle to its containing group (if any) - toggle_to_group = Dict{Symbol, Union{Symbol, Nothing}}() - for toggle in toggles - toggle_to_group[toggle] = nothing - for group_name in keys(groups) - if toggle in groups[group_name] - toggle_to_group[toggle] = group_name - break - end - end - end - - # Default preset - default_preset = Standard() - - # Capture runtime data for the generated functions - _preset_map = preset_map - - # Build the runtime helper function name - runtime_helper_name = Symbol("_build_$(name)_runtime") - - # Generate validation expressions for groups - group_params = collect(keys(groups)) - group_validations = [] - for group_name in group_params - push!(group_validations, quote - if $(group_name) !== nothing && !($(group_name) isa AbstractMessageLevel) - throw(ArgumentError("$($(QuoteNode(group_name))) must be a SciMLLogging.AbstractMessageLevel, got $(typeof($(group_name)))")) - end - end) - end - - # Generate validation for individual kwargs - all_toggle_symbols = collect(toggles) - kwargs_validation = quote - for (key, value) in pairs(kwargs) - if !(key in $all_toggle_symbols) - throw(ArgumentError("Unknown verbosity option: \$key. Valid options are: $($all_toggle_symbols)")) - end - if !(value isa AbstractMessageLevel) - throw(ArgumentError("\$key must be a SciMLLogging.AbstractMessageLevel, got \$(typeof(value))")) - end - end - end - - # Build the group application logic with precedence: individual > group > preset - group_application_entries = [] - for toggle in toggles - group = toggle_to_group[toggle] - if group === nothing - # Not in any group: individual > preset - push!(group_application_entries, quote - $toggle = haskey(kwargs, $(QuoteNode(toggle))) ? kwargs[$(QuoteNode(toggle))] : preset_config[$(QuoteNode(toggle))] - end) - else - # In a group: individual > group > preset - push!(group_application_entries, quote - $toggle = if haskey(kwargs, $(QuoteNode(toggle))) - kwargs[$(QuoteNode(toggle))] - elseif $group !== nothing - $group - else - preset_config[$(QuoteNode(toggle))] - end - end) - end - end - - # Build the runtime helper - runtime_helper = quote - function $(runtime_helper_name)($(group_params...), preset, kwargs) - # Validate group arguments - $(group_validations...) - - # Validate preset - if preset !== nothing && !(preset isa AbstractVerbosityPreset) - throw(ArgumentError("preset must be a SciMLLogging.AbstractVerbosityPreset, got \$(typeof(preset))")) - end - - # Validate individual kwargs - $kwargs_validation - - # Get preset configuration - preset_to_use = preset === nothing ? $default_preset : preset - preset_config = $_preset_map[typeof(preset_to_use).name.name] - - # Apply precedence: individual kwargs > group > preset - $(group_application_entries...) - - return $name($([t for t in toggles]...)) - end - end - - # Build fast path values for Standard preset - standard_config = preset_map[:Standard] - fast_path_values = [standard_config[t] for t in toggles] - - # Build the @generated wrapper function - wrapper_name = Symbol("_build_$(name)") - - # Build the expression that the @generated function will return for the fast path - fast_path_expr = :($name($(fast_path_values...))) - - # Build the expression for the slow path - slow_path_expr = :($runtime_helper_name($(group_params...), preset, kwargs)) - - # Build chained && conditions for compile-time check - compile_time_checks = [:($(g) === Nothing) for g in group_params] - push!(compile_time_checks, :(preset === Nothing)) - push!(compile_time_checks, :(kwargs <: NamedTuple{()})) - # Chain them with binary && operators - compile_time_condition = compile_time_checks[1] - for check in compile_time_checks[2:end] - compile_time_condition = :($compile_time_condition && $check) - end - - # Build chained && conditions for runtime check - runtime_checks = [:($(g) === nothing) for g in group_params] - push!(runtime_checks, :(preset === nothing)) - push!(runtime_checks, :(isempty(kwargs))) - # Chain them with binary && operators - runtime_condition = runtime_checks[1] - for check in runtime_checks[2:end] - runtime_condition = :($runtime_condition && $check) - end - - # Quote the expressions for the generated function to return - quoted_fast_path = QuoteNode(fast_path_expr) - quoted_slow_path = QuoteNode(slow_path_expr) - - wrapper_function = quote - function $(wrapper_name)($(group_params...), preset, kwargs) - if @generated - # Check if all params are Nothing and kwargs is empty (fast default path) - if $compile_time_condition - # Return expression that constructs the default directly - return $quoted_fast_path - else - # Delegate to runtime logic - return $quoted_slow_path - end - else - # Runtime fallback - if $runtime_condition - # Fast default path - return $name($(fast_path_values...)) - else - # Complex path - return $runtime_helper_name($(group_params...), preset, kwargs) - end - end - end - end - - # Build the main keyword constructor - kwarg_params = [] - push!(kwarg_params, Expr(:kw, :preset, :nothing)) - for g in group_params - push!(kwarg_params, Expr(:kw, g, :nothing)) - end - - main_constructor = quote - function $name(; $(kwarg_params...), kwargs...) - $(wrapper_name)($(group_params...), preset, NamedTuple(kwargs)) - end - end - - return (custom_preset_types = custom_preset_types, - struct_def = struct_def, - inner_constructor = inner_constructor, - preset_constructors = constructors, - runtime_helper = runtime_helper, - wrapper_function = wrapper_function, - main_constructor = main_constructor) -end - -function eval_verbosity_specifier(generated_code) - # Evaluate custom preset types first - if !isempty(generated_code.custom_preset_types) - for preset_type in generated_code.custom_preset_types - Core.eval(@__MODULE__, preset_type) - end - end - # Then evaluate the main struct - Core.eval(@__MODULE__, generated_code.struct_def) - - # Evaluate the inner constructor (type-inferring constructor) - Core.eval(@__MODULE__, generated_code.inner_constructor) - - # Then the constructors and helper functions - for constructor in generated_code.preset_constructors - Core.eval(@__MODULE__, constructor) - end - Core.eval(@__MODULE__, generated_code.runtime_helper) - Core.eval(@__MODULE__, generated_code.wrapper_function) - Core.eval(@__MODULE__, generated_code.main_constructor) - - return nothing -end - -""" - define_verbosity_specifier(name::Symbol, toggles, preset_map, groups) - -Convenience function that generates and evaluates a verbosity specifier in one step. -Equivalent to calling `generate_verbosity_specifier` followed by `eval_verbosity_specifier`. - -# Arguments -- `name`: Symbol for the struct name -- `toggles`: Tuple of symbols representing individual toggle names -- `preset_map`: NamedTuple mapping preset names to their configurations -- `groups`: NamedTuple mapping group names to tuples of toggle symbols - -# Example -```julia -define_verbosity_specifier(:MyVerbosity, - (:toggle1, :toggle2), - (Standard = (toggle1 = InfoLevel(), toggle2 = WarnLevel()),), - (group1 = (:toggle1,),)) -``` -""" -function define_verbosity_specifier(name::Symbol, toggles, preset_map, groups) - generated = generate_verbosity_specifier(name, toggles, preset_map, groups) - eval_verbosity_specifier(generated) - return nothing -end \ No newline at end of file diff --git a/src/verbspec_generation_macro.jl b/src/verbspec_generation_macro.jl new file mode 100644 index 0000000..02f4da6 --- /dev/null +++ b/src/verbspec_generation_macro.jl @@ -0,0 +1,273 @@ +""" + @define_verbosity_specifier name begin + toggles = (...) + preset_map = (...) + groups = (...) + end + +# Example +```julia +@define_verbosity_specifier MyVerbosity begin + toggles = (:toggle1, :toggle2) + + preset_map = ( + Standard = ( + toggle1 = InfoLevel(), + toggle2 = WarnLevel() + ), + ) + + groups = ( + group1 = (:toggle1,), + ) +end +``` +""" +macro define_verbosity_specifier(name, block) + # Extract the three assignments from the block + local toggles_expr = nothing + local preset_map_expr = nothing + local groups_expr = nothing + + # The block should be a :block expression with :line and assignment expressions + if block.head == :block + for ex in block.args + if ex isa Expr && ex.head == :(=) + lhs = ex.args[1] + rhs = ex.args[2] + if lhs == :toggles + toggles_expr = rhs + elseif lhs == :preset_map + preset_map_expr = rhs + elseif lhs == :groups + groups_expr = rhs + end + end + end + end + + toggles_expr !== nothing || throw(ArgumentError("toggles must be defined in block")) + preset_map_expr !== nothing || throw(ArgumentError("preset_map must be defined in block")) + groups_expr !== nothing || throw(ArgumentError("groups must be defined in block")) + + # Parse toggles - should be a tuple of symbols + toggles_expr.head == :tuple || throw(ArgumentError("toggles must be a tuple")) + # Extract the actual symbols from QuoteNode objects + toggles = [t.value for t in toggles_expr.args] + + # Parse preset_map - should be a NamedTuple + preset_map_expr.head == :tuple || throw(ArgumentError("preset_map must be a NamedTuple")) + presets_dict = Dict() + for preset_def in preset_map_expr.args + preset_def.head == :(=) || throw(ArgumentError("Each preset must be name = (...)")) + preset_name = preset_def.args[1] + preset_values = preset_def.args[2] + preset_values.head == :tuple || throw(ArgumentError("Preset values must be a NamedTuple")) + + # Parse the preset configuration + preset_config = Dict() + for toggle_def in preset_values.args + toggle_def.head == :(=) || throw(ArgumentError("Each toggle must be toggle_name = value")) + toggle_name = toggle_def.args[1] + toggle_value_expr = toggle_def.args[2] + preset_config[toggle_name] = toggle_value_expr + end + presets_dict[preset_name] = preset_config + end + + # Parse groups + groups_expr.head == :tuple || throw(ArgumentError("groups must be a NamedTuple")) + groups_dict = Dict() + for group_def in groups_expr.args + group_def.head == :(=) || throw(ArgumentError("Each group must be name = (...)")) + group_name = group_def.args[1] + group_toggles_expr = group_def.args[2] + group_toggles_expr.head == :tuple || throw(ArgumentError("Group toggles must be a tuple")) + # Extract the actual symbols from QuoteNode objects + group_toggles = [t isa QuoteNode ? t.value : t for t in group_toggles_expr.args] + groups_dict[group_name] = group_toggles + end + + # Now generate the code + preset_names = collect(keys(presets_dict)) + group_names = collect(keys(groups_dict)) + + # Standard presets that already exist + standard_presets = (:None, :Minimal, :Standard, :Detailed, :All) + custom_presets = filter(p -> !(p in standard_presets), preset_names) + + # Generate custom preset types + custom_preset_defs = [:(struct $p <: AbstractVerbosityPreset end) for p in custom_presets] + + # Generate parametric struct + type_params = [Symbol("T$i") for i in eachindex(toggles)] + struct_fields = [:($(toggles[i])::$(type_params[i])) for i in eachindex(toggles)] + + struct_def = :( + struct $name{$(type_params...)} <: AbstractVerbositySpecifier + $(struct_fields...) + end + ) + + # Generate inner constructor + inner_constructor = :( + function $name($(toggles...)) + return $name{$([:(typeof($t)) for t in toggles]...)}($(toggles...)) + end + ) + + Main.@infiltrate + # Generate preset constructors + preset_constructors = [] + for preset_name in preset_names + preset_config = presets_dict[preset_name] + field_values = [preset_config[t] for t in toggles] + + push!(preset_constructors, :( + function $name(::$preset_name) + return $name($(field_values...)) + end + )) + end + + # Build toggle to group mapping + toggle_to_group = Dict{Symbol, Union{Symbol, Nothing}}() + for toggle in toggles + toggle_to_group[toggle] = nothing + for (group_name, group_toggles) in groups_dict + if toggle in group_toggles + toggle_to_group[toggle] = group_name + break + end + end + end + + # Get Standard preset config for fast path + standard_config = presets_dict[:Standard] + fast_path_values = [standard_config[t] for t in toggles] + + # Build the runtime helper function + runtime_helper_name = Symbol("_build_$(name)_runtime") + + # Build validation for groups + group_validations = [] + for group_name in group_names + push!(group_validations, quote + if $(group_name) !== nothing && !($(group_name) isa AbstractMessageLevel) + throw(ArgumentError("$($(QuoteNode(group_name))) must be a SciMLLogging.AbstractMessageLevel, got $(typeof($(group_name)))")) + end + end) + end + + # Build precedence logic for each toggle + toggle_assignments = [] + for toggle in toggles + group = toggle_to_group[toggle] + toggle_key = QuoteNode(toggle) + + if group === nothing + # Not in any group: individual > preset + # Build preset_config inline as a tuple access + push!(toggle_assignments, :($toggle = haskey(kwargs, $toggle_key) ? kwargs[$toggle_key] : preset_config[$toggle_key])) + else + # In a group: individual > group > preset + push!(toggle_assignments, :($toggle = haskey(kwargs, $toggle_key) ? kwargs[$toggle_key] : ($group !== nothing ? $group : preset_config[$toggle_key]))) + end + end + + # Build preset_map as a constant NamedTuple for runtime access + # For each preset, create a NamedTuple of its toggle configurations + preset_configs = [] + for pname in preset_names + toggle_values = [presets_dict[pname][t] for t in toggles] + # Use tuple of symbols for NamedTuple type parameter + push!(preset_configs, :(NamedTuple{$(Tuple(toggles))}($(Tuple(toggle_values))))) + end + + # Use tuple of symbols for preset names + preset_map_const = :(const $(Symbol("_preset_map_", name)) = NamedTuple{$(Tuple(preset_names))}($(Tuple(preset_configs)))) + + runtime_helper = quote + function $(runtime_helper_name)($(group_names...), preset, kwargs) + # Validate groups + $(group_validations...) + + # Validate preset + if preset !== nothing && !(preset isa AbstractVerbosityPreset) + throw(ArgumentError("preset must be a SciMLLogging.AbstractVerbosityPreset, got \$(typeof(preset))")) + end + + # Validate kwargs + for (key, value) in pairs(kwargs) + if !(key in $(Tuple(toggles))) + throw(ArgumentError("Unknown verbosity option: \$key. Valid options are: $($(Tuple(toggles)))")) + end + if !(value isa AbstractMessageLevel) + throw(ArgumentError("\$key must be a SciMLLogging.AbstractMessageLevel, got \$(typeof(value))")) + end + end + + # Get preset config + preset_to_use = preset === nothing ? Standard() : preset + preset_config = $(Symbol("_preset_map_", name))[typeof(preset_to_use).name.name] + + # Apply precedence + $(toggle_assignments...) + + return $name($([t for t in toggles]...)) + end + end + + # Build wrapper function + wrapper_name = Symbol("_build_$(name)") + fast_path_expr = :($name($(fast_path_values...))) + slow_path_expr = :($runtime_helper_name($(group_names...), preset, kwargs)) + + # Build conditions + compile_time_condition = foldr((a, b) -> :($a && $b), [:($(g) === Nothing) for g in group_names]; init = :(preset === Nothing && kwargs <: NamedTuple{()})) + runtime_condition = foldr((a, b) -> :($a && $b), [:($(g) === nothing) for g in group_names]; init = :(preset === nothing && isempty(kwargs))) + + quoted_fast = QuoteNode(fast_path_expr) + quoted_slow = QuoteNode(slow_path_expr) + + wrapper_function = quote + function $(wrapper_name)($(group_names...), preset, kwargs) + if @generated + if $compile_time_condition + return $quoted_fast + else + return $quoted_slow + end + else + if $runtime_condition + return $name($(fast_path_values...)) + else + return $runtime_helper_name($(group_names...), preset, kwargs) + end + end + end + end + + # Build main constructor + kwarg_params = [Expr(:kw, :preset, :nothing); [Expr(:kw, g, :nothing) for g in group_names]] + + main_constructor = quote + function $name(; $(kwarg_params...), kwargs...) + $(wrapper_name)($(group_names...), preset, NamedTuple(kwargs)) + end + end + + # Assemble everything + result = quote + $(custom_preset_defs...) + $preset_map_const + $struct_def + $inner_constructor + $(preset_constructors...) + $runtime_helper + $wrapper_function + $main_constructor + end + + return esc(result) +end From 96977ba75d366920e379b5577dfe2df18f1dc95e Mon Sep 17 00:00:00 2001 From: jClugstor Date: Wed, 26 Nov 2025 11:21:10 -0500 Subject: [PATCH 3/8] make tests use macro --- test/generation_test.jl | 402 ++++++++++++++++++++-------------------- 1 file changed, 199 insertions(+), 203 deletions(-) diff --git a/test/generation_test.jl b/test/generation_test.jl index 822f591..6f960ad 100644 --- a/test/generation_test.jl +++ b/test/generation_test.jl @@ -1,82 +1,80 @@ -toggles = (:toggle1, :toggle2, :toggle3, :toggle4, :toggle5, - :toggle6, :toggle7, :toggle8, :toggle9, :toggle10) - -preset_map = ( - None = ( - toggle1 = Silent(), - toggle2 = Silent(), - toggle3 = Silent(), - toggle4 = Silent(), - toggle5 = Silent(), - toggle6 = Silent(), - toggle7 = Silent(), - toggle8 = Silent(), - toggle9 = Silent(), - toggle10 = Silent() - ), - Minimal = ( - toggle1 = WarnLevel(), - toggle2 = Silent(), - toggle3 = ErrorLevel(), - toggle4 = DebugLevel(), - toggle5 = InfoLevel(), - toggle6 = WarnLevel(), - toggle7 = Silent(), - toggle8 = InfoLevel(), - toggle9 = DebugLevel(), - toggle10 = ErrorLevel() - ), - Standard = ( - toggle1 = InfoLevel(), - toggle2 = WarnLevel(), - toggle3 = DebugLevel(), - toggle4 = ErrorLevel(), - toggle5 = Silent(), - toggle6 = InfoLevel(), - toggle7 = DebugLevel(), - toggle8 = WarnLevel(), - toggle9 = Silent(), - toggle10 = ErrorLevel() - ), - Detailed = ( - toggle1 = DebugLevel(), - toggle2 = InfoLevel(), - toggle3 = Silent(), - toggle4 = WarnLevel(), - toggle5 = ErrorLevel(), - toggle6 = DebugLevel(), - toggle7 = ErrorLevel(), - toggle8 = Silent(), - toggle9 = WarnLevel(), - toggle10 = InfoLevel() - ), - All = ( - toggle1 = ErrorLevel(), - toggle2 = DebugLevel(), - toggle3 = InfoLevel(), - toggle4 = Silent(), - toggle5 = WarnLevel(), - toggle6 = ErrorLevel(), - toggle7 = InfoLevel(), - toggle8 = DebugLevel(), - toggle9 = WarnLevel(), - toggle10 = Silent() +@define_verbosity_specifier VerbSpec begin + toggles = (:toggle1, :toggle2, :toggle3, :toggle4, :toggle5, + :toggle6, :toggle7, :toggle8, :toggle9, :toggle10) + + preset_map = ( + None = ( + toggle1 = Silent(), + toggle2 = Silent(), + toggle3 = Silent(), + toggle4 = Silent(), + toggle5 = Silent(), + toggle6 = Silent(), + toggle7 = Silent(), + toggle8 = Silent(), + toggle9 = Silent(), + toggle10 = Silent() + ), + Minimal = ( + toggle1 = WarnLevel(), + toggle2 = Silent(), + toggle3 = ErrorLevel(), + toggle4 = DebugLevel(), + toggle5 = InfoLevel(), + toggle6 = WarnLevel(), + toggle7 = Silent(), + toggle8 = InfoLevel(), + toggle9 = DebugLevel(), + toggle10 = ErrorLevel() + ), + Standard = ( + toggle1 = InfoLevel(), + toggle2 = WarnLevel(), + toggle3 = DebugLevel(), + toggle4 = ErrorLevel(), + toggle5 = Silent(), + toggle6 = InfoLevel(), + toggle7 = DebugLevel(), + toggle8 = WarnLevel(), + toggle9 = Silent(), + toggle10 = ErrorLevel() + ), + Detailed = ( + toggle1 = DebugLevel(), + toggle2 = InfoLevel(), + toggle3 = Silent(), + toggle4 = WarnLevel(), + toggle5 = ErrorLevel(), + toggle6 = DebugLevel(), + toggle7 = ErrorLevel(), + toggle8 = Silent(), + toggle9 = WarnLevel(), + toggle10 = InfoLevel() + ), + All = ( + toggle1 = ErrorLevel(), + toggle2 = DebugLevel(), + toggle3 = InfoLevel(), + toggle4 = Silent(), + toggle5 = WarnLevel(), + toggle6 = ErrorLevel(), + toggle7 = InfoLevel(), + toggle8 = DebugLevel(), + toggle9 = WarnLevel(), + toggle10 = Silent() + ) ) -) - -groups = ( - numerical = (:toggle1, :toggle2, :toggle3), - performance = (:toggle4, :toggle5, :toggle6, :toggle7), - error_control = (:toggle8, :toggle9, :toggle10) -) - -generated = generate_verbosity_specifier(:SciMLLogging.VerbSpec, toggles, preset_map, groups) -eval_verbosity_specifier(generated) + groups = ( + numerical = (:toggle1, :toggle2, :toggle3), + performance = (:toggle4, :toggle5, :toggle6, :toggle7), + error_control = (:toggle8, :toggle9, :toggle10) + ) +end using Test -@testset "SciMLLogging.VerbSpec Constructor Tests" begin +@testset "VerbSpec Constructor Tests" begin # Test 1: Default constructor (no arguments) - should use Standard preset @testset "Default constructor" begin v = SciMLLogging.VerbSpec() @@ -89,34 +87,34 @@ using Test # Test 2: Preset constructors @testset "Preset constructors" begin # None preset - v_none = SciMLLogging.SciMLLogging.VerbSpec(None()) + v_none = SciMLLogging.VerbSpec(None()) @test all(getfield(v_none, f) == Silent() for f in fieldnames(typeof(v_none))) # Minimal preset - v_min = SciMLLogging.SciMLLogging.VerbSpec(Minimal()) + v_min = SciMLLogging.VerbSpec(Minimal()) @test v_min.toggle1 == WarnLevel() @test v_min.toggle2 == Silent() @test v_min.toggle3 == ErrorLevel() # Standard preset - v_std = SciMLLogging.SciMLLogging.VerbSpec(Standard()) + v_std = SciMLLogging.VerbSpec(Standard()) @test v_std.toggle1 == InfoLevel() @test v_std.toggle2 == WarnLevel() # Detailed preset - v_det = SciMLLogging.SciMLLogging.VerbSpec(Detailed()) + v_det = SciMLLogging.VerbSpec(Detailed()) @test v_det.toggle1 == DebugLevel() @test v_det.toggle2 == InfoLevel() # All preset - v_all = SciMLLogging.SciMLLogging.VerbSpec(All()) + v_all = SciMLLogging.VerbSpec(All()) @test v_all.toggle1 == ErrorLevel() @test v_all.toggle2 == DebugLevel() end # Test 3: Keyword constructor with preset parameter @testset "Keyword constructor with preset" begin - v = SciMLLogging.SciMLLogging.VerbSpec(preset=Minimal()) + v = SciMLLogging.VerbSpec(preset=Minimal()) @test v.toggle1 == WarnLevel() @test v.toggle2 == Silent() end @@ -228,130 +226,128 @@ end # LinearVerbosity configuration -linear_toggles = ( - :default_lu_fallback, - :no_right_preconditioning, - :using_IterativeSolvers, - :IterativeSolvers_iterations, - :KrylovKit_verbosity, - :KrylovJL_verbosity, - :HYPRE_verbosity, - :pardiso_verbosity, - :blas_errors, - :blas_invalid_args, - :blas_info, - :blas_success, - :condition_number, - :convergence_failure, - :solver_failure, - :max_iters -) - -linear_preset_map = ( - None = ( - default_lu_fallback = Silent(), - no_right_preconditioning = Silent(), - using_IterativeSolvers = Silent(), - IterativeSolvers_iterations = Silent(), - KrylovKit_verbosity = Silent(), - KrylovJL_verbosity = Silent(), - HYPRE_verbosity = Silent(), - pardiso_verbosity = Silent(), - blas_errors = Silent(), - blas_invalid_args = Silent(), - blas_info = Silent(), - blas_success = Silent(), - condition_number = Silent(), - convergence_failure = Silent(), - solver_failure = Silent(), - max_iters = Silent() - ), - Minimal = ( - default_lu_fallback = Silent(), - no_right_preconditioning = Silent(), - using_IterativeSolvers = Silent(), - IterativeSolvers_iterations = Silent(), - KrylovKit_verbosity = Silent(), - KrylovJL_verbosity = Silent(), - HYPRE_verbosity = Silent(), - pardiso_verbosity = Silent(), - blas_errors = WarnLevel(), - blas_invalid_args = WarnLevel(), - blas_info = Silent(), - blas_success = Silent(), - condition_number = Silent(), - convergence_failure = Silent(), - solver_failure = Silent(), - max_iters = Silent() - ), - Standard = ( - default_lu_fallback = Silent(), - no_right_preconditioning = Silent(), - using_IterativeSolvers = Silent(), - IterativeSolvers_iterations = Silent(), - KrylovKit_verbosity = CustomLevel(1), - KrylovJL_verbosity = Silent(), - HYPRE_verbosity = InfoLevel(), - pardiso_verbosity = Silent(), - blas_errors = WarnLevel(), - blas_invalid_args = WarnLevel(), - blas_info = Silent(), - blas_success = Silent(), - condition_number = Silent(), - convergence_failure = WarnLevel(), - solver_failure = WarnLevel(), - max_iters = WarnLevel() - ), - Detailed = ( - default_lu_fallback = WarnLevel(), - no_right_preconditioning = InfoLevel(), - using_IterativeSolvers = InfoLevel(), - IterativeSolvers_iterations = Silent(), - KrylovKit_verbosity = CustomLevel(2), - KrylovJL_verbosity = CustomLevel(1), - HYPRE_verbosity = InfoLevel(), - pardiso_verbosity = CustomLevel(1), - blas_errors = WarnLevel(), - blas_invalid_args = WarnLevel(), - blas_info = InfoLevel(), - blas_success = InfoLevel(), - condition_number = Silent(), - convergence_failure = WarnLevel(), - solver_failure = WarnLevel(), - max_iters = WarnLevel() - ), - All = ( - default_lu_fallback = WarnLevel(), - no_right_preconditioning = InfoLevel(), - using_IterativeSolvers = InfoLevel(), - IterativeSolvers_iterations = InfoLevel(), - KrylovKit_verbosity = CustomLevel(3), - KrylovJL_verbosity = CustomLevel(1), - HYPRE_verbosity = InfoLevel(), - pardiso_verbosity = CustomLevel(1), - blas_errors = WarnLevel(), - blas_invalid_args = WarnLevel(), - blas_info = InfoLevel(), - blas_success = InfoLevel(), - condition_number = InfoLevel(), - convergence_failure = WarnLevel(), - solver_failure = WarnLevel(), - max_iters = WarnLevel() +@define_verbosity_specifier LinearVerbosity begin + toggles = ( + :default_lu_fallback, + :no_right_preconditioning, + :using_IterativeSolvers, + :IterativeSolvers_iterations, + :KrylovKit_verbosity, + :KrylovJL_verbosity, + :HYPRE_verbosity, + :pardiso_verbosity, + :blas_errors, + :blas_invalid_args, + :blas_info, + :blas_success, + :condition_number, + :convergence_failure, + :solver_failure, + :max_iters ) -) - -linear_groups = ( - error_control = (:default_lu_fallback, :blas_errors, :blas_invalid_args), - performance = (:no_right_preconditioning,), - numerical = (:using_IterativeSolvers, :IterativeSolvers_iterations, - :KrylovKit_verbosity, :KrylovJL_verbosity, :HYPRE_verbosity, - :pardiso_verbosity, :blas_info, :blas_success, :condition_number, - :convergence_failure, :solver_failure, :max_iters) -) - -# Generate LinearVerbosity -linear_generated = generate_verbosity_specifier(:LinearVerbosity, linear_toggles, linear_preset_map, linear_groups) -eval_verbosity_specifier(linear_generated) + + preset_map = ( + None = ( + default_lu_fallback = Silent(), + no_right_preconditioning = Silent(), + using_IterativeSolvers = Silent(), + IterativeSolvers_iterations = Silent(), + KrylovKit_verbosity = Silent(), + KrylovJL_verbosity = Silent(), + HYPRE_verbosity = Silent(), + pardiso_verbosity = Silent(), + blas_errors = Silent(), + blas_invalid_args = Silent(), + blas_info = Silent(), + blas_success = Silent(), + condition_number = Silent(), + convergence_failure = Silent(), + solver_failure = Silent(), + max_iters = Silent() + ), + Minimal = ( + default_lu_fallback = Silent(), + no_right_preconditioning = Silent(), + using_IterativeSolvers = Silent(), + IterativeSolvers_iterations = Silent(), + KrylovKit_verbosity = Silent(), + KrylovJL_verbosity = Silent(), + HYPRE_verbosity = Silent(), + pardiso_verbosity = Silent(), + blas_errors = WarnLevel(), + blas_invalid_args = WarnLevel(), + blas_info = Silent(), + blas_success = Silent(), + condition_number = Silent(), + convergence_failure = Silent(), + solver_failure = Silent(), + max_iters = Silent() + ), + Standard = ( + default_lu_fallback = Silent(), + no_right_preconditioning = Silent(), + using_IterativeSolvers = Silent(), + IterativeSolvers_iterations = Silent(), + KrylovKit_verbosity = CustomLevel(1), + KrylovJL_verbosity = Silent(), + HYPRE_verbosity = InfoLevel(), + pardiso_verbosity = Silent(), + blas_errors = WarnLevel(), + blas_invalid_args = WarnLevel(), + blas_info = Silent(), + blas_success = Silent(), + condition_number = Silent(), + convergence_failure = WarnLevel(), + solver_failure = WarnLevel(), + max_iters = WarnLevel() + ), + Detailed = ( + default_lu_fallback = WarnLevel(), + no_right_preconditioning = InfoLevel(), + using_IterativeSolvers = InfoLevel(), + IterativeSolvers_iterations = Silent(), + KrylovKit_verbosity = CustomLevel(2), + KrylovJL_verbosity = CustomLevel(1), + HYPRE_verbosity = InfoLevel(), + pardiso_verbosity = CustomLevel(1), + blas_errors = WarnLevel(), + blas_invalid_args = WarnLevel(), + blas_info = InfoLevel(), + blas_success = InfoLevel(), + condition_number = Silent(), + convergence_failure = WarnLevel(), + solver_failure = WarnLevel(), + max_iters = WarnLevel() + ), + All = ( + default_lu_fallback = WarnLevel(), + no_right_preconditioning = InfoLevel(), + using_IterativeSolvers = InfoLevel(), + IterativeSolvers_iterations = InfoLevel(), + KrylovKit_verbosity = CustomLevel(3), + KrylovJL_verbosity = CustomLevel(1), + HYPRE_verbosity = InfoLevel(), + pardiso_verbosity = CustomLevel(1), + blas_errors = WarnLevel(), + blas_invalid_args = WarnLevel(), + blas_info = InfoLevel(), + blas_success = InfoLevel(), + condition_number = InfoLevel(), + convergence_failure = WarnLevel(), + solver_failure = WarnLevel(), + max_iters = WarnLevel() + ) + ) + + groups = ( + error_control = (:default_lu_fallback, :blas_errors, :blas_invalid_args), + performance = (:no_right_preconditioning,), + numerical = (:using_IterativeSolvers, :IterativeSolvers_iterations, + :KrylovKit_verbosity, :KrylovJL_verbosity, :HYPRE_verbosity, + :pardiso_verbosity, :blas_info, :blas_success, :condition_number, + :convergence_failure, :solver_failure, :max_iters) + ) +end # Test LinearVerbosity @testset "LinearVerbosity Tests" begin From 472c2e4245df183dfede4bae1c5bb33eb1610fad Mon Sep 17 00:00:00 2001 From: jClugstor Date: Wed, 26 Nov 2025 11:55:03 -0500 Subject: [PATCH 4/8] remove the @generated path --- src/verbspec_generation_macro.jl | 71 ++++++++------------------------ 1 file changed, 18 insertions(+), 53 deletions(-) diff --git a/src/verbspec_generation_macro.jl b/src/verbspec_generation_macro.jl index 02f4da6..05fe3e2 100644 --- a/src/verbspec_generation_macro.jl +++ b/src/verbspec_generation_macro.jl @@ -29,7 +29,6 @@ macro define_verbosity_specifier(name, block) local preset_map_expr = nothing local groups_expr = nothing - # The block should be a :block expression with :line and assignment expressions if block.head == :block for ex in block.args if ex isa Expr && ex.head == :(=) @@ -116,7 +115,6 @@ macro define_verbosity_specifier(name, block) end ) - Main.@infiltrate # Generate preset constructors preset_constructors = [] for preset_name in preset_names @@ -146,9 +144,6 @@ macro define_verbosity_specifier(name, block) standard_config = presets_dict[:Standard] fast_path_values = [standard_config[t] for t in toggles] - # Build the runtime helper function - runtime_helper_name = Symbol("_build_$(name)_runtime") - # Build validation for groups group_validations = [] for group_name in group_names @@ -181,29 +176,40 @@ macro define_verbosity_specifier(name, block) for pname in preset_names toggle_values = [presets_dict[pname][t] for t in toggles] # Use tuple of symbols for NamedTuple type parameter - push!(preset_configs, :(NamedTuple{$(Tuple(toggles))}($(Tuple(toggle_values))))) + push!(preset_configs, :(NamedTuple{$(Tuple(toggles))}(($(toggle_values...),)))) end # Use tuple of symbols for preset names - preset_map_const = :(const $(Symbol("_preset_map_", name)) = NamedTuple{$(Tuple(preset_names))}($(Tuple(preset_configs)))) + preset_map_const = :(const $(Symbol("_preset_map_", name)) = NamedTuple{$(Tuple(preset_names))}(($(preset_configs...),))) + + # Build main constructor + kwarg_params = [Expr(:kw, :preset, :nothing); [Expr(:kw, g, :nothing) for g in group_names]] + runtime_condition = foldr((a, b) -> :($a && $b), [:($(g) === nothing) for g in group_names]; init = :(preset === nothing && isempty(kwargs))) + + main_constructor = quote + function $name(; $(kwarg_params...), kwargs...) + kwargs = NamedTuple(kwargs) + + # Fast path: all defaults + if $runtime_condition + return $name($(fast_path_values...)) + end - runtime_helper = quote - function $(runtime_helper_name)($(group_names...), preset, kwargs) # Validate groups $(group_validations...) # Validate preset if preset !== nothing && !(preset isa AbstractVerbosityPreset) - throw(ArgumentError("preset must be a SciMLLogging.AbstractVerbosityPreset, got \$(typeof(preset))")) + throw(ArgumentError("preset must be a SciMLLogging.AbstractVerbosityPreset, got $(typeof(preset))")) end # Validate kwargs for (key, value) in pairs(kwargs) if !(key in $(Tuple(toggles))) - throw(ArgumentError("Unknown verbosity option: \$key. Valid options are: $($(Tuple(toggles)))")) + throw(ArgumentError("Unknown verbosity option: $key. Valid options are: $($(Tuple(toggles)))")) end if !(value isa AbstractMessageLevel) - throw(ArgumentError("\$key must be a SciMLLogging.AbstractMessageLevel, got \$(typeof(value))")) + throw(ArgumentError("$key must be a SciMLLogging.AbstractMessageLevel, got $(typeof(value))")) end end @@ -218,45 +224,6 @@ macro define_verbosity_specifier(name, block) end end - # Build wrapper function - wrapper_name = Symbol("_build_$(name)") - fast_path_expr = :($name($(fast_path_values...))) - slow_path_expr = :($runtime_helper_name($(group_names...), preset, kwargs)) - - # Build conditions - compile_time_condition = foldr((a, b) -> :($a && $b), [:($(g) === Nothing) for g in group_names]; init = :(preset === Nothing && kwargs <: NamedTuple{()})) - runtime_condition = foldr((a, b) -> :($a && $b), [:($(g) === nothing) for g in group_names]; init = :(preset === nothing && isempty(kwargs))) - - quoted_fast = QuoteNode(fast_path_expr) - quoted_slow = QuoteNode(slow_path_expr) - - wrapper_function = quote - function $(wrapper_name)($(group_names...), preset, kwargs) - if @generated - if $compile_time_condition - return $quoted_fast - else - return $quoted_slow - end - else - if $runtime_condition - return $name($(fast_path_values...)) - else - return $runtime_helper_name($(group_names...), preset, kwargs) - end - end - end - end - - # Build main constructor - kwarg_params = [Expr(:kw, :preset, :nothing); [Expr(:kw, g, :nothing) for g in group_names]] - - main_constructor = quote - function $name(; $(kwarg_params...), kwargs...) - $(wrapper_name)($(group_names...), preset, NamedTuple(kwargs)) - end - end - # Assemble everything result = quote $(custom_preset_defs...) @@ -264,8 +231,6 @@ macro define_verbosity_specifier(name, block) $struct_def $inner_constructor $(preset_constructors...) - $runtime_helper - $wrapper_function $main_constructor end From e3a13bd443da41a47d56e883f847bc930c8284b4 Mon Sep 17 00:00:00 2001 From: jClugstor Date: Wed, 26 Nov 2025 12:20:43 -0500 Subject: [PATCH 5/8] change to presets from preset_map --- src/verbspec_generation_macro.jl | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/verbspec_generation_macro.jl b/src/verbspec_generation_macro.jl index 05fe3e2..de84fc1 100644 --- a/src/verbspec_generation_macro.jl +++ b/src/verbspec_generation_macro.jl @@ -1,16 +1,16 @@ """ - @define_verbosity_specifier name begin + @verbosity_specifier name begin toggles = (...) - preset_map = (...) + presets = (...) groups = (...) end # Example ```julia -@define_verbosity_specifier MyVerbosity begin +@verbosity_specifier MyVerbosity begin toggles = (:toggle1, :toggle2) - preset_map = ( + presets = ( Standard = ( toggle1 = InfoLevel(), toggle2 = WarnLevel() @@ -23,10 +23,10 @@ end ``` """ -macro define_verbosity_specifier(name, block) +macro verbosity_specifier(name, block) # Extract the three assignments from the block local toggles_expr = nothing - local preset_map_expr = nothing + local presets_expr = nothing local groups_expr = nothing if block.head == :block @@ -36,8 +36,8 @@ macro define_verbosity_specifier(name, block) rhs = ex.args[2] if lhs == :toggles toggles_expr = rhs - elseif lhs == :preset_map - preset_map_expr = rhs + elseif lhs == :presets + presets_expr = rhs elseif lhs == :groups groups_expr = rhs end @@ -46,7 +46,7 @@ macro define_verbosity_specifier(name, block) end toggles_expr !== nothing || throw(ArgumentError("toggles must be defined in block")) - preset_map_expr !== nothing || throw(ArgumentError("preset_map must be defined in block")) + presets_expr !== nothing || throw(ArgumentError("presets must be defined in block")) groups_expr !== nothing || throw(ArgumentError("groups must be defined in block")) # Parse toggles - should be a tuple of symbols @@ -54,10 +54,10 @@ macro define_verbosity_specifier(name, block) # Extract the actual symbols from QuoteNode objects toggles = [t.value for t in toggles_expr.args] - # Parse preset_map - should be a NamedTuple - preset_map_expr.head == :tuple || throw(ArgumentError("preset_map must be a NamedTuple")) + # Parse presets - should be a NamedTuple + presets_expr.head == :tuple || throw(ArgumentError("presets must be a NamedTuple")) presets_dict = Dict() - for preset_def in preset_map_expr.args + for preset_def in presets_expr.args preset_def.head == :(=) || throw(ArgumentError("Each preset must be name = (...)")) preset_name = preset_def.args[1] preset_values = preset_def.args[2] @@ -108,13 +108,6 @@ macro define_verbosity_specifier(name, block) end ) - # Generate inner constructor - inner_constructor = :( - function $name($(toggles...)) - return $name{$([:(typeof($t)) for t in toggles]...)}($(toggles...)) - end - ) - # Generate preset constructors preset_constructors = [] for preset_name in preset_names @@ -229,7 +222,6 @@ macro define_verbosity_specifier(name, block) $(custom_preset_defs...) $preset_map_const $struct_def - $inner_constructor $(preset_constructors...) $main_constructor end From cc35b955fc24d47a87cf3e83ab8bee38e77fbc5d Mon Sep 17 00:00:00 2001 From: jClugstor Date: Wed, 26 Nov 2025 12:23:04 -0500 Subject: [PATCH 6/8] add tests for generation --- test/generation_test.jl | 209 ++++++++++++++++++++++++++++++++++------ test/runtests.jl | 4 +- 2 files changed, 181 insertions(+), 32 deletions(-) diff --git a/test/generation_test.jl b/test/generation_test.jl index 6f960ad..71eca1e 100644 --- a/test/generation_test.jl +++ b/test/generation_test.jl @@ -1,8 +1,15 @@ -@define_verbosity_specifier VerbSpec begin +using SciMLLogging +using SciMLLogging: SciMLLogging, AbstractVerbositySpecifier, @SciMLMessage, + AbstractVerbosityPreset, AbstractMessageLevel, WarnLevel, InfoLevel, + ErrorLevel, Silent, None, All, Minimal, @verbosity_specifier +using Logging +using Test + +SciMLLogging.@verbosity_specifier VerbSpec begin toggles = (:toggle1, :toggle2, :toggle3, :toggle4, :toggle5, :toggle6, :toggle7, :toggle8, :toggle9, :toggle10) - preset_map = ( + presets = ( None = ( toggle1 = Silent(), toggle2 = Silent(), @@ -72,12 +79,10 @@ ) end -using Test - @testset "VerbSpec Constructor Tests" begin # Test 1: Default constructor (no arguments) - should use Standard preset @testset "Default constructor" begin - v = SciMLLogging.VerbSpec() + v = VerbSpec() @test v.toggle1 == InfoLevel() @test v.toggle2 == WarnLevel() @test v.toggle3 == DebugLevel() @@ -87,34 +92,34 @@ using Test # Test 2: Preset constructors @testset "Preset constructors" begin # None preset - v_none = SciMLLogging.VerbSpec(None()) + v_none = VerbSpec(None()) @test all(getfield(v_none, f) == Silent() for f in fieldnames(typeof(v_none))) # Minimal preset - v_min = SciMLLogging.VerbSpec(Minimal()) + v_min = VerbSpec(Minimal()) @test v_min.toggle1 == WarnLevel() @test v_min.toggle2 == Silent() @test v_min.toggle3 == ErrorLevel() # Standard preset - v_std = SciMLLogging.VerbSpec(Standard()) + v_std = VerbSpec(Standard()) @test v_std.toggle1 == InfoLevel() @test v_std.toggle2 == WarnLevel() # Detailed preset - v_det = SciMLLogging.VerbSpec(Detailed()) + v_det = VerbSpec(Detailed()) @test v_det.toggle1 == DebugLevel() @test v_det.toggle2 == InfoLevel() # All preset - v_all = SciMLLogging.VerbSpec(All()) + v_all = VerbSpec(All()) @test v_all.toggle1 == ErrorLevel() @test v_all.toggle2 == DebugLevel() end # Test 3: Keyword constructor with preset parameter @testset "Keyword constructor with preset" begin - v = SciMLLogging.VerbSpec(preset=Minimal()) + v = VerbSpec(preset=Minimal()) @test v.toggle1 == WarnLevel() @test v.toggle2 == Silent() end @@ -122,21 +127,21 @@ using Test # Test 4: Group parameters @testset "Group parameters" begin # Set all numerical toggles to ErrorLevel - v = SciMLLogging.VerbSpec(numerical=ErrorLevel()) + v = VerbSpec(numerical=ErrorLevel()) @test v.toggle1 == ErrorLevel() # numerical group @test v.toggle2 == ErrorLevel() # numerical group @test v.toggle3 == ErrorLevel() # numerical group @test v.toggle4 == ErrorLevel() # performance group (from Standard preset) # Set all performance toggles - v2 = SciMLLogging.VerbSpec(performance=InfoLevel()) + v2 = VerbSpec(performance=InfoLevel()) @test v2.toggle4 == InfoLevel() # performance group @test v2.toggle5 == InfoLevel() # performance group @test v2.toggle6 == InfoLevel() # performance group @test v2.toggle7 == InfoLevel() # performance group # Set all error_control toggles - v3 = SciMLLogging.VerbSpec(error_control=DebugLevel()) + v3 = VerbSpec(error_control=DebugLevel()) @test v3.toggle8 == DebugLevel() # error_control group @test v3.toggle9 == DebugLevel() # error_control group @test v3.toggle10 == DebugLevel() # error_control group @@ -144,12 +149,12 @@ using Test # Test 5: Individual toggle parameters @testset "Individual toggle parameters" begin - v = SciMLLogging.VerbSpec(toggle1=ErrorLevel()) + v = VerbSpec(toggle1=ErrorLevel()) @test v.toggle1 == ErrorLevel() @test v.toggle2 == WarnLevel() # from Standard preset # Multiple individual toggles - v2 = SciMLLogging.VerbSpec(toggle1=ErrorLevel(), toggle5=WarnLevel()) + v2 = VerbSpec(toggle1=ErrorLevel(), toggle5=WarnLevel()) @test v2.toggle1 == ErrorLevel() @test v2.toggle5 == WarnLevel() end @@ -157,18 +162,18 @@ using Test # Test 6: Precedence: individual > group > preset @testset "Precedence tests" begin # Individual overrides group - v = SciMLLogging.VerbSpec(numerical=WarnLevel(), toggle1=ErrorLevel()) + v = VerbSpec(numerical=WarnLevel(), toggle1=ErrorLevel()) @test v.toggle1 == ErrorLevel() # individual wins @test v.toggle2 == WarnLevel() # from group @test v.toggle3 == WarnLevel() # from group # Group overrides preset - v2 = SciMLLogging.VerbSpec(preset=None(), numerical=InfoLevel()) + v2 = VerbSpec(preset=None(), numerical=InfoLevel()) @test v2.toggle1 == InfoLevel() # from group @test v2.toggle4 == Silent() # from preset (not in numerical group) # All three levels - v3 = SciMLLogging.VerbSpec(preset=None(), performance=WarnLevel(), toggle4=ErrorLevel()) + v3 = VerbSpec(preset=None(), performance=WarnLevel(), toggle4=ErrorLevel()) @test v3.toggle4 == ErrorLevel() # individual wins @test v3.toggle5 == WarnLevel() # from group @test v3.toggle1 == Silent() # from preset @@ -176,7 +181,7 @@ using Test # Test 7: Combining preset with groups @testset "Preset with groups" begin - v = SciMLLogging.VerbSpec(preset=Minimal(), numerical=DebugLevel()) + v = VerbSpec(preset=Minimal(), numerical=DebugLevel()) @test v.toggle1 == DebugLevel() # from group @test v.toggle2 == DebugLevel() # from group @test v.toggle3 == DebugLevel() # from group @@ -185,7 +190,7 @@ using Test # Test 8: All three: preset, groups, and individual toggles @testset "Preset + groups + individual toggles" begin - v = SciMLLogging.VerbSpec( + v = VerbSpec( preset=Detailed(), numerical=ErrorLevel(), performance=WarnLevel(), @@ -202,31 +207,31 @@ using Test # Test 9: Type stability - ensure parametric types work @testset "Type stability" begin - v = SciMLLogging.VerbSpec() - @test typeof(v) <: SciMLLogging.VerbSpec + v = VerbSpec() + @test typeof(v) <: VerbSpec @test isconcretetype(typeof(v)) end # Test 10: Error handling @testset "Error handling" begin # Invalid group argument type - @test_throws ArgumentError SciMLLogging.VerbSpec(numerical="invalid") + @test_throws ArgumentError VerbSpec(numerical="invalid") # Invalid individual toggle type - @test_throws ArgumentError SciMLLogging.VerbSpec(toggle1="invalid") + @test_throws ArgumentError VerbSpec(toggle1="invalid") # Unknown toggle name - @test_throws ArgumentError SciMLLogging.VerbSpec(unknown_toggle=ErrorLevel()) + @test_throws ArgumentError VerbSpec(unknown_toggle=ErrorLevel()) # Invalid preset type - @test_throws ArgumentError SciMLLogging.VerbSpec(preset="invalid") + @test_throws ArgumentError VerbSpec(preset="invalid") end end # LinearVerbosity configuration -@define_verbosity_specifier LinearVerbosity begin +@verbosity_specifier LinearVerbosity begin toggles = ( :default_lu_fallback, :no_right_preconditioning, @@ -246,7 +251,7 @@ end :max_iters ) - preset_map = ( + presets = ( None = ( default_lu_fallback = Silent(), no_right_preconditioning = Silent(), @@ -375,3 +380,149 @@ end end end +# Test with preset types as toggle values (useful for verb specs that hold other verb specs, e.g. NonlinearVerbosity) +@verbosity_specifier HierarchicalVerbosity begin + toggles = (:component_a, :component_b, :component_c) + + presets = ( + None = ( + component_a = Silent(), + component_b = Silent(), + component_c = Silent() + ), + Minimal = ( + component_a = WarnLevel(), + component_b = Minimal(), # Reference to another preset + component_c = InfoLevel() + ), + Standard = ( + component_a = InfoLevel(), + component_b = Standard(), # Reference to another preset + component_c = WarnLevel() + ), + Detailed = ( + component_a = DebugLevel(), + component_b = Detailed(), # Reference to another preset + component_c = InfoLevel() + ), + All = ( + component_a = ErrorLevel(), + component_b = All(), # Reference to another preset + component_c = DebugLevel() + ) + ) + + groups = ( + main = (:component_a,), + auxiliary = (:component_b, :component_c) + ) +end + +@testset "HierarchicalVerbosity with Preset References" begin + @testset "Preset as toggle value" begin + v = HierarchicalVerbosity(preset=Minimal()) + @test v.component_a == WarnLevel() + @test v.component_b == Minimal() # Should be the preset type, not expanded + @test v.component_c == InfoLevel() + + v2 = HierarchicalVerbosity(preset=Standard()) + @test v2.component_a == InfoLevel() + @test v2.component_b == Standard() + @test v2.component_c == WarnLevel() + end + + @testset "Override preset-typed toggle" begin + # Override a preset-typed toggle with a message level + v = HierarchicalVerbosity(preset=Standard(), component_b=ErrorLevel()) + @test v.component_a == InfoLevel() + @test v.component_b == ErrorLevel() # Overridden + @test v.component_c == WarnLevel() + end +end + +# Test with custom presets +@verbosity_specifier CustomPresetVerbosity begin + toggles = (:solver, :preconditioner, :convergence) + + presets = ( + None = ( + solver = Silent(), + preconditioner = Silent(), + convergence = Silent() + ), + Minimal = ( + solver = WarnLevel(), + preconditioner = Silent(), + convergence = WarnLevel() + ), + Standard = ( + solver = InfoLevel(), + preconditioner = InfoLevel(), + convergence = WarnLevel() + ), + Detailed = ( + solver = DebugLevel(), + preconditioner = InfoLevel(), + convergence = InfoLevel() + ), + All = ( + solver = DebugLevel(), + preconditioner = DebugLevel(), + convergence = DebugLevel() + ), + # Custom presets + QuietSolver = ( + solver = Silent(), + preconditioner = InfoLevel(), + convergence = WarnLevel() + ), + VerboseSolver = ( + solver = DebugLevel(), + preconditioner = Silent(), + convergence = ErrorLevel() + ) + ) + + groups = ( + core = (:solver, :preconditioner), + diagnostics = (:convergence,) + ) +end + +@testset "CustomPresetVerbosity with Custom Presets" begin + @testset "Standard presets work" begin + v = CustomPresetVerbosity(preset=Standard()) + @test v.solver == InfoLevel() + @test v.preconditioner == InfoLevel() + @test v.convergence == WarnLevel() + end + + @testset "Custom preset QuietSolver" begin + v = CustomPresetVerbosity(preset=QuietSolver()) + @test v.solver == Silent() + @test v.preconditioner == InfoLevel() + @test v.convergence == WarnLevel() + end + + @testset "Custom preset VerboseSolver" begin + v = CustomPresetVerbosity(preset=VerboseSolver()) + @test v.solver == DebugLevel() + @test v.preconditioner == Silent() + @test v.convergence == ErrorLevel() + end + + @testset "Override custom preset with group" begin + v = CustomPresetVerbosity(preset=QuietSolver(), core=ErrorLevel()) + @test v.solver == ErrorLevel() # Overridden by group + @test v.preconditioner == ErrorLevel() # Overridden by group + @test v.convergence == WarnLevel() # From preset + end + + @testset "Override custom preset with individual toggle" begin + v = CustomPresetVerbosity(preset=VerboseSolver(), solver=InfoLevel()) + @test v.solver == InfoLevel() # Overridden + @test v.preconditioner == Silent() # From preset + @test v.convergence == ErrorLevel() # From preset + end +end + diff --git a/test/runtests.jl b/test/runtests.jl index cca768f..a085435 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,7 +1,5 @@ -using SciMLLogging -using SciMLLogging: None, Minimal, Standard, All, AbstractVerbosityPreset using Test using SafeTestsets @time @safetestset "Basic Tests" include("basics.jl") -@time @safetestset "Basic Tests" include("generation_test.jl") \ No newline at end of file +@time @safetestset "Verbosity Specifier Generation Tests" include("generation_test.jl") \ No newline at end of file From 32a82116218736f873aa0fe2068608631863129b Mon Sep 17 00:00:00 2001 From: jClugstor Date: Wed, 26 Nov 2025 13:01:22 -0500 Subject: [PATCH 7/8] add more to docstring --- src/verbspec_generation_macro.jl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/verbspec_generation_macro.jl b/src/verbspec_generation_macro.jl index de84fc1..5030b00 100644 --- a/src/verbspec_generation_macro.jl +++ b/src/verbspec_generation_macro.jl @@ -5,6 +5,28 @@ groups = (...) end +Generates a parametric struct and constructors for a verbosity specifier. + +# Input Format + +**toggles:** Tuple of symbols defining the verbosity toggle names (e.g., `(:toggle1, :toggle2)`). + +**presets:** Named tuple mapping preset names to toggle configurations. Each preset maps toggle names to message levels or preset types. +Must include at least `Standard`. Can define custom presets beyond the standard five (None, Minimal, Standard, Detailed, All). + +**groups:** Named tuple mapping group names to tuples of toggle symbols. Groups allow setting multiple toggles at once. + +# Generated Code + +**Struct:** Creates `name{T1, T2, ...} <: AbstractVerbositySpecifier` with fields for each toggle. + +**Constructors:** +- `name()`: Default constructor using Standard preset +- `name(preset::AbstractVerbosityPreset)`: Constructor from preset (e.g., `name(Minimal())`) for each preset in presets +- `name(; preset=nothing, groups..., kwargs...)`: Keyword constructor with precedence: individual > group > preset + +**Custom Preset Types:** Generates struct definitions for non-standard presets (beyond None, Minimal, Standard, Detailed, All). + # Example ```julia @verbosity_specifier MyVerbosity begin From 4bd28c19020fe172e238cc84992528322e13bbaf Mon Sep 17 00:00:00 2001 From: jClugstor Date: Wed, 3 Dec 2025 14:33:19 -0500 Subject: [PATCH 8/8] fix project.toml --- Project.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 57e9eb5..06048c9 100644 --- a/Project.toml +++ b/Project.toml @@ -1,8 +1,7 @@ name = "SciMLLogging" uuid = "a6db7da4-7206-11f0-1eab-35f2a5dbe1d1" -version = "1.5.0" authors = ["Jadon Clugston"] -version = "1.6.0" +version = "1.7.0" [deps] Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"