@@ -23,8 +23,10 @@ import {
2323 ModelicaIntegerLiteral ,
2424 ModelicaNameExpression ,
2525 ModelicaRealLiteral ,
26+ ModelicaSimpleEquation ,
2627 ModelicaStringLiteral ,
2728 ModelicaUnaryExpression ,
29+ ModelicaWhenEquation ,
2830} from "./dae.js" ;
2931import type { FmuOptions , FmuResult } from "./fmi.js" ;
3032import { ModelicaBinaryOperator , ModelicaUnaryOperator , ModelicaVariability } from "./syntax.js" ;
@@ -58,7 +60,7 @@ export function generateFmuCSources(dae: ModelicaDAE, fmuResult: FmuResult, opti
5860 const modelC = generateModelC ( id , dae , fmuResult ) ;
5961
6062 // ── fmi2Functions.c ──
61- const fmi2FunctionsC = generateFmi2FunctionsC ( id , nVars , nStates , nStringVars , fmuResult ) ;
63+ const fmi2FunctionsC = generateFmi2FunctionsC ( id , nVars , nStates , nStringVars , dae , fmuResult ) ;
6264
6365 // ── CMakeLists.txt ──
6466 const cmakeLists = generateCMakeLists ( id ) ;
@@ -236,6 +238,38 @@ function escapeCString(s: string): string {
236238 return s . replace ( / \\ / g, "\\\\" ) . replace ( / " / g, '\\"' ) . replace ( / \n / g, "\\n" ) ;
237239}
238240
241+ /**
242+ * Convert a when-equation condition to a C zero-crossing expression.
243+ * For relational operators (x < threshold), returns `(LHS) - (RHS)`.
244+ * For boolean conditions, returns the expression directly (non-zero = active).
245+ */
246+ function conditionToZeroCrossingC ( condition : ModelicaExpression ) : string {
247+ if ( condition instanceof ModelicaBinaryExpression ) {
248+ const op = condition . operator ;
249+ if (
250+ op === ModelicaBinaryOperator . LESS_THAN ||
251+ op === ModelicaBinaryOperator . LESS_THAN_OR_EQUAL ||
252+ op === ModelicaBinaryOperator . GREATER_THAN ||
253+ op === ModelicaBinaryOperator . GREATER_THAN_OR_EQUAL
254+ ) {
255+ return `(${ exprToC ( condition . operand1 ) } ) - (${ exprToC ( condition . operand2 ) } )` ;
256+ }
257+ }
258+ // Fallback: treat as boolean condition (1.0 if true, -1.0 if false)
259+ return `(${ exprToC ( condition ) } ? 1.0 : -1.0)` ;
260+ }
261+
262+ /**
263+ * Extract the assignment target variable name from the LHS of a simple equation.
264+ */
265+ function extractAssignmentTarget ( expr : ModelicaExpression ) : string | null {
266+ if ( expr instanceof ModelicaNameExpression ) return expr . name ;
267+ if ( expr && typeof expr === "object" && "name" in expr ) {
268+ return ( expr as { name : string } ) . name ;
269+ }
270+ return null ;
271+ }
272+
239273// ── File generators ──
240274
241275function generateModelH ( id : string , nVars : number , nStates : number , nStringVars : number , result : FmuResult ) : string {
@@ -268,6 +302,7 @@ function generateModelH(id: string, nVars: number, nStates: number, nStringVars:
268302 lines . push ( " double states[N_STATES + 1];" ) ;
269303 lines . push ( " double derivatives[N_STATES + 1];" ) ;
270304 lines . push ( " char* stringVars[N_STRING_VARS + 1]; /* string variable storage */" ) ;
305+ lines . push ( " double eventPrev[N_EVENT_INDICATORS + 1]; /* previous event indicator values */" ) ;
271306 lines . push ( " double time;" ) ;
272307 lines . push ( " int isDirtyValues;" ) ;
273308 lines . push ( `} ${ id } _Instance;` ) ;
@@ -370,10 +405,45 @@ function generateModelC(id: string, dae: ModelicaDAE, result: FmuResult): string
370405 lines . push ( "}" ) ;
371406 lines . push ( "" ) ;
372407
373- // ── getEventIndicators function (stub) ──
408+ // ── getEventIndicators function ──
409+ // Each when-equation condition becomes an event indicator
410+ const whenEqs = dae . equations . filter ( ( eq ) : eq is ModelicaWhenEquation => eq instanceof ModelicaWhenEquation ) ;
374411 lines . push ( `void ${ id } _getEventIndicators(${ id } _Instance* inst, double* indicators) {` ) ;
375- lines . push ( " (void)inst; (void)indicators;" ) ;
376- lines . push ( " /* No event indicators yet */" ) ;
412+
413+ if ( whenEqs . length === 0 ) {
414+ lines . push ( " (void)inst; (void)indicators;" ) ;
415+ } else {
416+ // Emit local variable aliases for referenced names
417+ const eventReferencedNames = new Set < string > ( ) ;
418+ for ( const weq of whenEqs ) {
419+ collectReferencedNames ( weq . condition , eventReferencedNames ) ;
420+ for ( const clause of weq . elseWhenClauses ) {
421+ collectReferencedNames ( clause . condition , eventReferencedNames ) ;
422+ }
423+ }
424+ if ( eventReferencedNames . has ( "time" ) ) {
425+ lines . push ( " double time = inst->time;" ) ;
426+ }
427+ for ( const sv of result . scalarVariables ) {
428+ if ( sv . causality === "independent" ) continue ;
429+ if ( ! eventReferencedNames . has ( sv . name ) ) continue ;
430+ const cName = varToC ( sv . name ) ;
431+ lines . push ( ` double ${ cName } = inst->vars[${ sv . valueReference } ];` ) ;
432+ }
433+ lines . push ( "" ) ;
434+
435+ let indicatorIdx = 0 ;
436+ for ( const weq of whenEqs ) {
437+ const zc = conditionToZeroCrossingC ( weq . condition ) ;
438+ lines . push ( ` indicators[${ indicatorIdx } ] = ${ zc } ; /* when condition */` ) ;
439+ indicatorIdx ++ ;
440+ for ( const clause of weq . elseWhenClauses ) {
441+ const zcElse = conditionToZeroCrossingC ( clause . condition ) ;
442+ lines . push ( ` indicators[${ indicatorIdx } ] = ${ zcElse } ; /* elsewhen condition */` ) ;
443+ indicatorIdx ++ ;
444+ }
445+ }
446+ }
377447 lines . push ( "}" ) ;
378448
379449 return lines . join ( "\n" ) ;
@@ -384,8 +454,11 @@ function generateFmi2FunctionsC(
384454 nVars : number ,
385455 nStates : number ,
386456 nStringVars : number ,
457+ dae : ModelicaDAE ,
387458 result : FmuResult ,
388459) : string {
460+ // Extract when-equations for event handling
461+ const whenEqs = dae . equations . filter ( ( eq ) : eq is ModelicaWhenEquation => eq instanceof ModelicaWhenEquation ) ;
389462 const lines : string [ ] = [ ] ;
390463 lines . push ( "/* Auto-generated by ModelScript — FMI 2.0 API implementation */" ) ;
391464 lines . push ( `#include "${ id } _model.h"` ) ;
@@ -624,6 +697,15 @@ function generateFmi2FunctionsC(
624697 ) ;
625698 }
626699 lines . push ( " t += h;" ) ;
700+ lines . push ( "" ) ;
701+
702+ // After each step, check for events and process them
703+ lines . push ( " /* Event detection after integration step */" ) ;
704+ lines . push ( " if (N_EVENT_INDICATORS > 0) {" ) ;
705+ lines . push ( " fmi2EventInfo info;" ) ;
706+ lines . push ( " fmi2NewDiscreteStates((fmi2Component)inst, &info);" ) ;
707+ lines . push ( " }" ) ;
708+
627709 lines . push ( " }" ) ;
628710 lines . push ( " inst->model.time = tEnd;" ) ;
629711 lines . push ( " return fmi2OK;" ) ;
@@ -703,9 +785,105 @@ function generateFmi2FunctionsC(
703785 lines . push (
704786 "fmi2Status fmi2GetNominalsOfContinuousStates(fmi2Component c, fmi2Real nominals[], size_t nx) { for (size_t i = 0; i < nx; i++) nominals[i] = 1.0; (void)c; return fmi2OK; }" ,
705787 ) ;
706- lines . push (
707- "fmi2Status fmi2NewDiscreteStates(fmi2Component c, fmi2EventInfo* info) { info->newDiscreteStatesNeeded = fmi2False; info->terminateSimulation = fmi2False; info->nominalsOfContinuousStatesChanged = fmi2False; info->valuesOfContinuousStatesChanged = fmi2False; info->nextEventTimeDefined = fmi2False; (void)c; return fmi2OK; }" ,
708- ) ;
788+ // ── fmi2NewDiscreteStates ──
789+ // Evaluate when-equation conditions and execute body assignments on rising edge
790+ lines . push ( "fmi2Status fmi2NewDiscreteStates(fmi2Component c, fmi2EventInfo* info) {" ) ;
791+ lines . push ( " FMUInstance* inst = (FMUInstance*)c;" ) ;
792+ lines . push ( " info->newDiscreteStatesNeeded = fmi2False;" ) ;
793+ lines . push ( " info->terminateSimulation = fmi2False;" ) ;
794+ lines . push ( " info->nominalsOfContinuousStatesChanged = fmi2False;" ) ;
795+ lines . push ( " info->valuesOfContinuousStatesChanged = fmi2False;" ) ;
796+ lines . push ( " info->nextEventTimeDefined = fmi2False;" ) ;
797+
798+ if ( whenEqs . length > 0 ) {
799+ lines . push ( "" ) ;
800+ lines . push ( " /* Evaluate current event indicators */" ) ;
801+ lines . push ( " double indicators[N_EVENT_INDICATORS];" ) ;
802+ lines . push ( ` ${ id } _getEventIndicators(&inst->model, indicators);` ) ;
803+ lines . push ( "" ) ;
804+
805+ // Emit local variable aliases for when-equation body assignments
806+ const bodyReferencedNames = new Set < string > ( ) ;
807+ for ( const weq of whenEqs ) {
808+ for ( const bodyEq of weq . equations ) {
809+ if ( bodyEq instanceof ModelicaSimpleEquation ) {
810+ collectReferencedNames ( bodyEq . expression1 , bodyReferencedNames ) ;
811+ collectReferencedNames ( bodyEq . expression2 , bodyReferencedNames ) ;
812+ }
813+ }
814+ for ( const clause of weq . elseWhenClauses ) {
815+ for ( const bodyEq of clause . equations ) {
816+ if ( bodyEq instanceof ModelicaSimpleEquation ) {
817+ collectReferencedNames ( bodyEq . expression1 , bodyReferencedNames ) ;
818+ collectReferencedNames ( bodyEq . expression2 , bodyReferencedNames ) ;
819+ }
820+ }
821+ }
822+ }
823+ if ( bodyReferencedNames . has ( "time" ) ) {
824+ lines . push ( " double time = inst->model.time;" ) ;
825+ }
826+ for ( const sv of result . scalarVariables ) {
827+ if ( sv . causality === "independent" ) continue ;
828+ if ( ! bodyReferencedNames . has ( sv . name ) ) continue ;
829+ const cName = varToC ( sv . name ) ;
830+ lines . push ( ` double ${ cName } = inst->model.vars[${ sv . valueReference } ];` ) ;
831+ }
832+ lines . push ( "" ) ;
833+
834+ let eventIdx = 0 ;
835+ for ( const weq of whenEqs ) {
836+ // Detect rising edge: indicator crossed from negative/zero to positive (or vice versa)
837+ lines . push ( ` /* when-equation ${ eventIdx } */` ) ;
838+ lines . push ( ` if ((indicators[${ eventIdx } ] > 0.0 && inst->model.eventPrev[${ eventIdx } ] <= 0.0) ||` ) ;
839+ lines . push ( ` (indicators[${ eventIdx } ] <= 0.0 && inst->model.eventPrev[${ eventIdx } ] > 0.0)) {` ) ;
840+
841+ // Execute the when-equation body assignments
842+ for ( const bodyEq of weq . equations ) {
843+ if ( bodyEq instanceof ModelicaSimpleEquation ) {
844+ const lhsName = extractAssignmentTarget ( bodyEq . expression1 ) ;
845+ if ( lhsName ) {
846+ // Find the VR for this variable
847+ const sv = result . scalarVariables . find ( ( v ) => v . name === lhsName ) ;
848+ if ( sv ) {
849+ lines . push (
850+ ` inst->model.vars[${ sv . valueReference } ] = ${ exprToC ( bodyEq . expression2 ) } ; /* ${ lhsName } */` ,
851+ ) ;
852+ }
853+ }
854+ }
855+ }
856+ lines . push ( " }" ) ;
857+ lines . push ( ` inst->model.eventPrev[${ eventIdx } ] = indicators[${ eventIdx } ];` ) ;
858+ eventIdx ++ ;
859+
860+ // elseWhen clauses
861+ for ( const clause of weq . elseWhenClauses ) {
862+ lines . push ( ` /* elsewhen-clause ${ eventIdx } */` ) ;
863+ lines . push ( ` if ((indicators[${ eventIdx } ] > 0.0 && inst->model.eventPrev[${ eventIdx } ] <= 0.0) ||` ) ;
864+ lines . push ( ` (indicators[${ eventIdx } ] <= 0.0 && inst->model.eventPrev[${ eventIdx } ] > 0.0)) {` ) ;
865+ for ( const bodyEq of clause . equations ) {
866+ if ( bodyEq instanceof ModelicaSimpleEquation ) {
867+ const lhsName = extractAssignmentTarget ( bodyEq . expression1 ) ;
868+ if ( lhsName ) {
869+ const sv = result . scalarVariables . find ( ( v ) => v . name === lhsName ) ;
870+ if ( sv ) {
871+ lines . push (
872+ ` inst->model.vars[${ sv . valueReference } ] = ${ exprToC ( bodyEq . expression2 ) } ; /* ${ lhsName } */` ,
873+ ) ;
874+ }
875+ }
876+ }
877+ }
878+ lines . push ( " }" ) ;
879+ lines . push ( ` inst->model.eventPrev[${ eventIdx } ] = indicators[${ eventIdx } ];` ) ;
880+ eventIdx ++ ;
881+ }
882+ }
883+ }
884+
885+ lines . push ( " return fmi2OK;" ) ;
886+ lines . push ( "}" ) ;
709887 lines . push ( "fmi2Status fmi2EnterContinuousTimeMode(fmi2Component c) { (void)c; return fmi2OK; }" ) ;
710888 lines . push ( "fmi2Status fmi2EnterEventMode(fmi2Component c) { (void)c; return fmi2OK; }" ) ;
711889 lines . push ( "fmi2Status fmi2CancelStep(fmi2Component c) { (void)c; return fmi2OK; }" ) ;
0 commit comments