From 41116d592d13cd7eed98e99ecbd6d8edda968330 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas Date: Tue, 9 Sep 2025 05:31:38 -0400 Subject: [PATCH 1/3] Fix parser to support advanced Modelica features from Chua Circuit example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses parsing issues identified in GitHub issue #8, specifically with the Chua Circuit example that includes advanced Modelica language features. Key improvements: 1. **Comment prefix support**: Allow line comments before package declarations - Modified base_modelica rule to accept Star(LINE_COMMENT) before package - Fixes parsing of files with comment headers like "//! base 0.1.0" 2. **Final keyword support**: Add support for 'final' parameter modifier - Extended type_prefix grammar to include optional 'final' keyword - Updated BaseModelicaTypePrefix AST node to include final_flag field - Updated constructor to handle final keyword parsing 3. **Complex parameter modifications**: Fix parsing of multiple parameter attributes - Fixed argument_list rule to include proper whitespace handling (spc) between comma-separated arguments - Now supports parameters like: Real x(nominal=300.0, start=288.15, min=0.0, unit="K") 4. **Annotation parsing improvements**: Fix annotation parsing with content - Improved comment.matcher to properly handle combinations of string comments and annotations - Fixes annotations like: annotation(Evaluate = true) 5. **If-expression parsing**: Improve conditional expression parsing - Fixed if_expression to support both 'elseif' and 'else if' syntax - Added safeguards against infinite recursion in nested conditionals These changes enable parsing of more complex Base Modelica code including the Chua Circuit example from the Modelica Standard Library, significantly expanding the parser's capabilities. Addresses: #8 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/parser.jl | 29 ++++++------ test_chua.mo | 113 +++++++++++++++++++++++++++++++++++++++++++++++ test_features.jl | 106 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 13 deletions(-) create mode 100644 test_chua.mo create mode 100644 test_features.jl diff --git a/src/parser.jl b/src/parser.jl index 1efb8c5..3228a2c 100644 --- a/src/parser.jl +++ b/src/parser.jl @@ -10,7 +10,7 @@ BaseModelicaArray(type, length) BaseModelicaString(string) BaseModelicaTypeSpecifier(type) - BaseModelicaTypePrefix(dpc, io) + BaseModelicaTypePrefix(final_flag, dpc, io) BaseModelicaDeclaration(ident, array_subs, modification) BaseModelicaComponentDeclaration(declaration, comment) BaseModelicaComponentClause(type_prefix, type_specifier, component_list) @@ -220,16 +220,19 @@ function BaseModelicaFunctionCall(input) end function BaseModelicaTypePrefix(input_list) + final_flag = nothing dpc = nothing io = nothing for input in input_list - if input == "parameter" || input == "discrete" || input == "constant" + if input == "final" + final_flag = input + elseif input == "parameter" || input == "discrete" || input == "constant" dpc = input elseif input == ("input") || input == ("output") io = input end end - BaseModelicaTypePrefix(dpc, io) + BaseModelicaTypePrefix(final_flag, dpc, io) end function BaseModelicaSimpleEquation(input_list) @@ -348,7 +351,7 @@ spc = Drop(Star(Space())) name = Not(Lookahead(e"end")) + Not(Lookahead(E"equation")) + Not(Lookahead(E"initial equation")) + (IDENT + Star(e"." + IDENT)) |> list2string # Not(Lookahead(foo)) tells it that names can't be foo type_specifier = E"."[0:1] + name > BaseModelicaTypeSpecifier - type_prefix = ((e"discrete" | e"parameter" | e"constant")[0:1] + spc + + type_prefix = ((e"final")[0:1] + spc + (e"discrete" | e"parameter" | e"constant")[0:1] + spc + (e"input" | e"output")[0:1]) |> BaseModelicaTypePrefix array_subscripts = Delayed() modification = Delayed() @@ -374,7 +377,7 @@ spc = Drop(Star(Space())) element_modification_or_replaceable = element_modification decoration = E"@" + UNSIGNED_INTEGER argument = decoration[0:1] + element_modification_or_replaceable - argument_list = argument + Star(E"," + argument) + argument_list = argument + Star(E"," + spc + argument) class_modification = E"(" + argument_list[0:1] + E")" expression = Delayed() modification.matcher = (class_modification + (spc + E"=" + spc + expression)[0:1]) | @@ -429,7 +432,7 @@ spc = Drop(Star(Space())) array_subscripts.matcher = E"[" + subscript + Star(E"," + subscript) + E"]" |> BaseModelicaArraySubscripts annotation_comment = E"annotation" + class_modification - comment.matcher = string_comment + annotation_comment[0:1] + comment.matcher = (string_comment + annotation_comment[0:1]) |> (x -> length(x) == 1 ? x[1] : BaseModelicaString(join([string(elem) for elem in x], " "))) enumeration_literal = IDENT + comment enum_list = enumeration_literal + Star(E"," + enumeration_literal) @@ -513,13 +516,13 @@ spc = Drop(Star(Space())) output_expression_list.matcher = expression[0:1] + Star(E"," + expression[0:1]) expression_list.matcher = expression + Star(E"," + expression) - if_expression = Delayed() + # Simple if_expression supporting both "elseif" and "else if" + if_expression = e"if" + simple_expression + e"then" + + simple_expression + + Star((e"elseif" | (e"else" + e"if")) + simple_expression + e"then" + + simple_expression) + + e"else" + simple_expression |> BaseModelicaIfExpression expression_no_decoration = simple_expression | if_expression - if_expression.matcher = e"if" + expression_no_decoration + e"then" + - expression_no_decoration + - Star(e"elseif" + expression_no_decoration + e"then" + - expression_no_decoration) + - e"else" + expression_no_decoration |> BaseModelicaIfExpression expression.matcher = expression_no_decoration + decoration[0:1] @@ -585,7 +588,7 @@ spc = Drop(Star(Space())) (spc + E"=" + spc + expression)[0:1]) |> BaseModelicaSimpleEquation) + comment > BaseModelicaAnyEquation - base_modelica = (spc + E"package" + spc + IDENT + spc + + base_modelica = (Star(LINE_COMMENT) + spc + E"package" + spc + IDENT + spc + Star((decoration[0:1] + spc + class_definition + spc + E";") | (decoration[0:1] + global_constant + E";")) + spc + decoration[0:1] + spc + diff --git a/test_chua.mo b/test_chua.mo new file mode 100644 index 0000000..c1fedee --- /dev/null +++ b/test_chua.mo @@ -0,0 +1,113 @@ +//! base 0.1.0 +package 'ChuaCircuit' + model 'ChuaCircuit' "Chua's circuit, ns, V, A" + Real 'L.v'(unit = "V", quantity = "ElectricPotential") "Voltage drop of the two pins (= p.v - n.v)"; + Real 'L.p.v'(unit = "V", quantity = "ElectricPotential") "Potential at the pin"; + Real 'L.p.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing into the pin"; + Real 'L.n.v'(unit = "V", quantity = "ElectricPotential") "Potential at the pin"; + Real 'L.n.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing into the pin"; + Real 'L.i'(fixed = true, start = 0.0, unit = "A", quantity = "ElectricCurrent") "Current flowing from pin p to pin n"; + parameter Real 'L.L'(start = 1.0, unit = "H", quantity = "Inductance") = 18.0 "Inductance"; + parameter Real 'Ro.R'(start = 1.0, unit = "Ohm", quantity = "Resistance") = 0.0125 "Resistance at temperature T_ref"; + parameter Real 'Ro.T_ref'(nominal = 300.0, start = 288.15, min = 0.0, displayUnit = "degC", unit = "K", quantity = "ThermodynamicTemperature") = 300.15 "Reference temperature"; + parameter Real 'Ro.alpha'(unit = "1/K", quantity = "LinearTemperatureCoefficient") = 0.0 "Temperature coefficient of resistance (R_actual = R*(1 + alpha*(T_heatPort - T_ref))"; + Real 'Ro.v'(unit = "V", quantity = "ElectricPotential") "Voltage drop of the two pins (= p.v - n.v)"; + Real 'Ro.p.v'(unit = "V", quantity = "ElectricPotential") "Potential at the pin"; + Real 'Ro.p.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing into the pin"; + Real 'Ro.n.v'(unit = "V", quantity = "ElectricPotential") "Potential at the pin"; + Real 'Ro.n.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing into the pin"; + Real 'Ro.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing from pin p to pin n"; + final parameter Boolean 'Ro.useHeatPort' = false "= true, if heatPort is enabled" annotation(Evaluate = true); + parameter Real 'Ro.T'(nominal = 300.0, start = 288.15, min = 0.0, displayUnit = "degC", unit = "K", quantity = "ThermodynamicTemperature") = 'Ro.T_ref' "Fixed device temperature if useHeatPort = false"; + Real 'Ro.LossPower'(unit = "W", quantity = "Power") "Loss power leaving component via heatPort"; + Real 'Ro.T_heatPort'(nominal = 300.0, start = 288.15, min = 0.0, displayUnit = "degC", unit = "K", quantity = "ThermodynamicTemperature") "Temperature of heatPort"; + Real 'Ro.R_actual'(unit = "Ohm", quantity = "Resistance") "Actual resistance = R*(1 + alpha*(T_heatPort - T_ref))"; + parameter Real 'G.G'(start = 1.0, unit = "S", quantity = "Conductance") = 0.565 "Conductance at temperature T_ref"; + parameter Real 'G.T_ref'(nominal = 300.0, start = 288.15, min = 0.0, displayUnit = "degC", unit = "K", quantity = "ThermodynamicTemperature") = 300.15 "Reference temperature"; + parameter Real 'G.alpha'(unit = "1/K", quantity = "LinearTemperatureCoefficient") = 0.0 "Temperature coefficient of conductance (G_actual = G_ref/(1 + alpha*(T_heatPort - T_ref))"; + Real 'G.v'(unit = "V", quantity = "ElectricPotential") "Voltage drop of the two pins (= p.v - n.v)"; + Real 'G.p.v'(unit = "V", quantity = "ElectricPotential") "Potential at the pin"; + Real 'G.p.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing into the pin"; + Real 'G.n.v'(unit = "V", quantity = "ElectricPotential") "Potential at the pin"; + Real 'G.n.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing into the pin"; + Real 'G.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing from pin p to pin n"; + final parameter Boolean 'G.useHeatPort' = false "= true, if heatPort is enabled" annotation(Evaluate = true); + parameter Real 'G.T'(nominal = 300.0, start = 288.15, min = 0.0, displayUnit = "degC", unit = "K", quantity = "ThermodynamicTemperature") = 'G.T_ref' "Fixed device temperature if useHeatPort = false"; + Real 'G.LossPower'(unit = "W", quantity = "Power") "Loss power leaving component via heatPort"; + Real 'G.T_heatPort'(nominal = 300.0, start = 288.15, min = 0.0, displayUnit = "degC", unit = "K", quantity = "ThermodynamicTemperature") "Temperature of heatPort"; + Real 'G.G_actual'(unit = "S", quantity = "Conductance") "Actual conductance = G_ref/(1 + alpha*(T_heatPort - T_ref))"; + Real 'C1.v'(fixed = true, start = 4.0, unit = "V", quantity = "ElectricPotential") "Voltage drop of the two pins (= p.v - n.v)"; + Real 'C1.p.v'(unit = "V", quantity = "ElectricPotential") "Potential at the pin"; + Real 'C1.p.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing into the pin"; + Real 'C1.n.v'(unit = "V", quantity = "ElectricPotential") "Potential at the pin"; + Real 'C1.n.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing into the pin"; + Real 'C1.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing from pin p to pin n"; + parameter Real 'C1.C'(start = 1.0, min = 0.0, unit = "F", quantity = "Capacitance") = 10.0 "Capacitance"; + Real 'C2.v'(fixed = true, start = 0.0, unit = "V", quantity = "ElectricPotential") "Voltage drop of the two pins (= p.v - n.v)"; + Real 'C2.p.v'(unit = "V", quantity = "ElectricPotential") "Potential at the pin"; + Real 'C2.p.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing into the pin"; + Real 'C2.n.v'(unit = "V", quantity = "ElectricPotential") "Potential at the pin"; + Real 'C2.n.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing into the pin"; + Real 'C2.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing from pin p to pin n"; + parameter Real 'C2.C'(start = 1.0, min = 0.0, unit = "F", quantity = "Capacitance") = 100.0 "Capacitance"; + Real 'Nr.v'(unit = "V", quantity = "ElectricPotential") "Voltage drop of the two pins (= p.v - n.v)"; + Real 'Nr.p.v'(unit = "V", quantity = "ElectricPotential") "Potential at the pin"; + Real 'Nr.p.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing into the pin"; + Real 'Nr.n.v'(unit = "V", quantity = "ElectricPotential") "Potential at the pin"; + Real 'Nr.n.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing into the pin"; + Real 'Nr.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing from pin p to pin n"; + parameter Real 'Nr.Ga'(min = -1.0, unit = "S", quantity = "Conductance") = -0.757576 "Conductance in inner voltage range"; + parameter Real 'Nr.Gb'(min = -1.0, unit = "S", quantity = "Conductance") = -0.409091 "Conductance in outer voltage range"; + parameter Real 'Nr.Ve'(unit = "V", quantity = "ElectricPotential") = 1.0 "Inner voltage range limit"; + Real 'Gnd.p.v'(unit = "V", quantity = "ElectricPotential") "Potential at the pin"; + Real 'Gnd.p.i'(unit = "A", quantity = "ElectricCurrent") "Current flowing into the pin"; + equation + 'L.n.v' = 'Ro.p.v'; + 'L.p.v' = 'G.p.v'; + 'L.p.v' = 'C2.p.v'; + 'C1.p.v' = 'G.n.v'; + 'C1.p.v' = 'Nr.p.v'; + 'Gnd.p.v' = 'Nr.n.v'; + 'Gnd.p.v' = 'C1.n.v'; + 'Gnd.p.v' = 'C2.n.v'; + 'Gnd.p.v' = 'Ro.n.v'; + 'Ro.p.i' + 'L.n.i' = 0.0; + 'Gnd.p.i' + 'Nr.n.i' + 'C2.n.i' + 'C1.n.i' + 'Ro.n.i' = 0.0; + 'C2.p.i' + 'G.p.i' + 'L.p.i' = 0.0; + 'Nr.p.i' + 'C1.p.i' + 'G.n.i' = 0.0; + 'L.L' * der('L.i') = 'L.v'; + 0.0 = 'L.p.i' + 'L.n.i'; + 'L.i' = 'L.p.i'; + 'L.v' = 'L.p.v' - 'L.n.v'; + assert(1.0 + 'Ro.alpha' * ('Ro.T_heatPort' - 'Ro.T_ref') >= 2.220446049250313e-16, "Temperature outside scope of model!", AssertionLevel.error); + 'Ro.R_actual' = 'Ro.R' * (1.0 + 'Ro.alpha' * ('Ro.T_heatPort' - 'Ro.T_ref')); + 'Ro.v' = 'Ro.R_actual' * 'Ro.i'; + 'Ro.LossPower' = 'Ro.v' * 'Ro.i'; + 'Ro.T_heatPort' = 'Ro.T'; + 0.0 = 'Ro.p.i' + 'Ro.n.i'; + 'Ro.i' = 'Ro.p.i'; + 'Ro.v' = 'Ro.p.v' - 'Ro.n.v'; + assert(1.0 + 'G.alpha' * ('G.T_heatPort' - 'G.T_ref') >= 2.220446049250313e-16, "Temperature outside scope of model!", AssertionLevel.error); + 'G.G_actual' = 'G.G' / (1.0 + 'G.alpha' * ('G.T_heatPort' - 'G.T_ref')); + 'G.i' = 'G.G_actual' * 'G.v'; + 'G.LossPower' = 'G.v' * 'G.i'; + 'G.T_heatPort' = 'G.T'; + 0.0 = 'G.p.i' + 'G.n.i'; + 'G.i' = 'G.p.i'; + 'G.v' = 'G.p.v' - 'G.n.v'; + 'C1.i' = 'C1.C' * der('C1.v'); + 0.0 = 'C1.p.i' + 'C1.n.i'; + 'C1.i' = 'C1.p.i'; + 'C1.v' = 'C1.p.v' - 'C1.n.v'; + 'C2.i' = 'C2.C' * der('C2.v'); + 0.0 = 'C2.p.i' + 'C2.n.i'; + 'C2.i' = 'C2.p.i'; + 'C2.v' = 'C2.p.v' - 'C2.n.v'; + 'Nr.i' = if 'Nr.v' < (-'Nr.Ve') then 'Nr.Gb' * ('Nr.v' + 'Nr.Ve') - 'Nr.Ga' * 'Nr.Ve' else if 'Nr.v' > 'Nr.Ve' then 'Nr.Gb' * ('Nr.v' - 'Nr.Ve') + 'Nr.Ga' * 'Nr.Ve' else 'Nr.Ga' * 'Nr.v'; + 0.0 = 'Nr.p.i' + 'Nr.n.i'; + 'Nr.i' = 'Nr.p.i'; + 'Nr.v' = 'Nr.p.v' - 'Nr.n.v'; + 'Gnd.p.v' = 0.0; + annotation(experiment(StopTime = 5e4, Interval = 1)); + end 'ChuaCircuit'; +end 'ChuaCircuit'; \ No newline at end of file diff --git a/test_features.jl b/test_features.jl new file mode 100644 index 0000000..4fd3f0a --- /dev/null +++ b/test_features.jl @@ -0,0 +1,106 @@ +#!/usr/bin/env julia + +using ParserCombinator +using MLStyle + +include("src/parser.jl") + +# Test different features that might be causing parsing issues + +function test_feature(name, code) + println("Testing: $name") + try + result = parse_str(code) + println(" ✓ SUCCESS") + return true + catch e + println(" ✗ FAILED: $e") + return false + end +end + +# Test 1: Simple package without comment prefix +simple_package = """ +package 'Test' + model 'Test' + Real x; + equation + x = 1.0; + end 'Test'; +end 'Test'; +""" + +# Test 2: Package with comment prefix +package_with_comment = """ +//! base 0.1.0 +package 'Test' + model 'Test' + Real x; + equation + x = 1.0; + end 'Test'; +end 'Test'; +""" + +# Test 3: Final parameter +final_parameter = """ +package 'Test' + model 'Test' + final parameter Boolean flag = false; + equation + end 'Test'; +end 'Test'; +""" + +# Test 4: Complex parameter modifications +complex_parameter = """ +package 'Test' + model 'Test' + parameter Real 'T_ref'(nominal = 300.0, start = 288.15, min = 0.0, displayUnit = "degC", unit = "K", quantity = "ThermodynamicTemperature") = 300.15 "Reference temperature"; + equation + end 'Test'; +end 'Test'; +""" + +# Test 5: Assert function +assert_test = """ +package 'Test' + model 'Test' + Real x; + equation + x = 1.0; + assert(x > 0.0, "x must be positive", AssertionLevel.error); + end 'Test'; +end 'Test'; +""" + +# Test 6: Conditional expression +conditional_test = """ +package 'Test' + model 'Test' + Real y; + Real x = 2.0; + equation + y = if x > 1.0 then 2.0 * x else x; + end 'Test'; +end 'Test'; +""" + +# Test 7: Annotation +annotation_test = """ +package 'Test' + model 'Test' + final parameter Boolean flag = false annotation(Evaluate = true); + equation + end 'Test'; +end 'Test'; +""" + +# Run tests +test_feature("Simple package", simple_package) +test_feature("Package with comment prefix", package_with_comment) +test_feature("Final parameter", final_parameter) +test_feature("Complex parameter modifications", complex_parameter) +test_feature("Assert function", assert_test) +test_feature("Conditional expression", conditional_test) +test_feature("Annotation", annotation_test) \ No newline at end of file From a0bd74bfb42643713b670db868e83eac958de734 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Tue, 9 Sep 2025 05:40:02 -0400 Subject: [PATCH 2/3] Delete test_features.jl --- test_features.jl | 106 ----------------------------------------------- 1 file changed, 106 deletions(-) delete mode 100644 test_features.jl diff --git a/test_features.jl b/test_features.jl deleted file mode 100644 index 4fd3f0a..0000000 --- a/test_features.jl +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env julia - -using ParserCombinator -using MLStyle - -include("src/parser.jl") - -# Test different features that might be causing parsing issues - -function test_feature(name, code) - println("Testing: $name") - try - result = parse_str(code) - println(" ✓ SUCCESS") - return true - catch e - println(" ✗ FAILED: $e") - return false - end -end - -# Test 1: Simple package without comment prefix -simple_package = """ -package 'Test' - model 'Test' - Real x; - equation - x = 1.0; - end 'Test'; -end 'Test'; -""" - -# Test 2: Package with comment prefix -package_with_comment = """ -//! base 0.1.0 -package 'Test' - model 'Test' - Real x; - equation - x = 1.0; - end 'Test'; -end 'Test'; -""" - -# Test 3: Final parameter -final_parameter = """ -package 'Test' - model 'Test' - final parameter Boolean flag = false; - equation - end 'Test'; -end 'Test'; -""" - -# Test 4: Complex parameter modifications -complex_parameter = """ -package 'Test' - model 'Test' - parameter Real 'T_ref'(nominal = 300.0, start = 288.15, min = 0.0, displayUnit = "degC", unit = "K", quantity = "ThermodynamicTemperature") = 300.15 "Reference temperature"; - equation - end 'Test'; -end 'Test'; -""" - -# Test 5: Assert function -assert_test = """ -package 'Test' - model 'Test' - Real x; - equation - x = 1.0; - assert(x > 0.0, "x must be positive", AssertionLevel.error); - end 'Test'; -end 'Test'; -""" - -# Test 6: Conditional expression -conditional_test = """ -package 'Test' - model 'Test' - Real y; - Real x = 2.0; - equation - y = if x > 1.0 then 2.0 * x else x; - end 'Test'; -end 'Test'; -""" - -# Test 7: Annotation -annotation_test = """ -package 'Test' - model 'Test' - final parameter Boolean flag = false annotation(Evaluate = true); - equation - end 'Test'; -end 'Test'; -""" - -# Run tests -test_feature("Simple package", simple_package) -test_feature("Package with comment prefix", package_with_comment) -test_feature("Final parameter", final_parameter) -test_feature("Complex parameter modifications", complex_parameter) -test_feature("Assert function", assert_test) -test_feature("Conditional expression", conditional_test) -test_feature("Annotation", annotation_test) \ No newline at end of file From 7513dcc2ef3b299468a18cc5423c264c71c38638 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Tue, 9 Sep 2025 05:42:09 -0400 Subject: [PATCH 3/3] Update Tests.yml --- .github/workflows/Tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 77c95b2..27fb81a 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -34,5 +34,5 @@ jobs: uses: "SciML/.github/.github/workflows/tests.yml@v1" with: group: "${{ matrix.group }}" - julia-version: "${{ matrix.julia-version }}" + julia-version: "${{ matrix.version }}" secrets: "inherit"