Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### 🐛 Bug Fixes

- **A constraint bound that depends on `v`, the state, the control or the time is now rejected with a clear `ParsingError`** ([#343](https://github.com/control-toolbox/CTParser.jl/issues/343)). Previously `x₂(0) == v` (with `v` the optimization variable) failed with an internal `UndefVarError: v##NNNN` leaked from generated code. Such a relation must be written as a functional constraint by moving the term to the constrained side, e.g. `x₂(0) - v == 0`.
- **Trace mode (`@def name … end true`) no longer prints the parsed model twice** ([#344](https://github.com/control-toolbox/CTParser.jl/issues/344)). When the `:exa` backend is active, `def_fun` parses the definition a second time to build the ExaModels artifact; that second pass was inheriting the `log` flag and re-emitting the whole trace. It now runs with `log=false`.

## [0.9.4-beta] - 2026-08-30
Expand Down
20 changes: 20 additions & 0 deletions src/onepass.jl
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,26 @@ function p_constraint!(
log && println("constraint ($c_type): $e1 ≤ $e2 ≤ $e3, ($label)")
label isa Int && (label = Symbol(:eq, label))
label isa Symbol || return __throw("forbidden label: $label", p.lnum, p.line)
# a constraint bound must be "effective": lb/ub are evaluated once at build time,
# so they cannot reference v/x/u/t (doing so leaks an internal gensym — see #343)
for (b, side) in ((e1, "lower"), (e3, "upper"))
isnothing(b) && continue
for (sym, what) in (
(p.v, "the variable"),
(p.x, "the state"),
(p.u, "the control"),
(p.t, "the time"),
)
isnothing(sym) && continue
has(b, sym) && return __throw(
"the $side bound of a constraint must not depend on $what; " *
"write a functional constraint instead by moving the term to the " *
"constrained side (e.g. `x₂(0) - v == 0` rather than `x₂(0) == v`)",
p.lnum,
p.line,
)
end
end
xut = __symgen(:xut)
ee2 = replace_call(e2, [p.x, p.u], p.t, [xut, xut])
has(ee2, p.t) && (p.is_autonomous = false)
Expand Down
15 changes: 15 additions & 0 deletions test/test_onepass_exa.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,21 @@ function __test_onepass_exa(
end
@test_throws ParsingError o(; backend=backend)

# a constraint bound must not depend on the variable (#343)
o = @def_exa begin
v ∈ R, variable
t ∈ [0, 1], time
x ∈ R⁴, state
u ∈ R⁵, control
x₂(0) == v
∂(x₁)(t) == x₁(t)
∂(x₂)(t) == x₁(t)
∂(x₃)(t) == x₁(t)
∂(x₄)(t) == x₁(t)
x₁(0) + 2cos(x₂(1)) → min
end
@test_throws ParsingError o(; backend=backend)

o = @def_exa begin
t ∈ [0, 1], time
x ∈ R⁴, state
Expand Down
31 changes: 31 additions & 0 deletions test/test_onepass_fun.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2833,6 +2833,37 @@ function test_onepass_fun()
ẋ(t) == A * x(t) + B * u(t)
∫(u(t)^2) / 2 → min # forbidden
end

# a constraint bound must be effective: it must not depend on the variable (#343)
@test_throws ParsingError @def o begin
v ∈ R, variable
t ∈ [0, 1], time
x ∈ R², state
u ∈ R, control
x₂(0) == v
ẋ(t) == [x₂(t), u(t)]
end

# ... nor on the state (bound side)
@test_throws ParsingError @def o begin
t ∈ [0, 1], time
x ∈ R², state
u ∈ R, control
x₁(0) ≤ x₂(0)
ẋ(t) == [x₂(t), u(t)]
end

# the documented work-around still builds fine
o = @def begin
v ∈ R, variable
t ∈ [0, 1], time
x ∈ R², state
u ∈ R, control
x₂(0) - v == 0
ẋ(t) == [x₂(t), u(t)]
∫(0.5u(t)^2) → min
end
@test o isa Model
end

# ---------------------------------------------------------------
Expand Down