Likely errors / bugs (with suggested corrections)
1) Typo/bug: control_fun used without :
In multiple @match branches the code uses control_fun (a bare identifier) instead of the symbol :control_fun.
Example (from your snippet around constraint codegen):
:state_fun || control_fun || :mixed => begin
This will either:
- throw
UndefVarError: control_fun not defined at macro-expansion/runtime (depending on how @match expands), or
- silently match something unintended (worst case).
Fix
Replace control_fun with :control_fun everywhere it appears in match patterns.
Search and fix in src/onepass.jl:
:state_fun || control_fun || :mixed
:state_fun || control_fun || :mixed in p_constraint_fun! too (I saw a similar pattern earlier: :state_fun || control_fun || :mixed => ...)
Permalink for one occurrence:
|
:state_fun || control_fun || :mixed => begin |
|
code = :(length($e1) == length($e3) == 1 || throw("this constraint must be scalar")) # (vs. __throw) since raised at runtime |
|
xt = __symgen(:xt) |
|
ut = __symgen(:ut) |
|
e2 = replace_call(e2, [p.x, p.u], p.t, [xt, ut]) |
|
j = __symgen(:j) |
|
e2 = subs2(e2, xt, p.x, j) |
|
e2 = subs2(e2, ut, p.u, j) |
|
e2 = subs(e2, p.t, :($(p.t0) + $j * $(p.dt))) |
|
concat( |
|
code, |
|
:($pref.constraint( |
|
$p_ocp, $e2 for $j in 0:grid_size; lcon=($e1), ucon=($e3) |
|
)), |
|
) |
|
end |
I used code search and results may be incomplete (search UI: https://github.com/control-toolbox/CTParser.jl/search?q=control_fun&type=code).
2) Possible semantic bug: as_range returns a Vector, but later code assumes a range-like object
as_range(x) = is_range(x) ? x : [x]
Then in several places you do:
length($e1) == length($e3) == length($rg) || throw("wrong bound dimension")
If rg is a scalar index (like 3), as_range(rg) returns [3], which is OK for length, but later some expressions expect rg to be valid in slicing contexts A[$rg] or in comprehensions for i in $rg.
That works for a Vector{Int} but:
- it changes iteration order/type vs
3:3
- it may allocate and inhibit inference in generated code (minor)
- more importantly, in other places you may rely on
rg being an Expr representing a range (not a realized array) if you’re building quoted code.
Safer fix (design + correctness)
Use an expression-level normalization:
- if it’s not a range, turn it into
:(($x):($x)) (a 1-length UnitRange) instead of [x].
- At AST level:
Expr(:call, :(:), x, x).
So something like:
as_range_expr(x) = is_range(x) ? x : :($x:$x)
and use that consistently in code generation paths (especially :exa), where rg is interpolated into quoted expressions.
3) Potential scoping hazard: __wrap introduces local ex, prints, then throw(ex)
__wrap is:
quote
local ex
try
$e
catch ex
println("Line ", $n, ": ", $line)
throw(ex)
end
end
This is mostly fine, but:
- it prints to stdout unconditionally (might be undesirable in libraries; better to use
@error or rethrow with enriched exception)
- it loses stack context if later code catches/rethrows (it preserves original exception though)
Improvement
Prefer rethrow() inside catch to preserve backtrace:
catch
println(...)
rethrow()
end
If you need the exception object, do:
catch ex
println(...)
rethrow(ex)
end
In Julia, rethrow() preserves the original backtrace best.
4) Alias substitution loop may be order/termination sensitive
In parse!:
for a in keys(p.aliases)
e = subs(e, a, p.aliases[a])
end
Because aliases can be Expr keys and values can introduce other aliases, a single pass may yield partially-expanded expressions, and alias ordering can change semantics.
Fix / improvement
- Either document “single-pass aliasing” explicitly, or
- Apply substitution until fixpoint (with max-iteration guard), or
- Separate “lexical sugar aliases” (like
<=) from “user aliases” and apply in phases.
Performance / design / refactoring suggestions
A) Reduce duplication via a backend “emitter” object (strategy pattern)
You currently have a pattern:
p_variable! does generic validation + updates p
- then calls
parsing(:variable, backend)(...)
- and separately you maintain
p_variable_fun!, p_variable_exa!, etc.
That’s already a strategy pattern, but it’s implemented via global dispatch tables (presumably parsing elsewhere) + many parallel functions.
Refactor idea
Define small backend types:
abstract type Backend end
struct FunBackend <: Backend end
struct ExaBackend <: Backend end
Then implement:
emit_variable!(::FunBackend, p, p_ocp, ...)
emit_variable!(::ExaBackend, p, p_ocp, ...)
This improves:
- discoverability (methods grouped by concept)
- testability (backend behavior can be unit-tested with the same front-end parser calls)
- avoids symbol-based dispatch and reduces runtime branching
B) Consolidate the huge objective parsing match table
The parse! objective section enumerates many algebraic permutations for Bolza/Lagrange with factors and signs, primarily to enforce “the prefactor must not depend on time”.
Refactor idea
Normalize objective expressions first:
- parse the objective as an AST
- extract
(mayer_term, lagrange_integrand, sense)
- rewrite
e1 - ∫(e2) into (e1, -e2) etc.
- check time-dependence in exactly one place
This will shrink parse! substantially and reduce bug surface.
C) Avoid repeated has(expr, p.t) scans by caching
You repeatedly compute autonomy checks like:
xut = __symgen(:xut)
ee = replace_call(e, [p.x, p.u], p.t, [xut, xut])
has(ee, p.t) && (p.is_autonomous = false)
has likely walks the whole AST each time.
Improvement
- Have
replace_call optionally return a flag “time present” while transforming, or
- Maintain a lightweight
contains_time::Bool returned by a single traversal.
D) Exa box constraints: accumulate data, not code
For :exa, you build code blocks p.box_x = concat(p.box_x, code_box) etc. That means you’re building large quoted expressions incrementally.
Improvement
Accumulate data in ParsingInfo (e.g., store a vector of (kind, rg, lb_expr, ub_expr, lineinfo)), then at the end generate code once. Benefits:
- less AST concatenation overhead
- easier to validate consistency (e.g., conflicting bounds)
- enables better error messages (“bound set twice”)
E) Consistent “range normalization” across fun/exa
Right now, range/index handling differs by backend (and some comments mention :fun allows i to be a range for dynamics, but :exa doesn’t). Consider defining explicit policy per construct:
- dynamics coord index: must be integer for all backends (or make a separate syntax for ranges)
- constraints range: always accept scalar or range, normalize to
UnitRange at AST level
This makes the DSL more predictable.
One concrete “must-fix” summary
- Replace every
control_fun in @match patterns with :control_fun.
- Consider changing
as_range to return an AST range (x:x) rather than allocating [x] when generating code (especially for :exa).
- Prefer
rethrow() in __wrap to preserve backtraces.
If you want, paste the remainder of onepass.jl after line ~996 (where your snippet cut off), and I can check the rest for the same control_fun typo and any similar symbol-pattern issues.
Likely errors / bugs (with suggested corrections)
1) Typo/bug:
control_funused without:In multiple
@matchbranches the code usescontrol_fun(a bare identifier) instead of the symbol:control_fun.Example (from your snippet around constraint codegen):
This will either:
UndefVarError: control_fun not definedat macro-expansion/runtime (depending on how@matchexpands), orFix
Replace
control_funwith:control_funeverywhere it appears in match patterns.Search and fix in
src/onepass.jl::state_fun || control_fun || :mixed:state_fun || control_fun || :mixedinp_constraint_fun!too (I saw a similar pattern earlier::state_fun || control_fun || :mixed => ...)Permalink for one occurrence:
CTParser.jl/src/onepass.jl
Lines 828 to 843 in 5aad902
I used code search and results may be incomplete (search UI: https://github.com/control-toolbox/CTParser.jl/search?q=control_fun&type=code).
2) Possible semantic bug:
as_rangereturns a Vector, but later code assumes a range-like objectas_range(x) = is_range(x) ? x : [x]Then in several places you do:
If
rgis a scalar index (like3),as_range(rg)returns[3], which is OK forlength, but later some expressions expectrgto be valid in slicing contextsA[$rg]or in comprehensionsfor i in $rg.That works for a
Vector{Int}but:3:3rgbeing anExprrepresenting a range (not a realized array) if you’re building quoted code.Safer fix (design + correctness)
Use an expression-level normalization:
:(($x):($x))(a 1-lengthUnitRange) instead of[x].Expr(:call, :(:), x, x).So something like:
and use that consistently in code generation paths (especially
:exa), wherergis interpolated into quoted expressions.3) Potential scoping hazard:
__wrapintroduceslocal ex, prints, thenthrow(ex)__wrapis:This is mostly fine, but:
@erroror rethrow with enriched exception)Improvement
Prefer
rethrow()inside catch to preserve backtrace:If you need the exception object, do:
In Julia,
rethrow()preserves the original backtrace best.4) Alias substitution loop may be order/termination sensitive
In
parse!:Because aliases can be
Exprkeys and values can introduce other aliases, a single pass may yield partially-expanded expressions, and alias ordering can change semantics.Fix / improvement
<=) from “user aliases” and apply in phases.Performance / design / refactoring suggestions
A) Reduce duplication via a backend “emitter” object (strategy pattern)
You currently have a pattern:
p_variable!does generic validation + updatespparsing(:variable, backend)(...)p_variable_fun!,p_variable_exa!, etc.That’s already a strategy pattern, but it’s implemented via global dispatch tables (presumably
parsingelsewhere) + many parallel functions.Refactor idea
Define small backend types:
Then implement:
This improves:
B) Consolidate the huge objective parsing match table
The
parse!objective section enumerates many algebraic permutations for Bolza/Lagrange with factors and signs, primarily to enforce “the prefactor must not depend on time”.Refactor idea
Normalize objective expressions first:
(mayer_term, lagrange_integrand, sense)e1 - ∫(e2)into(e1, -e2)etc.This will shrink
parse!substantially and reduce bug surface.C) Avoid repeated
has(expr, p.t)scans by cachingYou repeatedly compute autonomy checks like:
haslikely walks the whole AST each time.Improvement
replace_calloptionally return a flag “time present” while transforming, orcontains_time::Boolreturned by a single traversal.D) Exa box constraints: accumulate data, not code
For
:exa, you build code blocksp.box_x = concat(p.box_x, code_box)etc. That means you’re building large quoted expressions incrementally.Improvement
Accumulate data in
ParsingInfo(e.g., store a vector of(kind, rg, lb_expr, ub_expr, lineinfo)), then at the end generate code once. Benefits:E) Consistent “range normalization” across fun/exa
Right now, range/index handling differs by backend (and some comments mention
:funallowsito be a range for dynamics, but:exadoesn’t). Consider defining explicit policy per construct:UnitRangeat AST levelThis makes the DSL more predictable.
One concrete “must-fix” summary
control_funin@matchpatterns with:control_fun.as_rangeto return an AST range (x:x) rather than allocating[x]when generating code (especially for:exa).rethrow()in__wrapto preserve backtraces.If you want, paste the remainder of
onepass.jlafter line ~996 (where your snippet cut off), and I can check the rest for the samecontrol_funtypo and any similar symbol-pattern issues.