diff --git a/client/src/data/templates.json b/client/src/data/templates.json index ca33b252..1e2b900d 100644 --- a/client/src/data/templates.json +++ b/client/src/data/templates.json @@ -19287,6 +19287,93 @@ "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.prescribedHeatFlowRate": { "expression": true, "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.use_C_flow": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.heatPort": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.C_flow": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.initialize_p": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.simplify_mWat_flow": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.m_flow_nominal": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.nPorts": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.m_flow_small": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.allowFlowReversal": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.V": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.ports": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.T": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.U": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.p": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.m": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.Xi": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.mXi": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.C": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.mC": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.energyDynamics": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.massDynamics": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.substanceDynamics": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.traceDynamics": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.p_start": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.T_start": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.X_start": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.C_start": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.C_nominal": { + "final": true + }, + "Buildings.Fluid.HeatExchangers.HeaterCooler_u.vol.mSenFac": { + "final": true } }, "replaceable": false, @@ -48586,6 +48673,24 @@ "expression": "ctl.stdVen", "final": true }, + "Buildings.Templates.AirHandlersFans.VAVMultiZone.cfg.typ": { + "final": true + }, + "Buildings.Templates.AirHandlersFans.VAVMultiZone.cfg.typFanSup": { + "final": true + }, + "Buildings.Templates.AirHandlersFans.VAVMultiZone.cfg.typFanRet": { + "final": true + }, + "Buildings.Templates.AirHandlersFans.VAVMultiZone.cfg.typFanRel": { + "final": true + }, + "Buildings.Templates.AirHandlersFans.VAVMultiZone.cfg.have_souChiWat": { + "final": true + }, + "Buildings.Templates.AirHandlersFans.VAVMultiZone.cfg.have_souHeaWat": { + "final": true + }, "Buildings.Templates.AirHandlersFans.VAVMultiZone.dat": { "final": false, "redeclare": "Buildings.Templates.AirHandlersFans.Data.VAVMultiZone" @@ -52742,24 +52847,33 @@ }, "Buildings.Templates.ZoneEquipment.Interfaces.VAVBox.cfg.typCoiHea": { "expression": "coiHea.typ", - "final": false + "final": true }, "Buildings.Templates.ZoneEquipment.Interfaces.VAVBox.cfg.typValCoiHea": { "expression": "coiHea.typVal", - "final": false + "final": true }, "Buildings.Templates.ZoneEquipment.Interfaces.VAVBox.cfg.typDamVAV": { "expression": "damVAV.typ", - "final": false + "final": true }, "Buildings.Templates.ZoneEquipment.Interfaces.VAVBox.cfg.typCtl": { "expression": "ctl.typ", - "final": false, + "final": true, "recordBinding": true }, "Buildings.Templates.ZoneEquipment.Interfaces.VAVBox.cfg.stdVen": { "expression": "ctl.stdVen", - "final": false + "final": true + }, + "Buildings.Templates.ZoneEquipment.Interfaces.VAVBox.cfg.typ": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.Interfaces.VAVBox.cfg.have_souChiWat": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.Interfaces.VAVBox.cfg.have_souHeaWat": { + "final": true }, "Buildings.Templates.ZoneEquipment.Interfaces.VAVBox.dat": { "final": false, @@ -52883,6 +52997,57 @@ "final": true, "redeclare": "Buildings.Templates.Components.Coils.None" }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.typ": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.typVal": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.have_sou": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.have_weaBus": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.dat": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.mAir_flow_nominal": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.dpAir_nominal": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.Q_flow_nominal": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.tau": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.energyDynamics": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.allowFlowReversalAir": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.allowFlowReversalLiq": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.port_aSou": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.port_bSou": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.busWea": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.bus": { + "final": true + }, + "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.coiHea.datVal": { + "final": true + }, "Buildings.Templates.ZoneEquipment.VAVBoxCoolingOnly.ctl": { "final": false, "redeclare": "Buildings.Templates.ZoneEquipment.Components.Controls.G36VAVBoxCoolingOnly" diff --git a/client/tests/data/options-TestTemplate.json b/client/tests/data/options-TestTemplate.json index 4d8b1a74..9dd80280 100644 --- a/client/tests/data/options-TestTemplate.json +++ b/client/tests/data/options-TestTemplate.json @@ -1087,6 +1087,22 @@ "TestPackage.Template.TestTemplate.redeclare_param_01.replaceable_param": { "final": true, "redeclare": "TestPackage.Component.SecondComponent" + }, + "TestPackage.Template.TestTemplate.redeclare_param_01.replaceable_param.component_param": { + "expression": "\"From final redeclare\"", + "final": true + }, + "TestPackage.Template.TestTemplate.redeclare_param_01.replaceable_param.is_another_param": { + "final": true + }, + "TestPackage.Template.TestTemplate.redeclare_param_01.replaceable_param.inner_outer_param": { + "final": true + }, + "TestPackage.Template.TestTemplate.redeclare_param_01.replaceable_param.container": { + "final": true + }, + "TestPackage.Template.TestTemplate.redeclare_param_01.replaceable_param.icecream": { + "final": true } }, "replaceable": false, diff --git a/client/tests/data/templates.json.gz b/client/tests/data/templates.json.gz index 963f4ced..44d12d1b 100644 Binary files a/client/tests/data/templates.json.gz and b/client/tests/data/templates.json.gz differ diff --git a/server/src/parser/modification.ts b/server/src/parser/modification.ts index 89b1caf8..c84e311b 100644 --- a/server/src/parser/modification.ts +++ b/server/src/parser/modification.ts @@ -121,6 +121,53 @@ export function createModification( * - `redeclare NewType myParam = someValue` -> redeclare="NewType", value="someValue" * - `redeclare package Medium = NewMedium` -> redeclare="NewMedium", value=undefined */ +/** + * A `final` redeclare locks the component to its redeclared type: nothing + * downstream can touch it again, so any bindings written inline in that same + * redeclare clause are just as unmodifiable even when they don't repeat the + * `final` keyword themselves (Buildings authors are inconsistent about this). + */ +function markModsFinal(mods: Modification[]): void { + mods.forEach((m) => { + m.final = true; + if (m.mods.length) { + markModsFinal(m.mods); + } + }); +} + +/** + * A `final` redeclare locks down every parameter of the redeclared type, not + * just the ones bound inline in the redeclare clause: marks the already + * present child mods final, then synthesizes an unbound `final` Modification + * for every other own/inherited parameter of `element` so those can't be + * modified downstream either. + */ +function finalizeAllParams( + element: Element | undefined, + childBasePath: string, + childMods: Modification[], +): void { + markModsFinal(childMods); + + const boundNames = new Set(childMods.map((m) => m.name)); + const getChildElements = (element as { getChildElements?: () => Element[] }) + ?.getChildElements; + const allParams = + typeof getChildElements === "function" + ? getChildElements.call(element) + : []; + + allParams.forEach((el) => { + const paramName = el.modelicaPath.split(".").pop() as string; + if (!boundNames.has(paramName)) { + childMods.push( + new Modification(childBasePath, paramName, undefined, [], true), + ); + } + }); +} + function unpackRedeclaration(props: ModificationProps) { let { basePath, definition, baseType } = props; let redeclaration = (definition as mj.RedeclareMod).element_redeclaration; @@ -184,6 +231,14 @@ function unpackRedeclaration(props: ModificationProps) { } } + if (final) { + finalizeAllParams( + element, + [basePath, name].filter((s) => s).join("."), + childMods, + ); + } + // The redeclared type is stored under 'redeclare' property const redeclaredType = element.type; @@ -226,6 +281,14 @@ function unpackRedeclaration(props: ModificationProps) { ); } + if (final) { + finalizeAllParams( + aliasedType, + [basePath, name].filter((s) => s).join("."), + childMods, + ); + } + return new Modification( basePath, name, diff --git a/server/src/parser/template.ts b/server/src/parser/template.ts index b107e203..a4e33e90 100644 --- a/server/src/parser/template.ts +++ b/server/src/parser/template.ts @@ -133,8 +133,11 @@ export function flattenModifiers( modList .filter((m) => m !== undefined || m !== null) .map((mod) => { - // Include modifiers with defined value, OR redeclare modifiers - if (mod?.value !== undefined || mod?.redeclare) { + // Include modifiers with a defined value, a redeclare, or a `final` + // flag (a `final` redeclare synthesizes unbound entries for every + // parameter of the redeclared type; those must survive with no value + // so downstream still sees them as locked) + if (mod?.value !== undefined || mod?.redeclare || mod?.final) { mods[mod.modelicaPath] = { expression: mod.value, final: mod.final, diff --git a/server/tests/integration/parser/modifiers.test.ts b/server/tests/integration/parser/modifiers.test.ts index 7ca7e881..56a451f0 100644 --- a/server/tests/integration/parser/modifiers.test.ts +++ b/server/tests/integration/parser/modifiers.test.ts @@ -124,6 +124,41 @@ describe("Modifications", () => { expect(mod.expression).toBeUndefined(); }); + /** + * Regression test: `redeclare final Type comp(field=value)` locks `comp` + * to its redeclared type, so nothing downstream can override `field` + * either — it must be treated as final even though only the redeclare + * itself (not `field`) carries the `final` keyword in the Modelica source. + */ + it("Propagates 'final' from a redeclare onto its own inline bindings", () => { + const path = "TestPackage.Template.TestTemplate.redeclare_param_01"; + const nestedModPath = `${path}.replaceable_param.component_param`; + const option = tOptions[path]; + const mod = option.modifiers[nestedModPath]; + + expect(mod).toBeDefined(); + expect(mod.expression).toEqual('"From final redeclare"'); + expect(mod.final).toBeTruthy(); + }); + + /** + * Regression test: `redeclare final Type comp(...)` must lock down every + * parameter of `Type`, not just the ones bound inline in the redeclare + * clause — `is_another_param` has no binding in the redeclare clause but + * still belongs to the redeclared `SecondComponent` type, so it must be + * final too. + */ + it("Propagates 'final' from a redeclare onto every parameter of the redeclared type", () => { + const path = "TestPackage.Template.TestTemplate.redeclare_param_01"; + const unboundModPath = `${path}.replaceable_param.is_another_param`; + const option = tOptions[path]; + const mod = option.modifiers[unboundModPath]; + + expect(mod).toBeDefined(); + expect(mod.final).toBeTruthy(); + expect(mod.expression).toBeUndefined(); + }); + /** * Regression test for https://github.com/lbl-srg/ctrl-flow-dev/issues/418 * Element-level modifiers on a replaceable must be captured alongside diff --git a/server/tests/static-data/TestPackage/Template/TestTemplate.mo b/server/tests/static-data/TestPackage/Template/TestTemplate.mo index 9cdf7196..53a5aeab 100644 --- a/server/tests/static-data/TestPackage/Template/TestTemplate.mo +++ b/server/tests/static-data/TestPackage/Template/TestTemplate.mo @@ -174,7 +174,8 @@ model TestTemplate "Test Template" // redclare modifier params TestPackage.Component.FourthComponent redeclare_param_01( - redeclare final TestPackage.Component.SecondComponent replaceable_param + redeclare final TestPackage.Component.SecondComponent replaceable_param( + component_param="From final redeclare") ) "First Param to test component redeclares" annotation(Dialog(enable=true));