From 76558b3f035efa82e9486cefd85dfae34af63288 Mon Sep 17 00:00:00 2001 From: JKRT Date: Tue, 16 Jun 2026 11:46:48 +0200 Subject: [PATCH 01/12] Minor performance edits --- src/Backend/BDAECreate.jl | 19 +- src/CodeGeneration/MTK_CodeGeneration.jl | 6 +- src/SimulationCode/simCodeTraverse.jl | 113 ++++----- src/SimulationCode/simCodeUtil.jl | 286 +++++++++++++---------- src/util.jl | 15 +- 5 files changed, 237 insertions(+), 202 deletions(-) diff --git a/src/Backend/BDAECreate.jl b/src/Backend/BDAECreate.jl index 2c32e1e6..9e24845b 100644 --- a/src/Backend/BDAECreate.jl +++ b/src/Backend/BDAECreate.jl @@ -415,13 +415,18 @@ structuralHash(x::Vector, h::UInt) = begin end h end -function structuralHash(@nospecialize(x), h::UInt) - T = typeof(x) - h = hash(T, h) - for i in 1:fieldcount(T) - h = structuralHash(getfield(x, i), h) - end - h +#= Generic struct fold: unrolled per concrete type so each `getfield(x, i)` has a + concrete field type and the recursive call dispatches statically (no boxing). + Semantics identical to the previous runtime-loop fallback: hash the type, then + each field in order. =# +@generated function structuralHash(x, h::UInt) + local body = Expr(:block) + push!(body.args, :(h = hash($x, h))) + for i in 1:fieldcount(x) + push!(body.args, :(h = structuralHash(getfield(x, $i), h))) + end + push!(body.args, :(return h)) + return body end structuralHash(@nospecialize(x)) = structuralHash(x, zero(UInt)) diff --git a/src/CodeGeneration/MTK_CodeGeneration.jl b/src/CodeGeneration/MTK_CodeGeneration.jl index 6331f0a7..e4371d8d 100644 --- a/src/CodeGeneration/MTK_CodeGeneration.jl +++ b/src/CodeGeneration/MTK_CodeGeneration.jl @@ -149,6 +149,10 @@ function classifyVariables(simCode)::ClassifiedVariables local dataStructureVariables = String[] local statePriorityPairs = Tuple{Symbol, Int}[] local ht = simCode.stringToSimVarHT + #= Membership set built once: the per-variable `idx in matchOrder` below was an + O(V) scan of the matchOrder Vector, making classification O(V^2). matchOrder + is not mutated in this loop. =# + local matchOrderSet = OrderedSet{Int}(simCode.matchOrder) for varName in keys(ht) (idx, var) = ht[varName] local varType = var.varKind @@ -166,7 +170,7 @@ function classifyVariables(simCode)::ClassifiedVariables SimulationCode.ARRAY(__) => push!(stateVariables, varName) SimulationCode.DISCRETE(__) => push!(discreteVariables, varName) SimulationCode.ALG_VARIABLE(__) => begin - if idx in simCode.matchOrder + if idx in matchOrderSet push!(algebraicVariables, varName) elseif involvedInEvent(idx, simCode) push!(discreteVariables, varName) diff --git a/src/SimulationCode/simCodeTraverse.jl b/src/SimulationCode/simCodeTraverse.jl index e3ef13ed..f05e5e8a 100644 --- a/src/SimulationCode/simCodeTraverse.jl +++ b/src/SimulationCode/simCodeTraverse.jl @@ -81,52 +81,50 @@ function _traverseChildrenTopDown(e::IFEXP, visitor, arg) IFEXP(nc, nt, nl), a3) end -# List-bearing shapes (always rebuild — referenceEq on Vector is rare) -function _traverseChildrenTopDown(e::CALL, visitor, arg) - local out = Exp[] +# List-bearing shapes: share the input vector when no element changed, so the +# parent node is reused (matching the scalar shapes' identity-on-unchanged), and +# allocate a fresh vector only from the first changed element onward. +function _mapExpVec(xs::Vector, recur, visitor, arg) + local out = nothing local a = arg - for x in e.args - (nx, a) = traverseExpTopDown(x, visitor, a) - push!(out, nx) + local n = length(xs) + for i in 1:n + local x = xs[i] + (nx, a) = recur(x, visitor, a) + if out === nothing + if nx !== x + out = Vector{Exp}(undef, n) + for j in 1:(i - 1) + out[j] = xs[j] + end + out[i] = nx + end + else + out[i] = nx + end end - return (CALL(e.path, out, e.attr), a) + return (out === nothing ? xs : out, a) +end +function _traverseChildrenTopDown(e::CALL, visitor, arg) + (out, a) = _mapExpVec(e.args, traverseExpTopDown, visitor, arg) + return (out === e.args ? e : CALL(e.path, out, e.attr), a) end function _traverseChildrenTopDown(e::ARRAY_EXP, visitor, arg) - local out = Exp[] - local a = arg - for x in e.elements - (nx, a) = traverseExpTopDown(x, visitor, a) - push!(out, nx) - end - return (ARRAY_EXP(e.ty, e.scalar, out), a) + (out, a) = _mapExpVec(e.elements, traverseExpTopDown, visitor, arg) + return (out === e.elements ? e : ARRAY_EXP(e.ty, e.scalar, out), a) end function _traverseChildrenTopDown(e::RECORD, visitor, arg) - local out = Exp[] - local a = arg - for x in e.exps - (nx, a) = traverseExpTopDown(x, visitor, a) - push!(out, nx) - end - return (RECORD(e.path, out, e.fieldNames, e.ty), a) + (out, a) = _mapExpVec(e.exps, traverseExpTopDown, visitor, arg) + return (out === e.exps ? e : RECORD(e.path, out, e.fieldNames, e.ty), a) end function _traverseChildrenTopDown(e::TUPLE, visitor, arg) - local out = Exp[] - local a = arg - for x in e.PR - (nx, a) = traverseExpTopDown(x, visitor, a) - push!(out, nx) - end - return (TUPLE(out), a) + (out, a) = _mapExpVec(e.PR, traverseExpTopDown, visitor, arg) + return (out === e.PR ? e : TUPLE(out), a) end function _traverseChildrenTopDown(e::ASUB, visitor, arg) (nex, a1) = traverseExpTopDown(e.exp, visitor, arg) - local out = Exp[] - local a = a1 - for s in e.subs - (ns, a) = traverseExpTopDown(s, visitor, a) - push!(out, ns) - end - return (ASUB(nex, out), a) + (out, a) = _mapExpVec(e.subs, traverseExpTopDown, visitor, a1) + return ((nex === e.exp && out === e.subs) ? e : ASUB(nex, out), a) end # Reduction: only the body is SimCode-recursive; info/iterators are opaque. function _traverseChildrenTopDown(e::REDUCTION, visitor, arg) @@ -194,50 +192,25 @@ function _traverseChildrenBottomUp(e::IFEXP, visitor, arg) IFEXP(nc, nt, nl), a3) end function _traverseChildrenBottomUp(e::CALL, visitor, arg) - local out = Exp[] - local a = arg - for x in e.args - (nx, a) = traverseExpBottomUp(x, visitor, a) - push!(out, nx) - end - return (CALL(e.path, out, e.attr), a) + (out, a) = _mapExpVec(e.args, traverseExpBottomUp, visitor, arg) + return (out === e.args ? e : CALL(e.path, out, e.attr), a) end function _traverseChildrenBottomUp(e::ARRAY_EXP, visitor, arg) - local out = Exp[] - local a = arg - for x in e.elements - (nx, a) = traverseExpBottomUp(x, visitor, a) - push!(out, nx) - end - return (ARRAY_EXP(e.ty, e.scalar, out), a) + (out, a) = _mapExpVec(e.elements, traverseExpBottomUp, visitor, arg) + return (out === e.elements ? e : ARRAY_EXP(e.ty, e.scalar, out), a) end function _traverseChildrenBottomUp(e::RECORD, visitor, arg) - local out = Exp[] - local a = arg - for x in e.exps - (nx, a) = traverseExpBottomUp(x, visitor, a) - push!(out, nx) - end - return (RECORD(e.path, out, e.fieldNames, e.ty), a) + (out, a) = _mapExpVec(e.exps, traverseExpBottomUp, visitor, arg) + return (out === e.exps ? e : RECORD(e.path, out, e.fieldNames, e.ty), a) end function _traverseChildrenBottomUp(e::TUPLE, visitor, arg) - local out = Exp[] - local a = arg - for x in e.PR - (nx, a) = traverseExpBottomUp(x, visitor, a) - push!(out, nx) - end - return (TUPLE(out), a) + (out, a) = _mapExpVec(e.PR, traverseExpBottomUp, visitor, arg) + return (out === e.PR ? e : TUPLE(out), a) end function _traverseChildrenBottomUp(e::ASUB, visitor, arg) (nex, a1) = traverseExpBottomUp(e.exp, visitor, arg) - local out = Exp[] - local a = a1 - for s in e.subs - (ns, a) = traverseExpBottomUp(s, visitor, a) - push!(out, ns) - end - return (ASUB(nex, out), a) + (out, a) = _mapExpVec(e.subs, traverseExpBottomUp, visitor, a1) + return ((nex === e.exp && out === e.subs) ? e : ASUB(nex, out), a) end function _traverseChildrenBottomUp(e::REDUCTION, visitor, arg) (nb, a) = traverseExpBottomUp(e.body, visitor, arg) diff --git a/src/SimulationCode/simCodeUtil.jl b/src/SimulationCode/simCodeUtil.jl index 7b38d2b5..c9694b19 100644 --- a/src/SimulationCode/simCodeUtil.jl +++ b/src/SimulationCode/simCodeUtil.jl @@ -91,10 +91,17 @@ function runSimCodePass(passName::AbstractString, simCode::SIM_CODE, passFn::Function; cleanup::Bool = true)::SIM_CODE - local before = simCodeMetrics(simCode) - local t0 = time() - local afterPass = passFn(simCode) - logSimCodePassMetrics(passName, before, afterPass, time() - t0) + #= Pass metrics feed only the perf log, so compute them only when perf logging + is on; otherwise skip the two full hash-table sweeps per pass. Pass execution + and cleanup are unchanged, so this is codegen-neutral. =# + local perf = OMBackend.BACKEND_PERFLOG[] + local before = perf ? simCodeMetrics(simCode) : nothing + local stats = Base.@timed passFn(simCode) + local afterPass = stats.value + if perf + logSimCodePassMetrics(passName, before, afterPass, stats.time) + @info "[SIMCODE: $(simCode.name): $passName] alloc" bytes=stats.bytes + end if cleanup afterPass = cleanupTrivialResidualEquations(afterPass; sourcePass = passName) end @@ -416,7 +423,7 @@ function dumpVariableEqMapping(mapping::OrderedDict, residualEquations, ifEquati variablesAtEq *= "$(v)," end variablesAtEq *= "}" - println(dump, "Equation $e: involves: $(variablesAtEq): Eq $(BDAEUtil.string(e))\n") + println(dump, "Equation $e: involves: $(variablesAtEq)\n") end for (i, e) in enumerate(residualEquations) println(dump, string("Equation " * string(i) * ":" * string(e))) @@ -602,7 +609,7 @@ function createEquationVariableBidirectionGraph(equations::AbstractVector, allBackendVars::VARS, stringToSimVarHT)::OrderedDict where{IF_EQS, WHEN_EQS, VARS} local eqCounter::Int = 0 - local variableEqMapping = OrderedDict{String, Vector{Int}}() + local variableEqMapping = OrderedDict{Int, Vector{Int}}() local unknownVariables = filter((x) -> BDAEUtil.isVariable(x.varKind), allBackendVars) #=TODO: The set of discrete variables are currently not in use. =# local discreteVariables = filter((x) -> BDAEUtil.isDiscrete(x.varKind), allBackendVars) @@ -630,7 +637,7 @@ function createEquationVariableBidirectionGraph(equations::AbstractVector, # for idx in indices # println("\t", string(idx)) # end - variableEqMapping["e$(eqCounter)"] = sort(indices) + variableEqMapping[eqCounter] = sort(indices) end #= There is an additional case to consider. @@ -644,7 +651,7 @@ function createEquationVariableBidirectionGraph(equations::AbstractVector, for eq in ifEqBranch eqCounter += 1 variablesForEq = Backend.BDAEUtil.getAllVariables(eq, varByName) - variableEqMapping["e$(eqCounter)"] = sort(getIndiciesOfVariables(variablesForEq, stringToSimVarHT)) + variableEqMapping[eqCounter] = sort(getIndiciesOfVariables(variablesForEq, stringToSimVarHT)) end end #= @@ -664,7 +671,7 @@ function createEquationVariableBidirectionGraph(equations::AbstractVector, for wstmt in weq.whenEquation.whenStmtLst eqCounter += 1 variablesForEq = BDAEUtil.getAllVariables(wstmt, algebraicAndStateVariables) - variableEqMapping["e$(eqCounter)"] = sort(getIndiciesOfVariables(variablesForEq, stringToSimVarHT)) + variableEqMapping[eqCounter] = sort(getIndiciesOfVariables(variablesForEq, stringToSimVarHT)) end else for wstmt in weq.whenEquation.whenStmtLst @@ -675,7 +682,7 @@ function createEquationVariableBidirectionGraph(equations::AbstractVector, local simVar = getSimVarByName(refAsStr, stringToSimVarHT) eqCounter += 1 variablesForEq = BDAEUtil.getAllVariables(wstmt, algebraicAndStateVariables) - variableEqMapping["e$(eqCounter)"] = sort(getIndiciesOfVariables(variablesForEq, stringToSimVarHT)) + variableEqMapping[eqCounter] = sort(getIndiciesOfVariables(variablesForEq, stringToSimVarHT)) end end end @@ -696,7 +703,7 @@ function createEquationVariableBidirectionGraph(equations::RES_T, allBackendVars::VECTOR_VAR, stringToSimVarHT)::OrderedDict where{RES_T, VECTOR_VAR} local eqCounter::Int = 0 - local variableEqMapping = OrderedDict{String, Vector{Int}}() + local variableEqMapping = OrderedDict{Int, Vector{Int}}() local unknownVariables = filter((x) -> BDAEUtil.isVariable(x.varKind), allBackendVars) local discreteVariables = filter((x) -> BDAEUtil.isDiscrete(x.varKind), allBackendVars) local stateVariables = filter((x) -> BDAEUtil.isState(x.varKind), allBackendVars) @@ -711,7 +718,7 @@ function createEquationVariableBidirectionGraph(equations::RES_T, for eq in equations eqCounter += 1 variablesForEq = Backend.BDAEUtil.getAllVariables(eq, varByName) - variableEqMapping["e$(eqCounter)"] = sort(getIndiciesOfVariables(variablesForEq, stringToSimVarHT)) + variableEqMapping[eqCounter] = sort(getIndiciesOfVariables(variablesForEq, stringToSimVarHT)) end return variableEqMapping end @@ -1078,7 +1085,9 @@ function rebuildMatchOrder(simCode::SIM_CODE) #= Build the base name index for robust CREF extraction =# local baseNameToFullNames = buildBaseNameIndex(ht) #= Build bipartite adjacency: for each equation, which variable match indices does it reference? =# - local eqVarMapping = DataStructures.OrderedDict{String, Vector{Int}}() + #= Int-keyed: GraphAlgorithms.matching consumes only `.vals` positionally, so the + interpolated "e$(i)" string keys were pure allocation/hashing overhead. =# + local eqVarMapping = DataStructures.OrderedDict{Int, Vector{Int}}() for eqI in 1:nEqs local refs = collectEquationVarNames(toDAEExp(resEqs[eqI].exp), ht, baseNameToFullNames) local indices = Int[] @@ -1087,7 +1096,7 @@ function rebuildMatchOrder(simCode::SIM_CODE) push!(indices, nameToMatchIdx[refName]) end end - eqVarMapping["e$(eqI)"] = sort(unique(indices)) + eqVarMapping[eqI] = sort(unique(indices)) end #= The matching algorithm requires a square system (n used for both eq loop and assign array). For over-determined systems (nVars > nEqs), pad with @@ -1101,7 +1110,7 @@ function rebuildMatchOrder(simCode::SIM_CODE) local nMatch = nVars if nVars > nEqs for dummyI in (nEqs + 1):nVars - eqVarMapping["e$(dummyI)"] = Int[] + eqVarMapping[dummyI] = Int[] end end local matchOrder::Vector{Int} @@ -1128,30 +1137,66 @@ Returns `(outputOnlyVarNames::OrderedSet{String}, outputOnlyEqIndices::OrderedSe Variables in the returned set are purely "output" (they can be computed from states but do not feed back into any state derivative). """ -#= Recursively collect all CREF variable names referenced in a DAE expression. =# -# SIM-native cref-name collection: walk the SIM tree directly via traverseExpTopDown -# (no toDAEExp of the whole tree). The ASUB arm reconstructs the subscripted key -# (e.g. "R_T[1][1]") so the use-def chain matches the scalarized hash-table keys. -function _collectCrefNamesVisitor(@nospecialize(e), names::OrderedSet{String}) - if e isa EXP_CREF - push!(names, DAE_identifierToString(toDAECref(e.cref).componentRef)) - elseif e isa ASUB && e.exp isa EXP_CREF - local allConst = true - local sstr = "" - for s in e.subs - @match s begin - ICONST(i) => (sstr *= Base.string("[", i, "]")) - _ => (allConst = false) +#= Pure read-only cref-name collector over the SIM Exp tree. Walks the tree and + pushes referenced names without reconstructing any nodes (unlike + traverseExpTopDown, which rebuilds the tree and allocates). The ASUB arm + reconstructs the subscripted key (e.g. "R_T[1][1]") so the use-def chain + matches the scalarized hash-table keys, then descends into the base (pushing + the bare name) and the subscripts. =# +function collectCrefNames!(names::OrderedSet{String}, exp::Exp) + @match exp begin + EXP_CREF(__) => push!(names, DAE_identifierToString(toDAECref(exp.cref).componentRef)) + BINARY(__) => begin collectCrefNames!(names, exp.exp1); collectCrefNames!(names, exp.exp2) end + LBINARY(__) => begin collectCrefNames!(names, exp.exp1); collectCrefNames!(names, exp.exp2) end + RELATION(__) => begin collectCrefNames!(names, exp.exp1); collectCrefNames!(names, exp.exp2) end + UNARY(__) => collectCrefNames!(names, exp.exp) + LUNARY(__) => collectCrefNames!(names, exp.exp) + CAST(__) => collectCrefNames!(names, exp.exp) + TSUB(__) => collectCrefNames!(names, exp.exp) + RSUB(__) => collectCrefNames!(names, exp.exp) + IFEXP(__) => begin + collectCrefNames!(names, exp.cond) + collectCrefNames!(names, exp.thenExp) + collectCrefNames!(names, exp.elseExp) + end + ARRAY_EXP(__) => begin for x in exp.elements; collectCrefNames!(names, x) end end + CALL(__) => begin for x in exp.args; collectCrefNames!(names, x) end end + RECORD(__) => begin for x in exp.exps; collectCrefNames!(names, x) end end + TUPLE(__) => begin for x in exp.PR; collectCrefNames!(names, x) end end + REDUCTION(__) => begin + collectCrefNames!(names, exp.body) + #= iterators carry DAE.ReductionIterator range/guard exps (passed through by + toDAEExp); collect their crefs to match the DAE collector exactly. =# + for it in exp.iterators + @match it begin + DAE.REDUCTIONITER(exp = rangeExp) => collectCrefNames!(names, rangeExp) + _ => () + end end end - if allConst && !isempty(sstr) - push!(names, Base.string(DAE_identifierToString(toDAECref(e.exp.cref).componentRef), sstr)) + ASUB(__) => begin + if exp.exp isa EXP_CREF + local allConst = true + local sstr = "" + for s in exp.subs + @match s begin + ICONST(i) => (sstr *= Base.string("[", i, "]")) + _ => (allConst = false) + end + end + if allConst && !isempty(sstr) + push!(names, Base.string(DAE_identifierToString(toDAECref(exp.exp.cref).componentRef), sstr)) + end + end + collectCrefNames!(names, exp.exp) + for s in exp.subs + collectCrefNames!(names, s) + end end + _ => () end - return (e, true, names) + return names end -collectCrefNames!(names::OrderedSet{String}, exp::Exp) = - (traverseExpTopDown(exp, _collectCrefNamesVisitor, names); names) function collectCrefNames!(names::OrderedSet{String}, @nospecialize(exp)) @match exp begin @@ -3467,7 +3512,7 @@ function propagateConstants(simCode::SIM_CODE) local allBaseNames = OrderedSet{String}() for eq in resEqs local eqNames = OrderedSet{String}() - collectCrefNames!(eqNames, toDAEExp(eq.exp)) + collectCrefNames!(eqNames, eq.exp) for n in eqNames if !occursin('[', n) push!(allBaseNames, n) @@ -3560,6 +3605,10 @@ function propagateConstants(simCode::SIM_CODE) local newResEqs = RESIDUAL_EQUATION[] local elimPairs = Tuple{String, RESIDUAL_EQUATION, RESIDUAL_EQUATION}[] sizehint!(newResEqs, nEqs - length(allRemoved)) + #= Collect surviving cref names from the substituted expressions as we build + them, avoiding a second full traversal (and toDAEExp reconversion) in + Phase 3. =# + local allRefNames = OrderedSet{String}() for (i, eq) in enumerate(simCode.residualEquations) if i in allRemoved @@ -3569,6 +3618,7 @@ function propagateConstants(simCode::SIM_CODE) end else local (newExp, _) = traverseExpTopDown(eq.exp, substituteAliasCref, constMap) + collectCrefNames!(allRefNames, newExp) push!(newResEqs, typeof(eq)(newExp, eq.source, eq.attr)) end end @@ -3581,6 +3631,7 @@ function propagateConstants(simCode::SIM_CODE) local newBranchEqs = RESIDUAL_EQUATION[] for brEq in branch.residualEquations local (newBrExp, _) = traverseExpTopDown(brEq.exp, substituteAliasCref, constMap) + collectCrefNames!(allRefNames, newBrExp) push!(newBranchEqs, typeof(brEq)(newBrExp, brEq.source, brEq.attr)) end local (newCond, _) = traverseExpTopDown(branch.condition, substituteAliasCref, constMap) @@ -3612,44 +3663,28 @@ function propagateConstants(simCode::SIM_CODE) for initEq in simCode.initialEquations if initEq isa BDAE.RESIDUAL_EQUATION || initEq isa RESIDUAL_EQUATION local (newInitExp, _) = Util.traverseExpTopDown(toDAEExp(initEq.exp), substituteAliasCref, constMap) + collectCrefNames!(allRefNames, newInitExp) push!(newInitEqs, typeof(initEq)(newInitExp, initEq.source, initEq.attr)) elseif initEq isa BDAE.EQUATION local (newLhs, _) = Util.traverseExpTopDown(toDAEExp(initEq.lhs), substituteAliasCref, constMap) local (newRhs, _) = Util.traverseExpTopDown(toDAEExp(initEq.rhs), substituteAliasCref, constMap) + collectCrefNames!(allRefNames, newLhs) + collectCrefNames!(allRefNames, newRhs) push!(newInitEqs, BDAE.EQUATION(newLhs, newRhs, initEq.source, initEq.attributes)) elseif initEq isa EQUATION local (newLhs, _) = Util.traverseExpTopDown(toDAEExp(initEq.lhs), substituteAliasCref, constMap) local (newRhs, _) = Util.traverseExpTopDown(toDAEExp(initEq.rhs), substituteAliasCref, constMap) + collectCrefNames!(allRefNames, newLhs) + collectCrefNames!(allRefNames, newRhs) push!(newInitEqs, EQUATION(newLhs, newRhs, initEq.source, initEq.attr)) else push!(newInitEqs, initEq) end end - #= Phase 3: Verify and remove eliminated unknowns =# + #= Phase 3: Verify and remove eliminated unknowns. Surviving refs from + residual/if/init expressions were collected inline during substitution. =# local eliminatedSet = OrderedSet{String}(keys(constMap)) - local allRefNames = OrderedSet{String}() - for eq in newResEqs - collectCrefNames!(allRefNames, toDAEExp(eq.exp)) - end - for ifEq in newIfEqs - for branch in ifEq.branches - for brEq in branch.residualEquations - collectCrefNames!(allRefNames, toDAEExp(brEq.exp)) - end - end - end - for initEq in newInitEqs - if initEq isa BDAE.RESIDUAL_EQUATION || initEq isa RESIDUAL_EQUATION - collectCrefNames!(allRefNames, toDAEExp(initEq.exp)) - elseif initEq isa BDAE.EQUATION - collectCrefNames!(allRefNames, toDAEExp(initEq.lhs)) - collectCrefNames!(allRefNames, toDAEExp(initEq.rhs)) - elseif initEq isa EQUATION - collectCrefNames!(allRefNames, toDAEExp(initEq.lhs)) - collectCrefNames!(allRefNames, toDAEExp(initEq.rhs)) - end - end #= Also scan when-equation conditions and statement bodies for surviving references (mirrors eliminateAliasVariables). Without this, a constant-bound @@ -4125,21 +4160,21 @@ function eliminateAliasVariables(simCode::SIM_CODE) local survivingRefs = OrderedSet{String}() local allRefNames = OrderedSet{String}() for eq in newResEqs - collectCrefNames!(allRefNames, toDAEExp(eq.exp)) + collectCrefNames!(allRefNames, eq.exp) end for ifEq in newIfEqs for branch in ifEq.branches for brEq in branch.residualEquations - collectCrefNames!(allRefNames, toDAEExp(brEq.exp)) + collectCrefNames!(allRefNames, brEq.exp) end end end for initEq in newInitEqs if initEq isa BDAE.RESIDUAL_EQUATION || initEq isa RESIDUAL_EQUATION - collectCrefNames!(allRefNames, toDAEExp(initEq.exp)) + collectCrefNames!(allRefNames, initEq.exp) elseif initEq isa BDAE.EQUATION || initEq isa EQUATION - collectCrefNames!(allRefNames, toDAEExp(initEq.lhs)) - collectCrefNames!(allRefNames, toDAEExp(initEq.rhs)) + collectCrefNames!(allRefNames, initEq.lhs) + collectCrefNames!(allRefNames, initEq.rhs) end end for ia in newInitialAlgs @@ -4422,17 +4457,17 @@ function dropObservationOnlyVariables(simCode::SIM_CODE)::SIM_CODE local untouchable = OrderedSet{String}() for eq in simCode.initialEquations if eq isa BDAE.RESIDUAL_EQUATION || eq isa RESIDUAL_EQUATION - collectCrefNames!(untouchable, toDAEExp(eq.exp)) + collectCrefNames!(untouchable, eq.exp) elseif eq isa BDAE.EQUATION || eq isa EQUATION - collectCrefNames!(untouchable, toDAEExp(eq.lhs)) - collectCrefNames!(untouchable, toDAEExp(eq.rhs)) + collectCrefNames!(untouchable, eq.lhs) + collectCrefNames!(untouchable, eq.rhs) end end for ifEq in simCode.ifEquations for branch in ifEq.branches collectCrefNames!(untouchable, branch.condition) for brEq in branch.residualEquations - collectCrefNames!(untouchable, toDAEExp(brEq.exp)) + collectCrefNames!(untouchable, brEq.exp) end end end @@ -4448,7 +4483,7 @@ function dropObservationOnlyVariables(simCode::SIM_CODE)::SIM_CODE _collectIfexpConditionCrefs!(untouchable, toDAEExp(eq.exp)) end for eq in simCode.eliminatedEquations - collectCrefNames!(untouchable, toDAEExp(eq.exp)) + collectCrefNames!(untouchable, eq.exp) end _collectFunctionBodyCrefs!(untouchable, simCode.functions) @@ -4456,12 +4491,17 @@ function dropObservationOnlyVariables(simCode::SIM_CODE)::SIM_CODE local nEqs = length(simCode.residualEquations) local eqRefs = Vector{OrderedSet{String}}(undef, nEqs) local refCount = Dict{String, Int}() + #= Inverted index name -> ascending equation indices referencing it, built in the + same pass. Replaces the O(nEqs) linear scan for a candidate's defining equation + with an O(degree) lookup; ascending insertion preserves the old first-match. =# + local varToEqs = Dict{String, Vector{Int}}() for i in 1:nEqs local s = OrderedSet{String}() - collectCrefNames!(s, toDAEExp(simCode.residualEquations[i].exp)) + collectCrefNames!(s, simCode.residualEquations[i].exp) eqRefs[i] = s for n in s refCount[n] = get(refCount, n, 0) + 1 + push!(get!(() -> Int[], varToEqs, n), i) end end @@ -4479,11 +4519,9 @@ function dropObservationOnlyVariables(simCode::SIM_CODE)::SIM_CODE nref <= 1 || continue local definingEq = -1 if nref == 1 - for i in 1:nEqs + for i in get(varToEqs, name, Int[]) i in droppedEqs && continue - if name in eqRefs[i] - definingEq = i; break - end + definingEq = i; break end end if definingEq > 0 @@ -4525,21 +4563,21 @@ function eliminateDeadParameters(simCode::SIM_CODE)::SIM_CODE surface. Anything not in this set is dead. =# local referenced = OrderedSet{String}() for eq in simCode.residualEquations - collectCrefNames!(referenced, toDAEExp(eq.exp)) + collectCrefNames!(referenced, eq.exp) end for eq in simCode.initialEquations if eq isa BDAE.RESIDUAL_EQUATION || eq isa RESIDUAL_EQUATION - collectCrefNames!(referenced, toDAEExp(eq.exp)) + collectCrefNames!(referenced, eq.exp) elseif eq isa BDAE.EQUATION || eq isa EQUATION - collectCrefNames!(referenced, toDAEExp(eq.lhs)) - collectCrefNames!(referenced, toDAEExp(eq.rhs)) + collectCrefNames!(referenced, eq.lhs) + collectCrefNames!(referenced, eq.rhs) end end for ifEq in simCode.ifEquations for branch in ifEq.branches collectCrefNames!(referenced, branch.condition) for brEq in branch.residualEquations - collectCrefNames!(referenced, toDAEExp(brEq.exp)) + collectCrefNames!(referenced, brEq.exp) end end end @@ -4547,7 +4585,7 @@ function eliminateDeadParameters(simCode::SIM_CODE)::SIM_CODE _collectWhenCrefNames!(referenced, whenEq.whenEquation) end for eq in simCode.eliminatedEquations - collectCrefNames!(referenced, toDAEExp(eq.exp)) + collectCrefNames!(referenced, eq.exp) end for entry in simCode.aliasMap push!(referenced, entry.representativeName) @@ -4586,12 +4624,16 @@ function eliminateDeadParameters(simCode::SIM_CODE)::SIM_CODE _ => nothing end end - for htKey in keys(ht) - local bracketIdx = findfirst('[', htKey) - bracketIdx === nothing && continue - local baseName = htKey[1:bracketIdx-1] - if baseName in dsArrayBaseNames - push!(referenced, htKey) + #= Only scan HT keys for scalarized DS-array elements when there are DS-array + bases to match; otherwise this whole-HT scan does nothing. =# + if !isempty(dsArrayBaseNames) + for htKey in keys(ht) + local bracketIdx = findfirst('[', htKey) + bracketIdx === nothing && continue + local baseName = htKey[1:bracketIdx-1] + if baseName in dsArrayBaseNames + push!(referenced, htKey) + end end end @@ -4660,10 +4702,10 @@ function eliminateConstantParameters(simCode::SIM_CODE)::SIM_CODE end for eq in simCode.initialEquations if eq isa BDAE.RESIDUAL_EQUATION || eq isa RESIDUAL_EQUATION - collectCrefNames!(protectedNames, toDAEExp(eq.exp)) + collectCrefNames!(protectedNames, eq.exp) elseif eq isa BDAE.EQUATION || eq isa EQUATION - collectCrefNames!(protectedNames, toDAEExp(eq.lhs)) - collectCrefNames!(protectedNames, toDAEExp(eq.rhs)) + collectCrefNames!(protectedNames, eq.lhs) + collectCrefNames!(protectedNames, eq.rhs) end end #= IFEXP conditions inside residual equations and parameter bindings. =# @@ -4699,12 +4741,16 @@ function eliminateConstantParameters(simCode::SIM_CODE)::SIM_CODE _ => nothing end end - for htKey in keys(ht) - local bracketIdx = findfirst('[', htKey) - bracketIdx === nothing && continue - local baseName = htKey[1:bracketIdx-1] - if baseName in dsArrayBaseNames - push!(protectedNames, htKey) + #= Only scan HT keys for scalarized DS-array elements when there are DS-array + bases to match; otherwise this whole-HT scan does nothing. =# + if !isempty(dsArrayBaseNames) + for htKey in keys(ht) + local bracketIdx = findfirst('[', htKey) + bracketIdx === nothing && continue + local baseName = htKey[1:bracketIdx-1] + if baseName in dsArrayBaseNames + push!(protectedNames, htKey) + end end end @@ -4846,20 +4892,20 @@ function eliminateConstantParameters(simCode::SIM_CODE)::SIM_CODE somehow survived substitution (unflatten form, etc.), keep the param. =# local survivorCheck = OrderedSet{String}() for eq in newResiduals - collectCrefNames!(survivorCheck, toDAEExp(eq.exp)) + collectCrefNames!(survivorCheck, eq.exp) end for eq in newInitials if eq isa BDAE.RESIDUAL_EQUATION || eq isa RESIDUAL_EQUATION - collectCrefNames!(survivorCheck, toDAEExp(eq.exp)) + collectCrefNames!(survivorCheck, eq.exp) elseif eq isa BDAE.EQUATION || eq isa EQUATION - collectCrefNames!(survivorCheck, toDAEExp(eq.lhs)) - collectCrefNames!(survivorCheck, toDAEExp(eq.rhs)) + collectCrefNames!(survivorCheck, eq.lhs) + collectCrefNames!(survivorCheck, eq.rhs) end end for ifEq in newIfEquations for branch in ifEq.branches for brEq in branch.residualEquations - collectCrefNames!(survivorCheck, toDAEExp(brEq.exp)) + collectCrefNames!(survivorCheck, brEq.exp) end collectCrefNames!(survivorCheck, branch.condition) end @@ -5068,7 +5114,7 @@ separately via the equation walk; we only protect parameters that gate the trigger. """ function _collectWhenConditionCrefs!(out::OrderedSet{String}, whenStmts::WHEN_STMTS) - collectCrefNames!(out, toDAEExp(whenStmts.condition)) + collectCrefNames!(out, whenStmts.condition) if whenStmts.elsewhenPart !== nothing _collectWhenConditionCrefs!(out, whenStmts.elsewhenPart) end @@ -5076,7 +5122,7 @@ function _collectWhenConditionCrefs!(out::OrderedSet{String}, whenStmts::WHEN_ST end function _collectWhenConditionCrefs!(out::OrderedSet{String}, whenStmts::BDAE.WHEN_STMTS) - collectCrefNames!(out, toDAEExp(whenStmts.condition)) + collectCrefNames!(out, whenStmts.condition) @match whenStmts.elsewhenPart begin SOME(inner) => _collectWhenConditionCrefs!(out, inner) _ => nothing @@ -5484,7 +5530,7 @@ end Collect all CREF names from a WHEN_STMTS node (condition + statements + elsewhen). """ function _collectWhenCrefNames!(names::OrderedSet{String}, whenStmts::WHEN_STMTS) - collectCrefNames!(names, toDAEExp(whenStmts.condition)) + collectCrefNames!(names, whenStmts.condition) for stmt in whenStmts.whenStmtLst if stmt isa ASSIGN collectCrefNames!(names, stmt.left) @@ -5506,7 +5552,7 @@ function _collectWhenCrefNames!(names::OrderedSet{String}, whenStmts::WHEN_STMTS end function _collectWhenCrefNames!(names::OrderedSet{String}, whenStmts::BDAE.WHEN_STMTS) - collectCrefNames!(names, toDAEExp(whenStmts.condition)) + collectCrefNames!(names, whenStmts.condition) for stmt in whenStmts.whenStmtLst @match stmt begin BDAE.ASSIGN(__) => begin @@ -5535,19 +5581,19 @@ end function _collectInitialAlgorithmCrefNames!(names::OrderedSet{String}, ia::INITIAL_ALGORITHM) for stmt in ia.statements if stmt isa ASSIGN - collectCrefNames!(names, toDAEExp(stmt.left)) - collectCrefNames!(names, toDAEExp(stmt.right)) + collectCrefNames!(names, stmt.left) + collectCrefNames!(names, stmt.right) elseif stmt isa REINIT - collectCrefNames!(names, toDAEExp(stmt.stateVar)) - collectCrefNames!(names, toDAEExp(stmt.value)) + collectCrefNames!(names, stmt.stateVar) + collectCrefNames!(names, stmt.value) elseif stmt isa NORETCALL - collectCrefNames!(names, toDAEExp(stmt.exp)) + collectCrefNames!(names, stmt.exp) elseif stmt isa ASSERT - collectCrefNames!(names, toDAEExp(stmt.condition)) - collectCrefNames!(names, toDAEExp(stmt.message)) - collectCrefNames!(names, toDAEExp(stmt.level)) + collectCrefNames!(names, stmt.condition) + collectCrefNames!(names, stmt.message) + collectCrefNames!(names, stmt.level) elseif stmt isa TERMINATE - collectCrefNames!(names, toDAEExp(stmt.message)) + collectCrefNames!(names, stmt.message) end end _walkStatementsForCrefs!(names, ia.daeStatements) @@ -6030,7 +6076,7 @@ function _recomputeSCCsFromSimCode(simCode::SIM_CODE) local incidence = Vector{OrderedSet{String}}(undef, n_eqs) for (i, eq) in enumerate(res) local names = OrderedSet{String}() - collectCrefNames!(names, toDAEExp(eq.exp)) + collectCrefNames!(names, eq.exp) incidence[i] = intersect(names, surviving) end local var_to_eq = Dict{String, Int}() @@ -6755,7 +6801,7 @@ function _computeFrozenProtectedNames(simCode::SIM_CODE)::OrderedSet{String} for branch in ifEq.branches collectCrefNames!(protectedNames, branch.condition) for brEq in branch.residualEquations - collectCrefNames!(protectedNames, toDAEExp(brEq.exp)) + collectCrefNames!(protectedNames, brEq.exp) end end end @@ -7189,26 +7235,26 @@ function _foldExplicitSingleAssignOnePass(simCode::SIM_CODE, local foldKeys = OrderedSet{String}(keys(foldMap)) local survivorNames = OrderedSet{String}() for eq in newResEqs - collectCrefNames!(survivorNames, toDAEExp(eq.exp)) + collectCrefNames!(survivorNames, eq.exp) end for eq in newInitEqs if eq isa BDAE.RESIDUAL_EQUATION || eq isa RESIDUAL_EQUATION - collectCrefNames!(survivorNames, toDAEExp(eq.exp)) + collectCrefNames!(survivorNames, eq.exp) elseif eq isa BDAE.EQUATION || eq isa EQUATION - collectCrefNames!(survivorNames, toDAEExp(eq.lhs)) - collectCrefNames!(survivorNames, toDAEExp(eq.rhs)) + collectCrefNames!(survivorNames, eq.lhs) + collectCrefNames!(survivorNames, eq.rhs) end end for ifEq in newIfEqs for branch in ifEq.branches collectCrefNames!(survivorNames, branch.condition) for brEq in branch.residualEquations - collectCrefNames!(survivorNames, toDAEExp(brEq.exp)) + collectCrefNames!(survivorNames, brEq.exp) end end end for eq in newElimEqs - collectCrefNames!(survivorNames, toDAEExp(eq.exp)) + collectCrefNames!(survivorNames, eq.exp) end for whenEq in simCode.whenEquations _collectWhenCrefNames!(survivorNames, whenEq.whenEquation) diff --git a/src/util.jl b/src/util.jl index cb75e80a..a87311a5 100644 --- a/src/util.jl +++ b/src/util.jl @@ -111,6 +111,7 @@ end const COMPONENT_SEPARATOR = "_" function _joinCanonicalSegments(segs)::String + length(segs) == 1 && return String(segs[1]) return join((String(s) for s in segs), COMPONENT_SEPARATOR) end @@ -126,6 +127,7 @@ function _canonicalIdentSegments(ident::AbstractString)::Vector{String} end function _subscriptSuffix(subscriptLst)::String + iterate(subscriptLst) === nothing && return "" local buf = IOBuffer() for s in subscriptLst print(buf, "[") @@ -149,20 +151,23 @@ end function _canonicalCrefSegments(cr::DAE.CREF_IDENT) local segs = _canonicalIdentSegments(cr.ident) - segs[end] = string(segs[end], _subscriptSuffix(cr.subscriptLst)) + local suffix = _subscriptSuffix(cr.subscriptLst) + isempty(suffix) || (segs[end] = string(segs[end], suffix)) return segs end function _canonicalCrefSegments(cr::DAE.CREF_QUAL) local segs = _canonicalIdentSegments(cr.ident) - segs[end] = string(segs[end], _subscriptSuffix(cr.subscriptLst)) + local suffix = _subscriptSuffix(cr.subscriptLst) + isempty(suffix) || (segs[end] = string(segs[end], suffix)) return vcat(segs, _canonicalCrefSegments(cr.componentRef)) end function _canonicalCrefSegments(cr::DAE.CREF_ITER) local segs = _canonicalIdentSegments(cr.ident) - segs[end] = string(segs[end], _subscriptSuffix(cr.subscriptLst)) + local suffix = _subscriptSuffix(cr.subscriptLst) + isempty(suffix) || (segs[end] = string(segs[end], suffix)) return segs end @@ -179,7 +184,9 @@ function canonicalName(name::AbstractString)::String return s end if occursin('.', s) - return _joinCanonicalSegments(split(s, '.')) + #= Equivalent to joining the '.'-split segments with '_', in a single pass + without the intermediate segment vector. =# + return replace(s, '.' => '_') end return s end From 01b80e1d9ada80696eea44f4f4f4868e25057da9 Mon Sep 17 00:00:00 2001 From: JKRT Date: Wed, 17 Jun 2026 21:26:28 +0200 Subject: [PATCH 02/12] Several minor edits and adjustments concerning performances --- .github/workflows/CI.yml | 74 +++ src/Backend/BDAE.jl | 20 +- src/Backend/BDAECreate.jl | 46 +- src/Backend/BDAEUtil.jl | 6 +- src/Backend/Causalize.jl | 10 +- src/Backend/backendDump.jl | 12 +- src/CodeGeneration/CodeGenerationUtil.jl | 19 +- src/CodeGeneration/MTK_CodeGeneration.jl | 16 +- src/CodeGeneration/MTK_CodeGenerationUtil.jl | 38 +- src/CodeGeneration/algorithmic.jl | 2 +- src/CodeGeneration/codeGen.jl | 4 +- src/CodeGeneration/modelicaBuiltins.jl | 8 +- src/CodeGeneration/mtkExternals.jl | 31 +- src/CodeGeneration/structuralCallbacks.jl | 22 +- src/FrontendUtil/Util.jl | 60 ++- src/Runtime/Runtime.jl | 2 +- src/SimulationCode/simCodeData.jl | 28 +- src/SimulationCode/simCodeDump.jl | 6 +- src/SimulationCode/simCodeFunctions.jl | 71 +++ src/SimulationCode/simCodeStructureUtil.jl | 6 +- src/SimulationCode/simCodeUtil.jl | 438 +++++++++++++----- .../simulationCodeTransformation.jl | 10 +- 22 files changed, 653 insertions(+), 276 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 883cbeac..8ed1cc71 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -145,3 +145,77 @@ jobs: Commit: ${{ github.sha }} Branch: ${{ github.ref_name }} Matrix: ${{ matrix.os }} / Julia ${{ matrix.version }} / ${{ matrix.arch }} + + # Downstream integration gate: after the OMBackend tests pass, run the parent + # OM.jl test suite (runtests.jl) on Ubuntu with THIS OMBackend checkout dev'd + # in, so an OMBackend change that breaks the parent is caught here. + om-rt: + name: OM.jl RT - Ubuntu (downstream) + needs: test + runs-on: ubuntu-latest + timeout-minutes: 120 + env: + # This job builds the parent OM.jl project, not OMBackend; repoint the + # project so the cache action and bare `julia --project` calls key off it. + JULIA_PROJECT: OM.jl + steps: + # OMBackend under test: the current repo at the triggering ref, placed as a + # sibling so OM.jl's [sources] path ../OMBackend.jl resolves to it. + - name: Checkout OMBackend.jl (under test) + uses: actions/checkout@v4 + with: + path: OMBackend.jl + + # Parent OM.jl and the remaining siblings mirror OM.jl's own CI so RT runs + # in the same environment, with OMBackend swapped for the checkout above. + - { name: Checkout OM.jl, uses: actions/checkout@v4, with: { repository: JKRT/OM.jl, ref: master, path: OM.jl } } + - { name: Checkout Absyn.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/Absyn.jl, ref: master, path: Absyn.jl } } + - { name: Checkout ArrayUtil.jl, uses: actions/checkout@v4, with: { repository: JKRT/ArrayUtil.jl, ref: master, path: ArrayUtil.jl } } + - { name: Checkout DAE.jl, uses: actions/checkout@v4, with: { repository: JKRT/DAE.jl, ref: master, path: DAE.jl } } + - { name: Checkout DoubleEnded.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/DoubleEnded.jl, ref: master, path: DoubleEnded.jl } } + - { name: Checkout ImmutableList.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/ImmutableList.jl, ref: master, path: ImmutableList.jl } } + - { name: Checkout ListUtil.jl, uses: actions/checkout@v4, with: { repository: JKRT/ListUtil.jl, ref: master, path: ListUtil.jl } } + - { name: Checkout MetaModelica.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/MetaModelica.jl, ref: master, path: MetaModelica.jl } } + - { name: Checkout OMFrontend.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/OMFrontend.jl, ref: master, path: OMFrontend.jl } } + - { name: Checkout OMParser.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/OMParser.jl, ref: master, path: OMParser.jl } } + - { name: Checkout OMRuntimeExternalC.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/OMRuntimeExternalC.jl, ref: master, path: OMRuntimeExternalC.jl } } + - { name: Checkout SCode.jl, uses: actions/checkout@v4, with: { repository: OpenModelica/SCode.jl, ref: master, path: SCode.jl } } + + - uses: julia-actions/setup-julia@v2 + with: + version: '1.12' + arch: x64 + + - uses: julia-actions/cache@v2 + with: + cache-name: om-rt + + - name: Develop siblings, build native libs, precompile + shell: bash + working-directory: OM.jl + # Disable auto-precompile so the native-lib build runs before precompile + # (OMParser/OMRuntimeExternalC download shared libs in their build step). + env: + JULIA_PKG_PRECOMPILE_AUTO: '0' + run: | + julia --color=yes --project -e ' + import Pkg + Pkg.Registry.add("General") + Pkg.Registry.add(Pkg.RegistrySpec(url="https://github.com/OpenModelica/OpenModelicaRegistry.git")) + siblings = [ + "../Absyn.jl", "../ArrayUtil.jl", "../DAE.jl", "../DoubleEnded.jl", + "../ImmutableList.jl", "../ListUtil.jl", "../MetaModelica.jl", + "../OMBackend.jl", "../OMFrontend.jl", "../OMParser.jl", + "../OMRuntimeExternalC.jl", "../SCode.jl", + ] + Pkg.develop([Pkg.PackageSpec(path = p) for p in siblings]) + # Test-only deps used by test/testUtils.jl but absent from OM [deps]. + Pkg.add(["ADTypes", "Sundials", "Logging", "Test"]) + Pkg.build(["OMParser", "OMRuntimeExternalC"]; verbose=true) + Pkg.resolve() + Pkg.precompile()' + + - name: Test (OM.jl runtests) + shell: bash + working-directory: OM.jl/test # runtests.jl guards pwd() == @__DIR__ + run: julia --color=yes --project=.. -e 'include("runtests.jl")' diff --git a/src/Backend/BDAE.jl b/src/Backend/BDAE.jl index 5234f48d..a2e1beaa 100644 --- a/src/Backend/BDAE.jl +++ b/src/Backend/BDAE.jl @@ -173,7 +173,7 @@ using ExportAll - `BRANCH(ar, br)` — `Connections.branch(ar, br)`. - `STRUCTURAL_IF_EQUATION(ifEquation)` — DOCC if-equation preserved as a frontend `EQUATION_IF` so the runtime can replay the branch choice. - - `STRUCTURAL_TRANSISTION(fromState, toState, transistionCondition)` — + - `STRUCTURAL_TRANSITION(fromState, toState, transitionCondition)` — VSS transition; a structural callback is generated from the condition. (Name is a historical typo preserved across the codebase.) """ @@ -337,7 +337,7 @@ end end @Record STATE begin - index::Integer + index::Int derName::Option{DAE.ComponentRef} natural::Bool end @@ -477,7 +477,7 @@ const EQ_ATTR_DEFAULT_UNKNOWN = EQUATION_ATTRIBUTES(false, UNKNOWN_EQUATION_KIND right::DAE.Exp source::DAE.ElementSource attr::EquationAttributes - recordSize::Option{Integer} + recordSize::Option{Int} end @Record SOLVED_EQUATION begin @@ -494,7 +494,7 @@ const EQ_ATTR_DEFAULT_UNKNOWN = EQUATION_ATTRIBUTES(false, UNKNOWN_EQUATION_KIND end @Record ALGORITHM begin - size::Integer + size::Int alg::DAE.Algorithm source::DAE.ElementSource expand::DAE.Expand @@ -502,7 +502,7 @@ const EQ_ATTR_DEFAULT_UNKNOWN = EQUATION_ATTRIBUTES(false, UNKNOWN_EQUATION_KIND end @Record WHEN_EQUATION begin - size::Integer + size::Int whenEquation::WhenEquation source::DAE.ElementSource attr @@ -512,21 +512,21 @@ const EQ_ATTR_DEFAULT_UNKNOWN = EQUATION_ATTRIBUTES(false, UNKNOWN_EQUATION_KIND from WHEN_EQUATION so init-time bodies stay separated from runtime when-equations through the pipeline. =# @Record INITIAL_WHEN_EQUATION begin - size::Integer + size::Int whenEquation::WhenEquation source::DAE.ElementSource attr end @Record STRUCTURAL_WHEN_EQUATION begin - size::Integer + size::Int whenEquation::WhenEquation source::DAE.ElementSource attr::EquationAttributes end @Record COMPLEX_EQUATION begin - size::Integer + size::Int left::DAE.Exp right::DAE.Exp source::DAE.ElementSource @@ -573,10 +573,10 @@ const EQ_ATTR_DEFAULT_UNKNOWN = EQUATION_ATTRIBUTES(false, UNKNOWN_EQUATION_KIND ifEquation::OMFrontend.Frontend.EQUATION_IF end - @Record STRUCTURAL_TRANSISTION begin + @Record STRUCTURAL_TRANSITION begin fromState::String toState::String - transistionCondition::DAE.Exp + transitionCondition::DAE.Exp end end diff --git a/src/Backend/BDAECreate.jl b/src/Backend/BDAECreate.jl index 9e24845b..a120c5f8 100644 --- a/src/Backend/BDAECreate.jl +++ b/src/Backend/BDAECreate.jl @@ -226,9 +226,13 @@ function createEqSystem(flatModel::OMFrontend.Frontend.FlatModel) #= §17.4.4: lift discrete (Bool/Int/enum) equation-section definitions whose RHS is a discrete-time relation into event-driven held discretes, so the continuous integrator never interpolates step-valued logic. =# + #= Stringify each varName once; the four name-keyed sweeps below + (param/const, discrete-start, collision-resolve, dedup) all use the + default-separator string and share this vector instead of recomputing it. =# + local varNames = String[string(v.varName) for v in variables] if !isempty(equations) && !isempty(variables) - local _discParamConst = _collectParamOrConstNames(variables) - local _discStarts = _discreteStartExpLookup(variables) + local _discParamConst = _collectParamOrConstNames(variables, varNames) + local _discStarts = _discreteStartExpLookup(variables, varNames) local (_discEqs, _discLifted) = synthesizeWhenEquationsFromDiscreteEquations(equations, _discParamConst, _discStarts) if !isempty(_discLifted) @info "[BDAE: lifter] synthesizeWhenEquationsFromDiscreteEquations lifted $(length(_discLifted)) discrete equation(s)" lifted=collect(_discLifted) @@ -246,14 +250,16 @@ function createEqSystem(flatModel::OMFrontend.Frontend.FlatModel) end #= Distinct crefs can mangle to the same flat name (a.b vs a_b); resolve before the name-keyed deduplication silently swallows a variable. =# - resolveMangledNameCollisions!(variables, equations, initialEquations) + resolveMangledNameCollisions!(variables, equations, initialEquations, varNames) #= Deduplicate variables by name (handles inner/outer duplicate emission) =# - variables = deduplicateVariables(variables) + variables = deduplicateVariables(variables, varNames) #= Deduplicate explicit equations =# equations = deduplicateEquations(equations) #= The set of equations might also contain a set of "binding equations" =# local bindingEquations = createBindingEquations(variables) - equations = vcat(equations, bindingEquations) + if !isempty(bindingEquations) + equations = vcat(equations, bindingEquations) + end #= TODO Extract the simple equations =# local simpleEquations = BDAE.Equation[] return BDAE.EQSYSTEM(name, variables, equations, simpleEquations, initialEquations) @@ -280,10 +286,11 @@ end unique names, rewriting every occurrence (equations, initial equations and bindings) so the name-keyed passes downstream stay sound. """ -function resolveMangledNameCollisions!(variables::Vector, equations::Vector, initialEquations::Vector) +function resolveMangledNameCollisions!(variables::Vector, equations::Vector, initialEquations::Vector, + varNames::Vector{String} = String[string(v.varName) for v in variables]) local idxsByMangled = OrderedDict{String, Vector{Int}}() for (i, v) in enumerate(variables) - push!(get!(() -> Int[], idxsByMangled, string(v.varName)), i) + push!(get!(() -> Int[], idxsByMangled, varNames[i]), i) end local taken = OrderedSet{String}(keys(idxsByMangled)) local renames = OrderedDict{String, DAE.ComponentRef}() @@ -315,6 +322,8 @@ function resolveMangledNameCollisions!(variables::Vector, equations::Vector, ini renames[dotted] = newCref for i in gidxs variables[i].varName = newCref + #= Keep the shared name vector in sync so downstream dedup keys correctly. =# + varNames[i] = string(newCref) end end end @@ -358,12 +367,13 @@ end Deduplicate variables by their component reference name. Keeps the first occurrence of each uniquely-named variable. """ -function deduplicateVariables(variables::Vector)::Vector +function deduplicateVariables(variables::Vector, + varNames::Vector{String} = String[string(v.varName) for v in variables])::Vector local idxByName = Dict{String, Int}() local unique_vars = similar(variables, 0) local duplicateCount = 0 - for v in variables - local varStr = string(v.varName) + for (i, v) in enumerate(variables) + local varStr = varNames[i] local existing = get(idxByName, varStr, 0) if existing != 0 duplicateCount += 1 @@ -618,7 +628,7 @@ Base.@nospecializeinfer function equationToBackendEquation(@nospecialize(elem::D @match fromStateExp <| toStateExp <| conditionExp <| nil = expLst local fromStateIdent = string(fromStateExp) local toStateIdent = string(toStateExp) - BDAE.STRUCTURAL_TRANSISTION(fromStateIdent, toStateIdent, conditionExp) + BDAE.STRUCTURAL_TRANSITION(fromStateIdent, toStateIdent, conditionExp) end _ => begin #= Skip unknown NORETCALL statements (e.g. checkBoundary, assert-like calls). @@ -1133,13 +1143,14 @@ end names of every entry whose `varKind` is `PARAM` or `CONST`. Iterating the materialized BDAE vector avoids touching the lazier frontend list that triggered a multi-minute stall on first call. =# -function _collectParamOrConstNames(variables::Vector{BDAE.VAR})::OrderedSet{String} +function _collectParamOrConstNames(variables::Vector{BDAE.VAR}, + varNames::Vector{String} = String[string(v.varName) for v in variables])::OrderedSet{String} local names = OrderedSet{String}() sizehint!(names, 2 * length(variables)) - for var in variables + for (i, var) in enumerate(variables) local k = var.varKind if k isa BDAE.PARAM || k isa BDAE.CONST - local s = string(var.varName) + local s = varNames[i] push!(names, s) local u = replace(s, "." => "_") u === s || push!(names, u) @@ -2343,9 +2354,10 @@ end #= Start-attribute expression for each variable that has one (Bool/Int/Real/enum). Used to fold `pre(member)` at initialization: Modelica §8.6.2 — before the first event `pre(v)` is `v.start`. =# -Base.@nospecializeinfer function _discreteStartExpLookup(variables::Vector{BDAE.VAR})::Dict{String, DAE.Exp} +Base.@nospecializeinfer function _discreteStartExpLookup(variables::Vector{BDAE.VAR}, + varNames::Vector{String} = String[string(v.varName) for v in variables])::Dict{String, DAE.Exp} local d = Dict{String, DAE.Exp}() - for var in variables + for (i, var) in enumerate(variables) local s = @match var.values begin SOME(va) => @match va begin DAE.VAR_ATTR_BOOL(start = SOME(e)) => e @@ -2357,7 +2369,7 @@ Base.@nospecializeinfer function _discreteStartExpLookup(variables::Vector{BDAE. _ => nothing end s === nothing && continue - d[string(var.varName)] = s + d[varNames[i]] = s end return d end diff --git a/src/Backend/BDAEUtil.jl b/src/Backend/BDAEUtil.jl index a45cbebd..b87bd231 100644 --- a/src/Backend/BDAEUtil.jl +++ b/src/Backend/BDAEUtil.jl @@ -352,10 +352,10 @@ Base.@nospecializeinfer function traverseEquationExpressions(@nospecialize(eq::B end (eq, extArg) end - BDAE.STRUCTURAL_TRANSISTION(__) => begin - local cond = eq.transistionCondition + BDAE.STRUCTURAL_TRANSITION(__) => begin + local cond = eq.transitionCondition (cond, extArg) = Util.traverseExpTopDown(cond, traversalOperation, extArg) - @assign eq.transistionCondition = cond + @assign eq.transitionCondition = cond (eq, extArg) end _ => begin diff --git a/src/Backend/Causalize.jl b/src/Backend/Causalize.jl index 247d5c07..041b3330 100644 --- a/src/Backend/Causalize.jl +++ b/src/Backend/Causalize.jl @@ -878,7 +878,7 @@ end """ Expand a single array equation into multiple scalar EQUATION objects. """ -function expandSingleArrayEquation(size::Integer, left::DAE.Exp, right::DAE.Exp, +function expandSingleArrayEquation(size::Int, left::DAE.Exp, right::DAE.Exp, source::DAE.ElementSource, attr::BDAE.EquationAttributes) equations = BDAE.Equation[] leftFlat = flattenDAEArray(left) @@ -1371,9 +1371,9 @@ function flattenArrayCrefsEqSystem(syst::BDAE.EQSYSTEM)::BDAE.EQSYSTEM local v = syst.orderedVars[i] @match v.bindExp begin SOME(bindingExp) => begin - (newBindExp, _, _) = flattenWithPrefix(bindingExp, nothing) - #= Also traverse sub-expressions =# - (newBindExp, _) = Util.traverseExpTopDown(newBindExp, flattenWithPrefix, nothing) + #= traverseExpTopDown applies the func to the root first, then + descends, so a separate root-only call is redundant. =# + (newBindExp, _) = Util.traverseExpTopDown(bindingExp, flattenWithPrefix, nothing) if !(bindingExp === newBindExp) @assign syst.orderedVars[i].bindExp = SOME(newBindExp) end @@ -1395,6 +1395,8 @@ end Converts [INDEX(ICONST(1)), INDEX(ICONST(2))] → "[1][2]" """ function subscriptListToString(subscriptLst::List{DAE.Subscript})::String + #= Common case is an empty subscript list -> "" without touching IOBuffer. =# + listEmpty(subscriptLst) && return "" local buf = IOBuffer() for s in subscriptLst @match s begin diff --git a/src/Backend/backendDump.jl b/src/Backend/backendDump.jl index 1a9fc13d..a3d2506b 100644 --- a/src/Backend/backendDump.jl +++ b/src/Backend/backendDump.jl @@ -256,8 +256,8 @@ function Base.string(@nospecialize(eq::BDAE.Equation)) "INITIAL_STRUCTURAL_STATE(" * eq.initialState * ")" end - BDAE.STRUCTURAL_TRANSISTION(__) => begin - "STRUCTURAL_TRANSISTION " * eq.fromState * " -> " * eq.toState * "| if:" * string(eq.transistionCondition) + BDAE.STRUCTURAL_TRANSITION(__) => begin + "STRUCTURAL_TRANSITION " * eq.fromState * " -> " * eq.toState * "| if:" * string(eq.transitionCondition) end BDAE.DUMMY_EQUATION() => begin @@ -477,7 +477,7 @@ function Base.string(@nospecialize(op::DAE.Operator))::String DAE.USERDEFINED() => "[UNDEF OP]" - _ => throw("Unkown operator") + _ => throw("Unknown operator") end end @@ -485,7 +485,7 @@ end function Base.string(@nospecialize(exp::DAE.Exp))::String str = begin - local int::ModelicaInteger + local int::Int local real::ModelicaReal local bool::Bool local tmpStr::String @@ -715,7 +715,7 @@ function Base.string(path::Absyn.FULLYQUALIFIED; separator = OMBackend.COMPONENT return separator + string(path.path; separator = separator) end -function lstString(expLst::List{T}, seperator::String)::String where{T} +function lstString(expLst::List{T}, separator::String)::String where{T} str = begin local e::T local rest::List{T} @@ -723,7 +723,7 @@ function lstString(expLst::List{T}, seperator::String)::String where{T} (e <| rest) => begin str = string(e) for r in rest - str = str + seperator + string(r) + str = str + separator + string(r) end (str) end diff --git a/src/CodeGeneration/CodeGenerationUtil.jl b/src/CodeGeneration/CodeGenerationUtil.jl index 7949854d..67b62b07 100644 --- a/src/CodeGeneration/CodeGenerationUtil.jl +++ b/src/CodeGeneration/CodeGenerationUtil.jl @@ -228,7 +228,7 @@ end Base.@nospecializeinfer function expToJL(@nospecialize(exp::DAE.Exp), simCode::SimulationCode.SIM_CODE; varPrefix="x")::String hashTable = simCode.stringToSimVarHT str = begin - local int::ModelicaInteger + local int::Int local real::ModelicaReal local bool::Bool local tmpStr::String @@ -503,8 +503,19 @@ function _iterativePostwalk(f, root) push!(todo, (a, false)) end else - local newArgs = Any[get(newMap, a, a) for a in node.args] - newMap[node] = f(Expr(node.head, newArgs...)) + #= Lazy rebuild: only allocate a new args vector + Expr when a child + actually changed; otherwise pass the original node to `f`. `f` is a + pure function of structure, so a structurally identical node yields + the same result -> emitted code is unchanged. =# + local changed = false + for a in node.args + if get(newMap, a, a) !== a + changed = true + break + end + end + local rebuilt = changed ? Expr(node.head, Any[get(newMap, a, a) for a in node.args]...) : node + newMap[node] = f(rebuilt) end end return get(newMap, root, root) @@ -1106,7 +1117,7 @@ end """ Returns true if there is no discrete variables in the condition. """ -function isContinousCondition(cond::DAE.Exp, simCode) +function isContinuousCondition(cond::DAE.Exp, simCode) local allCrefs = Util.getAllCrefs(cond) local isContinuousCond = false if isone(length(allCrefs)) && string(first(allCrefs)) == "time" diff --git a/src/CodeGeneration/MTK_CodeGeneration.jl b/src/CodeGeneration/MTK_CodeGeneration.jl index e4371d8d..4ef69a40 100644 --- a/src/CodeGeneration/MTK_CodeGeneration.jl +++ b/src/CodeGeneration/MTK_CodeGeneration.jl @@ -2334,7 +2334,7 @@ function _fixedPointInitialConditions(ifEq::SimulationCode.IF_EQUATION, simCode, local conds = Any[] local closed = Bool[] for b in condBranches - push!(conds, transformToMTKContinousConditionEquation(b.condition, simCode)) + push!(conds, transformToMTKContinuousConditionEquation(b.condition, simCode)) push!(closed, MTK_CodeGenerationUtil.condClosedAtBoundary(b.condition)) end local valMap = nothing @@ -2417,7 +2417,7 @@ function createIfEquation(stateVariables::Vector, for b in ifEq.branches b.identifier == -1 && continue try - local mc = transformToMTKContinousConditionEquation(b.condition, simCode) + local mc = transformToMTKContinuousConditionEquation(b.condition, simCode) push!(allZcs, _extractZeroCrossingLHS(mc)) push!(allClosed, MTK_CodeGenerationUtil.condClosedAtBoundary(b.condition)) catch @@ -2457,7 +2457,7 @@ function createIfEquation(stateVariables::Vector, end SimulationCode.BRANCH(condition, residuals, _, targets, _, _, _, _, _) => begin condIdx += 1 - local mtkCond = transformToMTKContinousConditionEquation(branch.condition, simCode) + local mtkCond = transformToMTKContinuousConditionEquation(branch.condition, simCode) #= Evaluate the initial value condition; the original operator decides the zc == 0 boundary. Precomputed via the relay-aware fixed point. =# local _closedB = MTK_CodeGenerationUtil.condClosedAtBoundary(branch.condition) @@ -3383,13 +3383,13 @@ function createSelfSchedulingTimeWhenEvents(simCode)::Vector{Expr} isempty(modN.args[1].args) && continue local (fnI, obsI, modI) = _selfSchedAffectParts(weq, simCode; atInit = true) for rel in rels - #= transformToMTKContinousCondition emits `pre(nextTimeEvent) - time` for + #= transformToMTKContinuousCondition emits `pre(nextTimeEvent) - time` for `time >= pre(nextTimeEvent)`, which falls through zero as time reaches the event. Negate so the crossing RISES through zero exactly when the Modelica condition becomes true (the when's rising edge), and fire only on that positive edge (affect_neg = nothing). A monotone time event is one-directional, so a single edge is correct and avoids double-firing. =# - local zc = transformToMTKContinousCondition(rel, simCode) + local zc = transformToMTKContinuousCondition(rel, simCode) push!(events, :(ModelingToolkit.SymbolicContinuousCallback( (-($(zc)) ~ 0), ModelingToolkit.ImperativeAffect($(fn), $(modN); observed = $(obs)); @@ -3414,7 +3414,7 @@ function createDiscreteBoolWhenEvents(simCode)::Vector{Expr} local isEdge = _isEdgeWhenCondition(weq.whenEquation.condition) local assigns = _gatherClusterAssigns(weq, simCode) assigns === nothing && continue - #= One callback per relation in the cluster. transformToMTKContinousCondition + #= One callback per relation in the cluster. transformToMTKContinuousCondition normalises zc so relation-TRUE ⟺ zc<0: the `=>` affect is the up-crossing (relation becomes FALSE) and affect_neg the down-crossing (relation becomes TRUE). Each callback rewrites the WHOLE ordered cluster with THIS relation @@ -3429,7 +3429,7 @@ function createDiscreteBoolWhenEvents(simCode)::Vector{Expr} local hasInit = _condHasInitial(weq.whenEquation.condition) local affInit = Expr[_discreteAffectEqInit(d, r, ii, simCode) for (d, r, ii) in assigns] for (relIdx, rel) in enumerate(rels) - local zc = transformToMTKContinousCondition(rel, simCode) + local zc = transformToMTKContinuousCondition(rel, simCode) if preMem #= Live ImperativeAffects that commit to DISCRETE_PRE_MEM so a dt=0 cascade across the cluster's callbacks latches. The FIRING relation @@ -3748,7 +3748,7 @@ function createArrayParameterPrelude(simCode::SimulationCode.SIM_CODE)::Vector{E ARRAY_PARAMETERs in HT so we only emit parents that actually exist. =# local _residualCrefs = OrderedSet{String}() for eq in simCode.residualEquations - SimulationCode.collectCrefNames!(_residualCrefs, SimulationCode.toDAEExp(eq.exp)) + SimulationCode.collectCrefNames!(_residualCrefs, eq.exp) end for _n in _residualCrefs local _bracket = findfirst('[', _n) diff --git a/src/CodeGeneration/MTK_CodeGenerationUtil.jl b/src/CodeGeneration/MTK_CodeGenerationUtil.jl index 9172b1f5..1a848260 100644 --- a/src/CodeGeneration/MTK_CodeGenerationUtil.jl +++ b/src/CodeGeneration/MTK_CodeGenerationUtil.jl @@ -155,7 +155,7 @@ end """ Transforms a DAE Condition into a MTK continuous condition. """ -function transformToMTKContinousCondition(cond, simCode) +function transformToMTKContinuousCondition(cond, simCode) # @match patterns are DAE.* only; convert SIM-side conditions at entry. if cond isa SimulationCode.Exp cond = SimulationCode.toDAEExp(cond) @@ -187,22 +187,22 @@ function transformToMTKContinousCondition(cond, simCode) :(0.5 - $(expToJuliaExpMTK(cond, simCode))) end DAE.LBINARY(e1, DAE.OR(__), e2) => begin - :(min($(transformToMTKContinousCondition(e1, simCode)), - $(transformToMTKContinousCondition(e2, simCode)))) + :(min($(transformToMTKContinuousCondition(e1, simCode)), + $(transformToMTKContinuousCondition(e2, simCode)))) end DAE.LBINARY(e1, DAE.AND(__), e2) => begin - :(max($(transformToMTKContinousCondition(e1, simCode)), - $(transformToMTKContinousCondition(e2, simCode)))) + :(max($(transformToMTKContinuousCondition(e1, simCode)), + $(transformToMTKContinuousCondition(e2, simCode)))) end #= Logical NOT: negate the inner condition =# DAE.LUNARY(DAE.NOT(__), e) => begin - :(-($(transformToMTKContinousCondition(e, simCode)))) + :(-($(transformToMTKContinuousCondition(e, simCode)))) end #= Strip noEvent wrapper and recurse =# DAE.CALL(Absyn.IDENT("noEvent"), lst, _) => begin local innerArgs = collect(lst) if length(innerArgs) == 1 - transformToMTKContinousCondition(innerArgs[1], simCode) + transformToMTKContinuousCondition(innerArgs[1], simCode) else throw("noEvent with multiple arguments not supported in condition: " * string(cond)) end @@ -226,7 +226,7 @@ end """ Transforms a DAE Condition into a MTK continuous condition equation. """ -function transformToMTKContinousConditionEquation(cond, simCode) +function transformToMTKContinuousConditionEquation(cond, simCode) # @match patterns are DAE.* only; convert SIM-side conditions at entry. if cond isa SimulationCode.Exp cond = SimulationCode.toDAEExp(cond) @@ -263,22 +263,22 @@ function transformToMTKContinousConditionEquation(cond, simCode) :(0.5 - $(expToJuliaExpMTK(cond, simCode)) ~ 0) end DAE.LBINARY(e1, DAE.OR(__), e2) => begin - :(min($(transformToMTKContinousCondition(e1, simCode)), - $(transformToMTKContinousCondition(e2, simCode))) ~ 0) + :(min($(transformToMTKContinuousCondition(e1, simCode)), + $(transformToMTKContinuousCondition(e2, simCode))) ~ 0) end DAE.LBINARY(e1, DAE.AND(__), e2) => begin - :(max($(transformToMTKContinousCondition(e1, simCode)), - $(transformToMTKContinousCondition(e2, simCode))) ~ 0) + :(max($(transformToMTKContinuousCondition(e1, simCode)), + $(transformToMTKContinuousCondition(e2, simCode))) ~ 0) end #= Logical NOT: negate the inner condition =# DAE.LUNARY(DAE.NOT(__), e) => begin - :(-($(transformToMTKContinousCondition(e, simCode))) ~ 0) + :(-($(transformToMTKContinuousCondition(e, simCode))) ~ 0) end #= Strip noEvent wrapper and recurse =# DAE.CALL(Absyn.IDENT("noEvent"), lst, _) => begin local innerArgs = collect(lst) if length(innerArgs) == 1 - transformToMTKContinousConditionEquation(innerArgs[1], simCode) + transformToMTKContinuousConditionEquation(innerArgs[1], simCode) else throw("noEvent with multiple arguments not supported in condition: " * string(cond)) end @@ -496,10 +496,7 @@ expToJuliaExpMTK(exp::SimulationCode.WILD, simCode::SimulationCode.SIM_CODE; function expToJuliaExpMTK(exp::SimulationCode.ENUM_LITERAL, simCode::SimulationCode.SIM_CODE; varSuffix = "", varPrefix = "", derSymbol::Bool = false)::Expr - return quote - $(LineNumberNode(@__LINE__, "$(string(exp.path)) ENUM")) - $(exp.index) - end + return quote $(exp.index) end end function expToJuliaExpMTK(exp::SimulationCode.EXP_CREF, simCode::SimulationCode.SIM_CODE; @@ -523,10 +520,7 @@ function expToJuliaExpMTK(exp::SimulationCode.EXP_CREF, simCode::SimulationCode. string(nameStr, "[", join(exp.cref.subs, ","), "]") local htEntry = get(hashTable, lookUpStr, nothing) if htEntry !== nothing - return quote - $(LineNumberNode(@__LINE__, "SIM cref: $lookUpStr")) - $(Symbol(htEntry[2].name)) - end + return quote $(Symbol(htEntry[2].name)) end end local (aliasResolved, aliasExpr) = resolveAliasedCref(lookUpStr, simCode, hashTable, varPrefix = varPrefix, varSuffix = varSuffix) diff --git a/src/CodeGeneration/algorithmic.jl b/src/CodeGeneration/algorithmic.jl index 783ae26f..daf0325d 100644 --- a/src/CodeGeneration/algorithmic.jl +++ b/src/CodeGeneration/algorithmic.jl @@ -29,7 +29,7 @@ function ensureAlgArrayLength!(arr::Vector, idx) end ensureAlgArrayLength!(arr, idx) = arr -_algAssignedMaxIndex(idx::Integer) = Int(idx) +_algAssignedMaxIndex(idx::Int) = Int(idx) _algAssignedMaxIndex(idx::AbstractUnitRange) = isempty(idx) ? 0 : Int(last(idx)) _algAssignedMaxIndex(idx::Colon) = 0 function _algAssignedMaxIndex(idx) diff --git a/src/CodeGeneration/codeGen.jl b/src/CodeGeneration/codeGen.jl index 516d9547..a4e7322a 100644 --- a/src/CodeGeneration/codeGen.jl +++ b/src/CodeGeneration/codeGen.jl @@ -743,7 +743,7 @@ function eqToJulia(eq::Union{BDAE.WHEN_EQUATION, SimulationCode.WHEN_EQUATION}, DAE.CALL(Absyn.IDENT("sample"), args, attrs) => true _ => false end - local isContinuousCond::Bool = isContinousCondition(wEqCondDAE, simCode) + local isContinuousCond::Bool = isContinuousCondition(wEqCondDAE, simCode) #= Table / time-driven sources: a when whose condition is purely change(time>=c) thresholds must fire AT those times via PresetTimeCallback — a ContinuousCallback rootfinding on the spiky change() value never detects the crossings. =# @@ -760,7 +760,7 @@ function eqToJulia(eq::Union{BDAE.WHEN_EQUATION, SimulationCode.WHEN_EQUATION}, end end #= A `sample(start, period)` is a periodic clock even when its interval is a - parameter, which isContinousCondition mis-flags as continuous; keep all + parameter, which isContinuousCondition mis-flags as continuous; keep all samples on the periodic branch. =# if isContinuousCond && !isPeriodic local isElseIf = if wEq.elsewhenPart !== nothing diff --git a/src/CodeGeneration/modelicaBuiltins.jl b/src/CodeGeneration/modelicaBuiltins.jl index df8077b9..f6ebf956 100644 --- a/src/CodeGeneration/modelicaBuiltins.jl +++ b/src/CodeGeneration/modelicaBuiltins.jl @@ -341,7 +341,7 @@ until ndims(A) == n. For example, promote(vector, 2) turns a See Modelica Spec 3.4, Section 10.3.3. NOT the same as Julia's promote() which does type promotion. """ -function modelica_promote(A, n::Integer) +function modelica_promote(A, n::Int) currentDims = ndims(A) if currentDims >= n return A @@ -491,9 +491,9 @@ are accepted but ignored; the result is sufficient for any downstream use that just consumes the digits (e.g. table-name suffixes in Media examples). """ modelica_String(x) = string(x) -modelica_String(x, sigDigits::Integer) = string(x) -modelica_String(x, sigDigits::Integer, minLen::Integer) = string(x) -modelica_String(x, sigDigits::Integer, minLen::Integer, leftAdjust::Bool) = string(x) +modelica_String(x, sigDigits::Int) = string(x) +modelica_String(x, sigDigits::Int, minLen::Int) = string(x) +modelica_String(x, sigDigits::Int, minLen::Int, leftAdjust::Bool) = string(x) """ modelica_homotopy(actual, simplified) diff --git a/src/CodeGeneration/mtkExternals.jl b/src/CodeGeneration/mtkExternals.jl index ef4bc1dd..ad09d5a1 100644 --- a/src/CodeGeneration/mtkExternals.jl +++ b/src/CodeGeneration/mtkExternals.jl @@ -1113,25 +1113,28 @@ function wrapWithInvokelatest(expr::Expr) end return Expr(:call, newArgs...) end - #= Recursively process arguments; return original if nothing changed =# - local changed = false - local newArgs = copy(expr.args) + #= Recursively process arguments; allocate a new args vector only when a + child actually changes, else return the original Expr unchanged. =# + local newArgs = nothing for (i, a) in enumerate(expr.args) - r = wrapWithInvokelatest(a) - newArgs[i] = r - changed |= r !== a + local r = wrapWithInvokelatest(a) + if r !== a + newArgs === nothing && (newArgs = copy(expr.args)) + newArgs[i] = r + end end - return changed ? Expr(:call, newArgs...) : expr + return newArgs === nothing ? expr : Expr(:call, newArgs...) end - #= For all other Expr types; return original if nothing changed =# - local changed = false - local newArgs = copy(expr.args) + #= For all other Expr types; allocate only when a child changes. =# + local newArgs = nothing for (i, a) in enumerate(expr.args) - r = wrapWithInvokelatest(a) - newArgs[i] = r - changed |= r !== a + local r = wrapWithInvokelatest(a) + if r !== a + newArgs === nothing && (newArgs = copy(expr.args)) + newArgs[i] = r + end end - return changed ? Expr(expr.head, newArgs...) : expr + return newArgs === nothing ? expr : Expr(expr.head, newArgs...) end wrapWithInvokelatest(x) = x #= For non-Expr types, return as-is =# diff --git a/src/CodeGeneration/structuralCallbacks.jl b/src/CodeGeneration/structuralCallbacks.jl index 52ea9f4e..b514224b 100644 --- a/src/CodeGeneration/structuralCallbacks.jl +++ b/src/CodeGeneration/structuralCallbacks.jl @@ -46,13 +46,13 @@ end """ Creates a single structural callback for an explicit transition. """ -function createStructuralCallback(simCode, simCodeStructuralTransition::SimulationCode.EXPLICIT_STRUCTURAL_TRANSISTION, idx) +function createStructuralCallback(simCode, simCodeStructuralTransition::SimulationCode.EXPLICIT_STRUCTURAL_TRANSITION, idx) local structuralTransition = simCodeStructuralTransition local callbackName = createCallbackName(structuralTransition, 0) # SIM.Exp -> DAE.Exp at the boundary; DAE-typed helpers below. - local conditionDAE = SimulationCode.toDAEExp(structuralTransition.transistionCondition) + local conditionDAE = SimulationCode.toDAEExp(structuralTransition.transitionCondition) - if isContinousCondition(conditionDAE, simCode) + if isContinuousCondition(conditionDAE, simCode) local cond = transformToZeroCrossingCondition(conditionDAE) quote function $(Symbol(callbackName))(destinationSystem, callbacks) @@ -141,7 +141,7 @@ TODO: Also make sure to create possible other elements in the structural when equation """ function createStructuralCallback(simCode, - simCodeStructuralTransition::SimulationCode.IMPLICIT_STRUCTURAL_TRANSISTION, + simCodeStructuralTransition::SimulationCode.IMPLICIT_STRUCTURAL_TRANSITION, idx) local structuralTransition = simCodeStructuralTransition local callbackName = createCallbackName(structuralTransition, idx) @@ -198,7 +198,7 @@ function createStructuralCallback(simCode, end end _ #=Continuous or discrete =# => begin - if isContinousCondition(whenCondition, simCode) + if isContinuousCondition(whenCondition, simCode) local zeroCrossingCond = transformToZeroCrossingCondition(whenCondition) quote $(affect) @@ -323,10 +323,10 @@ function createStructuralAssignments(simCode, structuralTransitions::Vector{ST}) local idx = 1 for structuralTransisiton in structuralTransitions @match structuralTransisiton begin - SimulationCode.EXPLICIT_STRUCTURAL_TRANSISTION(__) => begin + SimulationCode.EXPLICIT_STRUCTURAL_TRANSITION(__) => begin push!(structuralAssignments, createStructuralAssignment(simCode, structuralTransisiton)) end - SimulationCode.IMPLICIT_STRUCTURAL_TRANSISTION(__) => begin + SimulationCode.IMPLICIT_STRUCTURAL_TRANSITION(__) => begin push!(structuralAssignments, createStructuralAssignment(simCode, structuralTransisiton, idx)) end SimulationCode.DYNAMIC_OVERCONSTRAINED_CONNECTOR_EQUATION(__) => begin @@ -347,7 +347,7 @@ end This function creates a structural assignment. That is the constructor for a structural callback guiding structural change. """ -function createStructuralAssignment(simCode, simCodeStructuralTransition::SimulationCode.EXPLICIT_STRUCTURAL_TRANSISTION) +function createStructuralAssignment(simCode, simCodeStructuralTransition::SimulationCode.EXPLICIT_STRUCTURAL_TRANSITION) local structuralTransition = simCodeStructuralTransition local callbackName = createCallbackName(structuralTransition) local toState = structuralTransition.toState @@ -367,7 +367,7 @@ end Creates a structural assignment for an implicit structural transition. These are numbered from 1->N """ -function createStructuralAssignment(simCode, simCodeStructuralTransition::SimulationCode.IMPLICIT_STRUCTURAL_TRANSISTION, idx::Int) +function createStructuralAssignment(simCode, simCodeStructuralTransition::SimulationCode.IMPLICIT_STRUCTURAL_TRANSITION, idx::Int) local structuralTransition = simCodeStructuralTransition local callbackName = createCallbackName(structuralTransition, idx) local integratorCallbackName = string(callbackName, "_CALLBACK") @@ -391,7 +391,7 @@ function createStructuralAssignment(simCode, simCodeStructuralTransition::Simula end end -function createCallbackName(structuralTransisiton::SimulationCode.EXPLICIT_STRUCTURAL_TRANSISTION, idx = 0) +function createCallbackName(structuralTransisiton::SimulationCode.EXPLICIT_STRUCTURAL_TRANSITION, idx = 0) return "structuralCallback" * structuralTransisiton.fromState * structuralTransisiton.toState end @@ -399,7 +399,7 @@ end Creates a structural callback for the when equation. The name is up to change. """ -function createCallbackName(structuralTransisiton::SimulationCode.IMPLICIT_STRUCTURAL_TRANSISTION, idx::Int) +function createCallbackName(structuralTransisiton::SimulationCode.IMPLICIT_STRUCTURAL_TRANSITION, idx::Int) return string("structuralCallbackWhenEquation", idx) end diff --git a/src/FrontendUtil/Util.jl b/src/FrontendUtil/Util.jl index 07a3d246..16ae3f69 100644 --- a/src/FrontendUtil/Util.jl +++ b/src/FrontendUtil/Util.jl @@ -33,7 +33,7 @@ Base.@nospecializeinfer function traverseExpTopDown1(continueTraversal::Bool, @n local attr::DAE.CallAttributes local cr::DAE.ComponentRef local cr_1::DAE.ComponentRef - local dim::ModelicaInteger + local dim::Int local e1::DAE.Exp local e1_1::DAE.Exp local e2::DAE.Exp @@ -52,9 +52,9 @@ Base.@nospecializeinfer function traverseExpTopDown1(continueTraversal::Bool, @n local ext_arg_3::Type_a local fieldNames::List{String} local fn::Absyn.Path - local i::ModelicaInteger - local index_::ModelicaInteger - local isExpisASUB::Option{Tuple{DAE.Exp, ModelicaInteger, ModelicaInteger}} + local i::Int + local index_::Int + local isExpisASUB::Option{Tuple{DAE.Exp, Int, Int}} local lstexpl::List{List{DAE.Exp}} local lstexpl_1::List{List{DAE.Exp}} local op::DAE.Operator @@ -160,22 +160,22 @@ Base.@nospecializeinfer function traverseExpTopDown1(continueTraversal::Bool, @n (DAE.CALL(path = fn, expLst = expl, attr = attr), rel, ext_arg) => begin (expl_1, ext_arg_1) = traverseExpListTopDown(expl, rel, ext_arg) - (DAE.CALL(fn, expl_1, attr), ext_arg_1) + (referenceEq(expl, expl_1) ? inExp : DAE.CALL(fn, expl_1, attr), ext_arg_1) end (DAE.RECORD(path = fn, exps = expl, comp = fieldNames, ty = tp), rel, ext_arg) => begin (expl_1, ext_arg_1) = traverseExpListTopDown(expl, rel, ext_arg) - (DAE.RECORD(fn, expl_1, fieldNames, tp), ext_arg_1) + (referenceEq(expl, expl_1) ? inExp : DAE.RECORD(fn, expl_1, fieldNames, tp), ext_arg_1) end (DAE.PARTEVALFUNCTION(fn, expl, tp, t), rel, ext_arg) => begin (expl_1, ext_arg_1) = traverseExpListTopDown(expl, rel, ext_arg) - (DAE.PARTEVALFUNCTION(fn, expl_1, tp, t), ext_arg_1) + (referenceEq(expl, expl_1) ? inExp : DAE.PARTEVALFUNCTION(fn, expl_1, tp, t), ext_arg_1) end (DAE.ARRAY(ty = tp, scalar = scalar, array = expl), rel, ext_arg) => begin (expl_1, ext_arg_1) = traverseExpListTopDown(expl, rel, ext_arg) - (DAE.ARRAY(tp, scalar, expl_1), ext_arg_1) + (referenceEq(expl, expl_1) ? inExp : DAE.ARRAY(tp, scalar, expl_1), ext_arg_1) end (DAE.MATRIX(ty = tp, integer = dim, matrix = lstexpl), rel, ext_arg) => begin @@ -206,7 +206,7 @@ Base.@nospecializeinfer function traverseExpTopDown1(continueTraversal::Bool, @n (DAE.TUPLE(PR = expl), rel, ext_arg) => begin (expl_1, ext_arg_1) = traverseExpListTopDown(expl, rel, ext_arg) - (DAE.TUPLE(expl_1), ext_arg_1) + (referenceEq(expl, expl_1) ? inExp : DAE.TUPLE(expl_1), ext_arg_1) end (DAE.CAST(ty = tp, exp = e1), rel, ext_arg) => begin @@ -276,7 +276,7 @@ Base.@nospecializeinfer function traverseExpTopDown1(continueTraversal::Bool, @n (DAE.LIST(expl), rel, ext_arg) => begin (expl_1, ext_arg_1) = traverseExpListTopDown(expl, rel, ext_arg) - (DAE.LIST(expl_1), ext_arg_1) + (referenceEq(expl, expl_1) ? inExp : DAE.LIST(expl_1), ext_arg_1) end (DAE.UNBOX(e1, tp), rel, ext_arg) => begin @@ -308,17 +308,31 @@ end function traverseExpListTopDown(expLst::List{DAE.Exp}, func::Function, inArg) outArg = inArg - newExpLst = DAE.Exp[] - allEqual = true + #= Allocate the result buffer lazily: a read-only or no-op traversal leaves + every element identity-equal, so the common case allocates nothing and + returns the original list. =# + local newExpLst::Union{Nothing, Vector{DAE.Exp}} = nothing + local i = 0 for e in expLst + i += 1 (newE, outArg) = traverseExpTopDown(e, func, outArg) - push!(newExpLst, newE) - if !referenceEq(e, newE) - allEqual = false + if newExpLst === nothing + if !referenceEq(e, newE) + #= First change: materialize the unchanged prefix, then this element. =# + newExpLst = DAE.Exp[] + local j = 0 + for pe in expLst + j += 1 + j < i || break + push!(newExpLst, pe) + end + push!(newExpLst, newE) + end + else + push!(newExpLst, newE) end end - #= Return original list if nothing changed to preserve identity =# - return allEqual ? (expLst, outArg) : (list(newExpLst...), outArg) + return newExpLst === nothing ? (expLst, outArg) : (list(newExpLst...), outArg) end """ @@ -425,7 +439,7 @@ function traverseExpTopDownSubs(inSubscript::List{<:DAE.Subscript}, rel::Functio local arg::Argument = iarg local acc::Vector{DAE.Subscript} local exp::DAE.Exp - local nEq::ModelicaInteger = 0 + local nEq::Int = 0 local nsub::DAE.Subscript local outSubscript::List{DAE.Subscript} @@ -529,10 +543,10 @@ Base.@nospecializeinfer function traverseExpBottomUp(@nospecialize(inExp::DAE.Ex local scalar::Bool local tp::DAE.Type local t::DAE.Type - local i::Integer + local i::Int local lstexpl_1::List{List{DAE.Exp}} local lstexpl::List{List{DAE.Exp}} - local dim::Integer + local dim::Int local str::String local localDecls::List{DAE.Element} local fieldNames::List{String} @@ -540,8 +554,8 @@ Base.@nospecializeinfer function traverseExpBottomUp(@nospecialize(inExp::DAE.Ex local cases::List{DAE.MatchCase} local cases_1::List{DAE.MatchCase} local matchTy::DAE.MatchType - local index_::Integer - local isExpisASUB::Option{Tuple{DAE.Exp, Integer, Integer}} + local index_::Int + local isExpisASUB::Option{Tuple{DAE.Exp, Int, Int}} local reductionInfo::DAE.ReductionInfo local riters::DAE.ReductionIterators local riters_1::DAE.ReductionIterators @@ -1032,7 +1046,7 @@ function traverseExpCref(inCref::DAE.ComponentRef, rel::Function, iarg::T) ::Tup local subs::List{DAE.Subscript} local subs_1::List{DAE.Subscript} local arg::Type_a - local ix::Integer + local ix::Int local instant::String @match (inCref, rel, iarg) begin (DAE.CREF_QUAL(ident = name, identType = ty, subscriptLst = subs, componentRef = cr), _, arg) => begin diff --git a/src/Runtime/Runtime.jl b/src/Runtime/Runtime.jl index 651eb98d..780c8969 100644 --- a/src/Runtime/Runtime.jl +++ b/src/Runtime/Runtime.jl @@ -638,7 +638,7 @@ function solve(omProblem::OM_ProblemRecompilation, tspan::Tuple, alg; kwargs...) =# @VSS_DEBUG @info "solutionAtChange.t" cb.solutionAtChange.t local stopIdx = findlast((x) -> x == timeBeforeCallbackWasApplied, cb.solutionAtChange.t) - @assert stopIdx !== nothing "Invalid callback occured during simulation" + @assert stopIdx !== nothing "Invalid callback occurred during simulation" @VSS_DEBUG @info "stopIdx" stopIdx solAtChange = cb.solutionAtChange #Used for error checking local modifiedSol = deepcopy(cb.solutionAtChange) diff --git a/src/SimulationCode/simCodeData.jl b/src/SimulationCode/simCodeData.jl index d87a8ea5..a730ed4f 100644 --- a/src/SimulationCode/simCodeData.jl +++ b/src/SimulationCode/simCodeData.jl @@ -56,9 +56,9 @@ end SimCref(sym::Symbol) = SimCref(sym, Int[]) SimCref(name::AbstractString) = SimCref(Symbol(name), Int[]) -SimCref(name::AbstractString, subs::AbstractVector{<:Integer}) = +SimCref(name::AbstractString, subs::AbstractVector{<:Int}) = SimCref(Symbol(name), Int[s for s in subs]) -SimCref(sym::Symbol, subs::AbstractVector{<:Integer}) = +SimCref(sym::Symbol, subs::AbstractVector{<:Int}) = SimCref(sym, Int[s for s in subs]) #= ---- Expression hierarchy (additive, no migration yet) ---- @@ -304,7 +304,7 @@ Modelica `algorithm` section. Side-effecting statements; emitted by backends as a sequence of assignments inside a generated function. """ struct ALGORITHM <: Equation - size::Integer + size::Int alg::DAE.Algorithm source::DAE.ElementSource expand::DAE.Expand @@ -385,7 +385,7 @@ end Runtime `when` clause with a SimCode-native `WHEN_STMTS` body. """ struct WHEN_EQUATION <: Equation - size::Integer + size::Int whenEquation::WHEN_STMTS source::DAE.ElementSource attr::EQ_ATTR @@ -399,7 +399,7 @@ runtime. Same shape as `WHEN_EQUATION`, distinct type so init-time and runtime when-bodies stay separated through codegen. """ struct INITIAL_WHEN_EQUATION <: Equation - size::Integer + size::Int whenEquation::WHEN_STMTS source::DAE.ElementSource attr::EQ_ATTR @@ -585,7 +585,7 @@ struct BRANCH{T1 <: Exp, T5 <: Vector{Int}, T6 <: Graphs.AbstractGraph, T7 <: Vector{Vector{Int}}, - T8 <: AbstractDict{String, Tuple{Integer, SimVar}}} <: Construct + T8 <: AbstractDict{String, Tuple{Int, SimVar}}} <: Construct condition::T1 residualEquations::T2 @@ -609,26 +609,26 @@ end abstract type StructuralTransition end """ - EXPLICIT_STRUCTURAL_TRANSISTION(fromState, toState, transistionCondition) + EXPLICIT_STRUCTURAL_TRANSITION(fromState, toState, transitionCondition) Explicit transition between two named structural submodels. Fires the -`transistionCondition` and recompiles into `toState`. +`transitionCondition` and recompiles into `toState`. """ -struct EXPLICIT_STRUCTURAL_TRANSISTION <: StructuralTransition +struct EXPLICIT_STRUCTURAL_TRANSITION <: StructuralTransition fromState::String toState::String - transistionCondition::Exp + transitionCondition::Exp end """ - IMPLICIT_STRUCTURAL_TRANSISTION(size, whenEquation, source, attr) + IMPLICIT_STRUCTURAL_TRANSITION(size, whenEquation, source, attr) Implicit structural transition embedded in a `when` clause; the final state is not known until the when-body executes. Body is a SimCode-native `WHEN_STMTS`. """ -struct IMPLICIT_STRUCTURAL_TRANSISTION <: StructuralTransition - size::Integer +struct IMPLICIT_STRUCTURAL_TRANSITION <: StructuralTransition + size::Int whenEquation::WHEN_STMTS source::DAE.ElementSource attr::EQ_ATTR @@ -707,7 +707,7 @@ The topmost model of a system consisting of several sub models lacks: This information is instead contained for each of the structural submodels, where one model is active at the time. """ struct SIM_CODE{T0<:String, - T1<:AbstractDict{String, Tuple{Integer, SimVar}}, + T1<:AbstractDict{String, Tuple{Int, SimVar}}, T2<:Vector{RESIDUAL_EQUATION}, T22, T4<:Vector{WHEN_EQUATION}, diff --git a/src/SimulationCode/simCodeDump.jl b/src/SimulationCode/simCodeDump.jl index a3c216ca..822bf9e8 100644 --- a/src/SimulationCode/simCodeDump.jl +++ b/src/SimulationCode/simCodeDump.jl @@ -395,14 +395,14 @@ function Base.string(ieq::SimulationCode.DYNAMIC_OVERCONSTRAINED_CONNECTOR_EQUAT "STRUCTURAL_DOCC_IF_EQUATION: " * string(ieq.ifEquation) * "\n" end -function string(st::IMPLICIT_STRUCTURAL_TRANSISTION) +function string(st::IMPLICIT_STRUCTURAL_TRANSITION) "STRUCTURAL_WHEN_EQUATION:\n" * string(st.whenEquation) end -function Base.string(simStructChange::SimulationCode.EXPLICIT_STRUCTURAL_TRANSISTION) +function Base.string(simStructChange::SimulationCode.EXPLICIT_STRUCTURAL_TRANSITION) return "STRUCTURAL_TRANSITION: FROM: <" * simStructChange.fromState * "> TO: <" * simStructChange.toState * - "> WHEN: " * string(simStructChange.transistionCondition) * "\n" + "> WHEN: " * string(simStructChange.transitionCondition) * "\n" end function string(f::EXTERNAL_MODELICA_FUNCTION) diff --git a/src/SimulationCode/simCodeFunctions.jl b/src/SimulationCode/simCodeFunctions.jl index 92190207..506c8ba2 100644 --- a/src/SimulationCode/simCodeFunctions.jl +++ b/src/SimulationCode/simCodeFunctions.jl @@ -714,6 +714,77 @@ function resolveConstantIfExp(exp::DAE.Exp, simCode::SIM_CODE)::DAE.Exp end end +#= SIM-native mirror of resolveConstantIfExp(::DAE.Exp, simCode): recurses on the + SimCode Exp spine so the per-residual caller (pruneConstantConditions via + _rewriteResidualIfExp) need not build a whole-tree DAE copy. === identity is + preserved so unchanged subtrees are reused (no per-node toSimExp round-trip); + only the small IFEXP condition round-trips through tryEvalCondition's DAE arm. + Arms mirror the DAE method 1:1 on SIM struct fields. =# +function resolveConstantIfExp(exp::Exp, simCode::SIM_CODE)::Exp + if exp isa IFEXP + local resolved = tryEvalCondition(exp.cond, simCode) + if resolved === true + return resolveConstantIfExp(exp.thenExp, simCode) + elseif resolved === false + return resolveConstantIfExp(exp.elseExp, simCode) + end + local nc = resolveConstantIfExp(exp.cond, simCode) + local nt = resolveConstantIfExp(exp.thenExp, simCode) + local ne = resolveConstantIfExp(exp.elseExp, simCode) + return (nc === exp.cond && nt === exp.thenExp && ne === exp.elseExp) ? exp : IFEXP(nc, nt, ne) + elseif exp isa BINARY + local n1 = resolveConstantIfExp(exp.exp1, simCode) + local n2 = resolveConstantIfExp(exp.exp2, simCode) + return (n1 === exp.exp1 && n2 === exp.exp2) ? exp : BINARY(n1, exp.op, n2) + elseif exp isa UNARY + local n1 = resolveConstantIfExp(exp.exp, simCode) + return n1 === exp.exp ? exp : UNARY(exp.op, n1) + elseif exp isa LBINARY + local n1 = resolveConstantIfExp(exp.exp1, simCode) + local n2 = resolveConstantIfExp(exp.exp2, simCode) + return (n1 === exp.exp1 && n2 === exp.exp2) ? exp : LBINARY(n1, exp.op, n2) + elseif exp isa LUNARY + local n1 = resolveConstantIfExp(exp.exp, simCode) + return n1 === exp.exp ? exp : LUNARY(exp.op, n1) + elseif exp isa RELATION + local n1 = resolveConstantIfExp(exp.exp1, simCode) + local n2 = resolveConstantIfExp(exp.exp2, simCode) + return (n1 === exp.exp1 && n2 === exp.exp2) ? exp : RELATION(n1, exp.op, n2, exp.index) + elseif exp isa CAST + local n1 = resolveConstantIfExp(exp.exp, simCode) + return n1 === exp.exp ? exp : CAST(exp.ty, n1) + elseif exp isa CALL + local changed = false + local newArgs = Exp[] + for arg in exp.args + local na = resolveConstantIfExp(arg, simCode) + changed |= na !== arg + push!(newArgs, na) + end + return changed ? CALL(exp.path, newArgs, exp.attr) : exp + elseif exp isa ARRAY_EXP + local changed = false + local newEls = Exp[] + for el in exp.elements + local nel = resolveConstantIfExp(el, simCode) + changed |= nel !== el + push!(newEls, nel) + end + return changed ? ARRAY_EXP(exp.ty, exp.scalar, newEls) : exp + elseif exp isa ASUB + local n1 = resolveConstantIfExp(exp.exp, simCode) + local changed = n1 !== exp.exp + local newSubs = Exp[] + for sub in exp.subs + local ns = resolveConstantIfExp(sub, simCode) + changed |= ns !== sub + push!(newSubs, ns) + end + return changed ? ASUB(n1, newSubs) : exp + end + return exp +end + """ Try to evaluate a DAE condition expression to a Bool. Returns `true`, `false`, or `nothing` if evaluation is not possible. diff --git a/src/SimulationCode/simCodeStructureUtil.jl b/src/SimulationCode/simCodeStructureUtil.jl index a98f69a3..120716eb 100644 --- a/src/SimulationCode/simCodeStructureUtil.jl +++ b/src/SimulationCode/simCodeStructureUtil.jl @@ -452,10 +452,10 @@ Base.@nospecializeinfer function ARRAY_EQUATION(@nospecialize(dims::Vector{Int64 return ARRAY_EQUATION(dims, toSimExp(l), r, src, attr) end -Base.@nospecializeinfer function EXPLICIT_STRUCTURAL_TRANSISTION(@nospecialize(fromState::String), +Base.@nospecializeinfer function EXPLICIT_STRUCTURAL_TRANSITION(@nospecialize(fromState::String), @nospecialize(toState::String), - @nospecialize(transistionCondition::DAE.Exp)) - return EXPLICIT_STRUCTURAL_TRANSISTION(fromState, toState, toSimExp(transistionCondition)) + @nospecialize(transitionCondition::DAE.Exp)) + return EXPLICIT_STRUCTURAL_TRANSITION(fromState, toState, toSimExp(transitionCondition)) end # ============================================================================ diff --git a/src/SimulationCode/simCodeUtil.jl b/src/SimulationCode/simCodeUtil.jl index c9694b19..adbe6895 100644 --- a/src/SimulationCode/simCodeUtil.jl +++ b/src/SimulationCode/simCodeUtil.jl @@ -512,8 +512,8 @@ It executes the following steps: 7. Data structure variables are only allowed as parameters and/or constants. They share the index with the parameters. The index of discretes and occ is updated after the state index is calculated. """ -function createIndices(simulationVars::Vector{SimulationCode.SIMVAR})::OrderedDict{String, Tuple{Integer, SimulationCode.SimVar}} - local ht::OrderedDict{String, Tuple{Integer, SimulationCode.SimVar}} = OrderedDict() +function createIndices(simulationVars::Vector{SimulationCode.SIMVAR})::OrderedDict{String, Tuple{Int, SimulationCode.SimVar}} + local ht::OrderedDict{String, Tuple{Int, SimulationCode.SimVar}} = OrderedDict() local stateCounter = 0 local parameterCounter = 0 local discretes = SimulationCode.SIMVAR[] @@ -732,7 +732,7 @@ This function returns the indices of these variables. That is, the index of the algebraic variable is offset by the total number of discrete variables """ function getIndiciesOfVariables(variables, - stringToSimVarHT::OrderedDict{String, Tuple{Integer, SimVar}}) + stringToSimVarHT::OrderedDict{String, Tuple{Int, SimVar}}) local indicies = Int[] for v in variables local varName = DAE_identifierToString(v) @@ -886,7 +886,7 @@ The known irreducibles should be state variables and variables directly involved function getIrreducibleVars(ifEquations::Vector{BDAE.IF_EQUATION}, whenEqs::Vector{BDAE.WHEN_EQUATION}, algebraicAndStateVariables::Vector{BDAE.VAR}, - ht::OrderedDict{String, Tuple{Integer, SimulationCode.SimVar}}) + ht::OrderedDict{String, Tuple{Int, SimulationCode.SimVar}}) local irreducibles::Vector{Any} = [] for eq in ifEquations variablesForEq = Backend.BDAEUtil.getAllVariables(eq, algebraicAndStateVariables) @@ -957,7 +957,7 @@ function handleZimmerThetaConstant(resEqs, irreducibleVars::Vector{String}, ht) return(resEqs, irreducibleVars) end -function getSimVarByName(name::String, ht::AbstractDict{String, Tuple{Integer, SimVar}}) +function getSimVarByName(name::String, ht::AbstractDict{String, Tuple{Int, SimVar}}) return last(ht[name]) end @@ -985,14 +985,14 @@ function makeDummyResidualEquation(equationSystemName::String, idx::Int = 1) end """ - buildBaseNameIndex(ht::OrderedDict{String, Tuple{Integer, SimVar}}) + buildBaseNameIndex(ht::OrderedDict{String, Tuple{Int, SimVar}}) Build a reverse index from base variable names (without subscripts) to all subscripted full names in the hash table. For example, if the HT contains "world_x[1]" and "world_x[2]", the result maps "world_x" => ["world_x[1]", "world_x[2]"]. This handles the ASUB case where `getAllCrefs` extracts a base CREF without subscripts. """ -function buildBaseNameIndex(ht::OrderedDict{String, Tuple{Integer, SimVar}})::Dict{String, Vector{String}} +function buildBaseNameIndex(ht::OrderedDict{String, Tuple{Int, SimVar}})::Dict{String, Vector{String}} local index = Dict{String, Vector{String}}() for (varName, _) in ht local bi = findfirst('[', varName) @@ -1009,7 +1009,7 @@ end """ collectEquationVarNames(exp::DAE.Exp, - ht::OrderedDict{String, Tuple{Integer, SimVar}}, + ht::OrderedDict{String, Tuple{Int, SimVar}}, baseNameToFullNames::Dict{String, Vector{String}}) Extract all variable names referenced by a DAE expression, using the robust @@ -1019,7 +1019,7 @@ matching for ASUB-wrapped CREFs where subscripts are separated from the CREF. Returns a OrderedSet{String} of variable names that exist in the HT. """ function collectEquationVarNames(exp::DAE.Exp, - ht::OrderedDict{String, Tuple{Integer, SimVar}}, + ht::OrderedDict{String, Tuple{Int, SimVar}}, baseNameToFullNames::Dict{String, Vector{String}})::OrderedSet{String} local crefs::List{DAE.ComponentRef} = Util.getAllCrefs(exp) local names = OrderedSet{String}() @@ -1314,10 +1314,15 @@ function _isSyntacticZeroResidual(@nospecialize(exp))::Bool end function _isTrivialResidualEquation(eq::Union{BDAE.RESIDUAL_EQUATION, RESIDUAL_EQUATION}, simCode::SIM_CODE)::Bool - local expDAE = toDAEExp(eq.exp) - if _hasUnknownCref(expDAE, simCode.stringToSimVarHT) + #= A residual referencing any unknown cref cannot be trivial. _hasUnknownCref + collects cref names via collectCrefNames!, which has a SIM-native arm, so + checking eq.exp directly bails WITHOUT a toDAEExp tree for the common case. + This runs after every SimCode pass (~16x), so the dropped per-residual + toDAEExp is heavily amplified. =# + if _hasUnknownCref(eq.exp, simCode.stringToSimVarHT) return false end + local expDAE = toDAEExp(eq.exp) if _isSyntacticZeroResidual(expDAE) return true end @@ -1456,9 +1461,10 @@ function cleanupTrivialResidualEquations(simCode::SIM_CODE; end function _rewriteResidualIfExp(eq::Union{BDAE.RESIDUAL_EQUATION, RESIDUAL_EQUATION}, simCode::SIM_CODE) - local expDAE = toDAEExp(eq.exp) - local newExp = resolveConstantIfExp(expDAE, simCode) - return newExp === expDAE ? eq : typeof(eq)(newExp, eq.source, eq.attr) + #= resolveConstantIfExp dispatches by type: SIM eq.exp -> SIM-native arm (no + whole-tree toDAEExp), DAE eq.exp -> DAE arm; === identity reuse preserved. =# + local newExp = resolveConstantIfExp(eq.exp, simCode) + return newExp === eq.exp ? eq : typeof(eq)(newExp, eq.source, eq.attr) end function _rewriteInitialIfExp(@nospecialize(eq), simCode::SIM_CODE) @@ -1470,11 +1476,9 @@ function _rewriteInitialIfExp(@nospecialize(eq), simCode::SIM_CODE) return (newLhs === eq.lhs && newRhs === eq.rhs) ? eq : BDAE.EQUATION(newLhs, newRhs, eq.source, eq.attributes) elseif eq isa EQUATION - local lhsDAE = toDAEExp(eq.lhs) - local rhsDAE = toDAEExp(eq.rhs) - local newLhs = resolveConstantIfExp(lhsDAE, simCode) - local newRhs = resolveConstantIfExp(rhsDAE, simCode) - return (newLhs === lhsDAE && newRhs === rhsDAE) ? eq : + local newLhs = resolveConstantIfExp(eq.lhs, simCode) + local newRhs = resolveConstantIfExp(eq.rhs, simCode) + return (newLhs === eq.lhs && newRhs === eq.rhs) ? eq : EQUATION(newLhs, newRhs, eq.source, eq.attr) end return eq @@ -2140,6 +2144,50 @@ Returns: side is an unknown and the other is a parameter - `nothing` if the equation does not match any constant pattern """ +#= Classify `unknown = (+/-) param` from the two extracted (name, cref, type) + operand results. Shared by the DAE and SIM-native entry points. =# +function _classifyConstEq(@nospecialize(r1), @nospecialize(r2), negated::Bool, ht) + if r1 === nothing || r2 === nothing + return nothing + end + local (n1, cr1, t1) = r1 + local (n2, cr2, t2) = r2 + if !haskey(ht, n1) || !haskey(ht, n2) + return nothing + end + local (_, sv1) = ht[n1] + local (_, sv2) = ht[n2] + local isUnk1 = isUnknownVarKind(sv1.varKind) + local isUnk2 = isUnknownVarKind(sv2.varKind) + if !isUnk1 && !isUnk2 + #= Both parameters: trivial equation, always satisfied =# + return (:trivial, nothing) + elseif isUnk1 && !isUnk2 + #= n1 is unknown, n2 is parameter: unknown = (+/-)param =# + return (:constprop, (n1, n2, negated, cr2, t2)) + elseif !isUnk1 && isUnk2 + #= n1 is parameter, n2 is unknown: unknown = (+/-)param =# + return (:constprop, (n2, n1, negated, cr1, t1)) + else + #= Both unknowns: handled by alias elimination, not us =# + return nothing + end +end + +#= SIM-native fast path: avoid building a parallel DAE tree per residual every + fixpoint round. Only a top-level `+`/`-` of two bare crefs can be a constant + equation, so bail on cheap `isa` checks; extractCrefName then converts only + the matched leaf. Equivalent to the DAE path: non-cref operands fail + extractCrefName and a WILD operand (the one non-EXP_CREF that maps to a + DAE.CREF) fails the haskey guard. =# +function detectConstantEquation(exp::Exp, ht) + exp isa BINARY || return nothing + (exp.op === OP_SUB || exp.op === OP_ADD) || return nothing + (exp.exp1 isa EXP_CREF && exp.exp2 isa EXP_CREF) || return nothing + return _classifyConstEq(extractCrefName(exp.exp1), extractCrefName(exp.exp2), + exp.op === OP_ADD, ht) +end + function detectConstantEquation(@nospecialize(exp), ht) @match exp begin DAE.BINARY(exp1 = e1, operator = op, exp2 = e2) => begin @@ -2154,35 +2202,7 @@ function detectConstantEquation(@nospecialize(exp), ht) if !isSub && !isAdd return nothing end - local r1 = extractCrefName(e1) - local r2 = extractCrefName(e2) - if r1 === nothing || r2 === nothing - return nothing - end - local (n1, cr1, t1) = r1 - local (n2, cr2, t2) = r2 - if !haskey(ht, n1) || !haskey(ht, n2) - return nothing - end - local (_, sv1) = ht[n1] - local (_, sv2) = ht[n2] - local isUnk1 = isUnknownVarKind(sv1.varKind) - local isUnk2 = isUnknownVarKind(sv2.varKind) - local negated = isAdd - - if !isUnk1 && !isUnk2 - #= Both parameters: trivial equation, always satisfied =# - return (:trivial, nothing) - elseif isUnk1 && !isUnk2 - #= n1 is unknown, n2 is parameter: unknown = (+/-)param =# - return (:constprop, (n1, n2, negated, cr2, t2)) - elseif !isUnk1 && isUnk2 - #= n1 is parameter, n2 is unknown: unknown = (+/-)param =# - return (:constprop, (n2, n1, negated, cr1, t1)) - else - #= Both unknowns: handled by alias elimination, not us =# - return nothing - end + return _classifyConstEq(extractCrefName(e1), extractCrefName(e2), isAdd, ht) end _ => return nothing end @@ -2391,7 +2411,7 @@ function foldParameterClosure(simCode::SIM_CODE)::SIM_CODE local defEqOfVar = Dict{String, Int}() #= varName -> eqIdx of its defining residual =# local defCountOfVar = Dict{String, Int}() #= varName -> #residuals with v on LHS of BINARY SUB =# for (i, eq) in enumerate(simCode.residualEquations) - local candidateName = extractBinarySubLhsCrefName(toDAEExp(eq.exp), algNames) + local candidateName = extractBinarySubLhsCrefName(eq.exp, algNames) if candidateName !== nothing defCountOfVar[candidateName] = get(defCountOfVar, candidateName, 0) + 1 if !haskey(defEqOfVar, candidateName) @@ -2499,6 +2519,29 @@ Used by the fold to pre-index "defining equations": residuals that name a variable in their top-level subtraction. If a name appears in more than one such residual, MTK owns the disambiguation. """ +#= Cheap SIM cref-like name: only EXP_CREF / ASUB(EXP_CREF) can yield a name, so + gate on those and convert just that small operand -- never a complex side. =# +_crefLikeNameSIM(e::Exp) = + (e isa EXP_CREF || (e isa ASUB && e.exp isa EXP_CREF)) ? extractCrefLikeName(toDAEExp(e)) : nothing + +#= SIM-native arm: inspect the top-level BINARY/SUB on the SimCode spine and + convert only the (small) cref-like operands, so non-matching residuals bail + with no whole-tree toDAEExp. Equivalent: a complex operand yields nothing in + both paths; EXP_CREF/ASUB(EXP_CREF) convert to the identical DAE name. =# +function extractBinarySubLhsCrefName(exp::Exp, candidateNames::OrderedSet{String}) + exp isa BINARY || return nothing + exp.op === OP_SUB || return nothing + local n1 = _crefLikeNameSIM(exp.exp1) + if n1 !== nothing && n1 in candidateNames + return n1 + end + local n2 = _crefLikeNameSIM(exp.exp2) + if n2 !== nothing && n2 in candidateNames + return n2 + end + return nothing +end + function extractBinarySubLhsCrefName(@nospecialize(exp), candidateNames::OrderedSet{String}) @match exp begin DAE.BINARY(exp1 = e1, operator = op, exp2 = e2) => begin @@ -2744,9 +2787,9 @@ function _canonicalizeSimVar(sv::SIMVAR, ctx::_CanonicalNameContext)::SIMVAR sv.attributes) end -function _canonicalizeSimVarHT(ht::AbstractDict{String, Tuple{Integer, SimVar}}, +function _canonicalizeSimVarHT(ht::AbstractDict{String, Tuple{Int, SimVar}}, ctx::_CanonicalNameContext) - local out = OrderedDict{String, Tuple{Integer, SimVar}}() + local out = OrderedDict{String, Tuple{Int, SimVar}}() for (name, (idx, sv)) in ht local canonicalName = get(ctx.rename, name, nothing) if canonicalName === nothing @@ -3091,12 +3134,12 @@ end function _canonicalizeStructuralTransition(tr::StructuralTransition, ctx::_CanonicalNameContext) - if tr isa EXPLICIT_STRUCTURAL_TRANSISTION - return EXPLICIT_STRUCTURAL_TRANSISTION(_canonicalVariableKey(tr.fromState, ctx), + if tr isa EXPLICIT_STRUCTURAL_TRANSITION + return EXPLICIT_STRUCTURAL_TRANSITION(_canonicalVariableKey(tr.fromState, ctx), _canonicalVariableKey(tr.toState, ctx), - _canonicalizeExp(tr.transistionCondition, ctx)) - elseif tr isa IMPLICIT_STRUCTURAL_TRANSISTION - return IMPLICIT_STRUCTURAL_TRANSISTION(tr.size, + _canonicalizeExp(tr.transitionCondition, ctx)) + elseif tr isa IMPLICIT_STRUCTURAL_TRANSITION + return IMPLICIT_STRUCTURAL_TRANSITION(tr.size, _canonicalizeWhenStmts(tr.whenEquation, ctx), tr.source, tr.attr) @@ -3407,11 +3450,13 @@ function simplifyEnumLiteralPaths(simCode::SIM_CODE)::SIM_CODE both forms. =# local _rewriteEq = function(eq) if eq isa BDAE.RESIDUAL_EQUATION || eq isa RESIDUAL_EQUATION - return typeof(eq)(_rewriteExp(toDAEExp(eq.exp)), eq.source, eq.attr) + #= SIM eq.exp -> _rewriteExp's SIM arm (already used on bindings above); + BDAE eq.exp -> its DAE arm. Drops the per-residual whole-tree toDAEExp. =# + return typeof(eq)(_rewriteExp(eq.exp), eq.source, eq.attr) elseif eq isa BDAE.EQUATION return BDAE.EQUATION(_rewriteExp(eq.lhs), _rewriteExp(eq.rhs), eq.source, eq.attributes) elseif eq isa EQUATION - return EQUATION(_rewriteExp(toDAEExp(eq.lhs)), _rewriteExp(toDAEExp(eq.rhs)), eq.source, eq.attr) + return EQUATION(_rewriteExp(eq.lhs), _rewriteExp(eq.rhs), eq.source, eq.attr) end return eq end @@ -3442,35 +3487,36 @@ function inlinePreOfConstantParameters(simCode::SIM_CODE)::SIM_CODE local resEqs = simCode.residualEquations local nReplaced = Ref(0) - local _isPreCall = function(e) - e isa DAE.CALL || return false - length(e.expLst) == 1 || return false - e.path isa Absyn.IDENT || return false - e.path.name in ("pre", "previous") - end - - local _argIsConstParam = function(e) - local arg = listHead(e.expLst) - arg isa DAE.CREF || return false - local name = string(arg.componentRef) + #= SIM-native pre(constParam) detector: bail on the SimCode spine; only the + small pre-arg cref is converted, keyed with the same string(cref) form the + old DAE path used. =# + local _isPreConstParam = function(exp) + exp isa CALL || return false + length(exp.args) == 1 || return false + exp.path isa Absyn.IDENT || return false + (exp.path.name == "pre" || exp.path.name == "previous") || return false + local arg = exp.args[1] + arg isa EXP_CREF || return false + local name = string(toDAECref(arg.cref).componentRef) haskey(ht, name) || return false local (_, sv) = ht[name] - sv.varKind isa PARAMETER || return false - return true + return sv.varKind isa PARAMETER end local _rewrite = function(exp, _) - if _isPreCall(exp) && _argIsConstParam(exp) + if _isPreConstParam(exp) nReplaced[] += 1 - return (listHead(exp.expLst), false, nothing) + return (exp.args[1], false, nothing) end return (exp, true, nothing) end local newEqs = RESIDUAL_EQUATION[] for eq in resEqs - local (newExp, _) = Util.traverseExpTopDown(toDAEExp(eq.exp), _rewrite, nothing) - push!(newEqs, typeof(eq)(newExp, eq.source, eq.attr)) + #= SIM traverser over eq.exp directly -- no whole-tree toDAEExp; reuse the + equation when nothing changed. =# + local (newExp, _) = traverseExpTopDown(eq.exp, _rewrite, nothing) + push!(newEqs, newExp === eq.exp ? eq : typeof(eq)(newExp, eq.source, eq.attr)) end if nReplaced[] > 0 @debug "[SIMCODE: $(simCode.name): inlinePreOfConstantParameters] replaced $(nReplaced[]) `pre(constParam)` occurrences with the parameter directly" @@ -3540,7 +3586,7 @@ function propagateConstants(simCode::SIM_CODE) if i in constEqIndices || i in trivialEqIndices continue end - local result = detectConstantEquation(toDAEExp(eq.exp), ht) + local result = detectConstantEquation(eq.exp, ht) if result === nothing continue end @@ -4351,47 +4397,102 @@ Defensive checks: name exists in the simvar hash table. Used to protect those scalar params from constant-elimination — codegen later flattens the complex CREF into the scalar field symbols, which must resolve at module eval time. =# -function _collectComplexFieldNames!(names::OrderedSet{String}, eqs, ht) - function visitor(exp, ctx) - @match exp begin - DAE.CREF(cr, ty) => begin - local baseName = DAE_identifierToString(cr) - if ty isa DAE.T_COMPLEX - for field in ty.varLst - local fname = field.name - local fieldName = Base.string(baseName, "_", fname) - if haskey(ht, fieldName) - push!(names, fieldName) - end - end - else - #= Fallback: any cref X whose X_re and X_im scalars exist in HT. - Codegen will flatten X via flattenRecordCallArg into [X_re, X_im]; - protect both even when the cref's ty was downgraded from T_COMPLEX. =# - local reName = Base.string(baseName, "_re") - local imName = Base.string(baseName, "_im") - if haskey(ht, reName) && haskey(ht, imName) - push!(names, reName) - push!(names, imName) +# Per-cref handler for complex-field protection: identical logic on a DAE.CREF +# leaf whether reached via the SIM walk or the DAE fallback. +function _complexCrefFields!(names::OrderedSet{String}, @nospecialize(dcref), ht) + @match dcref begin + DAE.CREF(cr, ty) => begin + local baseName = DAE_identifierToString(cr) + if ty isa DAE.T_COMPLEX + for field in ty.varLst + local fieldName = Base.string(baseName, "_", field.name) + if haskey(ht, fieldName) + push!(names, fieldName) end end + else + #= Fallback: any cref X whose X_re and X_im scalars exist in HT. + Codegen will flatten X via flattenRecordCallArg into [X_re, X_im]; + protect both even when the cref's ty was downgraded from T_COMPLEX. =# + local reName = Base.string(baseName, "_re") + local imName = Base.string(baseName, "_im") + if haskey(ht, reName) && haskey(ht, imName) + push!(names, reName) + push!(names, imName) + end end - _ => nothing end - return (exp, true, ctx) + _ => nothing end - for eq in eqs - local exps = if eq isa BDAE.RESIDUAL_EQUATION || eq isa RESIDUAL_EQUATION - DAE.Exp[toDAEExp(eq.exp)] - elseif eq isa BDAE.EQUATION || eq isa EQUATION - DAE.Exp[toDAEExp(eq.lhs), toDAEExp(eq.rhs)] - elseif eq isa BDAE.COMPLEX_EQUATION || eq isa BDAE.ARRAY_EQUATION || eq isa ARRAY_EQUATION - DAE.Exp[toDAEExp(eq.left), toDAEExp(eq.right)] - else - DAE.Exp[] + return nothing +end + +# Pure read-only walk over the SIM tree; convert only cref leaves to DAE +# (toDAEExp(EXP_CREF) gives the same DAE.CREF the whole-tree conversion would). +# Avoids building and rebuilding a parallel DAE tree per equation. +function _walkComplexSIM!(names::OrderedSet{String}, e::Exp, ht) + if e isa EXP_CREF + _complexCrefFields!(names, toDAEExp(e), ht) + elseif e isa IFEXP + _walkComplexSIM!(names, e.cond, ht) + _walkComplexSIM!(names, e.thenExp, ht) + _walkComplexSIM!(names, e.elseExp, ht) + elseif e isa BINARY || e isa LBINARY || e isa RELATION + _walkComplexSIM!(names, e.exp1, ht) + _walkComplexSIM!(names, e.exp2, ht) + elseif e isa UNARY || e isa LUNARY + _walkComplexSIM!(names, e.exp, ht) + elseif e isa CALL + for a in e.args + _walkComplexSIM!(names, a, ht) + end + elseif e isa ARRAY_EXP + for x in e.elements + _walkComplexSIM!(names, x, ht) end - for e in exps - Util.traverseExpTopDown(e, visitor, nothing) + elseif e isa ASUB + _walkComplexSIM!(names, e.exp, ht) + for s in e.subs + _walkComplexSIM!(names, s, ht) + end + elseif e isa TSUB || e isa RSUB || e isa CAST + _walkComplexSIM!(names, e.exp, ht) + elseif e isa RECORD + for x in e.exps + _walkComplexSIM!(names, x, ht) + end + elseif e isa TUPLE + for x in e.PR + _walkComplexSIM!(names, x, ht) + end + elseif e isa REDUCTION + _walkComplexSIM!(names, e.body, ht) + end + return names +end + +_complexCrefDAEVisitor(@nospecialize(exp), ctx) = + (_complexCrefFields!(ctx[1], exp, ctx[2]); (exp, true, ctx)) + +function _collectComplexFieldNames!(names::OrderedSet{String}, eqs, ht) + for eq in eqs + if eq isa RESIDUAL_EQUATION + _walkComplexSIM!(names, eq.exp, ht) + elseif eq isa EQUATION + _walkComplexSIM!(names, eq.lhs, ht) + _walkComplexSIM!(names, eq.rhs, ht) + elseif eq isa ARRAY_EQUATION + _walkComplexSIM!(names, eq.left, ht) + _walkComplexSIM!(names, eq.right, ht) + elseif eq isa BDAE.RESIDUAL_EQUATION + #= BDAE equations carry DAE.Exp fields; fall back to the DAE traversal. =# + Util.traverseExpTopDown(eq.exp, _complexCrefDAEVisitor, (names, ht)) + elseif eq isa BDAE.EQUATION + Util.traverseExpTopDown(eq.lhs, _complexCrefDAEVisitor, (names, ht)) + Util.traverseExpTopDown(eq.rhs, _complexCrefDAEVisitor, (names, ht)) + elseif eq isa BDAE.COMPLEX_EQUATION || eq isa BDAE.ARRAY_EQUATION + Util.traverseExpTopDown(eq.left, _complexCrefDAEVisitor, (names, ht)) + Util.traverseExpTopDown(eq.right, _complexCrefDAEVisitor, (names, ht)) end end return names @@ -4480,7 +4581,7 @@ function dropObservationOnlyVariables(simCode::SIM_CODE)::SIM_CODE end _collectAttributeCrefs!(untouchable, ht) for eq in simCode.residualEquations - _collectIfexpConditionCrefs!(untouchable, toDAEExp(eq.exp)) + _collectIfexpConditionCrefs!(untouchable, eq.exp) end for eq in simCode.eliminatedEquations collectCrefNames!(untouchable, eq.exp) @@ -4710,7 +4811,7 @@ function eliminateConstantParameters(simCode::SIM_CODE)::SIM_CODE end #= IFEXP conditions inside residual equations and parameter bindings. =# for eq in simCode.residualEquations - _collectIfexpConditionCrefs!(protectedNames, toDAEExp(eq.exp)) + _collectIfexpConditionCrefs!(protectedNames, eq.exp) end #= Names of array bases referenced as bare CREFs in DATA_STRUCTURE constructor calls (ExternalObject inits like CombiTable / CombiTimeTable). Array params @@ -5055,14 +5156,49 @@ protect parameters that gate runtime conditional branches from elimination. # SIM-native: collect crefs appearing in any IFEXP condition (protects params used # in conditions from constant-elimination). Walk the SIM tree; at each IFEXP collect # its condition's crefs (collectCrefNames! grabs all of them, nested ones included). -function _collectIfexpCondVisitor(@nospecialize(e), out::OrderedSet{String}) - if e isa IFEXP - collectCrefNames!(out, e.cond) +# Pure read-only recursive walk over the SIM tree: at each IFEXP collect its +# condition's crefs (collectCrefNames! grabs all, nested ones included), and +# descend into every child. Avoids both toDAEExp and the rebuilding +# traverseExpTopDown. +function _collectIfexpConditionCrefs!(out::OrderedSet{String}, exp::Exp) + if exp isa IFEXP + collectCrefNames!(out, exp.cond) + _collectIfexpConditionCrefs!(out, exp.cond) + _collectIfexpConditionCrefs!(out, exp.thenExp) + _collectIfexpConditionCrefs!(out, exp.elseExp) + elseif exp isa BINARY || exp isa LBINARY || exp isa RELATION + _collectIfexpConditionCrefs!(out, exp.exp1) + _collectIfexpConditionCrefs!(out, exp.exp2) + elseif exp isa UNARY || exp isa LUNARY + _collectIfexpConditionCrefs!(out, exp.exp) + elseif exp isa CALL + for a in exp.args + _collectIfexpConditionCrefs!(out, a) + end + elseif exp isa ARRAY_EXP + for x in exp.elements + _collectIfexpConditionCrefs!(out, x) + end + elseif exp isa ASUB + _collectIfexpConditionCrefs!(out, exp.exp) + for s in exp.subs + _collectIfexpConditionCrefs!(out, s) + end + elseif exp isa TSUB || exp isa RSUB || exp isa CAST + _collectIfexpConditionCrefs!(out, exp.exp) + elseif exp isa RECORD + for x in exp.exps + _collectIfexpConditionCrefs!(out, x) + end + elseif exp isa TUPLE + for x in exp.PR + _collectIfexpConditionCrefs!(out, x) + end + elseif exp isa REDUCTION + _collectIfexpConditionCrefs!(out, exp.body) end - return (e, true, out) + return out end -_collectIfexpConditionCrefs!(out::OrderedSet{String}, exp::Exp) = - (traverseExpTopDown(exp, _collectIfexpCondVisitor, out); out) function _collectIfexpConditionCrefs!(out::OrderedSet{String}, @nospecialize(exp)) @match exp begin @@ -6052,6 +6188,44 @@ function _peelUMinus(@nospecialize(exp)) end end +_peelUMinusSIM(e::Exp) = (e isa UNARY && e.op === OP_UMINUS) ? (e.exp, true) : (e, false) + +#= Cheap SIM cref test. extractCrefName converts its arg via toDAEExp, which is + expensive on a complex operand; only EXP_CREF/WILD map to DAE.CREF (the only + non-nothing cases), so gate on those and convert just the leaf. =# +_simCrefName(e::Exp) = (e isa EXP_CREF || e isa WILD) ? extractCrefName(e) : nothing + +#= SIM-native arm: the caller runs inside a fixpoint, so dropping the per-residual + full-tree toDAEExp(eq.exp) is amplified. Only the complex operand is converted + (string key must match the DAE form); non-matching residuals bail before any + conversion. toDAEExp is homomorphic, so converting the peeled complex side + equals peeling the converted tree -> the string key is byte-identical. =# +function _detectVarMinusExpr(exp::Exp, ht) + exp isa BINARY || return nothing + exp.op === OP_SUB || return nothing + local (e1p, e1Neg) = _peelUMinusSIM(exp.exp1) + local (e2p, e2Neg) = _peelUMinusSIM(exp.exp2) + local r1 = _simCrefName(e1p) + local r2 = _simCrefName(e2p) + if (r1 !== nothing && r2 !== nothing) || (r1 === nothing && r2 === nothing) + return nothing + end + local r, complexExp, crefNeg, complexSign + if r1 !== nothing + r = r1; complexExp = e2p; crefNeg = e1Neg; complexSign = e2Neg + else + r = r2; complexExp = e1p; crefNeg = e2Neg; complexSign = e1Neg + end + local (n, cr, ty) = r + haskey(ht, n) || return nothing + local (_, sv) = ht[n] + isUnknownVarKind(sv.varKind) || return nothing + local cls = _aliasTypeClass(ty) + cls === :other && return nothing + local negated = xor(crefNeg, complexSign) + return (n, cr, ty, string(toDAEExp(complexExp)), negated) +end + """ _recomputeSCCsFromSimCode(simCode) -> (sccs::Vector{Vector{Int}}, eq_to_var::Vector{String}) @@ -6154,7 +6328,7 @@ function eliminateRHSEquivalentEquations(simCode::SIM_CODE)::SIM_CODE local rhsGroups = Dict{String, Vector{Tuple{String, Int, DAE.ComponentRef, DAE.Type, Bool}}}() for (i, eq) in enumerate(resEqs) - local pair = _detectVarMinusExpr(toDAEExp(eq.exp), ht) + local pair = _detectVarMinusExpr(eq.exp, ht) pair === nothing && continue local (n, cr, ty, key, neg) = pair if !haskey(rhsGroups, key) @@ -6987,6 +7161,28 @@ Base.@nospecializeinfer function _detectVarMinusExprRaw(@nospecialize(exp), ht) end end +#= SIM-native arm (fixpoint caller, see _detectVarMinusExpr). rhs flows downstream + as a DAE.Exp (substituteFoldedVar / _containsDerCallDAE), so the single complex + operand is converted; non-matching residuals bail before any toDAEExp. =# +function _detectVarMinusExprRaw(exp::Exp, ht) + exp isa BINARY || return nothing + exp.op === OP_SUB || return nothing + local r1 = _simCrefName(exp.exp1) + local r2 = _simCrefName(exp.exp2) + if (r1 !== nothing && r2 !== nothing) || (r1 === nothing && r2 === nothing) + return nothing + end + local r, rhs + if r1 !== nothing + r = r1; rhs = exp.exp2 + else + r = r2; rhs = exp.exp1 + end + local (n, _cr, _ty) = r + haskey(ht, n) || return nothing + return (n, toDAEExp(rhs)) +end + #= Substitution callback that, for every leaf CREF whose name is a key of the fold map, returns the bound RHS expression and stops traversal so the substituted form is not re-walked. ASUB-wrapped CREFs are handled @@ -7131,7 +7327,7 @@ function _foldExplicitSingleAssignOnePass(simCode::SIM_CODE, end for (i, eq) in enumerate(resEqs) - local pair = _detectVarMinusExprRaw(toDAEExp(eq.exp), ht) + local pair = _detectVarMinusExprRaw(eq.exp, ht) pair === nothing && continue local (name, rhs) = pair occursin('[', name) && continue diff --git a/src/SimulationCode/simulationCodeTransformation.jl b/src/SimulationCode/simulationCodeTransformation.jl index 90085fbd..23522320 100644 --- a/src/SimulationCode/simulationCodeTransformation.jl +++ b/src/SimulationCode/simulationCodeTransformation.jl @@ -372,12 +372,12 @@ function createSimCodeStructuralTransitions(structuralTransitions::Vector{ST}) w local transitions = StructuralTransition[] for st in structuralTransitions sst = @match st begin - BDAE.STRUCTURAL_TRANSISTION(__) => - SimulationCode.EXPLICIT_STRUCTURAL_TRANSISTION(st.fromState, + BDAE.STRUCTURAL_TRANSITION(__) => + SimulationCode.EXPLICIT_STRUCTURAL_TRANSITION(st.fromState, st.toState, - st.transistionCondition) + st.transitionCondition) BDAE.STRUCTURAL_WHEN_EQUATION(__) => - SimulationCode.IMPLICIT_STRUCTURAL_TRANSISTION(st.size, + SimulationCode.IMPLICIT_STRUCTURAL_TRANSITION(st.size, SimulationCode.toWhenStmts(st.whenEquation), st.source, SimulationCode.toEqAttr(st.attr)) @@ -737,7 +737,7 @@ function allocateAndCollectSimulationEquations(equations::T, push!(initialWhenEquations, eq) elseif eqType === BDAE.IF_EQUATION push!(ifEquations, eq) - elseif eqType === BDAE.STRUCTURAL_TRANSISTION || eqType === BDAE.STRUCTURAL_WHEN_EQUATION + elseif eqType === BDAE.STRUCTURAL_TRANSITION || eqType === BDAE.STRUCTURAL_WHEN_EQUATION push!(structuralTransitions, eq) elseif eqType === BDAE.ALGORITHM _algorithmToResiduals!(regularEquations, eq) From 1049ef1ccb8a7593462d40a1ad450bb9150a2b31 Mon Sep 17 00:00:00 2001 From: JKRT Date: Sat, 20 Jun 2026 16:17:26 +0200 Subject: [PATCH 03/12] Various typing adjustment. Fixed world age error that occured for Julia 1.12.7 --- src/Backend/BDAE.jl | 6 --- src/Backend/BDAECreate.jl | 43 +++++++++---------- src/Backend/BDAEUtil.jl | 50 +---------------------- src/Backend/BackendEquation.jl | 2 +- src/CodeGeneration/CodeGenerationUtil.jl | 3 +- src/CodeGeneration/MTK_CodeGeneration.jl | 17 ++++---- src/CodeGeneration/mtkExternals.jl | 6 +-- src/CodeGeneration/structuralCallbacks.jl | 2 +- src/FrontendUtil/Prefix.jl | 2 +- src/FrontendUtil/Util.jl | 2 +- src/Runtime/Runtime.jl | 6 +-- src/SimulationCode/simCodeFunctions.jl | 2 - src/SimulationCode/simCodeUtil.jl | 2 +- 13 files changed, 42 insertions(+), 101 deletions(-) diff --git a/src/Backend/BDAE.jl b/src/Backend/BDAE.jl index a2e1beaa..57494451 100644 --- a/src/Backend/BDAE.jl +++ b/src/Backend/BDAE.jl @@ -33,13 +33,8 @@ #= TODO: -Change Integer -> Int here. Integer is an old artifact. -Make sure all types here properly typed! Move Var s.t it is close to the struct that defines it. Make all datatypes here immutable. I think having VAR as a immutable struct is better. - -Remove VarKind types we do not use. - =# """ @@ -436,7 +431,6 @@ end @Uniontype EvaluationStages begin @Record EVALUATION_STAGES begin - dynamicEval::Bool algebraicEval::Bool zerocrossEval::Bool diff --git a/src/Backend/BDAECreate.jl b/src/Backend/BDAECreate.jl index a120c5f8..0056eecb 100644 --- a/src/Backend/BDAECreate.jl +++ b/src/Backend/BDAECreate.jl @@ -1580,11 +1580,11 @@ Base.@nospecializeinfer function _innermostType(@nospecialize(cref)) end end -Base.@nospecializeinfer function _innermostSubscripts(@nospecialize(cref))::Vector +Base.@nospecializeinfer function _innermostSubscripts(@nospecialize(cref))::Vector{DAE.Subscript} @match cref begin DAE.CREF_IDENT(_, _, subs) => collect(subs) DAE.CREF_QUAL(_, _, _, cr) => _innermostSubscripts(cr) - _ => Any[] + _ => DAE.Subscript[] end end @@ -1679,20 +1679,21 @@ Base.@nospecializeinfer function _scalarLhsTargets(@nospecialize(lhs::DAE.CREF), local cr = lhs.componentRef local baseTy = _innermostType(cr) local dims = _arrayDimsFromType(baseTy) - isempty(dims) && return Any[(lhs, nothing, Int[])] + local _emptyTargets = Tuple{DAE.Exp, Union{Nothing, DAE.Exp}, Vector{Int}}[] + isempty(dims) && return [(lhs, nothing, Int[])] local rawDims = _rawArrayDims(baseTy) local subs = _innermostSubscripts(cr) if isempty(subs) - subs = Any[DAE.WHOLEDIM() for _ in dims] + subs = DAE.Subscript[DAE.WHOLEDIM() for _ in dims] elseif length(subs) < length(dims) - append!(subs, Any[DAE.WHOLEDIM() for _ in 1:(length(dims) - length(subs))]) + append!(subs, DAE.Subscript[DAE.WHOLEDIM() for _ in 1:(length(dims) - length(subs))]) end - length(subs) == length(dims) || return Any[] + length(subs) == length(dims) || return _emptyTargets local elemTy = _arrayElementType(baseTy) - local out = Any[] - function rec(pos::Int, newSubs::Vector, guard, rhsIdxs::Vector{Int}) + local out = Tuple{DAE.Exp, Union{Nothing, DAE.Exp}, Vector{Int}}[] + function rec(pos::Int, newSubs::Vector{DAE.Subscript}, guard, rhsIdxs::Vector{Int}) if pos > length(dims) local newCr = _replaceInnermostSubscripts(cr, newSubs) push!(out, (DAE.CREF(newCr, elemTy), guard, copy(rhsIdxs))) @@ -1701,24 +1702,24 @@ Base.@nospecializeinfer function _scalarLhsTargets(@nospecialize(lhs::DAE.CREF), local sub = subs[pos] if sub isa DAE.WHOLEDIM for k in 1:dims[pos] - rec(pos + 1, Any[newSubs...; DAE.INDEX(_dimIndexExp(rawDims[pos], k))], - guard, Int[rhsIdxs...; k]) + rec(pos + 1, DAE.Subscript[newSubs..., DAE.INDEX(_dimIndexExp(rawDims[pos], k))], + guard, Int[rhsIdxs..., k]) end elseif sub isa DAE.INDEX local idx = sub.exp if idx isa DAE.ICONST || idx isa DAE.ENUM_LITERAL - rec(pos + 1, Any[newSubs...; DAE.INDEX(idx)], guard, rhsIdxs) + rec(pos + 1, DAE.Subscript[newSubs..., DAE.INDEX(idx)], guard, rhsIdxs) else for k in 1:dims[pos] local g = _andCondition(guard, _indexEqualsCondition(idx, k)) - rec(pos + 1, Any[newSubs...; DAE.INDEX(_dimIndexExp(rawDims[pos], k))], g, rhsIdxs) + rec(pos + 1, DAE.Subscript[newSubs..., DAE.INDEX(_dimIndexExp(rawDims[pos], k))], g, rhsIdxs) end end else return end end - rec(1, Any[], nothing, Int[]) + rec(1, DAE.Subscript[], nothing, Int[]) return out end @@ -1734,15 +1735,15 @@ Base.@nospecializeinfer function _scalarizeCrefRead(@nospecialize(cr), local rawDims = _rawArrayDims(baseTy) local subs = _innermostSubscripts(cr) if isempty(subs) - subs = Any[DAE.WHOLEDIM() for _ in dims] + subs = DAE.Subscript[DAE.WHOLEDIM() for _ in dims] elseif length(subs) < length(dims) - append!(subs, Any[DAE.WHOLEDIM() for _ in 1:(length(dims) - length(subs))]) + append!(subs, DAE.Subscript[DAE.WHOLEDIM() for _ in 1:(length(dims) - length(subs))]) end length(subs) == length(dims) || return fallback local elemTy = _arrayElementType(baseTy) - local candidates = Any[] - function rec(pos::Int, rhsPos::Int, newSubs::Vector, guard) + local candidates = Tuple{Union{Nothing, DAE.Exp}, DAE.Exp}[] + function rec(pos::Int, rhsPos::Int, newSubs::Vector{DAE.Subscript}, guard) if pos > length(dims) local newCr = _replaceInnermostSubscripts(cr, newSubs) push!(candidates, (guard, DAE.CREF(newCr, elemTy))) @@ -1752,22 +1753,22 @@ Base.@nospecializeinfer function _scalarizeCrefRead(@nospecialize(cr), if sub isa DAE.WHOLEDIM rhsPos <= length(rhsIdxs) || return local k = rhsIdxs[rhsPos] - rec(pos + 1, rhsPos + 1, Any[newSubs...; DAE.INDEX(_dimIndexExp(rawDims[pos], k))], guard) + rec(pos + 1, rhsPos + 1, DAE.Subscript[newSubs..., DAE.INDEX(_dimIndexExp(rawDims[pos], k))], guard) elseif sub isa DAE.INDEX local idx = sub.exp if idx isa DAE.ICONST || idx isa DAE.ENUM_LITERAL - rec(pos + 1, rhsPos, Any[newSubs...; DAE.INDEX(idx)], guard) + rec(pos + 1, rhsPos, DAE.Subscript[newSubs..., DAE.INDEX(idx)], guard) else for k in 1:dims[pos] local g = _andCondition(guard, _indexEqualsCondition(idx, k)) - rec(pos + 1, rhsPos, Any[newSubs...; DAE.INDEX(_dimIndexExp(rawDims[pos], k))], g) + rec(pos + 1, rhsPos, DAE.Subscript[newSubs..., DAE.INDEX(_dimIndexExp(rawDims[pos], k))], g) end end else return end end - rec(1, 1, Any[], nothing) + rec(1, 1, DAE.Subscript[], nothing) isempty(candidates) && return fallback local result = fallback diff --git a/src/Backend/BDAEUtil.jl b/src/Backend/BDAEUtil.jl index b87bd231..e60b8e15 100644 --- a/src/Backend/BDAEUtil.jl +++ b/src/Backend/BDAEUtil.jl @@ -52,39 +52,6 @@ function convertVarArrayToBDAE_Variables(vars::Vector{BDAE.VAR})::Vector return variables end -""" - Creates a flat list of equation systems. -""" -function createEqSystems(frontendDAE::OMFrontend.Frontend.FlatModel)::BDAE.EQSYSTEM - #= Create the first main equation system. =# - local eqSystems = BDAE.EQSYSTEM[createEqSystem(frontendDAE)] - for subModel in frontendDAE.structuralSubmodels - push!(eqSystem, createEqSystems(subModel)) - end - #= - We will have a list of lists. - For code generation this does not matter. - Return a flat list. - =# - return [eqSystems...] -end - -""" - Creates a single equation system -""" -function createEqSystem(flatModel::OMFrontend.Frontend.FlatModel) - #= TODO Extract the simple equations =# - local equations = [equationToBackendEquation(eq) for eq in OMFrontend.Frontend.convertEquations(flatModel.equations)] - local variables = [variableToBackendVariable(var) for var in OMFrontend.Frontend.convertVariables(flatModel.variables, list())] - local algorithms = [alg for alg in flatModel.algorithms] - local iAlgorithms = [iAlg for iAlg in flatModel.initialAlgorithms] - local initialEquations = [equationToBackendEquation(ieq) for ieq in OMFrontend.Frontend.convertEquations(flatModel.initialEquations)] - eqSystems = [BDAEUtil.createEqSystem(flatModel.name, variables, equations)] - #= Treat structural submodels =# - subModels = [] - BDAE.EQSYSTEM(vars, eqs, []) -end - """ Traverse and update a given structure BDAE.BDAEStructure given a traversalOperation and optional arguments """ @@ -542,19 +509,6 @@ function detectStateExpression(exp::DAE.Exp, stateCrefs::Dict{DAE.ComponentRef, return (exp, cont, outCrefs) end -#=TODO. Did I do something stupid down below here.. ?=# - -function countAllUniqueVariablesInSetOfEquations(eqs::Vector{RES_EQ}, vars::Vector{VAR}) where {RES_EQ, VAR} - vars = OrderedSet() - for eq in eqs - varsForEq = getAllVariables(eq, vars) - for v in varsForEq - push!(vars, v) - end - end - return length(vars) -end - """ Author: johti17 input: Backend Equation, eq @@ -568,7 +522,7 @@ function getAllVariables(eq::BDAE.RESIDUAL_EQUATION, vars::Vector{BDAE.VAR})::Ve (_, stateElements) = traverseEquationExpressions(eq, detectStateExpression, stateCrefs) local stateElementArray = [string(cr) for cr in collect(keys(stateElements))] local componentReferencesNotStates = [string(cr) for cr in componentReferences] - variablesInEq::Vector = [] + variablesInEq = DAE.ComponentRef[] for var in vars local vn = string(var.varName) if vn in componentReferencesNotStates && isVariable(var.varKind) @@ -742,7 +696,7 @@ function isArray(cref::DAE.ComponentRef)::Bool end function getSubscriptAsIntArray(dims)::Array - local dimIndices = [] + local dimIndices = Int[] for d in dims if ! (typeof(d) == DAE.DIM_INTEGER) throw("Non integers dimensions for arrays are not supported by OMBackend. Variable was $(string(v))") diff --git a/src/Backend/BackendEquation.jl b/src/Backend/BackendEquation.jl index d0bec683..2529a16e 100644 --- a/src/Backend/BackendEquation.jl +++ b/src/Backend/BackendEquation.jl @@ -45,7 +45,7 @@ import ..FrontendUtil.Util Create an empty equation array """ function emptyEqns() - eqns::Array = [] + eqns = BDAE.Equation[] (eqns) end diff --git a/src/CodeGeneration/CodeGenerationUtil.jl b/src/CodeGeneration/CodeGenerationUtil.jl index 67b62b07..e63a2766 100644 --- a/src/CodeGeneration/CodeGenerationUtil.jl +++ b/src/CodeGeneration/CodeGenerationUtil.jl @@ -281,7 +281,6 @@ Base.@nospecializeinfer function expToJL(@nospecialize(exp::DAE.Exp), simCode::S DAE.RELATION(exp1 = e1, operator = op, exp2 = e2) => begin (expToJL(e1, simCode, varPrefix=varPrefix) + " " + SimulationCode.string(op) + " " + expToJL(e2, simCode,varPrefix=varPrefix)) end - #=TODO?=# DAE.IFEXP(expCond = e1, expThen = e2, expElse = e3) => begin "if" + expToJL(e1, simCode, varPrefix=varPrefix) + "\n" + expToJL(e2, simCode,varPrefix=varPrefix) + "else\n" + expToJL(e3, simCode,varPrefix=varPrefix) + "\nend" end @@ -1477,7 +1476,7 @@ function extractArrayDimsFromVar(v::DAE.VAR)::Expr end @match ty begin DAE.T_ARRAY(_, dims) => begin - local dimExprs = [] + local dimExprs = Union{Int,Symbol}[] for d in dims @match d begin DAE.DIM_INTEGER(n) => push!(dimExprs, n) diff --git a/src/CodeGeneration/MTK_CodeGeneration.jl b/src/CodeGeneration/MTK_CodeGeneration.jl index 4ef69a40..b19de66a 100644 --- a/src/CodeGeneration/MTK_CodeGeneration.jl +++ b/src/CodeGeneration/MTK_CodeGeneration.jl @@ -31,9 +31,6 @@ #= Author: John Tinnerholm - TODO: Remember the state derivative scheme. What did I mean with that? - TODO: Make duplicate code better... - TODO: Cleanup in general.. keep this simple. Remove hacks as the SCiML team adds new features to MTK. =# import ..OMBackend import .AlgorithmicCodeGeneration @@ -1150,7 +1147,7 @@ function ODE_MODE_MTK_MODEL_GENERATION(simCode::SimulationCode.SIM_CODE, modelNa This means that certain algebraic variables should not be listed among the variables (These are the discrete variables). =# $(varInnerRefs) - allVariables = [] + allVariables = Any[] #= Generate variables =# for constructor in variableConstructors vars = map(n -> (n, Symbolics.variable(n, T = Symbolics.FnType{Tuple, Real, Nothing})(t)), Base.invokelatest(constructor)) @@ -1185,21 +1182,21 @@ function ODE_MODE_MTK_MODEL_GENERATION(simCode::SimulationCode.SIM_CODE, modelNa $(decomposeParameterEquationsInline(PARAMETER_EQUATIONS)) #= Add ifCond discrete parameter values to pars dict =# $(generateIfCondParamAssignments(ifCondParamPairs)) - startEquationComponents = [] + startEquationComponents = Any[] $(decomposeStartEquationsInline(INITIAL_GUESS_EQUATIONS)) for constructor in startEquationConstructors push!(startEquationComponents, Base.invokelatest(constructor)) end initialValues = collect(Iterators.flatten(startEquationComponents)) #= Process the final initial guesses =# - startEquationComponents = [] + startEquationComponents = Any[] $(decomposeStartEquationsInline(INITIAL_VALUE_EQUATIONS; functionSuffix = "Final")) for constructor in startEquationConstructors push!(startEquationComponents, Base.invokelatest(constructor)) end finalInitialValues = collect(Iterators.flatten(startEquationComponents)) #= Equations =# - equationComponents = [] + equationComponents = Any[] $(stripBeginBlocks(decomposeEquationsInline(EQUATIONS, PARAMETER_ASSIGNMENTS))) for constructor in equationConstructorCalls push!(equationComponents, Base.invokelatest(constructor)) @@ -3983,7 +3980,7 @@ end function createParameterArray(parameters::Vector{T1}, parameterAssignments::Vector{T2}, simCode::SIM_T) where {T1, T2, SIM_T} - local paramArray = [] + local paramArray = Union{Float64, Symbol}[] local hT = simCode.stringToSimVarHT for param in parameters (index, simVar) = hT[param] @@ -4017,7 +4014,7 @@ function createParameterArray(parameters::Vector{T1}, parValue = :($(Symbol(param))) else @warn "[MTK GEN: createParameterArray] parameter $(param): no bind and non-literal start; substituting 0 in the legacy parameter array. Any event-driven read of this parameter via the aux[1] mirror will be wrong (the modern MTK path is unaffected)." - parValue = :(0) + parValue = :(0.0) end push!(paramArray, parValue) end @@ -4293,7 +4290,7 @@ function decomposeParametersDeclaration(parVariablesSym; chunkSize = CHUNK_SIZE[ local paramNameQuotes = [QuoteNode(s) for s in parVariablesSym] return quote $(exprs...) - local _allParamChunks = [] + local _allParamChunks = Any[] for _fn in [$(constructorNames...)] push!(_allParamChunks, Base.invokelatest(_fn)) end diff --git a/src/CodeGeneration/mtkExternals.jl b/src/CodeGeneration/mtkExternals.jl index ad09d5a1..03f4c1a9 100644 --- a/src/CodeGeneration/mtkExternals.jl +++ b/src/CodeGeneration/mtkExternals.jl @@ -1889,8 +1889,6 @@ function _demoteWideNumericsInEquations(eqs) end """ - TODO: - Document why some parts here are commented out The irreducible variables scheme does not work using plain simplify. It should be noted that for some models both running tearing and structural simplification are needed. @@ -1938,7 +1936,7 @@ function structural_simplify(sys::ModelingToolkit.AbstractSystem, push!(results, (path, :raw_array, expr)) end end - local allResults = [] + local allResults = Tuple{Int,String,Symbol,Any}[] for (i, eq) in enumerate(equations(sys)) local lhsR = Pair{String,Any}[] local rhsR = Pair{String,Any}[] @@ -2088,7 +2086,7 @@ function ode_order_lowering(eqs, iv, unknown_vars) var_order = OrderedDict{Any, Int}() D = Differential(iv) diff_eqs = Equation[] - diff_vars = [] + diff_vars = Any[] alge_eqs = Equation[] for (i, eq) in enumerate(eqs) if !isdiffeq(eq) diff --git a/src/CodeGeneration/structuralCallbacks.jl b/src/CodeGeneration/structuralCallbacks.jl index b514224b..a20a133e 100644 --- a/src/CodeGeneration/structuralCallbacks.jl +++ b/src/CodeGeneration/structuralCallbacks.jl @@ -337,7 +337,7 @@ function createStructuralAssignments(simCode, structuralTransitions::Vector{ST}) end result = quote structuralCallbacks = OMBackend.Runtime.AbstractStructuralChange[] - callbackSet = [] + callbackSet = Any[] $(structuralAssignments...) end return result diff --git a/src/FrontendUtil/Prefix.jl b/src/FrontendUtil/Prefix.jl index 211c988c..ab2cb71b 100644 --- a/src/FrontendUtil/Prefix.jl +++ b/src/FrontendUtil/Prefix.jl @@ -31,7 +31,7 @@ NOTE: Component prefixes are stored in inverse order c.b[2].a! =# @Uniontype ComponentPrefix begin @Record PRE begin prefix #= prefix name =#::String - dimensions #= dimensions =#::List#=TODO: Removed stuff here. se history=# + dimensions #= dimensions =#::List subscripts #= subscripts =#::List next #= next prefix =#::ComponentPrefix ci_state #= to be able to at least partially fill in type information properly for DAE.VAR =#::ClassInf.SMNode diff --git a/src/FrontendUtil/Util.jl b/src/FrontendUtil/Util.jl index 16ae3f69..b712c9a9 100644 --- a/src/FrontendUtil/Util.jl +++ b/src/FrontendUtil/Util.jl @@ -1332,7 +1332,7 @@ end Returns a Vector of DAE.Subscript. """ function getSubscriptsFromCref(cref::DAE.ComponentRef)::Vector - local subscripts = [] + local subscripts = DAE.Subscript[] for c in getAllCrefsAsVector(cref) if !isempty(c.subscriptLst) append!(subscripts, collect(c.subscriptLst)) diff --git a/src/Runtime/Runtime.jl b/src/Runtime/Runtime.jl index 780c8969..5a2bc6b4 100644 --- a/src/Runtime/Runtime.jl +++ b/src/Runtime/Runtime.jl @@ -248,7 +248,7 @@ function solve(omProblem::OM_ProblemStructural, tspan, alg; kwargs...) #= Create integrator =# integrator = init(problem, alg; kwargs...) add_tstop!(integrator, tspan[2]) - local oldSols = [] + local oldSols = Any[] #= Run the integrator=# @label START_OF_INTEGRATION for i in integrator @@ -507,7 +507,7 @@ function solve(omProblem::OM_ProblemRecompilation, tspan::Tuple, alg; kwargs...) local callbackConditions = omProblem.callbackConditions local activeModeName = omProblem.activeModeName local integrator = init(problem, alg; kwargs...) - local solutions = [] + local solutions = Any[] local tmpSolAtChange #= Run the integrator=# @label START_OF_INTEGRATION @@ -994,7 +994,7 @@ function returnRootIndices(activeModeName, local rootVariables = keys(variablestoReset) local ht = structuralCallback.stringToSimVarHT rootIndices = Int[] - variablesToSet = [] + variablesToSet = Any[] variablesToSetIdx = Vector{Int}[] for v in rootVariables indexOfRoot = first(ht[v]) diff --git a/src/SimulationCode/simCodeFunctions.jl b/src/SimulationCode/simCodeFunctions.jl index 506c8ba2..788a72fd 100644 --- a/src/SimulationCode/simCodeFunctions.jl +++ b/src/SimulationCode/simCodeFunctions.jl @@ -579,8 +579,6 @@ end """ # SIM.Exp delegation: BRANCH.condition / EQUATION.lhs|rhs are SIM.Exp post-migration. resolveConstantIfExp(exp::Exp)::Exp = toSimExp(resolveConstantIfExp(toDAEExp(exp))) -resolveConstantIfExp(exp::Exp, simCode::SIM_CODE)::Exp = - toSimExp(resolveConstantIfExp(toDAEExp(exp), simCode)) function resolveConstantIfExp(exp::DAE.Exp)::DAE.Exp @match exp begin diff --git a/src/SimulationCode/simCodeUtil.jl b/src/SimulationCode/simCodeUtil.jl index adbe6895..63a2f43a 100644 --- a/src/SimulationCode/simCodeUtil.jl +++ b/src/SimulationCode/simCodeUtil.jl @@ -446,7 +446,7 @@ output """ function makeLabels(digraph, matchOrder, variablesHT) variableIndexToName::OrderedDict = makeIndexVarNameDict(matchOrder, variablesHT) - labels = [] + labels = String[] for i in 1:length(matchOrder) try variableIdx = MetaGraphs.get_prop(digraph, i, :vID) From cfb21812bc6d63fee665e17d4c197bc9e549c1e7 Mon Sep 17 00:00:00 2001 From: JKRT Date: Sat, 20 Jun 2026 20:07:37 +0200 Subject: [PATCH 04/12] @match rewrites + caching to improve perf --- src/Backend/BDAE.jl | 34 +++++------ src/Backend/BDAECreate.jl | 24 ++++---- src/Backend/BDAEUtil.jl | 14 ++--- src/Backend/Causalize.jl | 4 +- src/BackendUtil/README.md | 2 +- src/CodeGeneration/MTK_CodeGeneration.jl | 4 +- src/FrontendUtil/Util.jl | 60 +++++++++++++------ src/SimulationCode/simCodeFunctions.jl | 37 ++++++++---- src/SimulationCode/simCodeStructureTypes.jl | 2 +- src/SimulationCode/simCodeTraverse.jl | 8 +-- src/SimulationCode/simCodeUtil.jl | 6 +- .../simulationCodeTransformation.jl | 2 +- 12 files changed, 115 insertions(+), 82 deletions(-) diff --git a/src/Backend/BDAE.jl b/src/Backend/BDAE.jl index 57494451..cb10554b 100644 --- a/src/Backend/BDAE.jl +++ b/src/Backend/BDAE.jl @@ -182,11 +182,7 @@ using ExportAll - `WHEN_STMTS(condition, whenStmtLst, elsewhenPart)`: - `condition`: the when-condition expression. - `whenStmtLst`: list of `WhenOperator` statements to run on trigger. - - `elsewhenPart`: chained `elsewhen`. Intentionally untyped: depending - on the call site it can be `NONE()`, an `Option{WHEN_STMTS}`, a - `SOME(WHEN_EQUATION)` (as produced by `lowerWhenEquation`), or plain - `nothing`. Until those call sites converge, constraining the field - type would be a breaking change. + - `elsewhenPart`: chained `elsewhen`, `NONE()` or `SOME(WHEN_EQUATION)`. """ @UniontypeDecl WhenEquation @@ -249,10 +245,10 @@ mutable struct VAR <: Var varKind::VarKind varDirection::DAE.VarDirection varType::DAE.Type - bindExp::Option - arryDim::List + bindExp::Option{DAE.Exp} + arryDim::List{DAE.Dimension} source::DAE.ElementSource - values::Option + values::Option{DAE.VariableAttributes} tearingSelectOption::Option connectorType::DAE.ConnectorType unreplaceable::Bool @@ -311,8 +307,8 @@ end struct SHARED globalKnownVars::Vector{VAR} localKnownVars::Vector{VAR} - metaModel::Option - flatModel::Option + metaModel::Option{SCode.CLASS} + flatModel::Option{OMFrontend.Frontend.FlatModel} DOCC_equations::Vector{Equation} end @@ -459,9 +455,9 @@ const EQ_ATTR_DEFAULT_UNKNOWN = EQUATION_ATTRIBUTES(false, UNKNOWN_EQUATION_KIND @Uniontype Equation begin @Record EQUATION begin - lhs - rhs - source + lhs::DAE.Exp + rhs::DAE.Exp + source::DAE.ElementSource attributes::EquationAttributes end @@ -482,9 +478,9 @@ const EQ_ATTR_DEFAULT_UNKNOWN = EQUATION_ATTRIBUTES(false, UNKNOWN_EQUATION_KIND end @Record RESIDUAL_EQUATION begin - exp - source - attr + exp::DAE.Exp + source::DAE.ElementSource + attr::EquationAttributes end @Record ALGORITHM begin @@ -499,7 +495,7 @@ const EQ_ATTR_DEFAULT_UNKNOWN = EQUATION_ATTRIBUTES(false, UNKNOWN_EQUATION_KIND size::Int whenEquation::WhenEquation source::DAE.ElementSource - attr + attr::EquationAttributes end #= Body of an `algorithm when initial() then ... end when` clause. Distinct @@ -509,7 +505,7 @@ const EQ_ATTR_DEFAULT_UNKNOWN = EQUATION_ATTRIBUTES(false, UNKNOWN_EQUATION_KIND size::Int whenEquation::WhenEquation source::DAE.ElementSource - attr + attr::EquationAttributes end @Record STRUCTURAL_WHEN_EQUATION begin @@ -578,7 +574,7 @@ end @Record WHEN_STMTS begin condition::DAE.Exp whenStmtLst::List{WhenOperator} - elsewhenPart + elsewhenPart::Option{WHEN_EQUATION} end end diff --git a/src/Backend/BDAECreate.jl b/src/Backend/BDAECreate.jl index 0056eecb..d1cb5036 100644 --- a/src/Backend/BDAECreate.jl +++ b/src/Backend/BDAECreate.jl @@ -957,11 +957,11 @@ function createBindingEquations(variables::Vector) local elsePart = BDAE.WHEN_STMTS(BDAEUtil.invertCondition(cond) #= The else when here has the inverted condition of the first part. =# ,list(BDAE.ASSIGN(lhs, elseExp, v.source)) ,nothing) - local elseWeqPart = BDAE.WHEN_EQUATION(1, elsePart, v.source, nothing) + local elseWeqPart = BDAE.WHEN_EQUATION(1, elsePart, v.source, BDAE.EQ_ATTR_DEFAULT_UNKNOWN) local stmts = BDAE.WHEN_STMTS(cond ,list(BDAE.ASSIGN(lhs, thenExp, v.source)) ,SOME(elseWeqPart)) - local weq = BDAE.WHEN_EQUATION(1, stmts, v.source, nothing) + local weq = BDAE.WHEN_EQUATION(1, stmts, v.source, BDAE.EQ_ATTR_DEFAULT_UNKNOWN) push!(bindingEqs, weq) end #= Boolean (non-ifexp), Integer or enumeration declaration binding -> @@ -1055,7 +1055,7 @@ function synthesizeFromInitialAlgorithms(iAlgorithms)::Vector{BDAE.Equation} length(alg.statements), BDAE.WHEN_STMTS(initialCall, whenOps, NONE()), alg.source, - nothing, + BDAE.EQ_ATTR_DEFAULT_UNKNOWN, ) _INIT_ALG_DAE_STMTS[node] = collect(daeStmts) push!(out, node) @@ -1949,7 +1949,7 @@ Base.@nospecializeinfer function _liftAlgorithmBodyToInitialWhen!(out::Vector{BD length(initOps), BDAE.WHEN_STMTS(initialCall, MetaModelica.list(initOps...), NONE()), source, - nothing, + BDAE.EQ_ATTR_DEFAULT_UNKNOWN, )) end #= Per Modelica spec §17.4.4: a non-when algorithm with discrete LHS fires @@ -1999,7 +1999,7 @@ Base.@nospecializeinfer function _liftAlgorithmBodyToInitialWhen!(out::Vector{BD length(runOps), BDAE.WHEN_STMTS(cond, MetaModelica.list(runOps...), NONE()), source, - nothing, + BDAE.EQ_ATTR_DEFAULT_UNKNOWN, )) end return @@ -2094,7 +2094,7 @@ Base.@nospecializeinfer function _liftStmtWhenToWhenEquations!(out::Vector{BDAE. length(initOps), BDAE.WHEN_STMTS(initialCall, MetaModelica.list(initOps...), NONE()), stmtWhen.source, - nothing, + BDAE.EQ_ATTR_DEFAULT_UNKNOWN, )) end end @@ -2501,13 +2501,13 @@ function _emitDiscreteCluster!(out::Vector{BDAE.Equation}, cluster::Vector{Strin 1, BDAE.WHEN_STMTS(bareInitial, MetaModelica.list(initAssigns...), NONE()), src0, - nothing, + BDAE.EQ_ATTR_DEFAULT_UNKNOWN, )) push!(out, BDAE.WHEN_EQUATION( 1, BDAE.WHEN_STMTS(changeCond, MetaModelica.list(runAssigns...), NONE()), src0, - nothing, + BDAE.EQ_ATTR_DEFAULT_UNKNOWN, )) for (lhs, _, _) in body; push!(liftedLhs, string(lhs.componentRef)); end return @@ -2595,7 +2595,7 @@ Base.@nospecializeinfer function _liftAlgIfToWhen!(out::Vector{BDAE.Equation}, 1, BDAE.WHEN_STMTS(cond, whenOps, NONE()), source, - nothing, + BDAE.EQ_ATTR_DEFAULT_UNKNOWN, )) return true end @@ -2656,7 +2656,7 @@ Base.@nospecializeinfer function _liftAlgAssignToInitialWhen!(out::Vector{BDAE.E MetaModelica.list(BDAE.ASSIGN(lhs, rhs, asrc)), NONE()), source, - nothing, + BDAE.EQ_ATTR_DEFAULT_UNKNOWN, )) #= Plus a runtime WHEN_EQUATION for any change(rhs) trigger so the assign re-fires whenever a non-parameter input changes. =# @@ -2665,7 +2665,7 @@ Base.@nospecializeinfer function _liftAlgAssignToInitialWhen!(out::Vector{BDAE.E 1, BDAE.WHEN_STMTS(changeCond, whenOps, NONE()), source, - nothing, + BDAE.EQ_ATTR_DEFAULT_UNKNOWN, )) end local lhsName::Union{String, Nothing} = try @@ -2698,7 +2698,7 @@ function synthesizeInitialWhenFromAlgorithms(algorithms)::Vector{BDAE.Equation} length(frontendBody), BDAE.WHEN_STMTS(daeCond, whenOps, NONE()), stmt.source, - nothing, + BDAE.EQ_ATTR_DEFAULT_UNKNOWN, ) _INIT_ALG_DAE_STMTS[node] = collect(daeStmts) push!(out, node) diff --git a/src/Backend/BDAEUtil.jl b/src/Backend/BDAEUtil.jl index e60b8e15..15a606d7 100644 --- a/src/Backend/BDAEUtil.jl +++ b/src/Backend/BDAEUtil.jl @@ -106,7 +106,7 @@ function mapEqSystemEquations(syst::BDAE.EQSYSTEM, traversalOperation::Function) end end -function mapEqSystemEquationsNoUpdate(syst::BDAE.EQSYSTEM, traversalOperation::Function, extArg) +function mapEqSystemEquationsNoUpdate(syst::BDAE.EQSYSTEM, traversalOperation::Function, extArg::T)::T where {T} extArg = begin local eqs::Array{BDAE.Equation,1} @match syst begin @@ -120,7 +120,7 @@ function mapEqSystemEquationsNoUpdate(syst::BDAE.EQSYSTEM, traversalOperation::F end end -function mapEqSystemVariablesNoUpdate(syst::BDAE.EQSYSTEM, traversalOperation::Function, extArg) +function mapEqSystemVariablesNoUpdate(syst::BDAE.EQSYSTEM, traversalOperation::Function, extArg::T)::T where {T} extArg = begin local varArr::Array{BDAE.Var,1} @match syst begin @@ -144,9 +144,9 @@ function crefLeafType(@nospecialize(cref)) end end -function _traverseComponentRef(@nospecialize(cref::DAE.ComponentRef), +function _traverseComponentRef(cref::DAE.ComponentRef, traversalOperation::Function, - extArg) + extArg::T)::Tuple{DAE.ComponentRef, T} where {T} local exp = DAE.CREF(cref, crefLeafType(cref)) local newExp (newExp, extArg) = Util.traverseExpTopDown(exp, traversalOperation, extArg) @@ -160,9 +160,9 @@ end Traverse a given equation using a traversalOperation. Mutates the given equation. """ -Base.@nospecializeinfer function traverseEquationExpressions(@nospecialize(eq::BDAE.Equation), - traversalOperation::Function, - extArg::T)::Tuple{BDAE.Equation,T} where{T} +function traverseEquationExpressions(eq::BDAE.Equation, + traversalOperation::Function, + extArg::T)::Tuple{BDAE.Equation,T} where{T} (eq, extArg) = begin local lhs::DAE.Exp local rhs::DAE.Exp diff --git a/src/Backend/Causalize.jl b/src/Backend/Causalize.jl index 041b3330..8d1682b7 100644 --- a/src/Backend/Causalize.jl +++ b/src/Backend/Causalize.jl @@ -1102,7 +1102,7 @@ end function _foldReductionScalar(fname::String, elems::Vector{DAE.Exp}, attr, foldTy::DAE.Type)::Union{DAE.Exp, Nothing} isempty(elems) && return nothing - local acc = elems[1] + local acc::DAE.Exp = elems[1] for k in 2:length(elems) acc = if fname == "sum" DAE.BINARY(acc, DAE.ADD(foldTy), elems[k]) @@ -1133,7 +1133,7 @@ function _reducingCallArgElems(@nospecialize(arg))::Union{Vector{DAE.Exp}, Nothi end function unrollReductionTraverser(exp::DAE.Exp, acc) - local newExp = exp + local newExp::DAE.Exp = exp @match exp begin DAE.CALL(Absyn.IDENT(fname), args, attr) where (fname == "max" || fname == "min" || fname == "sum" || fname == "product") => begin diff --git a/src/BackendUtil/README.md b/src/BackendUtil/README.md index 8a825d29..bad62137 100644 --- a/src/BackendUtil/README.md +++ b/src/BackendUtil/README.md @@ -1,3 +1,3 @@ -# A place for generic backend functions! +# This directory contains backend utility functions. E.g GraphAlgorithms.jl diff --git a/src/CodeGeneration/MTK_CodeGeneration.jl b/src/CodeGeneration/MTK_CodeGeneration.jl index b19de66a..27e479f9 100644 --- a/src/CodeGeneration/MTK_CodeGeneration.jl +++ b/src/CodeGeneration/MTK_CodeGeneration.jl @@ -302,7 +302,7 @@ function substituteRelayAliasesInWhens(simCode, relayAliases::Dict{Symbol, Symbo isempty(relayAliases) && return simCode local wanted = OrderedSet{String}(string(k) for k in keys(relayAliases)) local tyOf = Dict{String, DAE.Type}() - local collectTy = function (@nospecialize(e), acc) + local collectTy = function (e::DAE.Exp, acc) if e isa DAE.CREF local nm = string(e.componentRef) if nm in wanted && !haskey(tyOf, nm) @@ -2747,7 +2747,7 @@ end #= Replace every structural occurrence of relation `rel` in `exp` with `val`. =# function _substRelation(@nospecialize(exp), @nospecialize(rel), val::Bool) local relStr = string(rel) - function repl(@nospecialize(e), arg) + function repl(e::DAE.Exp, arg) (string(e) == relStr) ? (DAE.BCONST(val), arg) : (e, arg) end return first(Util.traverseExpBottomUp(exp, repl, nothing)) diff --git a/src/FrontendUtil/Util.jl b/src/FrontendUtil/Util.jl index b712c9a9..805a1abe 100644 --- a/src/FrontendUtil/Util.jl +++ b/src/FrontendUtil/Util.jl @@ -341,16 +341,28 @@ end """ function traverseExpList(expLst::List{DAE.Exp}, func::Function, inArg) outArg = inArg - newExpLst = DAE.Exp[] - allEqual = true + #= Allocate the result buffer lazily; a no-op traversal returns the original list. =# + local newExpLst::Union{Nothing, Vector{DAE.Exp}} = nothing + local i = 0 for e in expLst + i += 1 (newE, outArg) = traverseExpBottomUp(e, func, outArg) - push!(newExpLst, newE) - if !referenceEq(e, newE) - allEqual = false + if newExpLst === nothing + if !referenceEq(e, newE) + newExpLst = DAE.Exp[] + local j = 0 + for pe in expLst + j += 1 + j < i || break + push!(newExpLst, pe) + end + push!(newExpLst, newE) + end + else + push!(newExpLst, newE) end end - return allEqual ? (expLst, outArg) : (list(newExpLst...), outArg) + return newExpLst === nothing ? (expLst, outArg) : (list(newExpLst...), outArg) end """ @@ -359,16 +371,28 @@ end """ function traverseExpMatrix(inMatrix::List{List{DAE.Exp}}, func::Function, inArg) outArg = inArg - newRows = List{DAE.Exp}[] - same = true + #= Allocate lazily; an unchanged matrix returns the original list of rows. =# + local newRows::Union{Nothing, Vector{List{DAE.Exp}}} = nothing + local i = 0 for row in inMatrix + i += 1 (row_1, outArg) = traverseExpList(row, func, outArg) - push!(newRows, row_1) - if !referenceEq(row, row_1) - same = false + if newRows === nothing + if !referenceEq(row, row_1) + newRows = List{DAE.Exp}[] + local j = 0 + for pr in inMatrix + j += 1 + j < i || break + push!(newRows, pr) + end + push!(newRows, row_1) + end + else + push!(newRows, row_1) end end - return same ? (inMatrix, outArg) : (list(newRows...), outArg) + return newRows === nothing ? (inMatrix, outArg) : (list(newRows...), outArg) end """ @@ -521,7 +545,7 @@ end NOTE: The user-provided function is not allowed to fail! If you want to detect a failure, return NONE() in your user-provided datatype. """ -Base.@nospecializeinfer function traverseExpBottomUp(@nospecialize(inExp::DAE.Exp), inFunc::Function, inExtArg::T) where {T} +function traverseExpBottomUp(inExp::DAE.Exp, inFunc::Function, inExtArg::T) where {T} local outExtArg::T local outExp::DAE.Exp (outExp, outExtArg) = begin @@ -967,8 +991,8 @@ Base.@nospecializeinfer function traverseExpBottomUp(@nospecialize(inExp::DAE.Ex end -function traverseExpSubs(inSubscript::List{DAE.Subscript}, rel::Function, iarg::Type_a) ::Tuple{List{DAE.Subscript}, Type_a} - local outArg::Type_a +function traverseExpSubs(inSubscript::List{DAE.Subscript}, rel::Function, iarg::T) ::Tuple{List{DAE.Subscript}, T} where {T} + local outArg::T local outSubscript::List{DAE.Subscript} (outSubscript, outArg) = begin @@ -976,7 +1000,7 @@ function traverseExpSubs(inSubscript::List{DAE.Subscript}, rel::Function, iarg:: local sub_exp_1::DAE.Exp local rest::List{DAE.Subscript} local res::List{DAE.Subscript} - local arg::Type_a + local arg::T @match (inSubscript, rel, iarg) begin ( nil(), _, arg) => begin (inSubscript, arg) @@ -1036,7 +1060,7 @@ end """ function traverseExpCref(inCref::DAE.ComponentRef, rel::Function, iarg::T) ::Tuple{DAE.ComponentRef, T} where {T} - local outArg::Type_a + local outArg::T local outCref::DAE.ComponentRef (outCref, outArg) = begin local name::String @@ -1045,7 +1069,7 @@ function traverseExpCref(inCref::DAE.ComponentRef, rel::Function, iarg::T) ::Tup local ty::DAE.Type local subs::List{DAE.Subscript} local subs_1::List{DAE.Subscript} - local arg::Type_a + local arg::T local ix::Int local instant::String @match (inCref, rel, iarg) begin diff --git a/src/SimulationCode/simCodeFunctions.jl b/src/SimulationCode/simCodeFunctions.jl index 788a72fd..8930734f 100644 --- a/src/SimulationCode/simCodeFunctions.jl +++ b/src/SimulationCode/simCodeFunctions.jl @@ -752,23 +752,40 @@ function resolveConstantIfExp(exp::Exp, simCode::SIM_CODE)::Exp local n1 = resolveConstantIfExp(exp.exp, simCode) return n1 === exp.exp ? exp : CAST(exp.ty, n1) elseif exp isa CALL - local changed = false - local newArgs = Exp[] + #= Allocate lazily; an unchanged arg list returns the original node. =# + local newArgs::Union{Nothing, Vector{Exp}} = nothing + local i = 0 for arg in exp.args + i += 1 local na = resolveConstantIfExp(arg, simCode) - changed |= na !== arg - push!(newArgs, na) + if newArgs === nothing + if na !== arg + newArgs = Exp[] + for j in 1:(i - 1); push!(newArgs, exp.args[j]); end + push!(newArgs, na) + end + else + push!(newArgs, na) + end end - return changed ? CALL(exp.path, newArgs, exp.attr) : exp + return newArgs === nothing ? exp : CALL(exp.path, newArgs, exp.attr) elseif exp isa ARRAY_EXP - local changed = false - local newEls = Exp[] + local newEls::Union{Nothing, Vector{Exp}} = nothing + local i = 0 for el in exp.elements + i += 1 local nel = resolveConstantIfExp(el, simCode) - changed |= nel !== el - push!(newEls, nel) + if newEls === nothing + if nel !== el + newEls = Exp[] + for j in 1:(i - 1); push!(newEls, exp.elements[j]); end + push!(newEls, nel) + end + else + push!(newEls, nel) + end end - return changed ? ARRAY_EXP(exp.ty, exp.scalar, newEls) : exp + return newEls === nothing ? exp : ARRAY_EXP(exp.ty, exp.scalar, newEls) elseif exp isa ASUB local n1 = resolveConstantIfExp(exp.exp, simCode) local changed = n1 !== exp.exp diff --git a/src/SimulationCode/simCodeStructureTypes.jl b/src/SimulationCode/simCodeStructureTypes.jl index 6adca91b..18eee6fd 100644 --- a/src/SimulationCode/simCodeStructureTypes.jl +++ b/src/SimulationCode/simCodeStructureTypes.jl @@ -70,7 +70,7 @@ end _dimsToSim(@nospecialize(ds))::Vector{Int} = Int[_dimToInt(d) for d in ds] # Field directory (names + element STypes) from a `T_COMPLEX` varLst. -function _complexFieldsToSim(@nospecialize(varLst)) +function _complexFieldsToSim(@nospecialize(varLst))::Tuple{Vector{String}, Vector{SType}} local names = String[] local types = SType[] for v in varLst diff --git a/src/SimulationCode/simCodeTraverse.jl b/src/SimulationCode/simCodeTraverse.jl index f05e5e8a..0dbde7e3 100644 --- a/src/SimulationCode/simCodeTraverse.jl +++ b/src/SimulationCode/simCodeTraverse.jl @@ -14,9 +14,7 @@ # -------------------- traverseExpTopDown -------------------- -Base.@nospecializeinfer function traverseExpTopDown(@nospecialize(inExp::Exp), - @nospecialize(visitor), - @nospecialize(arg)) +function traverseExpTopDown(inExp::Exp, visitor::F, arg::T)::Tuple{Exp, T} where {F, T} (outExp, cont, outArg) = visitor(inExp, arg) if !cont return (outExp, outArg) @@ -134,9 +132,7 @@ end # -------------------- traverseExpBottomUp -------------------- -Base.@nospecializeinfer function traverseExpBottomUp(@nospecialize(inExp::Exp), - @nospecialize(visitor), - @nospecialize(arg)) +function traverseExpBottomUp(inExp::Exp, visitor::F, arg::T)::Tuple{Exp, T} where {F, T} (newExp, newArg) = _traverseChildrenBottomUp(inExp, visitor, arg) return visitor(newExp, newArg) end diff --git a/src/SimulationCode/simCodeUtil.jl b/src/SimulationCode/simCodeUtil.jl index 63a2f43a..a1bacfb5 100644 --- a/src/SimulationCode/simCodeUtil.jl +++ b/src/SimulationCode/simCodeUtil.jl @@ -949,7 +949,7 @@ function handleZimmerThetaConstant(resEqs, irreducibleVars::Vector{String}, ht) DAE.SUB(DAE.T_REAL_DEFAULT), DAE.RCONST(1.0)) push!(resEqs, - BDAE.RESIDUAL_EQUATION(tmpResEq, nothing, nothing)) + BDAE.RESIDUAL_EQUATION(tmpResEq, DAE.emptyElementSource, BDAE.EQ_ATTR_DEFAULT_DYNAMIC)) (zimmerThetaIdx, simVar) = ht[thetaConstant] @assign simVar.varKind = ALG_VARIABLE(0) ht[thetaConstant] = (zimmerThetaIdx, simVar) @@ -979,7 +979,7 @@ function makeDummyResidualEquation(equationSystemName::String, idx::Int = 1) DAE.CALL(Absyn.IDENT("der"), crefExpression <| MetaModelica.list(), DAE.callAttrBuiltinReal), DAE.SUB(DAE.T_REAL_DEFAULT), DAE.RCONST(0.0)), - DAE.T_SOURCEINFO_DEFAULT, + DAE.emptyElementSource, BDAE.EQ_ATTR_DEFAULT_DYNAMIC, ) end @@ -4331,7 +4331,7 @@ function eliminateAliasVariables(simCode::SIM_CODE) DAE.BINARY(uvExp, DAE.ADD(DAE.T_REAL_DEFAULT), repExp) : DAE.BINARY(uvExp, DAE.SUB(DAE.T_REAL_DEFAULT), repExp) push!(pairedElimVarNames, uv) - push!(pairedElimEqs, RESIDUAL_EQUATION(synExp, nothing, nothing)) + push!(pairedElimEqs, BDAE.RESIDUAL_EQUATION(synExp, DAE.emptyElementSource, BDAE.EQ_ATTR_DEFAULT_DYNAMIC)) end end end diff --git a/src/SimulationCode/simulationCodeTransformation.jl b/src/SimulationCode/simulationCodeTransformation.jl index 23522320..a586b4f6 100644 --- a/src/SimulationCode/simulationCodeTransformation.jl +++ b/src/SimulationCode/simulationCodeTransformation.jl @@ -590,7 +590,7 @@ function flattenNestedThenBranchIfs(ifEq::BDAE.IF_EQUATION)::BDAE.IF_EQUATION local conds = listArray(ifEq.conditions) local thens = listArray(ifEq.eqnstrue) local newConds = DAE.Exp[] - local newThens = Vector{Any}() + local newThens = List{BDAE.Equation}[] for i in 1:length(conds) local expanded = _expandBranchEquations(collect(BDAE.Equation, listArray(thens[i]))) if length(expanded) == 1 && expanded[1][1] === nothing From a6946ceb5e6b487abed3b0717f71ecc151c52c64 Mon Sep 17 00:00:00 2001 From: JKRT Date: Sun, 21 Jun 2026 11:49:39 +0200 Subject: [PATCH 05/12] Fixed lowering of higher index derivatives. Typing adjustments --- src/Backend/Causalize.jl | 130 +++++++++++++++++- src/CodeGeneration/CodeGenerationUtil.jl | 23 ++++ src/CodeGeneration/MTK_CodeGeneration.jl | 3 +- src/CodeGeneration/MTK_CodeGenerationUtil.jl | 18 +-- src/CodeGeneration/codeGen.jl | 10 +- src/FrontendUtil/Util.jl | 15 +- src/SimulationCode/simCodeFunctions.jl | 20 ++- src/SimulationCode/simCodeUtil.jl | 11 +- .../simulationCodeTransformation.jl | 4 +- src/backendAPI.jl | 4 + 10 files changed, 187 insertions(+), 51 deletions(-) diff --git a/src/Backend/Causalize.jl b/src/Backend/Causalize.jl index 8d1682b7..116013ab 100644 --- a/src/Backend/Causalize.jl +++ b/src/Backend/Causalize.jl @@ -438,6 +438,128 @@ function updateStates(vars::Vector, stateCrefs::Dict{DAE.ComponentRef, Bool}) end +""" + lowerHigherOrderDerivatives(dae) + +Order-lower nested derivatives so SimCode only ever sees a first-order `der` of a +plain variable: `der(der(x))` becomes `der(der_x)` plus `der_x = der(x)` +(recursively for higher orders). A no-op unless a variable-based nested `der` is present. +""" +function lowerHigherOrderDerivatives(dae::BDAE.BACKEND_DAE) + BDAEUtil.mapEqSystems(dae, lowerHigherOrderDerivativesEqSystem) +end + +function lowerHigherOrderDerivativesEqSystem(syst::BDAE.EQSYSTEM)::BDAE.EQSYSTEM + local auxiliaryVars = BDAE.VAR[] + local auxiliaryEqs = BDAE.Equation[] + #= innermost cref name -> auxiliary derivative cref, so a repeated der(x) shares + one auxiliary state and one defining equation. =# + local auxiliaryByName = Dict{String, DAE.ComponentRef}() + local accumulator = (auxiliaryVars, auxiliaryEqs, auxiliaryByName) + @match syst begin + BDAE.EQSYSTEM(__) => begin + for i in 1:length(syst.orderedEqs) + local eq = syst.orderedEqs[i] + (loweredEq, _) = BDAEUtil.traverseEquationExpressions(eq, lowerNestedDerExpression, accumulator) + if !(eq === loweredEq) + @assign syst.orderedEqs[i] = loweredEq + end + end + append!(syst.orderedEqs, auxiliaryEqs) + append!(syst.orderedVars, auxiliaryVars) + syst + end + end + return syst +end + +""" + lowerNestedDerExpression(exp, accumulator) + +Rewrite `der(der(...))` to `der()`; first-order `der(x)` is left +untouched because only a `der` over a `der` matches. +""" +function lowerNestedDerExpression(exp::DAE.Exp, accumulator) + local auxiliaryVars = accumulator[1] + local auxiliaryEqs = accumulator[2] + local auxiliaryByName = accumulator[3] + local result = begin + @match exp begin + DAE.CALL(Absyn.IDENT("der"), arg <| _, attr) where (_isNestedDerivative(arg)) => begin + local stateCref = _derivativeStateCref(arg, attr, auxiliaryVars, auxiliaryEqs, auxiliaryByName) + local loweredExp = DAE.CALL(Absyn.IDENT("der"), + list(DAE.CREF(stateCref, DAE.T_REAL_DEFAULT)), attr) + (loweredExp, false, accumulator) + end + _ => (exp, true, accumulator) + end + end + return result +end + +#= A der() whose argument is itself a der() bottoming out in a plain cref. =# +function _isNestedDerivative(exp::DAE.Exp)::Bool + return @match exp begin + DAE.CALL(Absyn.IDENT("der"), inner <| _, _) => _bottomsOutInCref(inner) + _ => false + end +end + +function _bottomsOutInCref(exp::DAE.Exp)::Bool + return @match exp begin + DAE.CREF(_, _) => true + DAE.CALL(Absyn.IDENT("der"), inner <| _, _) => _bottomsOutInCref(inner) + _ => false + end +end + +""" + _derivativeStateCref(derExp, attr, auxiliaryVars, auxiliaryEqs, auxiliaryByName) + +For `der(inner)`, return a cref whose value equals it, synthesising the auxiliary +state and defining equation `aux = der(innerCref)` (lowering `inner` first). +""" +function _derivativeStateCref(derExp::DAE.Exp, attr, + auxiliaryVars::Vector{BDAE.VAR}, + auxiliaryEqs::Vector{BDAE.Equation}, + auxiliaryByName::Dict{String, DAE.ComponentRef})::DAE.ComponentRef + return @match derExp begin + DAE.CALL(Absyn.IDENT("der"), inner <| _, _) => begin + local baseCref = _firstOrderArgumentCref(inner, attr, auxiliaryVars, auxiliaryEqs, auxiliaryByName) + local baseName = OMBackend.canonicalName(baseCref) + local existing = get(auxiliaryByName, baseName, nothing) + if existing !== nothing + existing + else + local auxiliaryName = "der_" * baseName + local auxiliaryCref = DAE.CREF_IDENT(auxiliaryName, DAE.T_REAL_DEFAULT, nil) + local definingRHS = DAE.CALL(Absyn.IDENT("der"), + list(DAE.CREF(baseCref, DAE.T_REAL_DEFAULT)), attr) + push!(auxiliaryEqs, BDAE.EQUATION(DAE.CREF(auxiliaryCref, DAE.T_REAL_DEFAULT), + definingRHS, DAE.emptyElementSource, + BDAE.EQ_ATTR_DEFAULT_UNKNOWN)) + push!(auxiliaryVars, BDAE.VAR(DAE.CREF_IDENT(auxiliaryName, DAE.T_UNKNOWN_DEFAULT, nil), + BDAE.VARIABLE(), DAE.T_REAL_DEFAULT)) + auxiliaryByName[baseName] = auxiliaryCref + auxiliaryCref + end + end + end +end + +#= Cref c such that der(c) represents der(exp): a plain cref yields itself; a + nested der is lowered to an auxiliary state first. =# +function _firstOrderArgumentCref(exp::DAE.Exp, attr, + auxiliaryVars::Vector{BDAE.VAR}, + auxiliaryEqs::Vector{BDAE.Equation}, + auxiliaryByName::Dict{String, DAE.ComponentRef})::DAE.ComponentRef + return @match exp begin + DAE.CREF(cref, _) => cref + DAE.CALL(Absyn.IDENT("der"), _, _) => _derivativeStateCref(exp, attr, auxiliaryVars, auxiliaryEqs, auxiliaryByName) + end +end + + """ Author: johti17 @@ -552,13 +674,7 @@ end Base.@nospecializeinfer function isAlreadyScalarizedCref(@nospecialize(cr::DAE.ComponentRef))::Bool @match cr begin DAE.CREF_QUAL(_, _, _, inner) => isAlreadyScalarizedCref(inner) - DAE.CREF_IDENT(_, _, subs) => begin - try - return !isempty(collect(subs)) - catch - return false - end - end + DAE.CREF_IDENT(_, _, subs) => !listEmpty(subs) _ => false end end diff --git a/src/CodeGeneration/CodeGenerationUtil.jl b/src/CodeGeneration/CodeGenerationUtil.jl index e63a2766..5ff9e4da 100644 --- a/src/CodeGeneration/CodeGenerationUtil.jl +++ b/src/CodeGeneration/CodeGenerationUtil.jl @@ -361,6 +361,29 @@ function DAE_OP_toJuliaOperator(@nospecialize(op::DAE.Operator)) end end +#= Direct SimCode OpKind -> Julia operator Symbol, equivalent to + DAE_OP_toJuliaOperator(toDAEOperator(k)) but without the throwaway + DAE.Operator allocation per operator node in codegen. =# +function opKindToJuliaOperator(k::SimulationCode.OpKind)::Symbol + if k === SimulationCode.OP_ADD; :+ + elseif k === SimulationCode.OP_SUB; :- + elseif k === SimulationCode.OP_MUL; :* + elseif k === SimulationCode.OP_DIV; :/ + elseif k === SimulationCode.OP_POW; :^ + elseif k === SimulationCode.OP_UMINUS; :- + elseif k === SimulationCode.OP_AND; :(&) + elseif k === SimulationCode.OP_OR; :(||) + elseif k === SimulationCode.OP_NOT; :(!) + elseif k === SimulationCode.OP_LESS; :(<) + elseif k === SimulationCode.OP_LESSEQ; :(<=) + elseif k === SimulationCode.OP_GREATER; :(>) + elseif k === SimulationCode.OP_GREATEREQ; :(>=) + elseif k === SimulationCode.OP_EQUAL; :(==) + elseif k === SimulationCode.OP_NEQUAL; :(!=) + else error("opKindToJuliaOperator: unhandled OpKind $k") + end +end + " TODO: Keeping it simple for now, we assume we only have one argument in the call diff --git a/src/CodeGeneration/MTK_CodeGeneration.jl b/src/CodeGeneration/MTK_CodeGeneration.jl index 27e479f9..b2e90307 100644 --- a/src/CodeGeneration/MTK_CodeGeneration.jl +++ b/src/CodeGeneration/MTK_CodeGeneration.jl @@ -150,8 +150,7 @@ function classifyVariables(simCode)::ClassifiedVariables O(V) scan of the matchOrder Vector, making classification O(V^2). matchOrder is not mutated in this loop. =# local matchOrderSet = OrderedSet{Int}(simCode.matchOrder) - for varName in keys(ht) - (idx, var) = ht[varName] + for (varName, (idx, var)) in ht local varType = var.varKind @match varType begin SimulationCode.INPUT(__) => begin diff --git a/src/CodeGeneration/MTK_CodeGenerationUtil.jl b/src/CodeGeneration/MTK_CodeGenerationUtil.jl index 1a848260..98171897 100644 --- a/src/CodeGeneration/MTK_CodeGenerationUtil.jl +++ b/src/CodeGeneration/MTK_CodeGenerationUtil.jl @@ -445,13 +445,7 @@ Base.@nospecializeinfer function _ifexpBranchIsNonReal(@nospecialize(e::DAE.Exp) @match e begin DAE.SCONST(_) => true DAE.CREF(_, ty) => ty isa DAE.T_STRING - DAE.CALL(_, _, attrs) => begin - try - attrs.ty isa DAE.T_STRING - catch - false - end - end + DAE.CALL(_, _, attrs) => attrs.ty isa DAE.T_STRING DAE.IFEXP(_, t, el) => _ifexpBranchIsNonReal(t) || _ifexpBranchIsNonReal(el) _ => false end @@ -537,7 +531,7 @@ function expToJuliaExpMTK(exp::SimulationCode.BINARY, simCode::SimulationCode.SI varPrefix = varPrefix, varSuffix = varSuffix, derSymbol = derSymbol) local rhs = expToJuliaExpMTK(exp.exp2, simCode; varPrefix = varPrefix, varSuffix = varSuffix, derSymbol = derSymbol) - local opSym = DAE_OP_toJuliaOperator(SimulationCode.toDAEOperator(exp.op)) + local opSym = opKindToJuliaOperator(exp.op) return :( $opSym($(lhs), $(rhs)) ) end @@ -545,7 +539,7 @@ function expToJuliaExpMTK(exp::SimulationCode.UNARY, simCode::SimulationCode.SIM varSuffix = "", varPrefix = "", derSymbol::Bool = false)::Expr local operand = expToJuliaExpMTK(exp.exp, simCode; varPrefix = varPrefix, varSuffix = varSuffix, derSymbol = derSymbol) - local opSym = DAE_OP_toJuliaOperator(SimulationCode.toDAEOperator(exp.op)) + local opSym = opKindToJuliaOperator(exp.op) return :( $opSym($(operand)) ) end @@ -556,7 +550,7 @@ function expToJuliaExpMTK(exp::SimulationCode.LUNARY, simCode::SimulationCode.SI if exp.op === SimulationCode.OP_NOT return :( 1 - $(operand) ) end - local opSym = DAE_OP_toJuliaOperator(SimulationCode.toDAEOperator(exp.op)) + local opSym = opKindToJuliaOperator(exp.op) return :( $opSym($(operand)) ) end @@ -571,7 +565,7 @@ function expToJuliaExpMTK(exp::SimulationCode.LBINARY, simCode::SimulationCode.S elseif exp.op === SimulationCode.OP_AND return :( $(lhs) * $(rhs) ) end - local opSym = DAE_OP_toJuliaOperator(SimulationCode.toDAEOperator(exp.op)) + local opSym = opKindToJuliaOperator(exp.op) return :( $opSym($(lhs), $(rhs)) ) end @@ -581,7 +575,7 @@ function expToJuliaExpMTK(exp::SimulationCode.RELATION, simCode::SimulationCode. varPrefix = varPrefix, varSuffix = varSuffix, derSymbol = derSymbol) local rhs = expToJuliaExpMTK(exp.exp2, simCode; varPrefix = varPrefix, varSuffix = varSuffix, derSymbol = derSymbol) - local opSym = DAE_OP_toJuliaOperator(SimulationCode.toDAEOperator(exp.op)) + local opSym = opKindToJuliaOperator(exp.op) return quote ($opSym($(lhs), $(rhs))) end end diff --git a/src/CodeGeneration/codeGen.jl b/src/CodeGeneration/codeGen.jl index a4e7322a..f77d0def 100644 --- a/src/CodeGeneration/codeGen.jl +++ b/src/CodeGeneration/codeGen.jl @@ -1262,7 +1262,7 @@ function expToJuliaExp(e::SimulationCode.EXP_CREF, context::C, varSuffix=""; var end function expToJuliaExp(e::SimulationCode.UNARY, context::C, varSuffix=""; varPrefix="x")::Expr where {C} - local o = DAE_OP_toJuliaOperator(SimulationCode.toDAEOperator(e.op)) + local o = opKindToJuliaOperator(e.op) quote $(o)($(expToJuliaExp(e.exp, context, varPrefix=varPrefix))) end @@ -1270,21 +1270,21 @@ end function expToJuliaExp(e::SimulationCode.BINARY, context::C, varSuffix=""; varPrefix="x")::Expr where {C} local a = expToJuliaExp(e.exp1, context, varPrefix=varPrefix) local b = expToJuliaExp(e.exp2, context, varPrefix=varPrefix) - local o = DAE_OP_toJuliaOperator(SimulationCode.toDAEOperator(e.op)) + local o = opKindToJuliaOperator(e.op) quote $o($(a), $(b)) end end function expToJuliaExp(e::SimulationCode.LUNARY, context::C, varSuffix=""; varPrefix="x")::Expr where {C} local lhs = expToJuliaExp(e.exp, context, varPrefix=varPrefix) - local o = DAE_OP_toJuliaOperator(SimulationCode.toDAEOperator(e.op)) + local o = opKindToJuliaOperator(e.op) quote $o($(lhs)) end end function expToJuliaExp(e::SimulationCode.LBINARY, context::C, varSuffix=""; varPrefix="x")::Expr where {C} local l = expToJuliaExp(e.exp1, context, varPrefix=varPrefix) - local o = DAE_OP_toJuliaOperator(SimulationCode.toDAEOperator(e.op)) + local o = opKindToJuliaOperator(e.op) local r = expToJuliaExp(e.exp2, context, varPrefix=varPrefix) quote $o($(l), $(r)) @@ -1292,7 +1292,7 @@ function expToJuliaExp(e::SimulationCode.LBINARY, context::C, varSuffix=""; varP end function expToJuliaExp(e::SimulationCode.RELATION, context::C, varSuffix=""; varPrefix="x")::Expr where {C} local lhs = expToJuliaExp(e.exp1, context, varPrefix=varPrefix) - local o = DAE_OP_toJuliaOperator(SimulationCode.toDAEOperator(e.op)) + local o = opKindToJuliaOperator(e.op) local rhs = expToJuliaExp(e.exp2, context, varPrefix=varPrefix) quote $o($(lhs), $(rhs)) diff --git a/src/FrontendUtil/Util.jl b/src/FrontendUtil/Util.jl index 805a1abe..f9bbd4ef 100644 --- a/src/FrontendUtil/Util.jl +++ b/src/FrontendUtil/Util.jl @@ -1355,11 +1355,11 @@ end Get all subscripts from a CREF, collecting from all levels. Returns a Vector of DAE.Subscript. """ -function getSubscriptsFromCref(cref::DAE.ComponentRef)::Vector +function getSubscriptsFromCref(cref::DAE.ComponentRef)::Vector{DAE.Subscript} local subscripts = DAE.Subscript[] for c in getAllCrefsAsVector(cref) if !isempty(c.subscriptLst) - append!(subscripts, collect(c.subscriptLst)) + append!(subscripts, c.subscriptLst) end end return subscripts @@ -1370,12 +1370,13 @@ end E.g., for R_w[1] returns "R_w". """ function getBaseNameWithoutSubscripts(cref::DAE.ComponentRef)::String - local crefs = getAllCrefsAsVector(cref) - local parts = String[] - for c in crefs - push!(parts, c.ident) + local buf = IOBuffer() + local first = true + for c in getAllCrefsAsVector(cref) + first ? (first = false) : print(buf, "_") + print(buf, c.ident) end - return join(parts, "_") + return String(take!(buf)) end end #=End Util=# diff --git a/src/SimulationCode/simCodeFunctions.jl b/src/SimulationCode/simCodeFunctions.jl index 8930734f..0afe9034 100644 --- a/src/SimulationCode/simCodeFunctions.jl +++ b/src/SimulationCode/simCodeFunctions.jl @@ -975,9 +975,13 @@ Base.@nospecializeinfer function tryEvalScalar(@nospecialize(exp::DAE.Exp), simC nothing else local (name, bindExp) = bound - local childSeen = copy(seen) - push!(childSeen, name) - tryEvalScalar(bindExp, simCode, childSeen) + #= Backtracking cycle guard on a shared set avoids copying `seen` per CREF. =# + push!(seen, name) + try + tryEvalScalar(bindExp, simCode, seen) + finally + delete!(seen, name) + end end end DAE.CALL(Absyn.IDENT("noEvent"), lst, _) => begin @@ -1012,9 +1016,13 @@ function _tryEvalNumeric(exp::DAE.Exp, simCode::SIM_CODE, seen::OrderedSet{Strin nothing else local (name, bindExp) = bound - local childSeen = copy(seen) - push!(childSeen, name) - _tryEvalNumeric(bindExp, simCode, childSeen) + #= Backtracking cycle guard on a shared set avoids copying `seen` per CREF. =# + push!(seen, name) + try + _tryEvalNumeric(bindExp, simCode, seen) + finally + delete!(seen, name) + end end end DAE.UNARY(DAE.UMINUS(__), inner) => begin diff --git a/src/SimulationCode/simCodeUtil.jl b/src/SimulationCode/simCodeUtil.jl index a1bacfb5..8af2059a 100644 --- a/src/SimulationCode/simCodeUtil.jl +++ b/src/SimulationCode/simCodeUtil.jl @@ -2905,16 +2905,7 @@ function _innermostType(cr::DAE.CREF_QUAL) return _innermostType(cr.componentRef) end -function _hasDimensions(dims)::Bool - try - return !listEmpty(dims) - catch - for _ in dims - return true - end - end - return false -end +_hasDimensions(dims)::Bool = !isempty(dims) function _declaredDaeVarCrefType(v::DAE.VAR)::DAE.Type local crefTy = _innermostType(v.componentRef) diff --git a/src/SimulationCode/simulationCodeTransformation.jl b/src/SimulationCode/simulationCodeTransformation.jl index a586b4f6..addac624 100644 --- a/src/SimulationCode/simulationCodeTransformation.jl +++ b/src/SimulationCode/simulationCodeTransformation.jl @@ -157,7 +157,7 @@ function transformToSimCode(equationSystems::Vector{BDAE.EQSYSTEM}, shared; mode local allSharedVars::Vector{BDAE.VAR} = getSharedVariablesLocalsAndGlobals(shared) local allBackendVars = vcat(equationSystem.orderedVars, allSharedVars) local simVars::Vector{SimulationCode.SIMVAR} = createAndCollectSimulationCodeVariables(allBackendVars, shared.flatModel) - local occVars = map((v)-> v.name, filter((v) -> isOCCVar(v), simVars)) + local occVars = String[v.name for v in simVars if isOCCVar(v)] #= Check if the model has state variables, if not introduce a dummy state =# @@ -1095,7 +1095,7 @@ an equation system. If no such data is present. Return two empty arrays function getSharedVariablesLocalsAndGlobals(shared::BDAE.SHARED) @match shared begin BDAE.SHARED(__) => vcat(shared.globalKnownVars, shared.localKnownVars) - _ => [] + _ => BDAE.VAR[] end end diff --git a/src/backendAPI.jl b/src/backendAPI.jl index cfe2b791..7b17c9ac 100644 --- a/src/backendAPI.jl +++ b/src/backendAPI.jl @@ -630,6 +630,8 @@ function lower(frontendDAE::DAE.DAE_LIST)::BDAE.BACKEND_DAE @debug "[BDAE] translated; full dump is available in backend/bdae logs when backend logging is enabled" #= Transform ASUB expressions: der(array)[i] to der(array[i]) =# bDAE = Causalize.transformASUBExpressions(bDAE) + #= Order-lower nested derivatives der(der(x)) into first-order auxiliary states =# + bDAE = Causalize.lowerHigherOrderDerivatives(bDAE) #= Mark state variables =# bDAE = Causalize.detectStates(bDAE) @debug "[BDAE] states marked" @@ -687,6 +689,8 @@ function lower(fm::OMFrontend.Frontend.FLAT_MODEL) bDAE = Causalize.resolveIntegerVariables(bDAE) #= Transform ASUB expressions: der(array)[i] to der(array[i]) =# bDAE = Causalize.transformASUBExpressions(bDAE) + #= Order-lower nested derivatives der(der(x)) into first-order auxiliary states =# + bDAE = Causalize.lowerHigherOrderDerivatives(bDAE) #= Mark state variables =# bDAE = Causalize.detectStates(bDAE) @BACKEND_LOGGING debugWrite(logPath("backend/bdae", "bdae_afterDetectStates.log"), BDAEUtil.stringHeading1(bDAE, "after detect states")) From e626a68d6fecd960e249523bf85e991cc55f50be Mon Sep 17 00:00:00 2001 From: JKRT Date: Sun, 21 Jun 2026 16:21:58 +0200 Subject: [PATCH 06/12] codegen: scalarise record values in algorithmic function bodies (fixes Spice3 in_c) Algorithmic Modelica function bodies kept a record used as a whole value as a bare symbol, so a record copy / call argument / return / field-index referenced an undeclared symbol (e.g. `in_c` in the Spice3 MOSFET functions), failing the in-backend structural_simplify build. Scalarise every record use onto the flattened `_` symbols (the naming flattenRecordInput already emits): - whole-record copy lhs := rhs -> per-field assignments - record call argument -> splat into its flat field symbols - record-valued call assignment -> scatter the flat-tuple return - record cref indexed by a constant -> the kth flat field symbol All four Spice3 examples (Inverter, Nor, Nand, FourInverters) now build via the in-backend structural_simplify path (previously they fell back). Detection is fully structural (cref/type pattern-matching, no string parsing); helpers carry strict argument types and signature docstrings. LRT green (289 pass / 0 fail / 6 broken) with this change. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/CodeGeneration/algorithmic.jl | 240 +++++++++++++++++++++++++++--- 1 file changed, 221 insertions(+), 19 deletions(-) diff --git a/src/CodeGeneration/algorithmic.jl b/src/CodeGeneration/algorithmic.jl index daf0325d..5027240c 100644 --- a/src/CodeGeneration/algorithmic.jl +++ b/src/CodeGeneration/algorithmic.jl @@ -627,10 +627,198 @@ function generateStatement(stmt::DAE.STMT_NORETCALL) end function generateStatement(stmt::DAE.STMT_ASSIGN) + local scalarised = _recordAssignment(stmt.exp1, stmt.exp) + scalarised === nothing || return scalarised local rhs = expToJuliaExpAlg(stmt.exp) return _algAssignment(stmt.exp1, rhs) end +""" + _recordAssignment(lhsExp::DAE.Exp, rhsExp::DAE.Exp) -> Union{Nothing,Expr} + +Scalarise a record-typed assignment onto its flattened `_` symbols +(the naming `flattenRecordInput` uses), or return `nothing` when `lhsExp` is not a +plain record cref. A record copy `lhs := rhs` becomes per-field assignments; a +record-valued call `lhs := f(args...)` scatters the flat-tuple return. +""" +function _recordAssignment(lhsExp::DAE.Exp, rhsExp::DAE.Exp)::Union{Nothing, Expr} + local lhs = _recordCrefFields(lhsExp) + lhs === nothing && return nothing + local lhsBase = lhs[1] + local fieldNames = lhs[2] + local rhsBase = _plainCrefName(rhsExp) + if rhsBase !== nothing + return Expr(:block, + Expr[:($(_flatFieldSymbol(lhsBase, f)) = $(_flatFieldSymbol(rhsBase, f))) for f in fieldNames]...) + elseif _isFunctionCall(rhsExp) + local targets = Expr(:tuple, [_flatFieldSymbol(lhsBase, f) for f in fieldNames]...) + return Expr(:(=), targets, expToJuliaExpAlg(rhsExp)) + end + return nothing +end + +""" + _flatFieldSymbol(base::AbstractString, field::AbstractString) -> Symbol + +Symbol for a flattened record field, ``, matching +the names emitted by `flattenRecordInput` for record parameters and locals. +""" +_flatFieldSymbol(base::AbstractString, field::AbstractString)::Symbol = Symbol(base, COMPONENT_SEPARATOR, field) + +""" + _isFunctionCall(exp::DAE.Exp) -> Bool + +True for a (record-valued) function-call expression. +""" +function _isFunctionCall(exp::DAE.Exp)::Bool + return @match exp begin + DAE.CALL(__) => true + _ => false + end +end + +""" + _algCallArgs(argExps::List) -> Vector{Any} + +Expand function-call arguments, replacing each record-typed cref with its flattened +`_` field symbols so a record argument is passed as its scalar fields, +matching the callee's flattened parameter list (`flattenRecordInput`). +""" +function _algCallArgs(argExps::List)::Vector{Any} + local out = Any[] + for arg in argExps + local rec = _recordCrefFields(arg) + if rec === nothing + push!(out, expToJuliaExpAlg(arg)) + else + for fieldName in rec[2] + push!(out, _flatFieldSymbol(rec[1], fieldName)) + end + end + end + return out +end + +""" + _recordCrefFields(exp::DAE.Exp) -> Union{Nothing,Tuple{String,Vector{String}}} + +`(baseIdent, fieldNames)` for a subscript-free record-typed simple CREF, else `nothing`. +""" +function _recordCrefFields(exp::DAE.Exp)::Union{Nothing, Tuple{String, Vector{String}}} + local base = _plainCrefName(exp) + base === nothing && return nothing + return @match exp begin + DAE.CREF(_, ty) => begin + local fieldNames = _recordFieldNames(ty) + isempty(fieldNames) ? nothing : (base, fieldNames) + end + _ => nothing + end +end + +""" + _recordFieldSymbol(base::AbstractString, recordType::DAE.Type, subscripts) -> Union{Nothing,Symbol} + +Flat field symbol `_` when `recordType` is a record and +`subscripts` is a single constant integer index in range; `nothing` otherwise. +This is field access on a record, not array indexing. +""" +function _recordFieldSymbol(base::AbstractString, recordType::DAE.Type, subscripts)::Union{Nothing, Symbol} + local fieldNames = _recordFieldNames(recordType) + isempty(fieldNames) && return nothing + length(subscripts) == 1 || return nothing + local index = _constantIntegerIndex(first(subscripts)) + (index === nothing || index < 1 || index > length(fieldNames)) && return nothing + return _flatFieldSymbol(base, fieldNames[index]) +end + +""" + _recordCrefIndexFieldSymbol(cref::DAE.ComponentRef) -> Union{Nothing,Symbol} + +Flat field symbol for a record cref carrying one constant-integer subscript, else `nothing`. +""" +function _recordCrefIndexFieldSymbol(cref::DAE.ComponentRef)::Union{Nothing, Symbol} + return @match cref begin + DAE.CREF_IDENT(ident, identType, subscripts) => _recordFieldSymbol(ident, identType, subscripts) + _ => nothing + end +end + +""" + _recordIndexFieldSymbol(arrExp::DAE.Exp, subscripts) -> Union{Nothing,Symbol} + +Flat field symbol for an ASUB indexing a record cref by one constant integer, else `nothing`. +""" +function _recordIndexFieldSymbol(arrExp::DAE.Exp, subscripts)::Union{Nothing, Symbol} + local base = _plainCrefName(arrExp) + base === nothing && return nothing + return @match arrExp begin + DAE.CREF(_, ty) => _recordFieldSymbol(base, ty, subscripts) + _ => nothing + end +end + +""" + _constantIntegerIndex(indexExp::DAE.Exp) -> Union{Nothing,Int} + +Constant integer value of an ASUB index expression (`a[k]` carries `DAE.ICONST`), +or `nothing` if the index is not a constant integer. +""" +function _constantIntegerIndex(indexExp::DAE.Exp)::Union{Nothing, Int} + return @match indexExp begin + DAE.ICONST(value) => value + _ => nothing + end +end + +""" + _constantIntegerIndex(subscript::DAE.Subscript) -> Union{Nothing,Int} + +Constant integer value of a subscripted-CREF subscript (`DAE.INDEX(exp)`), or +`nothing` if it is not a constant integer index. +""" +function _constantIntegerIndex(subscript::DAE.Subscript)::Union{Nothing, Int} + return @match subscript begin + DAE.INDEX(indexExp) => _constantIntegerIndex(indexExp) + _ => nothing + end +end + +""" + _recordFieldNames(ty::DAE.Type) -> Vector{String} + +Field names of a record type's `varLst` in declaration order; empty if `ty` is +not a record. +""" +function _recordFieldNames(ty::DAE.Type)::Vector{String} + local names = String[] + @match ty begin + DAE.T_COMPLEX(DAE.ClassInf.RECORD(__), varLst, _) => begin + for field in varLst + @match field begin + DAE.TYPES_VAR(fieldName, _, _, _, _) => push!(names, fieldName) + _ => nothing + end + end + end + _ => nothing + end + return names +end + +""" + _plainCrefName(exp::DAE.Exp) -> Union{String,Nothing} + +Identifier of a subscript-free simple CREF (the base for its flattened +`_` symbols), or `nothing` for qualified/subscripted/non-CREF exps. +""" +function _plainCrefName(exp::DAE.Exp)::Union{String, Nothing} + return @match exp begin + DAE.CREF(DAE.CREF_IDENT(ident, _, subscripts), _) => isempty(subscripts) ? ident : nothing + _ => nothing + end +end + function generateStatement(stmt::DAE.STMT_TUPLE_ASSIGN) #= Emit a proper Julia tuple destructuring `(a, _, b) = rhs`. The old code did `Symbol(string(expExpLst))`, producing a single @@ -654,6 +842,8 @@ Base.@nospecializeinfer function _crefToTupleTarget(@nospecialize(exp::DAE.Exp)) end function generateStatement(stmt::DAE.STMT_ASSIGN_ARR) + local scalarised = _recordAssignment(stmt.lhs, stmt.exp) + scalarised === nothing || return scalarised local rhs = expToJuliaExpAlg(stmt.exp) return _algAssignment(stmt.lhs, rhs) end @@ -867,15 +1057,24 @@ Base.@nospecializeinfer function expToJuliaExpAlg(@nospecialize(exp::DAE.Exp)):: end end DAE.CREF(cr, _) => begin - local varName::String = SimulationCode.string(cr) - local allSubscripts = collect(CodeGeneration.FrontendUtil.Util.getSubscriptsFromCref(cr)) - if !isempty(allSubscripts) - local baseName = first(split(varName, "[")) - local idxExprs = map(_algIndexExpr, allSubscripts) - Expr(:ref, Symbol(baseName), idxExprs...) - else + # A record cref with one constant-integer subscript is field access, not + # array indexing; emit the flat field symbol so it matches scalarised fields. + local recordField = _recordCrefIndexFieldSymbol(cr) + if recordField !== nothing quote - $(Symbol(varName)) + $(recordField) + end + else + local varName::String = SimulationCode.string(cr) + local allSubscripts = collect(CodeGeneration.FrontendUtil.Util.getSubscriptsFromCref(cr)) + if !isempty(allSubscripts) + local baseName = first(split(varName, "[")) + local idxExprs = map(_algIndexExpr, allSubscripts) + Expr(:ref, Symbol(baseName), idxExprs...) + else + quote + $(Symbol(varName)) + end end end end @@ -964,10 +1163,7 @@ Base.@nospecializeinfer function expToJuliaExpAlg(@nospecialize(exp::DAE.Exp)):: if !(attr.builtin) push!(expr.args, funcSym) end - local args = map(explst) do arg - expToJuliaExpAlg(arg) - end - append!(expr.args, args) + append!(expr.args, _algCallArgs(explst)) quote $(expr) end @@ -993,10 +1189,7 @@ Base.@nospecializeinfer function expToJuliaExpAlg(@nospecialize(exp::DAE.Exp)):: if utilRuntimeName === nothing && !(attr.builtin) push!(expr.args, funcSym) end - local args = map(expLst) do arg - expToJuliaExpAlg(arg) - end - append!(expr.args, args) + append!(expr.args, _algCallArgs(expLst)) expr end DAE.CAST(ty, exp) => begin @@ -1051,9 +1244,18 @@ Base.@nospecializeinfer function expToJuliaExpAlg(@nospecialize(exp::DAE.Exp)):: quote $index end end DAE.ASUB(arrExp, subLst) => begin - local arrExpr = expToJuliaExpAlg(arrExp) - local subs = map(_algIndexExpr, subLst) - Expr(:ref, arrExpr, subs...) + # A record cref indexed by a constant integer is field access, not array + # indexing; emit the flat field symbol so it matches the scalarised fields. + local recordField = _recordIndexFieldSymbol(arrExp, subLst) + if recordField !== nothing + quote + $(recordField) + end + else + local arrExpr = expToJuliaExpAlg(arrExp) + local subs = map(_algIndexExpr, subLst) + Expr(:ref, arrExpr, subs...) + end end DAE.RECORD(path, exps, fieldNames, ty) => begin #= Record constructor: generate as a simple tuple =# From 793ac23f306dcb97ace4baadaca0ebfd2da1b34d Mon Sep 17 00:00:00 2001 From: JKRT Date: Sun, 21 Jun 2026 17:53:10 +0200 Subject: [PATCH 07/12] Fixed brokens tests. Minor rewrite of other funcs --- Project.toml | 2 + src/CodeGeneration/MTK_CodeGeneration.jl | 6 +- src/OMBackend.jl | 2 + src/SimulationCode/simCodeUtil.jl | 292 ++++++++++++++++++----- 4 files changed, 236 insertions(+), 66 deletions(-) diff --git a/Project.toml b/Project.toml index 4a5401f1..bb413f83 100644 --- a/Project.toml +++ b/Project.toml @@ -26,6 +26,7 @@ ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78" OMFrontend = "b10394b5-f439-4073-a0c5-e09cb00cf46c" OMParser = "11f87224-cae7-4e99-a924-e50d12f62c59" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" SCode = "350d45b0-c210-11e9-3b9e-fdad65bb3d46" Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46" @@ -55,6 +56,7 @@ SCode = { path = "../SCode.jl" } [compat] ModelingToolkit = "= 11.21.0" +PrecompileTools = "1" julia = "1.12" [extras] diff --git a/src/CodeGeneration/MTK_CodeGeneration.jl b/src/CodeGeneration/MTK_CodeGeneration.jl index b2e90307..0cd9585d 100644 --- a/src/CodeGeneration/MTK_CodeGeneration.jl +++ b/src/CodeGeneration/MTK_CodeGeneration.jl @@ -4145,7 +4145,7 @@ function decomposeEquationsInline(equations, parameterAssignments; chunkSize::In local constructors = quote $(parameterAssignments...) local equationConstructors::Vector{Function} - local equationConstructorCalls::Vector + local equationConstructorCalls::Vector{Function} end push!(exprs, constructors) local i = 0 @@ -4156,14 +4156,14 @@ function decomposeEquationsInline(equations, parameterAssignments; chunkSize::In if isempty(csPreamble) push!(exprs, quote function $(fName)() - [$(eqv...)] + Symbolics.Equation[$(eqv...)] end end) else push!(exprs, quote function $(fName)() $(csPreamble...) - [$(csEqs...)] + Symbolics.Equation[$(csEqs...)] end end) end diff --git a/src/OMBackend.jl b/src/OMBackend.jl index 781b7e7f..657bb95f 100644 --- a/src/OMBackend.jl +++ b/src/OMBackend.jl @@ -48,4 +48,6 @@ include("$CURRENT_DIRECTORY/CodeGeneration/iMTKGen.jl") include("backendUtils.jl") #= Finally add the API=# include("backendAPI.jl") +#= Precompile workload: warm shared MTK/OrdinaryDiffEq build+solve instances. =# +include("precompile.jl") end #=OMBackend=# diff --git a/src/SimulationCode/simCodeUtil.jl b/src/SimulationCode/simCodeUtil.jl index 8af2059a..76b7fe41 100644 --- a/src/SimulationCode/simCodeUtil.jl +++ b/src/SimulationCode/simCodeUtil.jl @@ -1163,36 +1163,8 @@ function collectCrefNames!(names::OrderedSet{String}, exp::Exp) CALL(__) => begin for x in exp.args; collectCrefNames!(names, x) end end RECORD(__) => begin for x in exp.exps; collectCrefNames!(names, x) end end TUPLE(__) => begin for x in exp.PR; collectCrefNames!(names, x) end end - REDUCTION(__) => begin - collectCrefNames!(names, exp.body) - #= iterators carry DAE.ReductionIterator range/guard exps (passed through by - toDAEExp); collect their crefs to match the DAE collector exactly. =# - for it in exp.iterators - @match it begin - DAE.REDUCTIONITER(exp = rangeExp) => collectCrefNames!(names, rangeExp) - _ => () - end - end - end - ASUB(__) => begin - if exp.exp isa EXP_CREF - local allConst = true - local sstr = "" - for s in exp.subs - @match s begin - ICONST(i) => (sstr *= Base.string("[", i, "]")) - _ => (allConst = false) - end - end - if allConst && !isempty(sstr) - push!(names, Base.string(DAE_identifierToString(toDAECref(exp.exp.cref).componentRef), sstr)) - end - end - collectCrefNames!(names, exp.exp) - for s in exp.subs - collectCrefNames!(names, s) - end - end + REDUCTION(__) => collectCrefNamesForReduction(names, exp) + ASUB(__) => collectCrefNamesForAsub(names, exp) _ => () end return names @@ -1228,38 +1200,7 @@ function collectCrefNames!(names::OrderedSet{String}, @nospecialize(exp)) collectCrefNames!(names, e) end end - DAE.ASUB(exp = e, sub = subs) => begin - #= When ASUB wraps a CREF with constant integer subscripts, reconstruct the - subscripted name (e.g. "R_T[1][1]") to match the hash table key format. - Without this, the BFS use-def chain is broken: collectCrefNames collects - the base name "R_T" but the hash table has "R_T[1][1]". =# - local asubHandled = false - @match e begin - DAE.CREF(cr, _) => begin - local baseName = DAE_identifierToString(cr) - local allConst = true - local subscriptStr = "" - for s in subs - @match s begin - DAE.ICONST(i) => begin subscriptStr *= Base.string("[", i, "]") end - _ => begin allConst = false end - end - end - if allConst && !isempty(subscriptStr) - push!(names, Base.string(baseName, subscriptStr)) - end - push!(names, baseName) - asubHandled = true - end - _ => () - end - if !asubHandled - collectCrefNames!(names, e) - end - for s in subs - collectCrefNames!(names, s) - end - end + DAE.ASUB(exp = e, sub = subs) => collectCrefNamesForDAEAsub(names, e, subs) DAE.RELATION(exp1 = e1, exp2 = e2) => begin collectCrefNames!(names, e1) collectCrefNames!(names, e2) @@ -1281,6 +1222,105 @@ function collectCrefNames!(names::OrderedSet{String}, @nospecialize(exp)) return nothing end +""" + _simConstSubscriptSuffix(subs::Vector{Exp}) -> Union{String, Nothing} + +Build the `"[i][j]..."` suffix for an all-constant integer SIM subscript list. +Returns `nothing` if any subscript is non-constant or the list is empty. +""" +function _simConstSubscriptSuffix(subs::Vector{Exp})::Union{String, Nothing} + local suffix = "" + for s in subs + local piece = @match s begin + ICONST(i) => Base.string("[", i, "]") + _ => nothing + end + piece === nothing && return nothing + suffix = Base.string(suffix, piece) + end + return isempty(suffix) ? nothing : suffix +end + +""" + _daeConstSubscriptSuffix(subs) -> Union{String, Nothing} + +DAE-side counterpart of `_simConstSubscriptSuffix` over a `DAE.ICONST` subscript +list. Returns `nothing` if any subscript is non-constant or the list is empty. +""" +function _daeConstSubscriptSuffix(@nospecialize(subs))::Union{String, Nothing} + local suffix = "" + for s in subs + local piece = @match s begin + DAE.ICONST(i) => Base.string("[", i, "]") + _ => nothing + end + piece === nothing && return nothing + suffix = Base.string(suffix, piece) + end + return isempty(suffix) ? nothing : suffix +end + +"Collect cref names from a SIM `REDUCTION` body and its iterator range/guard exps." +function collectCrefNamesForReduction(names::OrderedSet{String}, exp::REDUCTION) + collectCrefNames!(names, exp.body) + #= iterators carry DAE.ReductionIterator range/guard exps (passed through by + toDAEExp); collect their crefs to match the DAE collector exactly. =# + for it in exp.iterators + @match it begin + DAE.REDUCTIONITER(exp = rangeExp) => collectCrefNames!(names, rangeExp) + _ => () + end + end + return names +end + +""" + collectCrefNamesForAsub(names::OrderedSet{String}, exp::ASUB) -> names + +Collect cref names from a SIM `ASUB`, reconstructing the subscripted key +(e.g. `"R_T[1][1]"`) for all-constant subscripts so the use-def chain matches +the scalarized hash-table keys. +""" +function collectCrefNamesForAsub(names::OrderedSet{String}, exp::ASUB) + if exp.exp isa EXP_CREF + local suffix = _simConstSubscriptSuffix(exp.subs) + if suffix !== nothing + push!(names, Base.string(DAE_identifierToString(toDAECref(exp.exp.cref).componentRef), suffix)) + end + end + collectCrefNames!(names, exp.exp) + for s in exp.subs + collectCrefNames!(names, s) + end + return names +end + +""" + collectCrefNamesForDAEAsub(names::OrderedSet{String}, e, subs) -> nothing + +DAE-side counterpart of `collectCrefNamesForAsub`: reconstructs the subscripted +key for a `DAE.CREF` base with all-constant subscripts, then descends into the +base and subscript expressions. +""" +function collectCrefNamesForDAEAsub(names::OrderedSet{String}, @nospecialize(e), @nospecialize(subs)) + local asubHandled = false + @match e begin + DAE.CREF(cr, _) => begin + local baseName = DAE_identifierToString(cr) + local suffix = _daeConstSubscriptSuffix(subs) + suffix === nothing || push!(names, Base.string(baseName, suffix)) + push!(names, baseName) + asubHandled = true + end + _ => () + end + asubHandled || collectCrefNames!(names, e) + for s in subs + collectCrefNames!(names, s) + end + return nothing +end + function _hasUnknownCref(exp, ht)::Bool local names = OrderedSet{String}() collectCrefNames!(names, exp) @@ -1800,6 +1840,123 @@ function _hasExplicitFixedStart(@nospecialize(attrs))::Bool end end +"`true` if `exp` is a literal `1` exponent, so `base ^ exp` stays affine in base." +function _isUnitExponent(exp::Exp)::Bool + @match exp begin + RCONST(v) => v == 1.0 + ICONST(v) => v == 1 + _ => false + end +end + +"ASUB scalar HT name for an all-constant-subscript cref base, else `nothing`." +function _asubScalarName(exp::ASUB)::Union{String, Nothing} + exp.exp isa EXP_CREF || return nothing + local suffix = _simConstSubscriptSuffix(exp.subs) + suffix === nothing && return nothing + return Base.string(DAE_identifierToString(toDAECref(exp.exp.cref).componentRef), suffix) +end + +""" + _simCrefScalarName(exp::Exp) -> Union{String, Nothing} + +Canonical scalar hash-table name for a cref-shaped `exp` (matching +`collectCrefNames!` keys), or `nothing` when `exp` is not a cref or has no +stable scalar name (e.g. an ASUB with non-constant subscripts). +""" +function _simCrefScalarName(exp::Exp)::Union{String, Nothing} + @match exp begin + EXP_CREF(__) => DAE_identifierToString(toDAECref(exp.cref).componentRef) + ASUB(__) => _asubScalarName(exp) + _ => nothing + end +end + +"`true` if `varName` is referenced anywhere in `exp` (reuses `collectCrefNames!`)." +function _occursAnywhere(exp::Exp, varName::AbstractString)::Bool + local names = OrderedSet{String}() + collectCrefNames!(names, exp) + return varName in names +end + +""" + _powLinearity(exponent, oBase, lBase, oExp) -> (occurs, linear) + +Linearity of `base ^ exponent` w.r.t. the target variable, from the base's +occurrence/linearity (`oBase`, `lBase`) and whether the exponent contains it +(`oExp`). Only `base ^ 1` with a linear base stays affine. +""" +function _powLinearity(exponent::Exp, oBase::Bool, lBase::Bool, oExp::Bool)::Tuple{Bool, Bool} + oExp && return (true, false) + oBase || return (false, true) + return _isUnitExponent(exponent) ? (true, lBase) : (true, false) +end + +"Occurrence/linearity of `varName` across a SIM `BINARY` node. Enum operators +are compared by value (`@match` treats a bare enum name as a capture binding)." +function _binaryLinearity(exp::BINARY, varName::AbstractString)::Tuple{Bool, Bool} + local (o1, l1) = _occursLinearly(exp.exp1, varName) + local (o2, l2) = _occursLinearly(exp.exp2, varName) + local op = exp.op + if op === OP_ADD || op === OP_SUB + return (o1 || o2, l1 && l2) + elseif op === OP_MUL + return (o1 || o2, l1 && l2 && !(o1 && o2)) + elseif op === OP_DIV + return (o1 || o2, l1 && !o2) + elseif op === OP_POW + return _powLinearity(exp.exp2, o1, l1, o2) + else + return (o1 || o2, !(o1 || o2)) + end +end + +"Occurrence/linearity of `varName` across a SIM `IFEXP` (var in cond ⇒ nonlinear)." +function _ifexpLinearity(exp::IFEXP, varName::AbstractString)::Tuple{Bool, Bool} + local (oc, _) = _occursLinearly(exp.cond, varName) + oc && return (true, false) + local (ot, lt) = _occursLinearly(exp.thenExp, varName) + local (oe, le) = _occursLinearly(exp.elseExp, varName) + return (ot || oe, lt && le) +end + +""" + _occursLinearly(exp::Exp, varName::AbstractString) -> (occurs::Bool, linear::Bool) + +Whether `varName` appears in `exp`, and if so only affinely (degree ≤ 1, never +inside a nonlinear operator or function argument). Conservative: any construct +whose linearity cannot be established yields `linear = false`, which keeps the +variable in the residual system (always semantically valid). +""" +function _occursLinearly(exp::Exp, varName::AbstractString)::Tuple{Bool, Bool} + local nm = _simCrefScalarName(exp) + nm === nothing || return (nm == varName, true) + @match exp begin + UNARY(__) || CAST(__) => _occursLinearly(exp.exp, varName) + BINARY(__) => _binaryLinearity(exp, varName) + IFEXP(__) => _ifexpLinearity(exp, varName) + ICONST(__) || RCONST(__) || BCONST(__) || SCONST(__) || ENUM_LITERAL(__) || WILD(__) => + (false, true) + _ => begin + local occurs = _occursAnywhere(exp, varName) + (occurs, !occurs) + end + end +end + +""" + _isLinearlySolvableFor(exp::Exp, varName::AbstractString) -> Bool + +`true` iff `varName` appears in residual `exp` and only affinely, so +`Symbolics.solve_for(0 ~ exp, varName)` yields a valid explicit observation. +An output-only sink variable failing this test (e.g. defined by a nonlinear +closure) must stay in the residual system for MTK to solve numerically. +""" +function _isLinearlySolvableFor(exp::Exp, varName::AbstractString)::Bool + local (occurs, linear) = _occursLinearly(exp, varName) + return occurs && linear +end + """ eliminateOutputOnlyVariables(simCode::SIM_CODE, options::EliminationOptions) @@ -1850,6 +2007,7 @@ function eliminateOutputOnlyVariables(simCode::SIM_CODE, options::EliminationOpt local eliminatedPairs = Tuple{String, Int}[] #= (varName, eqIdx) for pairing =# local nSkippedNonAlg = 0 local nSkippedUnmatched = 0 + local nSkippedNonlinear = 0 for eqIdx in outputOnlyEqIndices if !haskey(eqToMatchIdx, eqIdx) nSkippedUnmatched += 1 @@ -1872,6 +2030,14 @@ function eliminateOutputOnlyVariables(simCode::SIM_CODE, options::EliminationOpt _ => false end if isEliminable + #= The eliminated pair is later reconstructed via Symbolics.solve_for, a + linear solver. A variable defined by an equation nonlinear in itself + (e.g. a holonomic loop closure) must stay in the residual system for + MTK to solve numerically; eliminating it would trip `islinear`. =# + if !_isLinearlySolvableFor(simCode.residualEquations[eqIdx].exp, vn) + nSkippedNonlinear += 1 + continue + end push!(eqsToEliminate, eqIdx) push!(varsToRemove, vn) push!(eliminatedPairs, (vn, eqIdx)) @@ -2003,7 +2169,7 @@ function eliminateOutputOnlyVariables(simCode::SIM_CODE, options::EliminationOpt for varName in varsToRemove delete!(newHT, varName) end - @debug "[SIMCODE: $(simCode.name): eliminateNonDynamic] eliminated $(length(eqsToEliminate)) eq-var pairs, $(length(varsToRemove)) variables removed (rescued: $nRescued, skipped: $nSkippedNonAlg non-algebraic, $nSkippedUnmatched unmatched). $(length(newResEqs)) equations, $(length(newHT)) variables remain" + @debug "[SIMCODE: $(simCode.name): eliminateNonDynamic] eliminated $(length(eqsToEliminate)) eq-var pairs, $(length(varsToRemove)) variables removed (rescued: $nRescued, skipped: $nSkippedNonAlg non-algebraic, $nSkippedNonlinear nonlinear, $nSkippedUnmatched unmatched). $(length(newResEqs)) equations, $(length(newHT)) variables remain" @BACKEND_LOGGING begin local buf = IOBuffer() println(buf, "=== ELIMINATION DEBUG ===") From 9d9a83b72e32e646942c3b49e0046095f4ef73e6 Mon Sep 17 00:00:00 2001 From: JKRT Date: Mon, 22 Jun 2026 00:10:54 +0200 Subject: [PATCH 08/12] Precompile directives + IMTK compilation adjustments --- src/CodeGeneration/CodeGeneration.jl | 1 + src/CodeGeneration/DirectRHSGeneration.jl | 43 +++++++++++-- src/CodeGeneration/iMTKGen.jl | 19 ++++++ src/backendAPI.jl | 20 +++++- src/precompile.jl | 76 +++++++++++++++++++++++ 5 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 src/precompile.jl diff --git a/src/CodeGeneration/CodeGeneration.jl b/src/CodeGeneration/CodeGeneration.jl index 1ac7da3d..fd401c3c 100644 --- a/src/CodeGeneration/CodeGeneration.jl +++ b/src/CodeGeneration/CodeGeneration.jl @@ -42,6 +42,7 @@ using Setfield using DocStringExtensions using ModelingToolkit using LinearAlgebra +import DiffEqBase using ..FrontendUtil using ..Backend #Should maybe not be using here... since it can make certain overloads a bit tricky to follow. diff --git a/src/CodeGeneration/DirectRHSGeneration.jl b/src/CodeGeneration/DirectRHSGeneration.jl index 006d7ce4..cc2f32e9 100644 --- a/src/CodeGeneration/DirectRHSGeneration.jl +++ b/src/CodeGeneration/DirectRHSGeneration.jl @@ -48,6 +48,40 @@ # MTK-stage dump helpers (see CodeGeneration/mtkDump.jl). import .MTKDump: dumpBuildDirectRHSInputs, dumpRHSExpression +""" + _buildDirectODEFunction(rhsFunc, u0, p_vec, t0; mass_matrix, sys, jacFunc, jacProto) + +Build the `ODEFunction` for the direct-RHS problem. When +`OMBackend.DIRECT_RHS_TYPE_ERASE` is set the runtime-generated RHS (and the +symbolic Jacobian) are wrapped in `FunctionWrappers` so the resulting problem +type is constant across models; the solver then compiles its stepping / Newton +/ linear-solve machinery once instead of once per distinct model. `u0`, `p_vec` +and `t0` supply only the argument *types* the wrappers specialize on; their +values are immaterial. The mass matrix, Jacobian and `sys` are attached to the +function we build, so type erasure does not drop them. +""" +function _buildDirectODEFunction(rhsFunc, u0, p_vec, t0; + mass_matrix=nothing, sys=nothing, + jacFunc=nothing, jacProto=nothing) + if !OMBackend.DIRECT_RHS_TYPE_ERASE[] + local jacKw = jacFunc === nothing ? NamedTuple() : (; jac=jacFunc, jac_prototype=jacProto) + return mass_matrix === nothing ? + ModelingToolkit.ODEFunction{true}(rhsFunc; sys=sys, jacKw...) : + ModelingToolkit.ODEFunction{true}(rhsFunc; mass_matrix=mass_matrix, sys=sys, jacKw...) + end + local FW = ModelingToolkit.SciMLBase.FunctionWrapperSpecialize + #= Multi-variant wrapper (Float64 + ForwardDiff Dual signatures) so autodiff + solvers stay correct; the Jacobian is never called with Duals, so a single + variant suffices there. =# + local wrappedRHS = DiffEqBase.wrapfun_iip(rhsFunc, (u0, u0, p_vec, t0)) + local erasedKw = jacFunc === nothing ? NamedTuple() : + (; jac = DiffEqBase.wrapfun_jac_iip(jacFunc, (jacProto, u0, p_vec, t0)), + jac_prototype = jacProto) + return mass_matrix === nothing ? + ModelingToolkit.ODEFunction{true, FW}(wrappedRHS; sys=sys, erasedKw...) : + ModelingToolkit.ODEFunction{true, FW}(wrappedRHS; mass_matrix=mass_matrix, sys=sys, erasedKw...) +end + """ buildDirectRHSProblem(reducedSystem, finalInitialValues, pars, tspan, callbacks; allInitialValues=nothing) @@ -149,8 +183,6 @@ function buildDirectRHSProblem(reducedSystem, finalInitialValues, pars, tspan, c symbolic derivative surfaces only when the function runs, not at build. =# local (jacFunc, jacProto) = _buildSparseJacobian(rhs_list, states, params, iv, u0, p_vec, tspan[1]) - local jacKwargs = jacFunc === nothing ? NamedTuple() : - (; jac = jacFunc, jac_prototype = jacProto) # 4. Extract event callbacks from the reduced system and merge with custom callbacks. # Our structural_simplify wrapper uses split=false, so the compiled event @@ -164,7 +196,8 @@ function buildDirectRHSProblem(reducedSystem, finalInitialValues, pars, tspan, c local problem if massMatrix isa LinearAlgebra.UniformScaling @debug "DirectRHS: pure ODE (identity mass matrix)" - local f = ModelingToolkit.ODEFunction{true}(rhsFunc; sys=reducedSystem, jacKwargs...) + local f = _buildDirectODEFunction(rhsFunc, u0, p_vec, tspan[1]; + sys=reducedSystem, jacFunc=jacFunc, jacProto=jacProto) problem = ModelingToolkit.ODEProblem{true}(f, u0, tspan, p_vec; callback=allCallbacks) else @debug "DirectRHS: DAE with mass matrix" @@ -172,7 +205,9 @@ function buildDirectRHSProblem(reducedSystem, finalInitialValues, pars, tspan, c #= A sparse Jacobian prototype needs a sparse mass matrix, otherwise the solver's W = M - gamma*J assembly densifies or mismatches. =# local mmForF = jacFunc === nothing ? mm : Symbolics.SparseArrays.sparse(mm) - local f = ModelingToolkit.ODEFunction{true}(rhsFunc; mass_matrix=mmForF, sys=reducedSystem, jacKwargs...) + local f = _buildDirectODEFunction(rhsFunc, u0, p_vec, tspan[1]; + mass_matrix=mmForF, sys=reducedSystem, + jacFunc=jacFunc, jacProto=jacProto) #= Pinned indices: vars whose u0 came from a fixed=true Modelica init eq (after splitInitialValues). The DAE init solver must NOT modify these, otherwise an algebraic var pinned by `start=1, fixed=true` (e.g. diff --git a/src/CodeGeneration/iMTKGen.jl b/src/CodeGeneration/iMTKGen.jl index 39876e40..0c8b96f3 100644 --- a/src/CodeGeneration/iMTKGen.jl +++ b/src/CodeGeneration/iMTKGen.jl @@ -19,6 +19,12 @@ const DUMP_ENABLED = Ref(false) by the generated `Model(tspan)`: (problem, callbacks, ivs, _ivs_all, reducedSystem, tspan, pars, vars, irreducibleSyms). =# const BUILT = Dict{String, Tuple}() +#= Hash of (modelCode, build-affecting flags) for each cached build. A repeat + translate (e.g. overwriteCache) whose regenerated code and flags are + unchanged reuses the live module + cached build instead of re-eval'ing, which + would otherwise force redundant recompilation of the generated + `simulateFromBuild` / `simulate` methods on the next solve. =# +const BUILT_HASH = Dict{String, UInt64}() const REDUCED_SYSTEMS = Dict{String, Any}() const DUMP_PATHS = Dict{String, String}() #= Pristine parameter snapshot per build: event affects mutate the problem's @@ -63,6 +69,18 @@ end function _buildAndCache(modelName::String, modelCode::Expr) local OMB = _OMBackend() local cname = OMB.canonicalName(modelName) + #= Reuse path: identical regenerated code + build flags, a live module and a + cached build mean the compiled methods and the problem are still valid. + Re-eval'ing identical code would only invalidate `simulateFromBuild` and + friends, forcing a full recompile on the next solve. Flags read at build + time (not codegen) are folded into the hash so a flag flip still rebuilds. =# + local buildHash = hash((modelCode, OMB.DIRECT_RHS_GENERATION[], + OMB.DIRECT_JAC_GENERATION[], OMB.DIRECT_RHS_TYPE_ERASE[])) + if get(BUILT_HASH, cname, UInt64(0)) == buildHash && + haskey(BUILT, cname) && isdefined(OMB, Symbol(cname)) + @info "[IMTK GEN] regenerated code unchanged; reusing compiled module + cached build" model = modelName + return nothing + end try if get(ENV, "OMJL_STASH_MODELCODE", "") != "" LAST_MODELCODE[] = modelCode @@ -82,6 +100,7 @@ function _buildAndCache(modelName::String, modelCode::Expr) modelFn(IMTK_BUILD_TSPAN) end BUILT[cname] = res + BUILT_HASH[cname] = buildHash if res isa Tuple && length(res) >= 5 REDUCED_SYSTEMS[cname] = res[5] end diff --git a/src/backendAPI.jl b/src/backendAPI.jl index 7b17c9ac..0f9ead06 100644 --- a/src/backendAPI.jl +++ b/src/backendAPI.jl @@ -77,6 +77,21 @@ Toggle with: `OMBackend.DIRECT_JAC_GENERATION[] = false` to disable. """ const DIRECT_JAC_GENERATION = Ref{Bool}(true) +""" +Toggle type erasure of direct-RHS problems via `FunctionWrapperSpecialize`. +The generated RHS (and symbolic Jacobian) are runtime-generated functions +whose type is unique per model, so the resulting `ODEProblem` type differs +per model and `solve` re-specializes its whole stepping/Newton/linear-solve +machinery for every distinct model. Wrapping the inner functions in +`FunctionWrappers` makes the problem type constant across models, so that +machinery compiles once and is reused. Correctness is preserved: the mass +matrix, sparse Jacobian and initialization are attached to the `ODEFunction` +we build ourselves, not dropped by the wrapper. + +Toggle with: `OMBackend.DIRECT_RHS_TYPE_ERASE[] = false` to disable. +""" +const DIRECT_RHS_TYPE_ERASE = Ref{Bool}(true) + const OSMC_COPYRIGHT_HEADER = """ #= * This file is part of OpenModelica. @@ -206,7 +221,10 @@ function clearCaches!(; models::Bool=true, wrappers::Bool=true, extractors::Bool=true) cleared = String[] - models && (empty!(COMPILED_MODELS_MTK); push!(cleared, "models")) + models && (empty!(COMPILED_MODELS_MTK); + empty!(IMTKGen.BUILT); empty!(IMTKGen.BUILT_HASH); + empty!(IMTKGen.REDUCED_SYSTEMS); empty!(IMTKGen.PRISTINE_P); + push!(cleared, "models")) implementations && (empty!(CodeGeneration.MODELICA_FUNCTION_IMPLS); push!(cleared, "implementations")) wrappers && (empty!(CodeGeneration.MODELICA_FUNCTION_WRAPPERS); push!(cleared, "wrappers")) extractors && (empty!(CodeGeneration.ELEM_FUNC_CACHE); push!(cleared, "extractors")) diff --git a/src/precompile.jl b/src/precompile.jl new file mode 100644 index 00000000..11fd1039 --- /dev/null +++ b/src/precompile.jl @@ -0,0 +1,76 @@ +#= Warm the shared MTK + OrdinaryDiffEq method instances that every generated + model module invokes at build/solve time (structural_simplify, the init-solve + ODEProblem, and the Rodas5/FBDF mass-matrix solve). The per-model module is + eval'd after package load and cannot be cached into the image; these shared + callees can, which is where the first-call latency actually lives. + + The DirectRHS workload below is the load-bearing one: because type erasure + makes every DirectRHS problem share one concrete `ODEProblem` type (the + runtime-generated RHS/jac are hidden behind FunctionWrappers), `solve` on that + type is a single specialization that can be baked into the package image here. + A real model that produces the same erased type then reuses it from its first + solve in a fresh session — paid once at build, not once per model. =# +using PrecompileTools: @setup_workload, @compile_workload + +#= Small index-1 system whose algebraic row folds away, leaving a pure-ODE + reduced system (UniformScaling mass matrix) — the most common DirectRHS class. =# +function _precompileBuildReduced() + @independent_variables t + D = Differential(t) + @variables x(t) y(t) z(t) + @parameters a + eqs = [D(x) ~ -a * x + y, + D(y) ~ x - y, + 0 ~ z - (x + y)] + sys = ODESystem(eqs, t, [x, y, z], [a]; + name = :OMBackendPrecompileWarmup, + guesses = [z => 0.0], + initialization_eqs = ModelingToolkit.Equation[]) + local reduced = CodeGeneration.structural_simplify(sys; simplify = true, + allow_parameter = true, split = false) + return (reduced, [x => 1.0, y => 0.0], Dict(a => 1.0)) +end + +#= Mirrors the generated XModel build for the standard MTK System-form path + (structural transitions / non-DirectRHS modes). =# +function _precompileWarmup() + local (reduced, ivs, pars) = _precompileBuildReduced() + local prob = ODEProblem(reduced, merge(Dict(ivs), pars), (0.0, 1.0); + warn_initialize_determined = false, + build_initializeprob = true, fully_determined = false) + prob = ModelingToolkit.SciMLBase.remake(prob; tspan = (0.0, 2.0)) + solve(prob, Rodas5(autodiff = false)) + solve(prob, FBDF(autodiff = false)) + return nothing +end + +#= Bakes the type-erased DirectRHS solve into the image. Builds the problem + through the real `buildDirectRHSProblem` (so the type matches what models + produce) and solves with the two default solvers. =# +function _precompileWarmupDirectRHS() + local (reduced, ivs, pars) = _precompileBuildReduced() + local callbacks = ModelingToolkit.SciMLBase.CallbackSet() + local prob = CodeGeneration.buildDirectRHSProblem(reduced, ivs, pars, (0.0, 2.0), callbacks) + solve(prob, Rodas5(autodiff = false)) + solve(prob, FBDF(autodiff = false)) + return nothing +end + +@setup_workload begin + @compile_workload begin + #= Escape hatch for fast dev precompiles; a workload failure must never break + loading, so each is demoted to debug. =# + if get(ENV, "OMBACKEND_NO_PRECOMPILE_WORKLOAD", "") == "" + try + _precompileWarmup() + catch err + @debug "[OMBackend] precompile warmup skipped" exception = err + end + try + _precompileWarmupDirectRHS() + catch err + @debug "[OMBackend] DirectRHS precompile warmup skipped" exception = err + end + end + end +end From 34b65a6b3bcf29f65fb4e9ffd86c923b8903a331 Mon Sep 17 00:00:00 2001 From: JKRT Date: Tue, 23 Jun 2026 13:24:19 +0200 Subject: [PATCH 09/12] Function wrapper refactor. Work on improving precomp --- README.md | 2 +- src/Backend/BDAECreate.jl | 38 +-- src/Backend/BDAEUtil.jl | 14 +- src/Backend/Causalize.jl | 294 +++++++++------------- src/CodeGeneration/DirectRHSGeneration.jl | 161 ++++++++++++ src/CodeGeneration/MTK_CodeGeneration.jl | 9 + src/CodeGeneration/iMTKGen.jl | 25 +- src/FrontendUtil/Util.jl | 24 +- src/SimulationCode/simCodeCheck.jl | 95 ++++--- src/backendAPI.jl | 14 ++ src/precompile.jl | 88 +++++++ 11 files changed, 509 insertions(+), 255 deletions(-) diff --git a/README.md b/README.md index 2fa5bdef..b206ec9c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Github Action CI](https://github.com/JKRT/OMBackend.jl/workflows/CI/badge.svg)](https://github.com/JKRT/OMBackend.jl/actions) [![License: OSMC-PL](https://img.shields.io/badge/license-OSMC--PL-lightgrey.svg)](LICENSE.md) +[![Github Action CI](https://github.com/OpenModelica/OMBackend.jl/workflows/CI/badge.svg)](https://github.com/OpenModelica/OMBackend.jl/actions) [![License: OSMC-PL](https://img.shields.io/badge/license-OSMC--PL-lightgrey.svg)](LICENSE.md) # About OMBackend.jl diff --git a/src/Backend/BDAECreate.jl b/src/Backend/BDAECreate.jl index d1cb5036..4fae6325 100644 --- a/src/Backend/BDAECreate.jl +++ b/src/Backend/BDAECreate.jl @@ -1905,15 +1905,15 @@ Base.@nospecializeinfer function _collectDiscreteRhsCrefsFromWhenOps(ops::Vector local out = DAE.ComponentRef[] local seen = OrderedSet{String}() local blocked = copy(assignedLhs) - local ctx = (out, seen, blocked) + local ctx = DiscreteRhsCrefVisitor(out, seen, blocked) for op in ops @match op begin - BDAE.ASSIGN(_, rhs, _) => Util.traverseExpTopDown(rhs, _visitDiscreteRhsCref, ctx) - BDAE.NORETCALL(exp, _) => Util.traverseExpTopDown(exp, _visitDiscreteRhsCref, ctx) + BDAE.ASSIGN(_, rhs, _) => Util.traverseExpTopDown(rhs, ctx, nothing) + BDAE.NORETCALL(exp, _) => Util.traverseExpTopDown(exp, ctx, nothing) BDAE.ASSERT(c, m, l, _) => begin - Util.traverseExpTopDown(c, _visitDiscreteRhsCref, ctx) - Util.traverseExpTopDown(m, _visitDiscreteRhsCref, ctx) - Util.traverseExpTopDown(l, _visitDiscreteRhsCref, ctx) + Util.traverseExpTopDown(c, ctx, nothing) + Util.traverseExpTopDown(m, ctx, nothing) + Util.traverseExpTopDown(l, ctx, nothing) end _ => nothing end @@ -2121,14 +2121,20 @@ Base.@nospecializeinfer function _pushDiscreteCref!(out::Vector{DAE.ComponentRef return nothing end -Base.@nospecializeinfer function _visitDiscreteRhsCref(@nospecialize(exp), - ctx::Tuple{Vector{DAE.ComponentRef}, OrderedSet{String}, OrderedSet{String}}) +# Collect discrete RHS crefs into `out` (deduped via `seen`, skipping `blocked` +# reduction/for iterators). Typed functor replacing the threaded ctx tuple. +struct DiscreteRhsCrefVisitor + out::Vector{DAE.ComponentRef} + seen::OrderedSet{String} + blocked::OrderedSet{String} +end +Base.@nospecializeinfer function (v::DiscreteRhsCrefVisitor)(@nospecialize(exp), arg::Nothing) @match exp begin - DAE.CREF(cr, _) => _pushDiscreteCref!(ctx[1], ctx[2], ctx[3], cr) - DAE.REDUCTION(_, _, iters) => _collectReductionIterNames!(ctx[3], iters) + DAE.CREF(cr, _) => _pushDiscreteCref!(v.out, v.seen, v.blocked, cr) + DAE.REDUCTION(_, _, iters) => _collectReductionIterNames!(v.blocked, iters) _ => nothing end - return (exp, true, ctx) + return (exp, true, arg) end Base.@nospecializeinfer function _collectReductionIterNames!(blocked::OrderedSet{String}, @nospecialize(iters)) @@ -2145,13 +2151,13 @@ Base.@nospecializeinfer function _walkDiscreteStmtsForRhsCrefs!(out::Vector{DAE. seen::OrderedSet{String}, blocked::OrderedSet{String}, @nospecialize(stmts)) - local ctx = (out, seen, blocked) + local ctx = DiscreteRhsCrefVisitor(out, seen, blocked) for s in stmts @match s begin - DAE.STMT_ASSIGN(_, _, rhs, _) => Util.traverseExpTopDown(rhs, _visitDiscreteRhsCref, ctx) - DAE.STMT_ASSIGN_ARR(_, _, rhs, _) => Util.traverseExpTopDown(rhs, _visitDiscreteRhsCref, ctx) + DAE.STMT_ASSIGN(_, _, rhs, _) => Util.traverseExpTopDown(rhs, ctx, nothing) + DAE.STMT_ASSIGN_ARR(_, _, rhs, _) => Util.traverseExpTopDown(rhs, ctx, nothing) DAE.STMT_IF(cond, body, _, _) => begin - Util.traverseExpTopDown(cond, _visitDiscreteRhsCref, ctx) + Util.traverseExpTopDown(cond, ctx, nothing) _walkDiscreteStmtsForRhsCrefs!(out, seen, blocked, body) end DAE.STMT_FOR(_, _, iter, _, _, body, _) => begin @@ -2161,7 +2167,7 @@ Base.@nospecializeinfer function _walkDiscreteStmtsForRhsCrefs!(out::Vector{DAE. pushed && delete!(blocked, iter) end DAE.STMT_WHILE(cond, body, _) => begin - Util.traverseExpTopDown(cond, _visitDiscreteRhsCref, ctx) + Util.traverseExpTopDown(cond, ctx, nothing) _walkDiscreteStmtsForRhsCrefs!(out, seen, blocked, body) end _ => nothing diff --git a/src/Backend/BDAEUtil.jl b/src/Backend/BDAEUtil.jl index 15a606d7..b8f22a9a 100644 --- a/src/Backend/BDAEUtil.jl +++ b/src/Backend/BDAEUtil.jl @@ -55,7 +55,7 @@ end """ Traverse and update a given structure BDAE.BDAEStructure given a traversalOperation and optional arguments """ -function mapEqSystems(dae::BDAE.BACKEND_DAE, traversalOperation::Function, args...) +function mapEqSystems(dae::BDAE.BACKEND_DAE, traversalOperation, args...) dae = begin local eqs::Array{BDAE.EqSystem, 1} @match dae begin @@ -73,7 +73,7 @@ function mapEqSystems(dae::BDAE.BACKEND_DAE, traversalOperation::Function, args. end end -function mapEqSystems(dae::BDAE.BACKEND_DAE, traversalOperation::Function) +function mapEqSystems(dae::BDAE.BACKEND_DAE, traversalOperation) dae = begin local eqs::Vector{BDAE.EQSYSTEM} @match dae begin @@ -91,7 +91,7 @@ function mapEqSystems(dae::BDAE.BACKEND_DAE, traversalOperation::Function) end end -function mapEqSystemEquations(syst::BDAE.EQSYSTEM, traversalOperation::Function) +function mapEqSystemEquations(syst::BDAE.EQSYSTEM, traversalOperation) syst = begin local eqs::Array{BDAE.Equation,1} @match syst begin @@ -106,7 +106,7 @@ function mapEqSystemEquations(syst::BDAE.EQSYSTEM, traversalOperation::Function) end end -function mapEqSystemEquationsNoUpdate(syst::BDAE.EQSYSTEM, traversalOperation::Function, extArg::T)::T where {T} +function mapEqSystemEquationsNoUpdate(syst::BDAE.EQSYSTEM, traversalOperation, extArg::T)::T where {T} extArg = begin local eqs::Array{BDAE.Equation,1} @match syst begin @@ -120,7 +120,7 @@ function mapEqSystemEquationsNoUpdate(syst::BDAE.EQSYSTEM, traversalOperation::F end end -function mapEqSystemVariablesNoUpdate(syst::BDAE.EQSYSTEM, traversalOperation::Function, extArg::T)::T where {T} +function mapEqSystemVariablesNoUpdate(syst::BDAE.EQSYSTEM, traversalOperation, extArg::T)::T where {T} extArg = begin local varArr::Array{BDAE.Var,1} @match syst begin @@ -145,7 +145,7 @@ function crefLeafType(@nospecialize(cref)) end function _traverseComponentRef(cref::DAE.ComponentRef, - traversalOperation::Function, + traversalOperation, extArg::T)::Tuple{DAE.ComponentRef, T} where {T} local exp = DAE.CREF(cref, crefLeafType(cref)) local newExp @@ -161,7 +161,7 @@ end Mutates the given equation. """ function traverseEquationExpressions(eq::BDAE.Equation, - traversalOperation::Function, + traversalOperation, extArg::T)::Tuple{BDAE.Equation,T} where{T} (eq, extArg) = begin local lhs::DAE.Exp diff --git a/src/Backend/Causalize.jl b/src/Backend/Causalize.jl index 116013ab..c2f9907b 100644 --- a/src/Backend/Causalize.jl +++ b/src/Backend/Causalize.jl @@ -45,6 +45,110 @@ import ..FrontendUtil.Util import DAE import OMBackend +#= ── Traversal visitor functors ────────────────────────────────────────────── + Typed callable structs for the expression-traversal visitors. Each holds its + accumulator / captured state as concrete fields, so the traversal threads a + vestigial `nothing` arg and the result is read off the functor afterwards. + Call shape: (v::F)(exp::DAE.Exp, arg::Nothing) -> (exp, continue::Bool, arg). =# + +# Detect der() state crefs into a shared dict. +struct DetectStateExpression + stateCrefs::Dict{DAE.ComponentRef, Bool} +end +function (v::DetectStateExpression)(exp::DAE.Exp, arg::Nothing)::Tuple{DAE.Exp, Bool, Nothing} + @match exp begin + DAE.CALL(Absyn.IDENT("der"), DAE.CREF(state) <| _) => (v.stateCrefs[state] = true) + _ => nothing + end + return (exp, true, arg) +end + +# Detect parameter crefs (whose name is in `parStrs`, or any complex cref) into a dict. +struct DetectParamExpression + parStrs::OrderedSet{String} + paramCrefs::Dict{DAE.ComponentRef, Bool} +end +function (v::DetectParamExpression)(exp::DAE.Exp, arg::Nothing)::Tuple{DAE.Exp, Bool, Nothing} + @match exp begin + DAE.CREF(_, DAE.T_COMPLEX(__)) => (v.paramCrefs[exp.componentRef] = true) + DAE.CREF(__) => ((string(exp.componentRef) in v.parStrs) && (v.paramCrefs[exp.componentRef] = true)) + _ => nothing + end + return (exp, true, arg) +end + +# Flag whether any `time` cref appears; stops descent once found. +struct ScanForTime + found::Ref{Bool} +end +function (v::ScanForTime)(@nospecialize(exp::DAE.Exp), arg::Nothing)::Tuple{DAE.Exp, Bool, Nothing} + v.found[] && return (exp, false, arg) + @match exp begin + DAE.CREF(componentRef = c) => (string(c) == "time" && (v.found[] = true)) + _ => nothing + end + return (exp, true, arg) +end + +# Lift IFEXPs to auxiliary ifEq_tmp IF_EQUATIONs. timeDepOnly=false lifts every +# IFEXP (entry path); timeDepOnly=true lifts only time-dependent IFEXPs and recurses +# the rest (for branches of an already-lifted IFEXP). noEvent subtrees stay inline; +# identical (cond|then|else) shapes are deduped to one tmp var. +struct IfExpressionLifter + tmpVarToElement::OrderedDict{BDAE.VAR, BDAE.IF_EQUATION} + tick::Ref{Int} + dedup::Dict{String, DAE.Exp} + timeDepOnly::Bool +end +Base.@nospecializeinfer function (v::IfExpressionLifter)(@nospecialize(exp::DAE.Exp), arg::Nothing)::Tuple{DAE.Exp, Bool, Nothing} + local timeDep = IfExpressionLifter(v.tmpVarToElement, v.tick, v.dedup, true) + local (newExp, cont) = begin + @match exp begin + DAE.CALL(Absyn.IDENT("noEvent"), _, _) => (exp, false) + DAE.IFEXP(cond, expThen, expElse) => begin + if v.timeDepOnly && !_expDependsOnTime(cond) + #= State-dependent: keep inline (bool-product), recurse for nested + time-dependent IFEXPs. =# + local (lc, _) = Util.traverseExpTopDown(cond, timeDep, nothing) + local (lt, _) = Util.traverseExpTopDown(expThen, timeDep, nothing) + local (le, _) = Util.traverseExpTopDown(expElse, timeDep, nothing) + (DAE.IFEXP(lc, lt, le), false) + else + #= Lift this IFEXP. Recurse branches via the time-dep variant so nested + state-dependent IFEXPs keep their bool-product lowering. =# + local (liftedCond, _) = Util.traverseExpTopDown(cond, timeDep, nothing) + local (liftedThen, _) = Util.traverseExpTopDown(expThen, timeDep, nothing) + local (liftedElse, _) = Util.traverseExpTopDown(expElse, timeDep, nothing) + local key = string(liftedCond, "|", liftedThen, "|", liftedElse) + local existing = get(v.dedup, key, nothing) + if existing !== nothing + (existing, true) + else + local varType = DAE.T_REAL_DEFAULT + local varName = string("ifEq_tmp", v.tick.x) + v.tick.x += 1 + local var::DAE.ComponentRef = DAE.CREF_IDENT(varName, varType, nil) + local varAsCREF::DAE.CREF = DAE.CREF(var, varType) + local emptySource = DAE.emptyElementSource + local attr = BDAE.EQ_ATTR_DEFAULT_UNKNOWN + local backendVar = BDAE.VAR(DAE.CREF_IDENT(varName, DAE.T_UNKNOWN_DEFAULT, nil), + BDAE.VARIABLE(), varType) + v.tmpVarToElement[backendVar] = BDAE.IF_EQUATION(list(liftedCond), + list(list(BDAE.EQUATION(varAsCREF, liftedThen, emptySource, attr))), + list(BDAE.EQUATION(varAsCREF, liftedElse, emptySource, attr)), + emptySource, + BDAE.EQ_ATTR_DEFAULT_UNKNOWN) + v.dedup[key] = varAsCREF + (varAsCREF, true) + end + end + end + _ => (exp, true) + end + end + return (newExp, cont, arg) +end + """ Variable can be: Variable, Discrete, Constant and Parameters @@ -101,10 +205,11 @@ function detectStatesEqSystem(syst::BDAE.EQSYSTEM)::BDAE.EQSYSTEM local vars::Vector{BDAE.VAR} local eqs::Vector{BDAE.Equation} local stateCrefs = Dict{DAE.ComponentRef, Bool}() + local stateVisitor = DetectStateExpression(stateCrefs) @match syst begin BDAE.EQSYSTEM(name, vars, eqs, simpleEqs, initialEqs) => begin for eq in eqs - (_, stateCrefs) = BDAEUtil.traverseEquationExpressions(eq, detectStateExpression, stateCrefs) + BDAEUtil.traverseEquationExpressions(eq, stateVisitor, nothing) end #= Do replacements for stateCrefs =# @assign syst.orderedVars = updateStates(vars, stateCrefs) @@ -125,37 +230,7 @@ function detectParamsEqSystem(syst::BDAE.EQSYSTEM)::BDAE.EQSYSTEM local buffer = IOBuffer() @BACKEND_LOGGING write(OMBackend.logPath("backend/bdae", "allpars.log"), String(take!(buffer))) - function detectParamExpression(exp::DAE.Exp, paramCrefs::Dict{DAE.ComponentRef, Bool}) - local cont::Bool - local outCrefs = paramCrefs - (outCrefs, cont) = begin - local param::DAE.ComponentRef - @match exp begin - #= Ignore complex components =# - DAE.CREF(c, DAE.T_COMPLEX(__)) => begin - outCrefs[exp.componentRef] = true - (outCrefs, true) - end - DAE.CREF(__) => begin - local cand = string(exp.componentRef) - if (cand in parStrs) - #println("Located param in the variables:" * cand) - #println(exp) - outCrefs[exp.componentRef] = true - end - (outCrefs, true) - end - DAE.IFEXP(cond, expThen, expElse) => begin - #fail() - (outCrefs, true) - end - _ => begin - (outCrefs, true) - end - end - end - return (exp, cont, outCrefs) - end + # detectParamExpression moved to the DetectParamExpression functor at module top. function updateParams(vars::Vector, paramCrefs::Dict{DAE.ComponentRef, Bool}) local varArr::Vector{BDAE.VAR} = vars @@ -182,14 +257,15 @@ function detectParamsEqSystem(syst::BDAE.EQSYSTEM)::BDAE.EQSYSTEM local vars::BDAE.Variables local eqs::Array local paramCrefs = Dict{DAE.ComponentRef, Bool}() + local paramVisitor = DetectParamExpression(parStrs, paramCrefs) @match syst begin BDAE.EQSYSTEM(name, vars, eqs, simpleEqs, initialEqs) => begin for eq in eqs - (_, paramCrefs) = BDAEUtil.traverseEquationExpressions(eq, detectParamExpression, paramCrefs) + BDAEUtil.traverseEquationExpressions(eq, paramVisitor, nothing) end #= Go through the initial equations =# for ieq in initialEqs - (_, paramCrefs) = BDAEUtil.traverseEquationExpressions(ieq, detectParamExpression, paramCrefs) + BDAEUtil.traverseEquationExpressions(ieq, paramVisitor, nothing) end #= Do replacements for paramCrefs =# @assign syst.orderedVars = updateParams(vars, paramCrefs) @@ -225,7 +301,7 @@ function detectIfEquationsEqSystem(syst::BDAE.EQSYSTEM)::BDAE.EQSYSTEM leaving the other branch's ifCond parameter pinned at its initial value). =# local dedup = Dict{String, DAE.Exp}() - local tmpVarToElementAndTick = (tmpVarToElement, tick, dedup) + local ifLifter = IfExpressionLifter(tmpVarToElement, tick, dedup, false) @match syst begin BDAE.EQSYSTEM(__) => begin for i in 1:length(syst.orderedEqs) @@ -237,8 +313,7 @@ function detectIfEquationsEqSystem(syst::BDAE.EQSYSTEM)::BDAE.EQSYSTEM if eq isa BDAE.WHEN_EQUATION || eq isa BDAE.INITIAL_WHEN_EQUATION continue end - (eq2, _) = BDAEUtil.traverseEquationExpressions(eq, replaceIfExpressionWithTmpVar, - tmpVarToElementAndTick) + (eq2, _) = BDAEUtil.traverseEquationExpressions(eq, ifLifter, nothing) if ! (eq === eq2) @assign syst.orderedEqs[i] = eq2 end @@ -265,151 +340,14 @@ end """ function _expDependsOnTime(@nospecialize(e::DAE.Exp))::Bool local found = Ref{Bool}(false) - function _scan(@nospecialize(x::DAE.Exp), seen::Ref{Bool}) - seen[] && return (x, false, seen) - @match x begin - DAE.CREF(componentRef = c) => begin - local s = string(c) - if s == "time" - seen[] = true - end - (x, true, seen) - end - _ => (x, true, seen) - end - end - Util.traverseExpTopDown(e, _scan, found) + Util.traverseExpTopDown(e, ScanForTime(found), nothing) return found[] end -""" - Detects if expression. - We replace the if expression with our temporary variable. - These variables are assigned in newly created if equations that we add to the tmpVarToElement::Dict. - We create the mapping: - tmpVar -> equation it is assigned in -""" -Base.@nospecializeinfer function replaceIfExpressionWithTmpVar(@nospecialize(exp::DAE.Exp), tmpVarToElementAndTick::Tuple{AbstractDict{BDAE.VAR, BDAE.IF_EQUATION}, Ref{Int}, Dict{String, DAE.Exp}}) - (newExp, cont, tmpVarToElementAndTick) = begin - local tmpVarToElement::AbstractDict{BDAE.VAR, BDAE.IF_EQUATION} = tmpVarToElementAndTick[1] - local tick::Ref{Int} = tmpVarToElementAndTick[2] - local dedup::Dict{String, DAE.Exp} = tmpVarToElementAndTick[3] - @match exp begin - #= Per Modelica spec, `noEvent(expr)` takes relations literally and - triggers no events. An IFEXP inside it must stay inline (codegen emits - a continuous ifelse/bool-product) — lifting it to an event-driven - IF_EQUATION would latch the branch at the first crossing and break a - feedback saturation whose condition never re-crosses zero. Stop - descent so the whole noEvent subtree is preserved. =# - DAE.CALL(Absyn.IDENT("noEvent"), _, _) => (exp, false, tmpVarToElementAndTick) - DAE.IFEXP(cond, expThen, expElse) => begin - #= Recursively process the branches. Use the time-dependent-only - variant so that nested state-dependent IFEXPs (e.g. LimPID's - saturation) keep their bool-product lowering — one-shot events - would freeze the saturation at its first crossing. Nested - time-dependent IFEXPs DO get lifted so each transition produces - a proper SymbolicContinuousCallback. =# - local (liftedCond, _) = Util.traverseExpTopDown(cond, _replaceTimeDepIfExpressionWithTmpVar, tmpVarToElementAndTick) - local (liftedThen, _) = Util.traverseExpTopDown(expThen, _replaceTimeDepIfExpressionWithTmpVar, tmpVarToElementAndTick) - local (liftedElse, _) = Util.traverseExpTopDown(expElse, _replaceTimeDepIfExpressionWithTmpVar, tmpVarToElementAndTick) - #= Structural dedup: if a lifted IF_EQUATION with the same - (cond | then | else) already exists, reuse its CREF instead of - generating a duplicate. MTK drops SymbolicContinuousCallbacks - with byte-identical zero-crossing conditions; this dedup - prevents two separate ifCond parameters from depending on the - same event and ending up with one of them pinned. =# - local key = string(liftedCond, "|", liftedThen, "|", liftedElse) - local existing = get(dedup, key, nothing) - if existing !== nothing - (existing, true, tmpVarToElementAndTick) - else - #= Bump per IFEXP so siblings within the same residual get distinct - names; a single per-equation bump would collide every nested - IFEXP onto one symbolic var. =# - local varType = DAE.T_REAL_DEFAULT - local varName = string("ifEq_tmp", tick.x) - tick.x += 1 - local var::DAE.ComponentRef = DAE.CREF_IDENT(varName, varType, nil) - local varAsCREF::DAE.CREF = DAE.CREF(var, varType) - local emptySource = DAE.emptyElementSource - local attr = BDAE.EQ_ATTR_DEFAULT_UNKNOWN - local backendVar = BDAE.VAR(DAE.CREF_IDENT(varName, DAE.T_UNKNOWN_DEFAULT, nil), - BDAE.VARIABLE(), varType) - tmpVarToElement[backendVar] = BDAE.IF_EQUATION(list(liftedCond), - list(list(BDAE.EQUATION(varAsCREF, liftedThen, emptySource, attr))), - list(BDAE.EQUATION(varAsCREF, liftedElse, emptySource, attr)), - emptySource, - BDAE.EQ_ATTR_DEFAULT_UNKNOWN) - dedup[key] = varAsCREF - (varAsCREF, true, tmpVarToElementAndTick) - end - end - _ => begin - (exp, true, tmpVarToElementAndTick) - end - end - end - return (newExp, cont, tmpVarToElementAndTick) -end +# replaceIfExpressionWithTmpVar / _replaceTimeDepIfExpressionWithTmpVar moved to the +# IfExpressionLifter functor at module top. -""" - Recursive variant used inside `replaceIfExpressionWithTmpVar` for the - branches of an already-lifted IFEXP. Lifts only IFEXPs whose condition - references `time`; leaves state-dependent IFEXPs in place so codegen - emits a continuous bool-product (correct for non-monotonic conditions). - Recursion descends into the branches either way so deeper nested - time-dependent IFEXPs still reach the lifter. -""" -Base.@nospecializeinfer function _replaceTimeDepIfExpressionWithTmpVar(@nospecialize(exp::DAE.Exp), tmpVarToElementAndTick::Tuple{AbstractDict{BDAE.VAR, BDAE.IF_EQUATION}, Ref{Int}, Dict{String, DAE.Exp}}) - (newExp, cont, tmpVarToElementAndTick) = begin - @match exp begin - #= See replaceIfExpressionWithTmpVar: never lift inside noEvent. =# - DAE.CALL(Absyn.IDENT("noEvent"), _, _) => (exp, false, tmpVarToElementAndTick) - DAE.IFEXP(cond, expThen, expElse) => begin - if _expDependsOnTime(cond) - #= Time-dependent → delegate to the always-lift path. =# - replaceIfExpressionWithTmpVar(exp, tmpVarToElementAndTick) - else - #= State-dependent or parameter-only → do not lift. Still recurse - so nested time-dependent IFEXPs below get lifted. =# - local (lc, _) = Util.traverseExpTopDown(cond, _replaceTimeDepIfExpressionWithTmpVar, tmpVarToElementAndTick) - local (lt, _) = Util.traverseExpTopDown(expThen, _replaceTimeDepIfExpressionWithTmpVar, tmpVarToElementAndTick) - local (le, _) = Util.traverseExpTopDown(expElse, _replaceTimeDepIfExpressionWithTmpVar, tmpVarToElementAndTick) - (DAE.IFEXP(lc, lt, le), false, tmpVarToElementAndTick) - end - end - _ => begin - (exp, true, tmpVarToElementAndTick) - end - end - end - return (newExp, cont, tmpVarToElementAndTick) -end - -""" - kabdelhak: - Detects if a given expression is a der() call and adds the corresponding - cref to a hashmap -""" -function detectStateExpression(exp::DAE.Exp, stateCrefs::Dict{DAE.ComponentRef, Bool}) - local cont::Bool - local outCrefs = stateCrefs - (outCrefs, cont) = begin - local state::DAE.ComponentRef - @match exp begin - DAE.CALL(Absyn.IDENT("der"), DAE.CREF(state) <| _ ) => begin - #= Add state with boolean value that does not matter, - it is later only BDAE.BACKEND_DAE(eqs = eqs) checked if it exists at all =# - outCrefs[state] = true - (outCrefs, true) - end - _ => begin - (outCrefs, true) - end - end - end - return (exp, cont, outCrefs) -end +# detectStateExpression moved to the DetectStateExpression functor at module top. """ kabdelhak: diff --git a/src/CodeGeneration/DirectRHSGeneration.jl b/src/CodeGeneration/DirectRHSGeneration.jl index cc2f32e9..da7f1d0a 100644 --- a/src/CodeGeneration/DirectRHSGeneration.jl +++ b/src/CodeGeneration/DirectRHSGeneration.jl @@ -188,6 +188,11 @@ function buildDirectRHSProblem(reducedSystem, finalInitialValues, pars, tspan, c # Our structural_simplify wrapper uses split=false, so the compiled event # callbacks expect a flat parameter vector matching our p_vec format. local allCallbacks = _extractAndMergeEventCallbacks(reducedSystem, callbacks) + #= The callback is stored UN-collapsed; the erasure happens at SOLVE time + (simulateIMTK), in a settled world, so the FunctionWrappers it builds dispatch + correctly to RGF-backed MTK callbacks (build-time collapse captured a stale world + -> wrong events). The solve-time collapse also yields the model-independent erased + type whose `solve` is baked into the image. =# # 5. Construct ODEProblem, handling mass matrix for DAE systems. # Attach reducedSystem via sys= so callbacks can look up state/parameter @@ -999,6 +1004,162 @@ function _extractAndMergeEventCallbacks(reducedSystem, customCallbacks) end +# Trivial reinit: no post-event DAE re-initialization, so the merged callback's +# default (nothing) is behaviour-preserving. +_isTrivialReinit(ia)::Bool = ia === nothing || ia isa ModelingToolkit.SciMLBase.NoInit + +#= Typed callable structs for the merged continuous callback. Typed fields and a + concrete struct type keep the merged condition/affect inferred (vs a closure + boxing its captures); the FunctionWrapper in `_eraseContinuousCallbacks` erases + the outer type for the image bake. =# +struct _MergedContinuousCondition{S} + subs::S + offsets::Vector{Int} + nsub::Int +end + +# `integrator` stays untyped: the FunctionWrapper declares it `Any` to keep the +# wrapped callback type model-independent. +# `out` / `u` are AbstractVector, NOT Vector: SciML's VectorContinuousCallback passes +# `out` as a SubArray view of the rootfind buffer. A concrete `Vector{Float64}` arg (here +# or in the FunctionWrapper signature) forces a convert/copy, so writes to `out` land in a +# discarded copy and no crossing is ever detected. +function (c::_MergedContinuousCondition)(out::AbstractVector{Float64}, u::AbstractVector{Float64}, + t::Float64, integrator)::Nothing + local SB = ModelingToolkit.SciMLBase + for k in 1:c.nsub + local s = c.subs[k] + if s isa SB.VectorContinuousCallback + s.condition(view(out, (c.offsets[k] + 1):c.offsets[k + 1]), u, t, integrator) + else + out[c.offsets[k] + 1] = s.condition(u, t, integrator) + end + end + return nothing +end + +# One struct serves both affect! and affect_neg! (selected by `neg`). The vector +# affect signature is (integrator, componentIndex). +struct _MergedContinuousAffect{S} + subs::S + offsets::Vector{Int} + lens::Vector{Int} + nsub::Int + neg::Bool +end + +function (a::_MergedContinuousAffect)(integrator, gidx::Int)::Nothing + local SB = ModelingToolkit.SciMLBase + # Map the global 1-based component index to (subIndex, localIndex). + local k::Int = a.nsub + local li::Int = a.lens[a.nsub] + for kk in 1:a.nsub + if gidx <= a.offsets[kk + 1] + k = kk + li = gidx - a.offsets[kk] + break + end + end + local s = a.subs[k] + local aff = a.neg ? s.affect_neg! : s.affect! + aff === nothing && return nothing + s isa SB.VectorContinuousCallback ? aff(integrator, li) : aff(integrator) + return nothing +end + +# Runs every sub-callback's `initialize` at integration start. Lets a sub carrying a +# custom initialize (e.g. chua's DAE event) collapse WITHOUT dropping it; the merge +# is FunctionWrapper-erased so the VCC's initialize param stays model-independent. +struct _MergedContinuousInitialize{S} + subs::S + nsub::Int +end + +function (m::_MergedContinuousInitialize)(c, u, t, integrator)::Nothing + for k in 1:m.nsub + local s = m.subs[k] + s.initialize(s, u, t, integrator) + end + return nothing +end + +""" + _eraseContinuousCallbacks(cbset) + +Collapse the continuous callbacks of a `CallbackSet` into a single +`VectorContinuousCallback` whose combined condition / affect! / affect_neg! are +typed callable structs wrapped in `FunctionWrappers` (integrator typed `Any`) +that dispatch to the original per-component callbacks by index. This removes the +two model-specific axes of the callback type, the tuple arity (number of +continuous callbacks) and the per-event closure types, so the `CallbackSet` type +is constant across models and `solve` can be compiled once and baked into the +image. Per-component event semantics are preserved exactly: each component's own +condition, affect! and affect_neg! are called unchanged. Discrete callbacks are +passed through. + +Called at SOLVE time (see `simulateIMTK`) in a settled world, so it collapses any +callable — OM-generated closures and MTK `process_events` `CompiledCondition` / +`FunctionalAffect` alike (the integrator never dispatches on the concrete type). +Returns `cbset` unchanged (no collapse) when there is no continuous callback, or on +any structural surprise: a non-`CallbackSet` argument, a continuous entry that is +neither a scalar `ContinuousCallback` nor a `VectorContinuousCallback`, non-uniform +`rootfind` / `save_positions` across components, or any component carrying event +metadata a flat merge cannot represent (a custom `initialize` / `finalize`, an +`idxs` slice, or a non-trivial reinitialization algorithm). +""" +function _eraseContinuousCallbacks(cbset) + local SB = ModelingToolkit.SciMLBase + cbset isa SB.CallbackSet || return cbset + local subs = collect(cbset.continuous_callbacks) + local dc = cbset.discrete_callbacks + isempty(subs) && return cbset + for s in subs + (s isa SB.ContinuousCallback || s isa SB.VectorContinuousCallback) || return cbset + end + #= No parentmodule check: this runs at SOLVE time (see simulateIMTK), in a settled + world, so MTK process_events callbacks (CompiledCondition / FunctionalAffect, + RGF-backed) collapse correctly too. The structural guard below is the only safety + bound the integrator needs (it never dispatches on the concrete callback type). =# + #= A flat merge is faithful only when no sub-callback carries event metadata the + merge cannot represent: a custom `finalize` (would be dropped), an `idxs` slice + (the condition would read the wrong state), or a non-trivial reinitialization + algorithm (post-event DAE consistency would change). A custom `initialize` IS + allowed: it is preserved via the merged initialize below (chua's DAE event). =# + for s in subs + (s.finalize === SB.FINALIZE_DEFAULT && + s.idxs === nothing && + _isTrivialReinit(s.initializealg)) || return cbset + end + #= A single VectorContinuousCallback applies one rootfind / save_positions to + every component, so only collapse when these already agree. =# + local rootfind = subs[1].rootfind + local savePos = subs[1].save_positions + for s in subs + (s.rootfind == rootfind && s.save_positions == savePos) || return cbset + end + local lens::Vector{Int} = Int[(s isa SB.VectorContinuousCallback) ? s.len : 1 for s in subs] + local offsets::Vector{Int} = cumsum(vcat(0, lens)) # offsets[k] = #components before sub k + local total::Int = offsets[end] + local nsub::Int = length(subs) + local condF = _MergedContinuousCondition(subs, offsets, nsub) + local affF = _MergedContinuousAffect(subs, offsets, lens, nsub, false) + local affNF = _MergedContinuousAffect(subs, offsets, lens, nsub, true) + local initF = _MergedContinuousInitialize(subs, nsub) + local FW = DiffEqBase.FunctionWrapper + local condW = FW{Nothing, Tuple{AbstractVector{Float64}, AbstractVector{Float64}, Float64, Any}}(condF) + local affW = FW{Nothing, Tuple{Any, Int}}(affF) + local affNW = FW{Nothing, Tuple{Any, Int}}(affNF) + #= Always FunctionWrapper-wrap the merged initialize (even when every sub uses the + default) so the VCC's initialize param is the SAME model-independent type whether or + not a sub carries a custom initialize -> chua and the synthetic bake share one type. =# + local initW = FW{Nothing, Tuple{Any, Any, Any, Any}}(initF) + local vcc = SB.VectorContinuousCallback(condW, affW, affNW, total; + initialize = initW, + rootfind = rootfind, save_positions = savePos) + return SB.CallbackSet(vcc, dc...) +end + + """ _toFloat64(val; resolvedParams::Union{Dict{String,Float64},Nothing}=nothing) diff --git a/src/CodeGeneration/MTK_CodeGeneration.jl b/src/CodeGeneration/MTK_CodeGeneration.jl index 0cd9585d..8d737c27 100644 --- a/src/CodeGeneration/MTK_CodeGeneration.jl +++ b/src/CodeGeneration/MTK_CodeGeneration.jl @@ -66,6 +66,15 @@ are tolerated when the binding "already has a value" (re-registration is idempotent) and rethrown otherwise. """ function evalGeneratedFunctionsAndRegister!(modelName, functions, simCode) + #= Under precompile / image generation, eval'ing the model's generated Modelica functions + into the already-closed `CodeGeneration` module is rejected by Julia ("breaks incremental + compilation"). Skip the eval + registration here: the codegen that PRODUCED `functions` + has already run (so its method instances are warmed for the bake), and a real runtime + translate registers them normally — the guard is precompile-only. Mirrors the + jl_generating_output guard in generateIMTKCode (iMTKGen.jl). =# + if ccall(:jl_generating_output, Cint, ()) != 0 + return nothing + end for f in functions try eval(f) diff --git a/src/CodeGeneration/iMTKGen.jl b/src/CodeGeneration/iMTKGen.jl index 0c8b96f3..157e1420 100644 --- a/src/CodeGeneration/iMTKGen.jl +++ b/src/CodeGeneration/iMTKGen.jl @@ -66,17 +66,18 @@ end #= Eval the module, invoke `Model(IMTK_BUILD_TSPAN)` (runs structural_simplify), and cache the resulting 9-tuple. The post-simplify System (element 5) is also stashed for the optional dump and external inspection via `reducedSystem`. =# -function _buildAndCache(modelName::String, modelCode::Expr) +function _buildAndCache(modelName::String, modelCode::Expr; overwriteCache::Bool = false) local OMB = _OMBackend() local cname = OMB.canonicalName(modelName) #= Reuse path: identical regenerated code + build flags, a live module and a cached build mean the compiled methods and the problem are still valid. Re-eval'ing identical code would only invalidate `simulateFromBuild` and friends, forcing a full recompile on the next solve. Flags read at build - time (not codegen) are folded into the hash so a flag flip still rebuilds. =# + time (not codegen) are folded into the hash so a flag flip still rebuilds. + `overwriteCache` bypasses this reuse check to force a fresh rebuild. =# local buildHash = hash((modelCode, OMB.DIRECT_RHS_GENERATION[], OMB.DIRECT_JAC_GENERATION[], OMB.DIRECT_RHS_TYPE_ERASE[])) - if get(BUILT_HASH, cname, UInt64(0)) == buildHash && + if !overwriteCache && get(BUILT_HASH, cname, UInt64(0)) == buildHash && haskey(BUILT, cname) && isdefined(OMB, Symbol(cname)) @info "[IMTK GEN] regenerated code unchanged; reusing compiled module + cached build" model = modelName return nothing @@ -164,13 +165,25 @@ function simulateIMTK(modelName::String, tspan, solver; kwargs...) if haskey(PRISTINE_P, cname) prob = OMB.Runtime.ModelingToolkit.SciMLBase.remake(prob; p = deepcopy(PRISTINE_P[cname])) end - local rebuilt = (prob, cached[2], cached[3], cached[4], cached[5], - tspan, cached[7], cached[8], cached[9]) #= Route through `mod.simulate(...; cached_build = rebuilt)` using the same closure form as the MTK path, so the body executes inside the model module and `global LATEST_REDUCED_SYSTEM = …` / `global LATEST_PROBLEM = …` are - visible to subsequent introspection on the module. =# + visible to subsequent introspection on the module. + Collapse the continuous callbacks HERE, inside invokelatest (a settled world, + the build call having returned), then remake: the FunctionWrappers built around + the RGF-backed MTK callbacks now resolve to the live methods (build-time collapse + captured a stale world -> wrong events), and the collapsed prob has the + model-independent erased type whose `solve` is baked into the image. =# return Base.invokelatest() do + local SB = OMB.Runtime.ModelingToolkit.SciMLBase + local p = prob + if OMB.DIRECT_RHS_TYPE_ERASE[] + local cb = get(p.kwargs, :callback, nothing) + cb === nothing || + (p = SB.remake(p; callback = OMB.CodeGeneration._eraseContinuousCallbacks(cb))) + end + local rebuilt = (p, cached[2], cached[3], cached[4], cached[5], + tspan, cached[7], cached[8], cached[9]) getfield(OMB, Symbol(cname)).simulate(tspan, solver; cached_build = rebuilt, kwargs...) end catch e diff --git a/src/FrontendUtil/Util.jl b/src/FrontendUtil/Util.jl index f9bbd4ef..f39a52aa 100644 --- a/src/FrontendUtil/Util.jl +++ b/src/FrontendUtil/Util.jl @@ -16,7 +16,7 @@ const Argument = Any The first returning an expression, the second returning a boolean indicating if the traversal should continue and the last is the out argument. """ -Base.@nospecializeinfer function traverseExpTopDown(@nospecialize(inExp::DAE.Exp), func::Function, ext_arg::Type_a)::Tuple{DAE.Exp, Type_a} +Base.@nospecializeinfer function traverseExpTopDown(@nospecialize(inExp::DAE.Exp), func, ext_arg::Type_a)::Tuple{DAE.Exp, Type_a} local outArg::Type_a local outExp::DAE.Exp local cont::Bool @@ -25,7 +25,7 @@ Base.@nospecializeinfer function traverseExpTopDown(@nospecialize(inExp::DAE.Exp (outExp, outArg) end -Base.@nospecializeinfer function traverseExpTopDown1(continueTraversal::Bool, @nospecialize(inExp::DAE.Exp), func::Function, inArg::Type_a) ::Tuple{DAE.Exp, Type_a} +Base.@nospecializeinfer function traverseExpTopDown1(continueTraversal::Bool, @nospecialize(inExp::DAE.Exp), func, inArg::Type_a) ::Tuple{DAE.Exp, Type_a} local outArg local outExp::DAE.Exp (outExp, outArg) = begin @@ -59,7 +59,7 @@ Base.@nospecializeinfer function traverseExpTopDown1(continueTraversal::Bool, @n local lstexpl_1::List{List{DAE.Exp}} local op::DAE.Operator local reductionInfo::DAE.ReductionInfo - local rel::Function + local rel local riters::DAE.ReductionIterators local scalar::Bool local t::DAE.Type @@ -306,7 +306,7 @@ Base.@nospecializeinfer function traverseExpTopDown1(continueTraversal::Bool, @n (outExp, outArg) end -function traverseExpListTopDown(expLst::List{DAE.Exp}, func::Function, inArg) +function traverseExpListTopDown(expLst::List{DAE.Exp}, func, inArg) outArg = inArg #= Allocate the result buffer lazily: a read-only or no-op traversal leaves every element identity-equal, so the common case allocates nothing and @@ -339,7 +339,7 @@ end Calls traverseExpBottomUp for each element of list. Mirrors Expression.traverseExpList from OpenModelica. """ -function traverseExpList(expLst::List{DAE.Exp}, func::Function, inArg) +function traverseExpList(expLst::List{DAE.Exp}, func, inArg) outArg = inArg #= Allocate the result buffer lazily; a no-op traversal returns the original list. =# local newExpLst::Union{Nothing, Vector{DAE.Exp}} = nothing @@ -369,7 +369,7 @@ end Helper function to traverseExpBottomUp, traverses matrix expressions. Mirrors Expression.traverseExpMatrix from OpenModelica. """ -function traverseExpMatrix(inMatrix::List{List{DAE.Exp}}, func::Function, inArg) +function traverseExpMatrix(inMatrix::List{List{DAE.Exp}}, func, inArg) outArg = inArg #= Allocate lazily; an unchanged matrix returns the original list of rows. =# local newRows::Union{Nothing, Vector{List{DAE.Exp}}} = nothing @@ -398,7 +398,7 @@ end """ Traverse reduction iterators, applying the traversal function to each iterator expression. """ -function traverseReductionIteratorsTopDown(riters::DAE.ReductionIterators, func::Function, extArg) +function traverseReductionIteratorsTopDown(riters::DAE.ReductionIterators, func, extArg) outIters = DAE.ReductionIterator[] outArg = extArg for riter in riters @@ -419,7 +419,7 @@ function traverseReductionIteratorsTopDown(riters::DAE.ReductionIterators, func: return (list(outIters...), outArg) end -Base.@nospecializeinfer function traverseExpTopDownCrefHelper(@nospecialize(inCref::DAE.ComponentRef), rel::Function, iarg::Argument) ::Tuple{DAE.ComponentRef, Argument} +Base.@nospecializeinfer function traverseExpTopDownCrefHelper(@nospecialize(inCref::DAE.ComponentRef), rel, iarg::Argument) ::Tuple{DAE.ComponentRef, Argument} local outArg::Argument local outCref::DAE.ComponentRef (outCref, outArg) = begin @@ -458,7 +458,7 @@ Base.@nospecializeinfer function traverseExpTopDownCrefHelper(@nospecialize(inCr (outCref, outArg) end -function traverseExpTopDownSubs(inSubscript::List{<:DAE.Subscript}, rel::Function, iarg::Argument) ::Tuple{List{DAE.Subscript}, Argument} +function traverseExpTopDownSubs(inSubscript::List{<:DAE.Subscript}, rel, iarg::Argument) ::Tuple{List{DAE.Subscript}, Argument} local allEq::Bool = true local arg::Argument = iarg local acc::Vector{DAE.Subscript} @@ -545,7 +545,7 @@ end NOTE: The user-provided function is not allowed to fail! If you want to detect a failure, return NONE() in your user-provided datatype. """ -function traverseExpBottomUp(inExp::DAE.Exp, inFunc::Function, inExtArg::T) where {T} +function traverseExpBottomUp(inExp::DAE.Exp, inFunc, inExtArg::T) where {T} local outExtArg::T local outExp::DAE.Exp (outExp, outExtArg) = begin @@ -991,7 +991,7 @@ function traverseExpBottomUp(inExp::DAE.Exp, inFunc::Function, inExtArg::T) whe end -function traverseExpSubs(inSubscript::List{DAE.Subscript}, rel::Function, iarg::T) ::Tuple{List{DAE.Subscript}, T} where {T} +function traverseExpSubs(inSubscript::List{DAE.Subscript}, rel, iarg::T) ::Tuple{List{DAE.Subscript}, T} where {T} local outArg::T local outSubscript::List{DAE.Subscript} @@ -1059,7 +1059,7 @@ end Takes a function and an extra argument passed through the traversal. """ -function traverseExpCref(inCref::DAE.ComponentRef, rel::Function, iarg::T) ::Tuple{DAE.ComponentRef, T} where {T} +function traverseExpCref(inCref::DAE.ComponentRef, rel, iarg::T) ::Tuple{DAE.ComponentRef, T} where {T} local outArg::T local outCref::DAE.ComponentRef (outCref, outArg) = begin diff --git a/src/SimulationCode/simCodeCheck.jl b/src/SimulationCode/simCodeCheck.jl index b191a5cc..e771a4b4 100644 --- a/src/SimulationCode/simCodeCheck.jl +++ b/src/SimulationCode/simCodeCheck.jl @@ -76,6 +76,62 @@ struct CheckResult elapsed_s::Float64 end +#= ── Traversal visitor functors (SimCode-native traverseExpTopDown) ───────── + Typed callable structs replacing the per-check inline lambdas; each holds its + accumulator / context as concrete fields. Call shape + (v)(e::Exp, arg::Nothing) -> (e, continue::Bool, arg). =# +struct CollectDerNames + names::OrderedSet{String} +end +function (v::CollectDerNames)(e::Exp, arg::Nothing)::Tuple{Exp, Bool, Nothing} + if e isa CALL && e.path isa Absyn.IDENT && e.path.name == "der" && !isempty(e.args) + _pushDerArgNames!(v.names, toDAEExp(first(e.args))) + end + return (e, true, arg) +end + +struct CollectCrefNames + missingNames::Vector{String} + known::OrderedSet{String} +end +function (v::CollectCrefNames)(e::Exp, arg::Nothing)::Tuple{Exp, Bool, Nothing} + if e isa EXP_CREF + local nm = DAE_identifierToString(toDAECref(e.cref).componentRef) + !(nm in v.known) && push!(v.missingNames, nm) + end + return (e, true, arg) +end + +struct FlagCanonicalExp + out::Vector{CheckViolation} + whereStr::String +end +function (v::FlagCanonicalExp)(e::Exp, arg::Nothing)::Tuple{Exp, Bool, Nothing} + if e isa EXP_CREF + _flagCanonicalComponentRef!(v.out, toDAECref(e.cref).componentRef, v.whereStr) + elseif e isa CALL + _flagCanonicalPath!(v.out, e.path, v.whereStr * ".call") + elseif e isa RECORD + _flagCanonicalPath!(v.out, e.path, v.whereStr * ".record") + end + return (e, true, arg) +end + +struct FlagLiteralInDerPre + out::Vector{CheckViolation} + whereStr::String +end +function (v::FlagLiteralInDerPre)(e::Exp, arg::Nothing)::Tuple{Exp, Bool, Nothing} + if e isa CALL && e.path isa Absyn.IDENT && (e.path.name == "der" || e.path.name == "pre") && !isempty(e.args) + local lit = toDAEExp(first(e.args)) + if _isDAEConstant(lit) + push!(v.out, CheckViolation(:no_literal_in_der_pre, :warn, v.whereStr, + "$(e.path.name)(literal $(typeof(lit))) reached codegen — likely upstream parameter-eval substitution")) + end + end + return (e, true, arg) +end + #= ── Top-level driver ──────────────────────────────────────────────── =# """ @@ -435,12 +491,7 @@ end # SIM-native twin: walk the SimCode Exp; reuse the DAE der-arg extraction via a per-node projection. function _collectDerFromExp!(names::OrderedSet{String}, exp::Exp) - traverseExpTopDown(exp, (e, _) -> begin - if e isa CALL && e.path isa Absyn.IDENT && e.path.name == "der" && !isempty(e.args) - _pushDerArgNames!(names, toDAEExp(first(e.args))) - end - (e, true, nothing) - end, nothing) + traverseExpTopDown(exp, CollectDerNames(names), nothing) return names end @@ -503,15 +554,7 @@ end # SIM-native twin: walk the SimCode Exp; collect cref names via a per-cref projection. function _collectCrefNames!(missing::Vector{String}, exp::Exp, known::OrderedSet{String}) - traverseExpTopDown(exp, (e, _) -> begin - if e isa EXP_CREF - local nm = DAE_identifierToString(toDAECref(e.cref).componentRef) - if !(nm in known) - push!(missing, nm) - end - end - (e, true, nothing) - end, nothing) + traverseExpTopDown(exp, CollectCrefNames(missing, known), nothing) return missing end @@ -769,16 +812,7 @@ end # SIM-native twin: walk the SimCode Exp directly, reusing the per-node checks via a per-cref projection. function _flagCanonicalExp!(out::Vector{CheckViolation}, exp::Exp, where::String) - traverseExpTopDown(exp, (e, _) -> begin - if e isa EXP_CREF - _flagCanonicalComponentRef!(out, toDAECref(e.cref).componentRef, where) - elseif e isa CALL - _flagCanonicalPath!(out, e.path, where * ".call") - elseif e isa RECORD - _flagCanonicalPath!(out, e.path, where * ".record") - end - (e, true, nothing) - end, nothing) + traverseExpTopDown(exp, FlagCanonicalExp(out, where), nothing) return out end @@ -903,16 +937,7 @@ end # SIM-native twin: walk the SimCode Exp; reuse the DAE literal check via a per-arg projection. function _flagLiteralInDerPre!(out::Vector{CheckViolation}, exp::Exp, where::String) - traverseExpTopDown(exp, (e, _) -> begin - if e isa CALL && e.path isa Absyn.IDENT && (e.path.name == "der" || e.path.name == "pre") && !isempty(e.args) - local arg = toDAEExp(first(e.args)) - if _isDAEConstant(arg) - push!(out, CheckViolation(:no_literal_in_der_pre, :warn, where, - "$(e.path.name)(literal $(typeof(arg))) reached codegen — likely upstream parameter-eval substitution")) - end - end - (e, true, nothing) - end, nothing) + traverseExpTopDown(exp, FlagLiteralInDerPre(out, where), nothing) return out end diff --git a/src/backendAPI.jl b/src/backendAPI.jl index 0f9ead06..dcef88e5 100644 --- a/src/backendAPI.jl +++ b/src/backendAPI.jl @@ -89,6 +89,11 @@ matrix, sparse Jacobian and initialization are attached to the `ODEFunction` we build ourselves, not dropped by the wrapper. Toggle with: `OMBackend.DIRECT_RHS_TYPE_ERASE[] = false` to disable. + +This also governs event-callback erasure: when on, `_eraseContinuousCallbacks` +collapses the OM-generated continuous callbacks into a single, model-independent +`VectorContinuousCallback` (MTK `process_events` callbacks are left unchanged), so +event-ful models in the legacy when-equation class also share the problem type. """ const DIRECT_RHS_TYPE_ERASE = Ref{Bool}(true) @@ -1032,6 +1037,15 @@ function simulateModel(modelName::String; rethrow(err) end elseif MODE == IMTK_MODE + #= overwriteCache forces a fresh build: bypass the codeHash reuse check so the + cached problem is rebuilt (not just remade) before simulate. =# + if overwriteCache + try + IMTKGen._buildAndCache(modelName, getCompiledModel(modelName); overwriteCache = true) + catch err + @warn "[IMTK] overwriteCache rebuild failed; using existing cached build" model = modelName exception = err + end + end #= Reuse the build cached in the backend at translate time (no structural_simplify re-run); simulateIMTK falls back to module simulate. =# local _runDir = get(MODEL_RUN_DIRS, modelName, nothing) diff --git a/src/precompile.jl b/src/precompile.jl index 11fd1039..5cac565e 100644 --- a/src/precompile.jl +++ b/src/precompile.jl @@ -56,6 +56,84 @@ function _precompileWarmupDirectRHS() return nothing end +#= Plain OMBackend-parented callback functions for the event-ful DirectRHS bake. + They live in OMBackend so the callback-erasure whitelist guard collapses them + exactly as it does the generated when-equation callbacks. The condition crosses + zero during the warmup solve so the event-handling path is compiled too. =# +function _precompileCBCondition(u::Vector{Float64}, t::Float64, integrator)::Float64 + return u[1] - 0.5 +end + +function _precompileCBAffect!(integrator)::Nothing + return nothing +end + +#= Builds a pure-ODE DirectRHS problem carrying one continuous callback, so the + erased callback collapses to the single model-independent + `VectorContinuousCallback` that the legacy when-equation class produces. Kept + separate from the solve so the baked type can be checked against a real model. =# +# Mirror simulateIMTK's solve-time collapse so the baked solve is over the same erased +# callback type the runtime produces. +function _precompileCollapseCallback(prob) + local cb = get(prob.kwargs, :callback, nothing) + cb === nothing && return prob + return ModelingToolkit.SciMLBase.remake(prob; + callback = CodeGeneration._eraseContinuousCallbacks(cb)) +end + +function _precompileBuildDirectRHSEventfulProblem() + local (reduced, ivs, pars) = _precompileBuildReduced() + local SB = ModelingToolkit.SciMLBase + local cbset = SB.CallbackSet(SB.ContinuousCallback(_precompileCBCondition, + _precompileCBAffect!)) + local prob = CodeGeneration.buildDirectRHSProblem(reduced, ivs, pars, (0.0, 2.0), cbset) + return _precompileCollapseCallback(prob) +end + +#= Bakes the type-erased event-ful DirectRHS solve into the image. With RHS/jac + erasure the `F` param already matches the pure-ODE warmup; the only remaining + model-specific axis is the callback, now collapsed to a model-independent type. + Solving here compiles `solve(::ErasedEventFulType, ::alg)` once for the whole + legacy when-equation class. =# +function _precompileWarmupDirectRHSEventful()::Nothing + local prob = _precompileBuildDirectRHSEventfulProblem() + solve(prob, Rodas5(autodiff = false)) + solve(prob, FBDF(autodiff = false)) + return nothing +end + +#= Small index-1 DAE (a retained nonlinear algebraic constraint keeps a non-identity + mass matrix) with a continuous event. The pure-ODE warmups above never exercise the + DAE mass-matrix solve (sparse W assembly / reinit) nor the MTK `process_events` + callback path that the chua-class models pay for at first run, so this bakes both. =# +function _precompileBuildReducedEventfulDAE() + @independent_variables t + D = Differential(t) + @variables x(t) y(t) + @parameters a + local eqs = [D(x) ~ -a * x + y, 0 ~ y^3 + y - x] + local ev = ModelingToolkit.SymbolicContinuousCallback([x ~ 0.5] => [x ~ 0.4]; + reinitializealg = ModelingToolkit.SciMLBase.NoInit()) + local sys = ODESystem(eqs, t, [x, y], [a]; name = :OMBackendPrecompileEventfulDAE, + continuous_events = [ev], guesses = [y => 0.0], + initialization_eqs = ModelingToolkit.Equation[]) + local reduced = CodeGeneration.structural_simplify(sys; simplify = true, + allow_parameter = true, split = false) + return (reduced, [x => 1.0, y => 0.0], Dict(a => 1.0)) +end + +#= Bakes the DAE-with-continuous-callback DirectRHS solve (chua class: non-identity + mass matrix + an MTK process_events callback) into the image. =# +function _precompileWarmupMTKCallbackDAE()::Nothing + local (reduced, ivs, pars) = _precompileBuildReducedEventfulDAE() + local prob = CodeGeneration.buildDirectRHSProblem(reduced, ivs, pars, (0.0, 2.0), + ModelingToolkit.SciMLBase.CallbackSet()) + prob = _precompileCollapseCallback(prob) + solve(prob, Rodas5(autodiff = false)) + solve(prob, FBDF(autodiff = false)) + return nothing +end + @setup_workload begin @compile_workload begin #= Escape hatch for fast dev precompiles; a workload failure must never break @@ -71,6 +149,16 @@ end catch err @debug "[OMBackend] DirectRHS precompile warmup skipped" exception = err end + try + _precompileWarmupDirectRHSEventful() + catch err + @debug "[OMBackend] event-ful DirectRHS precompile warmup skipped" exception = err + end + try + _precompileWarmupMTKCallbackDAE() + catch err + @debug "[OMBackend] MTK-callback DAE precompile warmup skipped" exception = err + end end end end From 254e068de7fa7cb20746e6339685b15c5999fe20 Mon Sep 17 00:00:00 2001 From: JKRT Date: Wed, 24 Jun 2026 13:08:44 +0200 Subject: [PATCH 10/12] Minor fixes --- src/CodeGeneration/MTK_CodeGeneration.jl | 37 ++++++++++++++---------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/CodeGeneration/MTK_CodeGeneration.jl b/src/CodeGeneration/MTK_CodeGeneration.jl index 8d737c27..e0e0644f 100644 --- a/src/CodeGeneration/MTK_CodeGeneration.jl +++ b/src/CodeGeneration/MTK_CodeGeneration.jl @@ -1876,10 +1876,11 @@ function _buildTimeEventRefreshCallbacks(allPT::Vector, ptOwners::Vector, simCod local retNT = Expr(:tuple, Expr(:parameters, retKws...)) local fExpr = :((modified, observed, ctx, integrator) -> $(retNT)) if isempty(obsKws) - affect = :(ModelingToolkit.ImperativeAffect($(fExpr), $(modNT))) + affect = :(ModelingToolkit.ImperativeAffect($(fExpr), $(modNT); skip_checks = true)) else local obsNT = Expr(:tuple, Expr(:parameters, obsKws...)) - affect = :(ModelingToolkit.ImperativeAffect($(fExpr), $(modNT); observed = $(obsNT))) + affect = :(ModelingToolkit.ImperativeAffect($(fExpr), $(modNT); + observed = $(obsNT), skip_checks = true)) end end push!(cbs, :(ModelingToolkit.SymbolicContinuousCallback( @@ -2131,9 +2132,11 @@ function _composedIfCondAffectExpr(ifKws::Vector{Expr}, modIfKws::Vector{Expr}, local clusterModKws = Expr[Expr(:kw, d, d) for assigns in chained for (d, _, _) in assigns] local modNT = Expr(:tuple, Expr(:parameters, vcat(modIfKws, clusterModKws)...)) local obsKws = vcat(extraObsKws, Expr[Expr(:kw, k, v) for (k, v) in obsAcc]) - isempty(obsKws) && return :(ModelingToolkit.ImperativeAffect($(fexpr), $(modNT))) + isempty(obsKws) && return :(ModelingToolkit.ImperativeAffect($(fexpr), $(modNT); + skip_checks = true)) local obsNT = Expr(:tuple, Expr(:parameters, obsKws...)) - return :(ModelingToolkit.ImperativeAffect($(fexpr), $(modNT); observed = $(obsNT))) + return :(ModelingToolkit.ImperativeAffect($(fexpr), $(modNT); + observed = $(obsNT), skip_checks = true)) end function createIfEquations(stateVariables, algebraicVariables, simCode) @@ -2513,7 +2516,8 @@ function createIfEquation(stateVariables::Vector, end local liveFn = :((modified, observed, ctx, integrator) -> $(Expr(:tuple, Expr(:parameters, liveKws...)))) local liveObsNT = Expr(:tuple, Expr(:parameters, liveObsKws...)) - liveAffect = :(ModelingToolkit.ImperativeAffect($(liveFn), $(modifiedNT); observed = $(liveObsNT))) + liveAffect = :(ModelingToolkit.ImperativeAffect($(liveFn), $(modifiedNT); + observed = $(liveObsNT), skip_checks = true)) end #= Compose the chained cluster recomputes into both flip directions; the up edge holds the condition FALSE (own ifCond 0.0), the down edge TRUE. =# @@ -2576,7 +2580,8 @@ function createIfEquation(stateVariables::Vector, local _zcTest = _closedB ? :(observed.zc <= 0) : :(observed.zc < 0) local initRetNT::Expr = Expr(:tuple, Expr(:parameters, Expr(:kw, thisSym, :($(_zcTest) ? 1.0 : 0.0)))) local initFExpr::Expr = :((modified, observed, ctx, integrator) -> $initRetNT) - local initAffect::Expr = :(ModelingToolkit.ImperativeAffect($(initFExpr), $(initModifiedNT); observed = $(initObservedNT))) + local initAffect::Expr = :(ModelingToolkit.ImperativeAffect($(initFExpr), $(initModifiedNT); + observed = $(initObservedNT), skip_checks = true)) cond = :(ModelingToolkit.SymbolicContinuousCallback( ($(mtkCond)) => $(affectTuple); affect_neg = $(affectNegTuple), @@ -3397,9 +3402,9 @@ function createSelfSchedulingTimeWhenEvents(simCode)::Vector{Expr} local zc = transformToMTKContinuousCondition(rel, simCode) push!(events, :(ModelingToolkit.SymbolicContinuousCallback( (-($(zc)) ~ 0), - ModelingToolkit.ImperativeAffect($(fn), $(modN); observed = $(obs)); + ModelingToolkit.ImperativeAffect($(fn), $(modN); observed = $(obs), skip_checks = true); affect_neg = nothing, - initialize = ModelingToolkit.ImperativeAffect($(fnI), $(modI); observed = $(obsI)), + initialize = ModelingToolkit.ImperativeAffect($(fnI), $(modI); observed = $(obsI), skip_checks = true), rootfind = SciMLBase.RightRootFind, reinitializealg = SciMLBase.NoInit()))) end @@ -3457,9 +3462,9 @@ function createDiscreteBoolWhenEvents(simCode)::Vector{Expr} local (fnI, obsI, modI) = _preMemAffectParts(assigns, simCode; atInit = true, flagModified = false) push!(events, :(ModelingToolkit.SymbolicContinuousCallback( ($(zc) ~ 0), - ModelingToolkit.ImperativeAffect($(fnUp), $(modUp); observed = $(obsUp)); - affect_neg = ModelingToolkit.ImperativeAffect($(fnDn), $(modDn); observed = $(obsDn)), - initialize = ModelingToolkit.ImperativeAffect($(fnI), $(modI); observed = $(obsI)), + ModelingToolkit.ImperativeAffect($(fnUp), $(modUp); observed = $(obsUp), skip_checks = true); + affect_neg = ModelingToolkit.ImperativeAffect($(fnDn), $(modDn); observed = $(obsDn), skip_checks = true), + initialize = ModelingToolkit.ImperativeAffect($(fnI), $(modI); observed = $(obsI), skip_checks = true), rootfind = SciMLBase.RightRootFind, reinitializealg = $(_fsmReinitAlg())))) elseif relIdx == 1 && hasInit @@ -3471,16 +3476,16 @@ function createDiscreteBoolWhenEvents(simCode)::Vector{Expr} local (fnNF, obsNF, modNF) = _preMemAffectParts(assigns, simCode; flagModified = false) push!(events, :(ModelingToolkit.SymbolicContinuousCallback( ($(zc) ~ 0), - ModelingToolkit.ImperativeAffect($(fnUp), $(modUp); observed = $(obsUp)); - affect_neg = ModelingToolkit.ImperativeAffect($(fnDn), $(modDn); observed = $(obsDn)), - initialize = ModelingToolkit.ImperativeAffect($(fnNF), $(modNF); observed = $(obsNF)), + ModelingToolkit.ImperativeAffect($(fnUp), $(modUp); observed = $(obsUp), skip_checks = true); + affect_neg = ModelingToolkit.ImperativeAffect($(fnDn), $(modDn); observed = $(obsDn), skip_checks = true), + initialize = ModelingToolkit.ImperativeAffect($(fnNF), $(modNF); observed = $(obsNF), skip_checks = true), rootfind = SciMLBase.RightRootFind, reinitializealg = $(_fsmReinitAlg())))) else push!(events, :(ModelingToolkit.SymbolicContinuousCallback( ($(zc) ~ 0), - ModelingToolkit.ImperativeAffect($(fnUp), $(modUp); observed = $(obsUp)); - affect_neg = ModelingToolkit.ImperativeAffect($(fnDn), $(modDn); observed = $(obsDn)), + ModelingToolkit.ImperativeAffect($(fnUp), $(modUp); observed = $(obsUp), skip_checks = true); + affect_neg = ModelingToolkit.ImperativeAffect($(fnDn), $(modDn); observed = $(obsDn), skip_checks = true), rootfind = SciMLBase.RightRootFind, reinitializealg = $(_fsmReinitAlg())))) end From 62341e6bd663d1be453ba8bb406c5d929438cf14 Mon Sep 17 00:00:00 2001 From: JKRT Date: Wed, 24 Jun 2026 18:29:55 +0200 Subject: [PATCH 11/12] test: align mocks with current SIM_CODE and WHEN_EQUATION types Three test-only fixes for type mismatches against production: - mock stringToSimVarHT used Tuple{Integer, SimVar}; SIM_CODE requires Tuple{Int, SimVar} (backendTestMocks.jl, simCodeTraverseTests.jl) - mkWhenEq passed nothing for the attr field added to WHEN_EQUATION; use EQ_ATTR_DEFAULT_UNKNOWN (runtests.jl) Suite: 228/228 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/backendTestMocks.jl | 2 +- test/runtests.jl | 2 +- test/simCodeTraverseTests.jl | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/backendTestMocks.jl b/test/backendTestMocks.jl index 5d45ac4f..e979521c 100644 --- a/test/backendTestMocks.jl +++ b/test/backendTestMocks.jl @@ -30,7 +30,7 @@ Minimal SIM_CODE with an empty equation/when/if system and the given equations as a separate argument (e.g. `planDemotions`) and only consult the simcode for when-assigned / cyclic-SCC / duplicate-residual accounting. """ -function mockSimCode(ht = Dict{String, Tuple{Integer, SC.SimVar}}()) +function mockSimCode(ht = Dict{String, Tuple{Int, SC.SimVar}}()) return SC.SIM_CODE("mock", ht, SC.RESIDUAL_EQUATION[], SC.Equation[], SC.WHEN_EQUATION[], SC.IF_EQUATION[], false, Int[], SC.Graphs.SimpleDiGraph(0), [], SC.StructuralTransition[], [], diff --git a/test/runtests.jl b/test/runtests.jl index c735388d..a45ab343 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -265,7 +265,7 @@ import .ExampleDAEs 0, BDAE.WHEN_STMTS(cond, mkNoretBody(), NONE()), DAE.emptyElementSource, - nothing, + BDAE.EQ_ATTR_DEFAULT_UNKNOWN, ) @testset "bare initial() condition is extracted" begin diff --git a/test/simCodeTraverseTests.jl b/test/simCodeTraverseTests.jl index 496926f8..b3c02edb 100644 --- a/test/simCodeTraverseTests.jl +++ b/test/simCodeTraverseTests.jl @@ -255,7 +255,7 @@ simCall(fn, args...) = SC.toSimExp(daeCall(fn, args...)) end @testset "expToJuliaExpMTK SIM-native == DAE oracle" begin - sc = mockSimCode(Dict{String, Tuple{Integer, SC.SimVar}}( + sc = mockSimCode(Dict{String, Tuple{Int, SC.SimVar}}( "x" => (0, SC.SIMVAR("x", SOME(0), SC.ALG_VARIABLE(0), NONE())))) @test e2jEqualsOracle(SC.BCONST(true), sc) @test e2jEqualsOracle(SC.ICONST(7), sc) @@ -277,7 +277,7 @@ simCall(fn, args...) = SC.toSimExp(daeCall(fn, args...)) @testset "expToJuliaExpDE SIM-native == DAE oracle" begin # DEMode resolves STATE -> u[i], PARAMETER -> p[i], time -> t; the layout is built # from the mock HT's varKinds. No MSL test exercises DEMode, so this is the coverage. - sc = mockSimCode(Dict{String, Tuple{Integer, SC.SimVar}}( + sc = mockSimCode(Dict{String, Tuple{Int, SC.SimVar}}( "x" => (0, SC.SIMVAR("x", SOME(0), SC.STATE(), NONE())), "p" => (1, SC.SIMVAR("p", SOME(1), SC.PARAMETER(NONE()), NONE())))) layout = CG.DEGen.buildDELayout(sc) @@ -302,7 +302,7 @@ simCall(fn, args...) = SC.toSimExp(daeCall(fn, args...)) @testset "expToJuliaExp SIM-native == DAE oracle" begin # generic emitter resolves STATE/ALG -> [i], PARAMETER -> p[i], time -> t. # CALL / CAST arms need real model context (LRT-gated, as for expToJuliaExpMTK). - sc = mockSimCode(Dict{String, Tuple{Integer, SC.SimVar}}( + sc = mockSimCode(Dict{String, Tuple{Int, SC.SimVar}}( "x" => (0, SC.SIMVAR("x", SOME(0), SC.ALG_VARIABLE(0), NONE())), "p" => (1, SC.SIMVAR("p", SOME(1), SC.PARAMETER(NONE()), NONE())), "s" => (2, SC.SIMVAR("s", SOME(2), SC.STATE(), NONE())))) From c6bf37a3c1fd6281691a24816c8c6c87e32f8625 Mon Sep 17 00:00:00 2001 From: JKRT Date: Wed, 24 Jun 2026 20:10:19 +0200 Subject: [PATCH 12/12] ci: re-enable Windows test matrix --- .github/workflows/CI.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8ed1cc71..626138b6 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -35,14 +35,9 @@ jobs: - os: macos-latest arch: arm64 version: '1.12' - # Windows is disabled while the silent test-suite stall is being - # debugged locally. The 90-minute job cap is hit consistently even - # after warm depot cache, D:-drive depot, coverage off and - # check_bounds=auto reduce setup + pre-test precompile to ~3 - # minutes. The test runner then goes silent for 70+ minutes with - # no output before the cap, so the stall is inside the test suite - # itself. Re-enable once the hung @testset is identified and - # either fixed or skipped. + - os: windows-latest + arch: x64 + version: '1.12' steps: - name: Cap precompile concurrency (macOS)