Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,208 changes: 2,499 additions & 709 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
4 changes: 4 additions & 0 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,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}
2 changes: 1 addition & 1 deletion server/bin/install-modelica-dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -x

MODELICA_BUILDINGS_COMMIT=b399379315641da39b231033b0660100fd6489a5
MODELICA_JSON_COMMIT=a46a361c3047c0a2b3d1cfc9bc8b0a4ced16006a
MODELICA_JSON_COMMIT=71b45ba6af2a7534c9d62abecf79e62240671e38

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

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
4 changes: 2 additions & 2 deletions server/src/parser/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function getClassNameFromRelativePath(filePath: string) {
export function getClassNameFromJson(json: any): string {
return (
(json.within ? json.within + "." : "") +
json.class_definition[0].class_specifier.long_class_specifier.identifier
json.stored_class_definitions[0].class_specifier.long_class_specifier.identifier
);
}

Expand All @@ -57,7 +57,7 @@ export function getClassNameFromJson(json: any): string {
* @returns A TemplateNode object with class information
*/
function createTemplateNode(json: any): TemplateNode {
const classDefinition = json.class_definition[0];
const classDefinition = json.stored_class_definitions[0];
return {
className: getClassNameFromJson(json),
description:
Expand Down
Loading