Skip to content

Commit a5d94c5

Browse files
authored
Merge pull request #105 from SymbolicML/read-only-nodes
feat!: create `ReadOnlyNode` for `StructuredExpression` `get_tree` access
2 parents 9c5e4c4 + 04f9816 commit a5d94c5

File tree

7 files changed

+109
-4
lines changed

7 files changed

+109
-4
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "DynamicExpressions"
22
uuid = "a40a106e-89c9-4ca8-8020-a735e8728b6b"
33
authors = ["MilesCranmer <[email protected]>"]
4-
version = "1.2.0"
4+
version = "1.3.0"
55

66
[deps]
77
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"

src/DynamicExpressions.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ using DispatchDoctor: @stable, @unstable
2121
include("Random.jl")
2222
include("Parse.jl")
2323
include("ParametricExpression.jl")
24+
include("ReadOnlyNode.jl")
2425
include("StructuredExpression.jl")
2526
end
2627

@@ -92,6 +93,7 @@ import .ExpressionModule:
9293
@reexport import .ParseModule: @parse_expression, parse_expression
9394
import .ParseModule: parse_leaf
9495
@reexport import .ParametricExpressionModule: ParametricExpression, ParametricNode
96+
import .ReadOnlyNodeModule: ReadOnlyNode
9597
@reexport import .StructuredExpressionModule: StructuredExpression
9698
import .StructuredExpressionModule: AbstractStructuredExpression
9799

src/Interfaces.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ using ..ExpressionModule:
5252
with_metadata,
5353
default_node_type
5454
using ..ParametricExpressionModule: ParametricExpression, ParametricNode
55+
using ..ReadOnlyNodeModule: AbstractReadOnlyNode
5556
using ..StructuredExpressionModule: StructuredExpression
5657

5758
###############################################################################
@@ -68,7 +69,7 @@ function _check_get_metadata(ex::AbstractExpression)
6869
return new_ex == ex && new_ex isa typeof(ex)
6970
end
7071
function _check_get_tree(ex::AbstractExpression{T,N}) where {T,N}
71-
return get_tree(ex) isa N
72+
return get_tree(ex) isa N || get_tree(ex) isa AbstractReadOnlyNode{T,N}
7273
end
7374
function _check_get_operators(ex::AbstractExpression)
7475
return get_operators(ex) isa AbstractOperatorEnum
@@ -134,7 +135,8 @@ function _check_constructorof(ex::AbstractExpression)
134135
return constructorof(typeof(ex)) isa Base.Callable
135136
end
136137
function _check_tree_mapreduce(ex::AbstractExpression{T,N}) where {T,N}
137-
return tree_mapreduce(node -> [node], vcat, ex) isa Vector{N}
138+
return tree_mapreduce(node -> [node], vcat, ex) isa
139+
(Vector{N2} where {N2<:Union{N,AbstractReadOnlyNode{T,N}}})
138140
end
139141

140142
#! format: off

src/ReadOnlyNode.jl

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
module ReadOnlyNodeModule
2+
3+
using ..NodeModule: AbstractExpressionNode, Node
4+
import ..NodeModule: default_allocator, with_type_parameters, constructorof
5+
6+
abstract type AbstractReadOnlyNode{T,N<:AbstractExpressionNode{T}} <:
7+
AbstractExpressionNode{T} end
8+
9+
"""A type of expression node that also stores a parameter index"""
10+
struct ReadOnlyNode{T,N} <: AbstractReadOnlyNode{T,N}
11+
_inner::N
12+
13+
ReadOnlyNode(n::N) where {T,N<:AbstractExpressionNode{T}} = new{T,N}(n)
14+
end
15+
constructorof(::Type{<:ReadOnlyNode}) = ReadOnlyNode
16+
@inline function Base.getproperty(n::AbstractReadOnlyNode, s::Symbol)
17+
out = getproperty(getfield(n, :_inner), s)
18+
if out isa AbstractExpressionNode
19+
return constructorof(typeof(n))(out)
20+
else
21+
return out
22+
end
23+
end
24+
function Base.setproperty!(::AbstractReadOnlyNode, ::Symbol, v)
25+
return error("Cannot set properties on a ReadOnlyNode")
26+
end
27+
Base.propertynames(n::AbstractReadOnlyNode) = propertynames(getfield(n, :_inner))
28+
Base.copy(n::AbstractReadOnlyNode) = ReadOnlyNode(copy(getfield(n, :_inner)))
29+
30+
end

