Skip to content

Commit 70f6407

Browse files
committed
feat(core): implement external object lifecycles, wire assertion and logging callbacks, map enumeration xml exports, add child_process cmake compilation route
1 parent 05dead9 commit 70f6407

6 files changed

Lines changed: 295 additions & 16 deletions

File tree

packages/core/src/compiler/modelica/dae.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export class ModelicaDAE {
3333
functions: ModelicaDAE[] = [];
3434
/** External function declaration text (e.g. `external "C" ...`). */
3535
externalDecl: string | null = null;
36+
/** Extracted annotation(Library="...") references. */
37+
externalLibraries: string[] = [];
38+
/** Extracted annotation(Include="...") references. */
39+
externalIncludes: string[] = [];
3640
/**
3741
* Descriptors for variables whose type extends `ExternalObject`.
3842
* Track constructor/destructor names for lifecycle management.

packages/core/src/compiler/modelica/flattener.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4886,6 +4886,38 @@ class ModelicaSyntaxFlattener extends ModelicaSyntaxVisitor<ModelicaExpression,
48864886
}
48874887
declText += ";";
48884888
fnDae.externalDecl = declText;
4889+
4890+
if (ext.annotationClause?.classModification?.modificationArguments) {
4891+
for (const arg of ext.annotationClause.classModification.modificationArguments) {
4892+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4893+
const modArg = arg as any;
4894+
if (modArg.name?.text && modArg.modification?.expression) {
4895+
const nameText = modArg.name.text;
4896+
if (nameText === "Include" || nameText === "Library") {
4897+
const exprCsn = modArg.modification.expression.concreteSyntaxNode;
4898+
if (exprCsn) {
4899+
// Recursively extract all STRING tokens from the AST subtree
4900+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4901+
const extractStrings = (node: any): string[] => {
4902+
const result: string[] = [];
4903+
if (node.type === "STRING" && node.text) {
4904+
const text = node.text.substring(1, node.text.length - 1);
4905+
result.push(text.replace(/""/g, '"').replace(/\\"/g, '"'));
4906+
}
4907+
for (let i = 0; i < node.childCount; i++) {
4908+
const child = node.child(i);
4909+
if (child) result.push(...extractStrings(child));
4910+
}
4911+
return result;
4912+
};
4913+
const values = extractStrings(exprCsn);
4914+
if (nameText === "Include") fnDae.externalIncludes.push(...values);
4915+
else fnDae.externalLibraries.push(...values);
4916+
}
4917+
}
4918+
}
4919+
}
4920+
}
48894921
}
48904922
}
48914923
// fnDae was pushed early to prevent recursion during body flattening.

packages/core/src/compiler/modelica/fmi.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -597,9 +597,11 @@ function generateModelDescriptionXml(
597597
for (const [typeName, literals] of opts.enumTypes) {
598598
lines.push(` <SimpleType name="${escapeXml(typeName)}">`);
599599
lines.push(" <Enumeration>");
600-
for (const lit of literals) {
600+
for (let i = 0; i < literals.length; i++) {
601+
const lit = literals[i];
602+
if (!lit) continue;
601603
const descAttr = lit.description ? ` description="${escapeXml(lit.description)}"` : "";
602-
lines.push(` <Item name="${escapeXml(lit.name)}"${descAttr} />`);
604+
lines.push(` <Item name="${escapeXml(lit.name)}" value="${i + 1}"${descAttr} />`);
603605
}
604606
lines.push(" </Enumeration>");
605607
lines.push(" </SimpleType>");
@@ -781,6 +783,22 @@ export function generateFmi3ModelDescriptionXml(
781783
lines.push(` stopTime="${opts.stopTime ?? 1}"`);
782784
lines.push(` stepSize="${opts.stepSize ?? 0.001}" />`);
783785

786+
if (opts.enumTypes.size > 0) {
787+
lines.push("");
788+
lines.push(" <TypeDefinitions>");
789+
for (const [typeName, literals] of opts.enumTypes) {
790+
lines.push(` <EnumerationType name="${escapeXml(typeName)}">`);
791+
for (let i = 0; i < literals.length; i++) {
792+
const lit = literals[i];
793+
if (!lit) continue;
794+
const descAttr = lit.description ? ` description="${escapeXml(lit.description)}"` : "";
795+
lines.push(` <Item name="${escapeXml(lit.name)}" value="${i + 1}"${descAttr} />`);
796+
}
797+
lines.push(" </EnumerationType>");
798+
}
799+
lines.push(" </TypeDefinitions>");
800+
}
801+
784802
lines.push("");
785803
lines.push(" <ModelVariables>");
786804
const groupedVars = groupFmi3Variables(variables);

packages/core/src/compiler/modelica/fmu-archive.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,3 +371,47 @@ fmi2Status fmi2GetRealOutputDerivatives(fmi2Component, const fmi2ValueReference[
371371
372372
#endif
373373
`;
374+
375+
/**
376+
* Compiles the generated C sources into a shared library (.dll, .so, .dylib) via CMake.
377+
* NOTE: This requires Node.js (fs, path, os, child_process), CMake, and a C compiler available on the system.
378+
*/
379+
export async function compileFmuBinary(
380+
id: string,
381+
sources: { modelC: string; modelH: string; fmi2FunctionsC: string; fmi3FunctionsC: string; cmakeLists: string },
382+
): Promise<Uint8Array> {
383+
const [fs, path, os, { execSync }] = await Promise.all([
384+
import("fs"),
385+
import("path"),
386+
import("os"),
387+
import("child_process"),
388+
]);
389+
390+
const tmpPrefix = path.join(os.tmpdir(), `modelscript-fmu-${id}-`);
391+
const tmpDir = fs.mkdtempSync(tmpPrefix);
392+
393+
try {
394+
fs.writeFileSync(path.join(tmpDir, `${id}_model.c`), sources.modelC);
395+
fs.writeFileSync(path.join(tmpDir, `${id}_model.h`), sources.modelH);
396+
fs.writeFileSync(path.join(tmpDir, "fmi2Functions.c"), sources.fmi2FunctionsC);
397+
fs.writeFileSync(path.join(tmpDir, "fmi3Functions.c"), sources.fmi3FunctionsC);
398+
fs.writeFileSync(path.join(tmpDir, "fmi2Functions.h"), FMI2_FUNCTIONS_H);
399+
fs.writeFileSync(path.join(tmpDir, "fmi2TypesPlatform.h"), FMI2_TYPES_PLATFORM_H);
400+
fs.writeFileSync(path.join(tmpDir, "fmi2FunctionTypes.h"), FMI2_FUNCTION_TYPES_H);
401+
fs.writeFileSync(path.join(tmpDir, "CMakeLists.txt"), sources.cmakeLists);
402+
403+
execSync(`cmake -B build -S . -DCMAKE_BUILD_TYPE=Release`, { cwd: tmpDir, stdio: "pipe" });
404+
execSync(`cmake --build build --config Release`, { cwd: tmpDir, stdio: "pipe" });
405+
406+
const buildDir = path.join(tmpDir, "build");
407+
const files = fs.readdirSync(buildDir);
408+
const libFile = files.find(
409+
(f) => f.startsWith(id) && (f.endsWith(".dll") || f.endsWith(".so") || f.endsWith(".dylib")),
410+
);
411+
if (!libFile) throw new Error("Shared library not found after CMake compilation.");
412+
413+
return new Uint8Array(fs.readFileSync(path.join(buildDir, libFile)));
414+
} finally {
415+
fs.rmSync(tmpDir, { recursive: true, force: true });
416+
}
417+
}

packages/core/src/compiler/modelica/fmu-codegen.ts

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { ModelicaDAE, ModelicaExpression } from "./dae.js";
1818
import {
1919
ModelicaBinaryExpression,
2020
ModelicaBooleanLiteral,
21+
ModelicaFunctionCallEquation,
2122
ModelicaFunctionCallExpression,
2223
ModelicaIfElseExpression,
2324
ModelicaInitialStateEquation,
@@ -64,8 +65,13 @@ export function generateFmuCSources(dae: ModelicaDAE, fmuResult: FmuResult, opti
6465
// ── fmi3Functions.c ──
6566
const fmi3FunctionsC = generateFmi3FunctionsC(id, dae);
6667

68+
const externalLibs = new Set<string>();
69+
for (const fn of dae.functions) {
70+
for (const lib of fn.externalLibraries) externalLibs.add(lib);
71+
}
72+
6773
// ── CMakeLists.txt ──
68-
const cmakeLists = generateCMakeLists(id);
74+
const cmakeLists = generateCMakeLists(id, Array.from(externalLibs));
6975

7076
return { modelH, modelC, fmi2FunctionsC, fmi3FunctionsC, cmakeLists };
7177
}
@@ -123,11 +129,15 @@ function exprToC(expr: ModelicaExpression): string {
123129
// Message is a string literal — extract it or use a default
124130
const msgExpr = expr.args[1];
125131
const msg = msgExpr instanceof ModelicaStringLiteral ? msgExpr.value.replace(/"/g, '\\"') : "Assertion failed";
126-
return `((${cond}) ? 0.0 : (inst->callbacks.logger(inst, inst->instanceName, fmi2Error, "assert", "${msg}"), 0.0))`;
132+
return `((${cond}) ? 0.0 : (inst->logger ? inst->logger(inst->fmuInstance, "assert", "${msg}") : (void)0, inst->terminate ? inst->terminate(inst->fmuInstance) : (void)0, 0.0))`;
127133
}
128134
// terminate(message) — signal simulation termination
129135
if (expr.functionName === "terminate") {
130-
return "(inst->terminateRequested = 1, 0.0)";
136+
const msg =
137+
expr.args[0] instanceof ModelicaStringLiteral
138+
? expr.args[0].value.replace(/"/g, '\\"')
139+
: "Simulation terminated";
140+
return `(inst->logger ? inst->logger(inst->fmuInstance, "terminate", "${msg}") : (void)0, inst->terminate ? inst->terminate(inst->fmuInstance) : (void)0, 0.0)`;
131141
}
132142
// spatialDistribution(in0, in1, x, positiveVelocity) — 1D advection
133143
if (expr.functionName === "spatialDistribution" && expr.args.length >= 4) {
@@ -420,6 +430,9 @@ function generateModelH(
420430
);
421431
}
422432

433+
lines.push(` void* fmuInstance; /* parent FMU struct pointer */`);
434+
lines.push(` void (*logger)(void* fmuInstance, const char* category, const char* message);`);
435+
lines.push(` void (*terminate)(void* fmuInstance);`);
423436
lines.push(`} ${id}_Instance;`);
424437
lines.push("");
425438
lines.push(`void ${id}_initialize(${id}_Instance* inst);`);
@@ -433,7 +446,14 @@ function generateModelH(
433446
function generateAlgebraicLoopSolvers(id: string, dae: ModelicaDAE, result: FmuResult): string {
434447
const lines: string[] = [];
435448
lines.push("/* Numerical solver for Algebraic Loops */");
436-
lines.push("static void solve_linear_sys(int n, double* A, double* b, double* x) {");
449+
lines.push(`#define LOG_ERROR(inst, msg) \\`);
450+
lines.push(` do { \\`);
451+
lines.push(` if ((inst)->logger) (inst)->logger((inst)->fmuInstance, "error", msg); \\`);
452+
lines.push(` if ((inst)->terminate) (inst)->terminate((inst)->fmuInstance); \\`);
453+
lines.push(` } while(0)`);
454+
lines.push("");
455+
lines.push("static void solve_linear_sys(void* inst_ptr, int n, double* A, double* b, double* x) {");
456+
lines.push(` ${id}_Instance* inst = (${id}_Instance*)inst_ptr;`);
437457
lines.push(" for (int i = 0; i < n; i++) {");
438458
lines.push(" int pivot = i;");
439459
lines.push(" for (int j = i + 1; j < n; j++) {");
@@ -443,16 +463,20 @@ function generateAlgebraicLoopSolvers(id: string, dae: ModelicaDAE, result: FmuR
443463
lines.push(" double tmp = A[i*n + j]; A[i*n + j] = A[pivot*n + j]; A[pivot*n + j] = tmp;");
444464
lines.push(" }");
445465
lines.push(" double tmp = b[i]; b[i] = b[pivot]; b[pivot] = tmp;");
466+
lines.push(" if (fabs(A[i*n + i]) < 1e-14) {");
467+
lines.push(` LOG_ERROR(inst, "Singular algebraic loop matrix encountered");`);
468+
lines.push(" return;");
469+
lines.push(" }");
446470
lines.push(" for (int j = i + 1; j < n; j++) {");
447-
lines.push(" double factor = A[i*n + i] == 0.0 ? 0.0 : A[j*n + i] / A[i*n + i];");
471+
lines.push(" double factor = A[j*n + i] / A[i*n + i];");
448472
lines.push(" for (int k = i; k < n; k++) A[j*n + k] -= factor * A[i*n + k];");
449473
lines.push(" b[j] -= factor * b[i];");
450474
lines.push(" }");
451475
lines.push(" }");
452476
lines.push(" for (int i = n - 1; i >= 0; i--) {");
453477
lines.push(" double sum = 0.0;");
454478
lines.push(" for (int j = i + 1; j < n; j++) sum += A[i*n + j] * x[j];");
455-
lines.push(" x[i] = A[i*n + i] == 0.0 ? 0.0 : (b[i] - sum) / A[i*n + i];");
479+
lines.push(" x[i] = (b[i] - sum) / A[i*n + i];");
456480
lines.push(" }");
457481
lines.push("}");
458482
lines.push("");
@@ -539,7 +563,7 @@ function generateAlgebraicLoopSolvers(id: string, dae: ModelicaDAE, result: FmuR
539563
lines.push(` }`);
540564

541565
lines.push(` for (int i = 0; i < ${N}; i++) F[i] = -F[i];`);
542-
lines.push(` solve_linear_sys(${N}, J, F, dx);`);
566+
lines.push(` solve_linear_sys(inst, ${N}, J, F, dx);`);
543567
lines.push(` for (int j = 0; j < ${N}; j++) {`);
544568
lines.push(` int vr = -1;`);
545569
for (let j = 0; j < N; j++) {
@@ -563,6 +587,19 @@ function generateModelC(id: string, dae: ModelicaDAE, result: FmuResult): string
563587
lines.push("/* Auto-generated by ModelScript — do not edit */");
564588
lines.push(`#include "${id}_model.h"`);
565589
lines.push("#include <stdio.h>");
590+
591+
const externalIncludes = new Set<string>();
592+
for (const fn of dae.functions) {
593+
for (const inc of fn.externalIncludes) externalIncludes.add(inc);
594+
}
595+
for (const inc of externalIncludes) {
596+
if (inc.trim().startsWith("#")) {
597+
lines.push(inc);
598+
} else {
599+
// Sometimes just a header file name is given
600+
lines.push(inc.includes(";") || inc.includes("int ") || inc.includes("void ") ? inc : `#include "${inc}"`);
601+
}
602+
}
566603
lines.push("");
567604

568605
// Count delay() calls to determine if delay helpers are needed
@@ -876,6 +913,21 @@ function generateFmi2FunctionsC(
876913
lines.push("} FMUInstance;");
877914
lines.push("");
878915

916+
// ── Logger & Terminate Impl ──
917+
lines.push("static void fmi2_logger_impl(void* fmuInstance, const char* category, const char* message) {");
918+
lines.push(" FMUInstance* inst = (FMUInstance*)fmuInstance;");
919+
lines.push(" if (inst->callbacks.logger) {");
920+
lines.push(
921+
" inst->callbacks.logger(inst->callbacks.componentEnvironment, inst->instanceName, fmi2Error, category, message);",
922+
);
923+
lines.push(" }");
924+
lines.push("}");
925+
lines.push("static void fmi2_terminate_impl(void* fmuInstance) {");
926+
lines.push(" FMUInstance* inst = (FMUInstance*)fmuInstance;");
927+
lines.push(" inst->terminateRequested = 1;");
928+
lines.push("}");
929+
lines.push("");
930+
879931
// ── fmi2Instantiate ──
880932
lines.push("fmi2Component fmi2Instantiate(fmi2String instanceName, fmi2Type fmuType,");
881933
lines.push(" fmi2String fmuGUID, fmi2String fmuResourceLocation,");
@@ -887,6 +939,9 @@ function generateFmi2FunctionsC(
887939
lines.push(" inst->callbacks = *functions;");
888940
lines.push(" inst->loggingOn = loggingOn;");
889941
lines.push(` inst->stepSize = ${result.modelStructure.derivatives.length > 0 ? "0.001" : "0.001"};`);
942+
lines.push(" inst->model.fmuInstance = inst;");
943+
lines.push(" inst->model.logger = fmi2_logger_impl;");
944+
lines.push(" inst->model.terminate = fmi2_terminate_impl;");
890945
lines.push(` ${id}_initialize(&inst->model);`);
891946
lines.push(" return (fmi2Component)inst;");
892947
lines.push("}");
@@ -1394,6 +1449,16 @@ function generateFmi2FunctionsC(
13941449
lines.push("/* --- Stubs --- */");
13951450
lines.push("fmi2Status fmi2Reset(fmi2Component c) {");
13961451
lines.push(" FMUInstance* inst = (FMUInstance*)c;");
1452+
for (let ei = 0; ei < dae.externalObjects.length; ei++) {
1453+
const eo = dae.externalObjects[ei];
1454+
if (!eo) continue;
1455+
const ctorName = sanitizeIdentifier(eo.constructorName);
1456+
const dtorName = sanitizeIdentifier(eo.destructorName);
1457+
lines.push(` if (inst->model.extObj_${ei}) {`);
1458+
lines.push(` ${dtorName}(inst->model.extObj_${ei});`);
1459+
lines.push(` }`);
1460+
lines.push(` inst->model.extObj_${ei} = ${ctorName}();`);
1461+
}
13971462
lines.push(` ${id}_initialize(&inst->model);`);
13981463
lines.push(" return fmi2OK;");
13991464
lines.push("}");
@@ -1522,6 +1587,20 @@ function generateFmi2FunctionsC(
15221587
);
15231588
}
15241589
}
1590+
} else if (bodyEq instanceof ModelicaFunctionCallEquation) {
1591+
if (bodyEq.call.functionName === "reinit" && bodyEq.call.args.length === 2) {
1592+
const stateRef = bodyEq.call.args[0];
1593+
const newValue = bodyEq.call.args[1];
1594+
if (stateRef instanceof ModelicaNameExpression && newValue) {
1595+
const sv = result.scalarVariables.find((v) => v.name === stateRef.name);
1596+
if (sv) {
1597+
lines.push(
1598+
` inst->model.vars[${sv.valueReference}] = ${exprToC(newValue)}; /* reinit(${stateRef.name}) */`,
1599+
);
1600+
lines.push(` info->valuesOfContinuousStatesChanged = fmi2True;`);
1601+
}
1602+
}
1603+
}
15251604
}
15261605
}
15271606
lines.push(" }");
@@ -1544,6 +1623,20 @@ function generateFmi2FunctionsC(
15441623
);
15451624
}
15461625
}
1626+
} else if (bodyEq instanceof ModelicaFunctionCallEquation) {
1627+
if (bodyEq.call.functionName === "reinit" && bodyEq.call.args.length === 2) {
1628+
const stateRef = bodyEq.call.args[0];
1629+
const newValue = bodyEq.call.args[1];
1630+
if (stateRef instanceof ModelicaNameExpression && newValue) {
1631+
const sv = result.scalarVariables.find((v) => v.name === stateRef.name);
1632+
if (sv) {
1633+
lines.push(
1634+
` inst->model.vars[${sv.valueReference}] = ${exprToC(newValue)}; /* reinit(${stateRef.name}) */`,
1635+
);
1636+
lines.push(` info->valuesOfContinuousStatesChanged = fmi2True;`);
1637+
}
1638+
}
1639+
}
15471640
}
15481641
}
15491642
lines.push(" }");
@@ -1833,8 +1926,7 @@ function generateFmi2FunctionsC(
18331926

18341927
// ── CMakeLists.txt generator ──
18351928

1836-
function generateCMakeLists(id: string, externalSources: string[] = []): string {
1837-
const extSourceLines = externalSources.map((s) => ` ${s}`).join("\n");
1929+
function generateCMakeLists(id: string, externalLibraries: string[] = []): string {
18381930
return `# Auto-generated by ModelScript — CMake build for FMU shared library
18391931
cmake_minimum_required(VERSION 3.10)
18401932
project(${id} C)
@@ -1860,11 +1952,13 @@ endif()
18601952
add_library(${id} SHARED
18611953
${id}_model.c
18621954
fmi2Functions.c
1863-
fmi3Functions.c${extSourceLines ? "\n" + extSourceLines : ""}
1955+
fmi3Functions.c
18641956
)
18651957
18661958
target_include_directories(${id} PRIVATE \${CMAKE_CURRENT_SOURCE_DIR})
18671959
1960+
${externalLibraries.length > 0 ? `target_link_libraries(${id} PRIVATE ${externalLibraries.join(" ").replace(/\\/g, "/")})` : ""}
1961+
18681962
# Export FMI symbols, hide everything else
18691963
set_target_properties(${id} PROPERTIES
18701964
PREFIX ""

0 commit comments

Comments
 (0)