Skip to content
Merged
3,202 changes: 2,497 additions & 705 deletions client/src/data/templates.json

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions client/src/interpreter/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ export function resolvePaths(
type Comparator = ">" | ">=" | "<" | "<=";
export type OperatorType =
| "none"
| "!"
| "=="
| "!="
| "&&"
Expand Down Expand Up @@ -433,6 +434,17 @@ export const evaluate = (
val = expression.operator.includes("!") ? !isEqual : isEqual;
break;
}
case "!": {
if (expression.operands.length !== 1) {
throw new Error("Invalid number of operands for ! operator");
}
const operand = expression.operands[0];
const resolvedOperand = isExpression(operand)
? evaluate(operand, context, scope)
: resolveToValue(operand, context, scope);
val = !resolvedOperand;
break;
}
case "||": {
val = expression.operands.reduce(
(acc, cur) => !!(evaluate(cur, context, scope) || acc),
Expand Down
5 changes: 5 additions & 0 deletions client/tests/interpreter/interpreter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ describe("Test set", () => {
expect(evaluate(buildExpression("<=", [5, 4]))).toBeFalsy();
});

it("Handles !", () => {
expect(evaluate(buildExpression("!", [true]))).toBeFalsy();
expect(evaluate(buildExpression("!", [buildExpression(">=", [2, 3])]))).toBeTruthy();
});

it("Handles == and !=", () => {
expect(evaluate(buildExpression("==", [1, 1, 1, 1]))).toBeTruthy();
expect(evaluate(buildExpression("==", [3]))).toBeTruthy();
Expand Down
9 changes: 4 additions & 5 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,8 @@ RUN npm run install-python-deps
RUN apt-get -y install make curl git
RUN npm run install-modelica-deps
WORKDIR /dependencies
# These files have typos that need to be fixed before we can bundle with webpack
RUN sed -i 's/condition_attribute/conditionAttribute/g' ./modelica-json/json2mo/componentDeclaration.js
RUN sed -i 's/for_indices/forIndices/g' ./modelica-json/json2mo/forStatement.js
RUN sed -i 's/named_arguments/namedArguments/g' ./modelica-json/json2mo/functionArgument.js
ENV MODELICAPATH=/dependencies/ModelicaStandardLibrary:/dependencies/modelica-buildings
RUN mkdir template-json
# This fails to run on amd64, must be arm64 platform
RUN node modelica-json/app.js -f modelica-buildings/Buildings/Templates -o json -d template-json

# Adds Modelica dependencies to the resulting image
Expand All @@ -57,6 +52,10 @@ RUN npx webpack

ARG PORT

# Needed to keep rdflib (modelica-json dep) out of project's dependencies
# but Node still requires it at runtime for some reason.
ENV NODE_PATH=/dependencies/modelica-json/node_modules

# For when image is run
CMD ["node", "dist/index.js"]
EXPOSE ${PORT}
5 changes: 3 additions & 2 deletions server/bin/install-modelica-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
set -x

MODELICA_BUILDINGS_COMMIT=b399379315641da39b231033b0660100fd6489a5
MODELICA_JSON_COMMIT=a46a361c3047c0a2b3d1cfc9bc8b0a4ced16006a
MODELICA_STANDARD_TAG=v4.0.0 # This should be driven by 'uses' annotation from Buildings/package.mo
MODELICA_JSON_COMMIT=b715c09d3092192779e8eccd80c813f08ea1a8e6

parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )

Expand All @@ -19,7 +20,7 @@ cd modelica-json
git checkout $MODELICA_JSON_COMMIT
cd -

git clone -b v3.2.3+build.4 --single-branch --depth 1 https://github.com/modelica/ModelicaStandardLibrary.git
git clone -b $MODELICA_STANDARD_TAG --single-branch --depth 1 https://github.com/modelica/ModelicaStandardLibrary.git

cd modelica-json
git apply $parent_path/bin/maven-install.patch
Expand Down
1 change: 0 additions & 1 deletion server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ app.post("/api/modelicatojson", async (req, res) => {
// To get around this read from the file that gets output during parsing
parser.getJsons(
[modelicaFile.name],
parseMode,
format,
tempDirPath,
prettyPrint,
Expand Down
271 changes: 240 additions & 31 deletions server/src/parser/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,47 @@ export type Expression = {
operands: Array<Literal | Expression>;
};

function expandStringOperand(
operand: string,
basePath: string,
baseType: string,
): Literal | Expression {
let myoperand = operand;
try {
myoperand = JSON.parse(operand as string);
} catch {
/** deserialization failed */
}
/*
* After try and catch above:
* "Buildings.Type" → "Buildings.Type" (deserialization failed)
* "\"String literal\"" → "String literal"
* "false" → false
* Only attempt to expand as a type if still a string, and original operand not literal
*/
if (typeof myoperand === "string" && !/^".*"$/.test(operand)) {
const element =
typeStore.get(myoperand, basePath) || typeStore.get(myoperand, baseType);
myoperand = element ? element.modelicaPath : myoperand;
}
return myoperand;
}

function buildArithmeticExpression(
expression: any,
operator: any,
basePath: string,
baseType: string,
): Expression {
// TODO: attempt to expand operands as types
const arithmetic_expression: Expression = {
operator: operator === "<>" ? "!=" : operator,
operands: [expression[0].name, expression[1].name],
operands: expression.map((operand: any) =>
typeof operand === "string"
? expandStringOperand(operand, basePath, baseType)
: getExpression(operand, basePath, baseType),
),
};

// arithmetic_expression.operands = arithmetic_expression.operands.map((o, i) => {
// // if (typeof o === "string") {
// // // left hand side of expression is most likely a variable reference - favor basePath first
// // const element = (i === 0) ? typeStore.get(o, basePath) || typeStore.get(o, baseType)
// // : typeStore.get(o, baseType) || typeStore.get(o, basePath);
// // return (element) ? element.modelicaPath : o;
// // }
// return o;
// });

return arithmetic_expression;
}

Expand All @@ -54,12 +73,33 @@ function buildLogicalExpression(
};

if (expression.arithmetic_expressions) {
return buildArithmeticExpression(
expression.arithmetic_expressions,
expression.relation_operator,
basePath,
baseType,
);
let result: Expression;
// Single arithmetic_expression without relation_operator is a boolean reference
if (
expression.arithmetic_expressions.length === 1 &&
!expression.relation_operator
) {
result = buildSimpleExpression(
expression.arithmetic_expressions[0],
basePath,
baseType,
);
} else {
result = buildArithmeticExpression(
expression.arithmetic_expressions,
expression.relation_operator,
basePath,
baseType,
);
}
// Include ! operator if "not": true
if (expression.not) {
return {
operator: "!",
operands: [result],
};
}
return result;
}

if (expression.logical_or?.length === 1) {
Expand Down Expand Up @@ -221,27 +261,196 @@ function buildIfExpression(
return if_expression;
}

function buildTermExpression(
term: any,
basePath: string,
baseType: string,
): Expression | Literal {
// A term can be a string, or an object with operators and factors
if (typeof term === "string") {
return expandStringOperand(term, basePath, baseType);
}

if (typeof term === "object" && term.factors) {
// term: { operators: ["*", "/"], factors: [...] }
// operators may be absent if there's only one factor
// Build a nested expression for multiplication/division
const factors = term.factors.map((factor: any) =>
buildFactorExpression(factor, basePath, baseType),
);

if (factors.length === 1) {
return factors[0];
}

// Build left-associative expression: ((a * b) / c)
// Per grammar: term = factor { mul-operator factor }
// So operators.length === factors.length - 1
const operators: string[] = term.operators || [];
let result = factors[0];
for (let i = 0; i < operators.length; i++) {
result = {
operator: operators[i],
operands: [result, factors[i + 1]],
};
}
return result;
}

// Fallback: treat as simple expression
return buildSimpleExpression(term, basePath, baseType);
}

function buildPrimaryExpression(
primary: any,
basePath: string,
baseType: string,
): Expression | Literal {
// primary can be a string or an array of expression objects
// expression: { simple_expression?: ..., if_expression?: ... }
if (typeof primary === "string") {
return expandStringOperand(primary, basePath, baseType);
}

if (Array.isArray(primary)) {
// Array of expression objects
const expressions = primary.map((expr: any) => {
// Each expr is an expression object with simple_expression and/or if_expression
// Use getExpression which handles both cases
return getExpression(expr, basePath, baseType);
});

if (expressions.length === 1) {
if (expressions[0].operator === "none") {
// We avoid the additional nesting level
// { operator: 'none', operands: ['string'] }
return expressions[0].operands[0];
} else {
return expressions[0];
}
}

// Multiple expressions - return as array expression
// This construct is not well understood, probably used to cover the case
// PRIMARY = "[" expression-list { ";" expression-list } "]" from the grammar
// This is a noop in the client interpreter.
return {
operator: "primary_array",
operands: expressions,
};
}

// Single object - use getExpression to handle simple_expression or if_expression
return getExpression(primary, basePath, baseType);
}

function buildFactorExpression(
factor: any,
basePath: string,
baseType: string,
): Expression | Literal {
// A factor can be:
// - a string
// - an object with { primary1, operator?, primary2? } for exponentiation
if (typeof factor === "string") {
return expandStringOperand(factor, basePath, baseType);
}

if (typeof factor === "object" && factor.primary1 !== undefined) {
const primary1Expr = buildPrimaryExpression(
factor.primary1,
basePath,
baseType,
);

// Check for exponentiation: { primary1, operator: "^" or ".^", primary2 }
if (factor.operator && factor.primary2 !== undefined) {
const primary2Expr = buildPrimaryExpression(
factor.primary2,
basePath,
baseType,
);
return {
operator: factor.operator, // "^" or ".^"
operands: [primary1Expr, primary2Expr],
};
}

return primary1Expr;
}

// Fallback
return buildSimpleExpression(factor, basePath, baseType);
}

function buildSimpleExpression(
expression: any,
basePath: string,
baseType: string,
): Expression {
let operand = expression;

if (typeof expression === "object")
console.log("Unknown Expression: ", expression);
if (typeof expression === "string") {
try {
operand = JSON.parse(expression as string);
} catch {
/** deserialization failed */
}
if (typeof operand === "string") {
// Attempt to expand operand as a type
const element =
typeStore.get(operand, basePath) || typeStore.get(operand, baseType); // TODO: may only need to check basePath
operand = element ? element.modelicaPath : operand;
// Handle object-type simple_expression with terms and addOps
if (typeof expression === "object" && expression !== null) {
if (expression.terms) {
// simple_expression: { addOps: ["+", "-"], terms: [...] }
const terms = expression.terms.map((term: any) =>
buildTermExpression(term, basePath, baseType),
);

if (terms.length === 1 && !expression.addOps) {
return terms[0];
}

const addOps: string[] = expression.addOps || [];
// Determine if there's a leading unary operator
// If addOps.length === terms.length, the first addOp is a leading unary operator
const hasLeadingOp = addOps.length === terms.length;

if (terms.length === 1 && hasLeadingOp) {
// Single term with unary operator (e.g., "-x")
return {
operator: addOps[0],
operands: [terms[0]],
};
}

// Build left-associative expression for addition/subtraction
let result: Expression;
let opIndex = 0;

if (hasLeadingOp) {
// First operator is unary
if (addOps[0] === "-") {
result = {
operator: "-",
operands: [terms[0]],
};
} else {
result = terms[0];
}
opIndex = 1;
} else {
result = terms[0];
}

for (let i = 1; i < terms.length; i++) {
result = {
operator: addOps[opIndex],
operands: [result, terms[i]],
};
opIndex++;
}

return result;
}

// Unknown object structure - log for debugging
console.log("Unknown Expression: ", JSON.stringify(expression, null, 2));
}

if (typeof expression === "string") {
operand = expandStringOperand(expression, basePath, baseType);
}

const simple_expression: Expression = {
Expand Down
Loading