src/StructuredExpression.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import ..ExpressionModule:
1919
node_type,
2020
get_scalar_constants,
2121
set_scalar_constants!
22+
import ..ReadOnlyNodeModule: ReadOnlyNode
2223

2324
abstract type AbstractStructuredExpression{
2425
T,F<:Function,N<:AbstractExpressionNode{T},E<:AbstractExpression{T,N},D<:NamedTuple
@@ -129,7 +130,7 @@ function get_metadata(e::AbstractStructuredExpression)
129130
return e.metadata
130131
end
131132
function get_tree(e::AbstractStructuredExpression)
132-
return get_tree(get_metadata(e).structure(get_contents(e)))
133+
return ReadOnlyNode(get_tree(get_metadata(e).structure(get_contents(e))))
133134
end
134135
function get_operators(
135136
e::AbstractStructuredExpression, operators::Union{AbstractOperatorEnum,Nothing}=nothing

test/test_readonlynode.jl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
@testitem "ReadOnlyNode construction and access" begin
2+
using DynamicExpressions
3+
using DynamicExpressions: ReadOnlyNode
4+
5+
inner_node = Node{Float64}(; val=42.0)
6+
readonly_node = ReadOnlyNode(inner_node)
7+
8+
@test readonly_node isa ReadOnlyNode
9+
@test getfield(readonly_node, :_inner) === inner_node
10+
@test readonly_node.degree == inner_node.degree
11+
@test readonly_node.constant == inner_node.constant
12+
@test readonly_node.val == inner_node.val
13+
end
14+
15+
@testitem "ReadOnlyNode immutability" begin
16+
using DynamicExpressions
17+
using DynamicExpressions: ReadOnlyNode
18+
19+
inner_node = Node{Float64}(; val=42.0)
20+
readonly_node = ReadOnlyNode(inner_node)
21+
22+
@test_throws ErrorException readonly_node.val = 100.0
23+
@test_throws "Cannot set properties on a ReadOnlyNode" readonly_node.val = 100.0
24+
end
25+
26+
@testitem "ReadOnlyNode - accessing children should return ReadOnlyNode" begin
27+
using DynamicExpressions
28+
using DynamicExpressions: ReadOnlyNode
29+
30+
operators = OperatorEnum(; binary_operators=(+, -, *, /), unary_operators=(sin, exp))
31+
x1 = Node{Float64}(; feature=1)
32+
x2 = Node{Float64}(; feature=2)
33+
tree = 2 * x1 - sin(x2)
34+
readonly_node = ReadOnlyNode(tree)
35+
36+
@test typeof(readonly_node.l) === typeof(readonly_node)
37+
end
38+
39+
@testitem "ReadOnlyNode copy" begin
40+
using DynamicExpressions
41+
using DynamicExpressions: ReadOnlyNode
42+
43+
inner_node = Node{Float64}(; val=42.0)
44+
readonly_node = ReadOnlyNode(inner_node)
45+
copied_node = copy(readonly_node)
46+
47+
@test copied_node !== readonly_node
48+
@test copied_node == readonly_node
49+
end
50+
51+
@testitem "StructuredExpression returns ReadOnlyNode" begin
52+
using DynamicExpressions
53+
using DynamicExpressions: ReadOnlyNode
54+
using DynamicExpressions: StructuredExpression
55+
56+
operators = OperatorEnum(; binary_operators=[+, -, *, /], unary_operators=[-, cos, exp])
57+
variable_names = ["x", "y"]
58+
kws = (; operators, variable_names)
59+
f = parse_expression(:(x * x - cos(2.5f0 * y + -0.5f0)); kws...)
60+
g = parse_expression(:(exp(-(y * y))); kws...)
61+
62+
structured_expr = StructuredExpression((; f, g); structure=nt -> nt.f + nt.g, kws...)
63+
64+
tree = get_tree(structured_expr)
65+
@test tree isa ReadOnlyNode
66+
@test string_tree(tree, operators; variable_names) ==
67+
"((x * x) - cos((2.5 * y) + -0.5)) + exp(-(y * y))"
68+
@test getfield(tree, :_inner) isa Node
69+
end

test/unittest.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,4 @@ include("test_operator_construction_edgecases.jl")
129129
include("test_node_interface.jl")
130130
include("test_expression_math.jl")
131131
include("test_structured_expression.jl")
132+
include("test_readonlynode.jl")

0 commit comments

Comments
 (0)