From 47765d4048b95e91ba3e60057016c16b161b34f2 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Mon, 31 Aug 2026 15:28:57 +0200 Subject: [PATCH] fix(parser): reject constraint bounds that depend on v/x/u/t (#343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A boundary/path constraint whose bound referenced the optimization variable (e.g. `xβ‚‚(0) == v`) failed with a leaked internal gensym `UndefVarError: v##NNNN` instead of a clear error, because `lb`/`ub` are evaluated once at build time and cannot see a function-argument name. `p_constraint!` now checks both bounds and returns a `ParsingError` pointing to the fix (`xβ‚‚(0) - v == 0`). Backend-agnostic: covers both `:fun` and `:exa`. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 6 ++++++ src/onepass.jl | 20 ++++++++++++++++++++ test/test_onepass_exa.jl | 15 +++++++++++++++ test/test_onepass_fun.jl | 31 +++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9e2c91..e97150d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ All notable changes to CTParser will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### πŸ› 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`. + ## [0.9.4-beta] - 2026-08-30 ### βœ… Compatibility diff --git a/src/onepass.jl b/src/onepass.jl index 5595744..bd8f134 100644 --- a/src/onepass.jl +++ b/src/onepass.jl @@ -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) diff --git a/test/test_onepass_exa.jl b/test/test_onepass_exa.jl index 60c86a9..d86f06b 100644 --- a/test/test_onepass_exa.jl +++ b/test/test_onepass_exa.jl @@ -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 diff --git a/test/test_onepass_fun.jl b/test/test_onepass_fun.jl index 901f93d..dae17f1 100644 --- a/test/test_onepass_fun.jl +++ b/test/test_onepass_fun.jl @@ -2806,6 +2806,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 # ---------------------------------------------------------------