From 741031c44ec84e44b2a53a3caba5bc8d50ba2bbf Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Wed, 4 Mar 2026 14:11:45 +0000 Subject: [PATCH 1/6] fix: treat im as imaginary unit in string parsing Co-authored-by: Miles Cranmer --- src/Parse.jl | 16 ++++++++++++++++ test/test_parse.jl | 27 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/Parse.jl b/src/Parse.jl index 10d121db..bdfdb8ea 100644 --- a/src/Parse.jl +++ b/src/Parse.jl @@ -200,6 +200,21 @@ end ) end +@unstable @inline _replace_imaginary_unit_symbol(ex) = ex +@unstable @inline _replace_imaginary_unit_symbol(ex::Symbol) = ex === :im ? im : ex +@unstable @inline function _replace_imaginary_unit_symbol(ex::Expr) + return Expr(ex.head, map(_replace_imaginary_unit_symbol, ex.args)...) +end + +@unstable @inline function _normalize_expression_for_parse( + ex, variable_names::Union{AbstractVector{<:AbstractString},Nothing} +) + if variable_names !== nothing && ("im" in variable_names) + return ex + end + return _replace_imaginary_unit_symbol(ex) +end + """Parse an expression Julia `Expr` object.""" @unstable function parse_expression( ex; @@ -237,6 +252,7 @@ end operators end + ex = _normalize_expression_for_parse(ex, variable_names) tree = _parse_expression(ex, operators, variable_names, N, E, evaluate_on; kws...) return constructorof(E)(tree; operators, variable_names, kws...) end diff --git a/test/test_parse.jl b/test/test_parse.jl index 50121888..a62304ac 100644 --- a/test/test_parse.jl +++ b/test/test_parse.jl @@ -44,6 +44,33 @@ end end +@testitem "String parse treats Julia imaginary unit `im` as a constant" begin + using DynamicExpressions + using Test + + operators = OperatorEnum(2 => [+, -, *, /]) + + ex = parse_expression("0.1im + x"; operators, variable_names=["x"]) + @test typeof(ex) <: Expression + + function count_vars(n) + if n.degree == 0 + return n.constant ? 0 : 1 + end + s = 0 + for i in 1:Int(n.degree) + s += count_vars(n.children[i].x) + end + return s + end + + @test count_vars(ex.tree) == 1 + + ex2 = parse_expression("im + x2"; operators, variable_names=["im", "x2"]) + @test typeof(ex2) <: Expression + @test count_vars(ex2.tree) == 2 +end + @testitem "Can also parse just a float" begin using DynamicExpressions operators = OperatorEnum() # Tests empty operators From c5f6bbacfed9c0c48c62aaa214d3280104643c03 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Wed, 4 Mar 2026 16:55:42 +0000 Subject: [PATCH 2/6] fix: address review suggestions for im parse Co-authored-by: Miles Cranmer --- src/Parse.jl | 2 +- test/test_parse.jl | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Parse.jl b/src/Parse.jl index bdfdb8ea..fea30703 100644 --- a/src/Parse.jl +++ b/src/Parse.jl @@ -202,7 +202,7 @@ end @unstable @inline _replace_imaginary_unit_symbol(ex) = ex @unstable @inline _replace_imaginary_unit_symbol(ex::Symbol) = ex === :im ? im : ex -@unstable @inline function _replace_imaginary_unit_symbol(ex::Expr) +@inline function _replace_imaginary_unit_symbol(ex::Expr) return Expr(ex.head, map(_replace_imaginary_unit_symbol, ex.args)...) end diff --git a/test/test_parse.jl b/test/test_parse.jl index a62304ac..0f2da6fd 100644 --- a/test/test_parse.jl +++ b/test/test_parse.jl @@ -51,7 +51,7 @@ end operators = OperatorEnum(2 => [+, -, *, /]) ex = parse_expression("0.1im + x"; operators, variable_names=["x"]) - @test typeof(ex) <: Expression + @test typeof(ex) <: Expression{ComplexF64} function count_vars(n) if n.degree == 0 @@ -66,9 +66,26 @@ end @test count_vars(ex.tree) == 1 + # Check evaluation + eltype + Xr = reshape([1.0], 1, :) + yr = ex(Xr) + @test eltype(yr) == ComplexF64 + @test yr[1] ≈ 1.0 + 0.1im + + Xc = reshape([1.0 + 2.0im], 1, :) + yc = ex(Xc) + @test eltype(yc) == ComplexF64 + @test yc[1] ≈ 1.0 + 2.1im + + # If `"im"` is in `variable_names`, it should be treated as a variable ex2 = parse_expression("im + x2"; operators, variable_names=["im", "x2"]) @test typeof(ex2) <: Expression @test count_vars(ex2.tree) == 2 + + X2 = reshape(Float32[2, 3], 2, :) + y2 = ex2(X2) + @test eltype(y2) == Float32 + @test y2[1] == 5f0 end @testitem "Can also parse just a float" begin From 63d79005f7ba9a1f263b2b5a7d6211d11f9c1689 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Wed, 4 Mar 2026 17:17:56 +0000 Subject: [PATCH 3/6] style: run JuliaFormatter --- test/test_parse.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_parse.jl b/test/test_parse.jl index 0f2da6fd..5d185a06 100644 --- a/test/test_parse.jl +++ b/test/test_parse.jl @@ -85,7 +85,7 @@ end X2 = reshape(Float32[2, 3], 2, :) y2 = ex2(X2) @test eltype(y2) == Float32 - @test y2[1] == 5f0 + @test y2[1] == 5.0f0 end @testitem "Can also parse just a float" begin From e83527903b10196294137a736de32b84733ac437 Mon Sep 17 00:00:00 2001 From: MilesCranmer Date: Sun, 8 Mar 2026 14:53:39 +0000 Subject: [PATCH 4/6] style: remove unnecessary inline parse helpers --- src/Parse.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Parse.jl b/src/Parse.jl index fea30703..3f9ca871 100644 --- a/src/Parse.jl +++ b/src/Parse.jl @@ -200,13 +200,13 @@ end ) end -@unstable @inline _replace_imaginary_unit_symbol(ex) = ex -@unstable @inline _replace_imaginary_unit_symbol(ex::Symbol) = ex === :im ? im : ex -@inline function _replace_imaginary_unit_symbol(ex::Expr) +_replace_imaginary_unit_symbol(ex) = ex +@unstable _replace_imaginary_unit_symbol(ex::Symbol) = ex === :im ? im : ex +function _replace_imaginary_unit_symbol(ex::Expr) return Expr(ex.head, map(_replace_imaginary_unit_symbol, ex.args)...) end -@unstable @inline function _normalize_expression_for_parse( +@unstable function _normalize_expression_for_parse( ex, variable_names::Union{AbstractVector{<:AbstractString},Nothing} ) if variable_names !== nothing && ("im" in variable_names) From 59815bbfb3860bd787444f4a4218aec83a4a7c32 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Sun, 8 Mar 2026 15:55:48 +0000 Subject: [PATCH 5/6] fix: adapt im parsing backport for release-v1 --- src/Parse.jl | 2 ++ test/test_parse.jl | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Parse.jl b/src/Parse.jl index 3f9ca871..066d5945 100644 --- a/src/Parse.jl +++ b/src/Parse.jl @@ -258,6 +258,8 @@ end end end +@unstable parse_expression(ex::String; kws...) = parse_expression(Meta.parse(ex); kws...) + """An empty module for evaluation without collisions.""" module EmptyModule end diff --git a/test/test_parse.jl b/test/test_parse.jl index 5d185a06..bf156ae9 100644 --- a/test/test_parse.jl +++ b/test/test_parse.jl @@ -48,7 +48,7 @@ end using DynamicExpressions using Test - operators = OperatorEnum(2 => [+, -, *, /]) + operators = OperatorEnum(; binary_operators=[+, -, *, /], unary_operators=[], define_helper_functions=false) ex = parse_expression("0.1im + x"; operators, variable_names=["x"]) @test typeof(ex) <: Expression{ComplexF64} From be2755012df85babb936322d5d848c37daff6493 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Sun, 8 Mar 2026 18:58:37 +0000 Subject: [PATCH 6/6] fix: repair parse test traversal for Node on release-v1 Co-authored-by: Miles Cranmer --- test/test_parse.jl | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/test_parse.jl b/test/test_parse.jl index bf156ae9..7e9c8f99 100644 --- a/test/test_parse.jl +++ b/test/test_parse.jl @@ -48,7 +48,9 @@ end using DynamicExpressions using Test - operators = OperatorEnum(; binary_operators=[+, -, *, /], unary_operators=[], define_helper_functions=false) + operators = OperatorEnum(; + binary_operators=[+, -, *, /], unary_operators=[], define_helper_functions=false + ) ex = parse_expression("0.1im + x"; operators, variable_names=["x"]) @test typeof(ex) <: Expression{ComplexF64} @@ -56,12 +58,11 @@ end function count_vars(n) if n.degree == 0 return n.constant ? 0 : 1 + elseif n.degree == 1 + return count_vars(n.l) + else + return count_vars(n.l) + count_vars(n.r) end - s = 0 - for i in 1:Int(n.degree) - s += count_vars(n.children[i].x) - end - return s end @test count_vars(ex.tree) == 1