From 42e7c37bdb3641f205a05c19288bbad21ff99d12 Mon Sep 17 00:00:00 2001
From: Manolis Dermitzakis
Date: Tue, 17 Feb 2026 15:31:13 +0200
Subject: [PATCH 01/21] feat: add ArcscriptVariable class
---
TypeScript/src/ArcscriptFunctions.ts | 4 +-
TypeScript/src/ArcscriptState.ts | 69 +++++++++++++------
TypeScript/src/ArcscriptVariable.ts | 56 +++++++++++++++
.../src/Generated/ArcscriptParserBase.ts | 35 +++++-----
TypeScript/src/antlr4.interpreter.ts | 6 +-
5 files changed, 125 insertions(+), 45 deletions(-)
create mode 100644 TypeScript/src/ArcscriptVariable.ts
diff --git a/TypeScript/src/ArcscriptFunctions.ts b/TypeScript/src/ArcscriptFunctions.ts
index cdbe6d8..2456d14 100644
--- a/TypeScript/src/ArcscriptFunctions.ts
+++ b/TypeScript/src/ArcscriptFunctions.ts
@@ -107,7 +107,7 @@ export default class ArcscriptFunctions {
const ids = args.map(name => {
if (typeof name !== 'string' || !this.state.getVar(name)) {
throw new RuntimeError(
- `Invalid argument ${name} in function resetAll. Expected a variable`
+ `Invalid argument ${name} in function reset. Expected a variable`
);
}
const v = this.state.getVar(name);
@@ -132,7 +132,7 @@ export default class ArcscriptFunctions {
}
return v.id;
});
- const all = Object.keys(this.state.getInitialVarValues());
+ const all = Object.keys(this.state.variables);
const resetIds = all.filter(id => !except.includes(id));
this.state.resetVarValues(resetIds);
}
diff --git a/TypeScript/src/ArcscriptState.ts b/TypeScript/src/ArcscriptState.ts
index 56906e7..88ec841 100644
--- a/TypeScript/src/ArcscriptState.ts
+++ b/TypeScript/src/ArcscriptState.ts
@@ -1,5 +1,5 @@
-import cloneDeep from 'lodash.clonedeep';
import { VarObject, VarValue } from './types.js';
+import ArcscriptVariable from './ArcscriptVariable.js';
type OutputObject = {
output?: string;
@@ -10,13 +10,11 @@ type OutputObject = {
};
export default class ArcscriptState {
- varValues: Record;
- varObjects: Record;
+ variables: Record;
elementVisits: Record;
currentElement: string;
outputs: OutputObject[];
conditionDepth: number;
- changes: Record;
emit: (event: string, data?: unknown) => void;
outputDoc: Document;
rootElement: HTMLElement;
@@ -30,13 +28,13 @@ export default class ArcscriptState {
currentElement: string,
emit: (event: string, data?: unknown) => void
) {
- this.varValues = varValues;
- this.varObjects = varObjects;
+ this.variables = this.initializeVariables(varObjects, varValues);
+
this.elementVisits = elementVisits;
this.currentElement = currentElement;
this.outputs = [];
this.conditionDepth = 0;
- this.changes = {};
+
this.emit = emit;
this.outputDoc = document.implementation.createHTMLDocument();
@@ -46,33 +44,62 @@ export default class ArcscriptState {
this.insertBlockquote = false;
}
+ /**
+ *
+ * @param {*} varObjects
+ * @param {*} varValues
+ * @returns {Record} An object with the variables of the script, where the keys are the variable IDs and the values are ArcscriptVariable instances
+ */
+ initializeVariables(
+ varObjects: Record,
+ varValues: Record
+ ): Record {
+ const variables: Record = {};
+ Object.entries(varObjects).forEach(([, varObject]) => {
+ if (varObject.children) return;
+
+ const variable = new ArcscriptVariable({
+ id: varObject.id,
+ name: varObject.name,
+ type: varObject.type,
+ value: varValues[varObject.id],
+ defaultValue: varValues[varObject.id],
+ });
+ variables[varObject.id] = variable;
+ });
+ return variables;
+ }
+
getVar(name: string) {
- return Object.values(this.varObjects).find(v => v.name === name);
+ return Object.values(this.variables).find(v => v.name === name);
}
getVarValue(id: string) {
- if (id in this.changes) return this.changes[id];
- return this.varValues[id];
+ return this.variables[id].getValue();
}
setVarValues(ids: string[], values: VarValue[]) {
ids.forEach((id, index) => {
- this.changes[id] = values[index];
+ this.variables[id].setValue(values[index]);
});
}
- getInitialVarValues() {
- return Object.fromEntries(
- Object.entries(this.varObjects)
- .filter(([, v]) => !v.children)
- .map(([k, v]) => [k, v.value])
- );
+ resetVarValues(ids: string[]): void {
+ Object.entries(this.variables).forEach(([id, variable]) => {
+ if (ids.includes(id)) {
+ variable.reset();
+ }
+ });
}
- resetVarValues(ids: string[]) {
- const initial = cloneDeep(this.getInitialVarValues());
- const values = ids.map(id => initial[id]);
- this.setVarValues(ids, values);
+ getChanges(): Record {
+ const changes: Record = {};
+ Object.entries(this.variables).forEach(([id, variable]) => {
+ if (variable.changed) {
+ changes[id] = variable.getValue();
+ }
+ });
+ return changes;
}
/**
diff --git a/TypeScript/src/ArcscriptVariable.ts b/TypeScript/src/ArcscriptVariable.ts
new file mode 100644
index 0000000..2a16572
--- /dev/null
+++ b/TypeScript/src/ArcscriptVariable.ts
@@ -0,0 +1,56 @@
+import { VarValue } from './types.js';
+
+type ArcscriptVariableParams = {
+ id: string;
+ name: string;
+ type: string;
+ defaultValue: VarValue;
+ value?: VarValue;
+};
+
+export default class ArcscriptVariable {
+ id: string;
+ name: string;
+ type: string;
+ value: VarValue;
+ defaultValue: VarValue;
+ changed = false;
+
+ constructor({
+ id,
+ name,
+ defaultValue,
+ type,
+ value,
+ }: ArcscriptVariableParams) {
+ this.id = id;
+ this.name = name;
+ this.type = type;
+ this.defaultValue = defaultValue;
+
+ if (value !== undefined) {
+ this.value = value;
+ this.changed = true;
+ } else {
+ this.value = defaultValue;
+ }
+ }
+
+ reset() {
+ this.value = this.defaultValue;
+ this.changed = true;
+ }
+
+ getValue() {
+ return this.value;
+ }
+
+ setValue(newValue: VarValue) {
+ this.value = newValue;
+ this.changed = true;
+ }
+
+ getType() {
+ return this.type;
+ }
+}
diff --git a/TypeScript/src/Generated/ArcscriptParserBase.ts b/TypeScript/src/Generated/ArcscriptParserBase.ts
index 12960d9..0a72a3d 100644
--- a/TypeScript/src/Generated/ArcscriptParserBase.ts
+++ b/TypeScript/src/Generated/ArcscriptParserBase.ts
@@ -15,23 +15,24 @@ type ArcscriptParserOptions = {
type ArcscriptFunctionInfo = {
minArgs: number;
maxArgs: number | null;
+ returnType?: 'number' | 'void';
};
export default class ArcscriptParserBase extends Parser {
- static arcscriptFunctionsArgLength: Record = {
- abs: { minArgs: 1, maxArgs: 1 },
- max: { minArgs: 2, maxArgs: null },
- min: { minArgs: 2, maxArgs: null },
- random: { minArgs: 0, maxArgs: 0 },
- roll: { minArgs: 1, maxArgs: 2 },
- round: { minArgs: 1, maxArgs: 1 },
- sqr: { minArgs: 1, maxArgs: 1 },
- sqrt: { minArgs: 1, maxArgs: 1 },
- visits: { minArgs: 0, maxArgs: 1 },
- show: { minArgs: 1, maxArgs: null },
- reset: { minArgs: 1, maxArgs: null },
- resetAll: { minArgs: 0, maxArgs: null },
- resetVisits: { minArgs: 0, maxArgs: 0 },
+ static arcscriptFunctionsInfo: Record = {
+ abs: { minArgs: 1, maxArgs: 1, returnType: 'number' },
+ max: { minArgs: 2, maxArgs: null, returnType: 'number' },
+ min: { minArgs: 2, maxArgs: null, returnType: 'number' },
+ random: { minArgs: 0, maxArgs: 0, returnType: 'number' },
+ roll: { minArgs: 1, maxArgs: 2, returnType: 'number' },
+ round: { minArgs: 1, maxArgs: 1, returnType: 'number' },
+ sqr: { minArgs: 1, maxArgs: 1, returnType: 'number' },
+ sqrt: { minArgs: 1, maxArgs: 1, returnType: 'number' },
+ visits: { minArgs: 0, maxArgs: 1, returnType: 'number' },
+ show: { minArgs: 1, maxArgs: null, returnType: 'void' },
+ reset: { minArgs: 1, maxArgs: null, returnType: 'void' },
+ resetAll: { minArgs: 0, maxArgs: null, returnType: 'void' },
+ resetVisits: { minArgs: 0, maxArgs: 0, returnType: 'void' },
};
varObjects: Record = {};
@@ -67,10 +68,8 @@ export default class ArcscriptParserBase extends Parser {
argListLength = argListCtx.VARIABLE_list().length;
}
}
- const min =
- ArcscriptParserBase.arcscriptFunctionsArgLength[token.text].minArgs;
- const max =
- ArcscriptParserBase.arcscriptFunctionsArgLength[token.text].maxArgs;
+ const min = ArcscriptParserBase.arcscriptFunctionsInfo[token.text].minArgs;
+ const max = ArcscriptParserBase.arcscriptFunctionsInfo[token.text].maxArgs;
if (
(min !== null && argListLength < min) ||
diff --git a/TypeScript/src/antlr4.interpreter.ts b/TypeScript/src/antlr4.interpreter.ts
index f936f71..b5e042b 100644
--- a/TypeScript/src/antlr4.interpreter.ts
+++ b/TypeScript/src/antlr4.interpreter.ts
@@ -48,16 +48,14 @@ export default class Interpreter {
this.currentElement,
this.emit
);
- Object.entries(varValues).forEach(([key, value]) => {
- visitor.state.changes[key] = value;
- });
+
const result = tree.accept(visitor);
let output = visitor.state.generateOutput();
output = clearBlockStyle(output);
return {
- changes: visitor.state.changes,
+ changes: visitor.state.getChanges(),
output,
result,
};
From bed14ac45a603db167c64ec502d8ae49ab3a2765 Mon Sep 17 00:00:00 2001
From: Manolis Dermitzakis
Date: Tue, 17 Feb 2026 18:02:13 +0200
Subject: [PATCH 02/21] chore: lint lexer
---
grammar/ArcscriptLexer.g4 | 121 +++++++++++++++++---------------------
1 file changed, 53 insertions(+), 68 deletions(-)
diff --git a/grammar/ArcscriptLexer.g4 b/grammar/ArcscriptLexer.g4
index 92a9a29..0f798fc 100644
--- a/grammar/ArcscriptLexer.g4
+++ b/grammar/ArcscriptLexer.g4
@@ -1,9 +1,11 @@
lexer grammar ArcscriptLexer;
-CODESTART: ''))* '>'))* '>' -> pushMode(CODESEGMENT);
+CODESTART:
+ ''))* '>'))* '>' -> pushMode(CODESEGMENT);
-PARAGRAPHSTART: (('')|('
'))* '>')) -> pushMode(PARAGRAPH);
-BLOCKQUOTESTART: '
'))* '>' -> pushMode(BLOCKQUOTE);
+PARAGRAPHSTART: (('') | ('
'))* '>')) -> pushMode(PARAGRAPH);
+BLOCKQUOTESTART:
+ '
'))* '>' -> pushMode(BLOCKQUOTE);
NORMAL_WHITESPACE: [ \t\r\n]+ -> skip;
@@ -12,8 +14,9 @@ mode PARAGRAPH;
PARAGRAPHEND: .*? '
' -> popMode;
mode BLOCKQUOTE;
-BQ_CODESTART: ''))* '>'))* '>' -> pushMode(CODESEGMENT);
-BQ_PARAGRAPHSTART: (('')|('
'))* '>')) -> pushMode(PARAGRAPH);
+BQ_CODESTART:
+ '
'))* '>'))* '>' -> pushMode(CODESEGMENT);
+BQ_PARAGRAPHSTART: (('') | ('
'))* '>')) -> pushMode(PARAGRAPH);
BLOCKQUOTEEND: '' -> popMode;
BQ_WHITESPACE: [ \t\r\n]+ -> skip;
@@ -23,7 +26,7 @@ CODEEND: '
' -> popMode;
MENTION_TAG_OPEN: ' pushMode(MENTIONSEGMENT);
-FLOAT: DIGIT*'.'DIGIT+;
+FLOAT: DIGIT* '.' DIGIT+;
INTEGER: DIGIT+;
@@ -65,15 +68,16 @@ RBRACE: '}';
BOOLEAN: 'true' | 'false';
-FNAME: 'abs'|
- 'max'|
- 'min'|
- 'random'|
- 'roll'|
- 'round'|
- 'sqr'|
- 'sqrt'|
- 'visits';
+FNAME:
+ 'abs'
+ | 'max'
+ | 'min'
+ | 'random'
+ | 'roll'
+ | 'round'
+ | 'sqr'
+ | 'sqrt'
+ | 'visits';
VFNAME: 'show' | 'resetVisits';
@@ -95,14 +99,8 @@ ISKEYWORD: 'is';
NOTKEYWORD: 'not';
-STRING
- : '"' STRING_CONTENT* '"'
- | '\'' STRING_CONTENT* '\''
- ;
-fragment STRING_CONTENT
- : ~[\\\r\n'"]
- | '\\' [abfnrtv'"\\]
- ;
+STRING: '"' STRING_CONTENT* '"' | '\'' STRING_CONTENT* '\'';
+fragment STRING_CONTENT: ~[\\\r\n'"] | '\\' [abfnrtv'"\\];
VARIABLE: [A-Za-z$_][0-9A-Za-z$_]*;
@@ -128,48 +126,35 @@ MENTION_LABEL: ~'<'+;
mode MENTION_ATTR_SEGMENT;
-ATTR_VALUE
- : ' '* ATTRIBUTE -> popMode;
-
-ATTRIBUTE
- : DOUBLE_QUOTE_STRING
- | SINGLE_QUOTE_STRING
- | ATTCHARS
- | HEXCHARS
- | DECCHARS
- ;
-
-fragment ATTCHARS
- : ATTCHAR+ ' '?
- ;
-
-fragment ATTCHAR
- : '-'
- | '_'
- | '.'
- | '/'
- | '+'
- | ','
- | '?'
- | '='
- | ':'
- | ';'
- | '#'
- | [0-9a-zA-Z]
- ;
-
-fragment HEXCHARS
- : '#' [0-9a-fA-F]+
- ;
-
-fragment DECCHARS
- : [0-9]+ '%'?
- ;
-
-fragment DOUBLE_QUOTE_STRING
- : '"' ~[<"]* '"'
- ;
-
-fragment SINGLE_QUOTE_STRING
- : '\'' ~[<']* '\''
- ;
+ATTR_VALUE: ' '* ATTRIBUTE -> popMode;
+
+ATTRIBUTE:
+ DOUBLE_QUOTE_STRING
+ | SINGLE_QUOTE_STRING
+ | ATTCHARS
+ | HEXCHARS
+ | DECCHARS;
+
+fragment ATTCHARS: ATTCHAR+ ' '?;
+
+fragment ATTCHAR:
+ '-'
+ | '_'
+ | '.'
+ | '/'
+ | '+'
+ | ','
+ | '?'
+ | '='
+ | ':'
+ | ';'
+ | '#'
+ | [0-9a-zA-Z];
+
+fragment HEXCHARS: '#' [0-9a-fA-F]+;
+
+fragment DECCHARS: [0-9]+ '%'?;
+
+fragment DOUBLE_QUOTE_STRING: '"' ~[<"]* '"';
+
+fragment SINGLE_QUOTE_STRING: '\'' ~[<']* '\'';
\ No newline at end of file
From 252b1c3b722155a700f7f4155b41c8805a89980e Mon Sep 17 00:00:00 2001
From: Manolis Dermitzakis
Date: Wed, 18 Feb 2026 19:58:31 +0200
Subject: [PATCH 03/21] fix: add missing typescript dependency
---
TypeScript/package.json | 1 +
TypeScript/pnpm-lock.yaml | 3 +++
2 files changed, 4 insertions(+)
diff --git a/TypeScript/package.json b/TypeScript/package.json
index c9011c2..3a8acf8 100644
--- a/TypeScript/package.json
+++ b/TypeScript/package.json
@@ -45,6 +45,7 @@
"jiti": "^2.6.1",
"pkg-pr-new": "^0.0.63",
"prettier": "3.8.1",
+ "typescript": "^5.9.3",
"typescript-eslint": "^8.56.0",
"vitest": "^4.0.18"
},
diff --git a/TypeScript/pnpm-lock.yaml b/TypeScript/pnpm-lock.yaml
index ad4a31b..ba7bd33 100644
--- a/TypeScript/pnpm-lock.yaml
+++ b/TypeScript/pnpm-lock.yaml
@@ -45,6 +45,9 @@ importers:
prettier:
specifier: 3.8.1
version: 3.8.1
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
typescript-eslint:
specifier: ^8.56.0
version: 8.56.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3)
From 6fe0b35b60597a186d2e2f5a44e986f3457d7430 Mon Sep 17 00:00:00 2001
From: Manolis Dermitzakis
Date: Fri, 20 Feb 2026 15:13:20 +0200
Subject: [PATCH 04/21] feat: refactor grammar to support objects
---
TypeScript/src/ArcscriptFunctions.ts | 15 +-
TypeScript/src/ArcscriptVisitor.ts | 527 ++++++++----------
.../src/Generated/ArcscriptParserBase.ts | 70 ++-
TypeScript/src/antlr4.interpreter.ts | 2 +-
grammar/ArcscriptLexer.g4 | 16 +-
grammar/ArcscriptParser.g4 | 104 ++--
6 files changed, 356 insertions(+), 378 deletions(-)
diff --git a/TypeScript/src/ArcscriptFunctions.ts b/TypeScript/src/ArcscriptFunctions.ts
index 2456d14..d1c8fd2 100644
--- a/TypeScript/src/ArcscriptFunctions.ts
+++ b/TypeScript/src/ArcscriptFunctions.ts
@@ -1,6 +1,7 @@
import { RuntimeError } from './errors/index.js';
import ArcscriptState from './ArcscriptState.js';
import { MentionResult, VarValue } from './types.js';
+import ArcscriptVariable from './ArcscriptVariable.js';
export type FunctionName = keyof ArcscriptFunctions;
@@ -19,7 +20,7 @@ export type ArcscriptNonVoidFunctionKeys = Exclude<
ArcscriptVoidFunctionKeys
>;
-type ArgumentTypes = (VarValue | MentionResult)[];
+type ArgumentTypes = (VarValue | MentionResult | ArcscriptVariable)[];
export default class ArcscriptFunctions {
private state: ArcscriptState;
@@ -157,7 +158,7 @@ export default class ArcscriptFunctions {
visits(...args: ArgumentTypes): number {
let elementId = this.state.currentElement;
if (args.length > 0) {
- const mention = args[0];
+ const mention = args[0] as MentionResult;
if (
typeof mention !== 'object' ||
typeof mention.attrs['data-id'] !== 'string'
@@ -185,7 +186,10 @@ export default class ArcscriptFunctions {
* @param {string} name The function name
* @param {VarValue} arg The argument to check
*/
- private assertNumber(name: string, arg: VarValue | MentionResult) {
+ private assertNumber(
+ name: string,
+ arg: VarValue | MentionResult | ArcscriptVariable
+ ) {
if (typeof arg !== 'number' || Number.isNaN(arg)) {
throw new RuntimeError(
`Invalid argument ${arg} in function ${name}. Expected number (integer or float)`
@@ -198,7 +202,10 @@ export default class ArcscriptFunctions {
* @param {string} name The function name
* @param {VarValue} arg The argument to check
*/
- private assertPositiveInteger(name: string, arg: VarValue | MentionResult) {
+ private assertPositiveInteger(
+ name: string,
+ arg: VarValue | MentionResult | ArcscriptVariable
+ ) {
if (typeof arg !== 'number' || Number.isNaN(arg)) {
throw new RuntimeError(
`Invalid argument ${arg} in function ${name}. Expected number (integer)`
diff --git a/TypeScript/src/ArcscriptVisitor.ts b/TypeScript/src/ArcscriptVisitor.ts
index ded70ea..f441746 100644
--- a/TypeScript/src/ArcscriptVisitor.ts
+++ b/TypeScript/src/ArcscriptVisitor.ts
@@ -2,43 +2,46 @@ import BigNumber from 'bignumber.js';
import ArcscriptParserVisitor from './Generated/ArcscriptParserVisitor.js';
import ArcscriptFunctions, {
ArcscriptNonVoidFunctionKeys,
- ArcscriptVoidFunctionKeys,
} from './ArcscriptFunctions.js';
import ArcscriptState from './ArcscriptState.js';
import { RuntimeError } from './errors/index.js';
import { MentionResult, VarObject, VarValue } from './types.js';
import {
- Additive_numeric_expressionContext,
+ AdditiveExpressionContext,
Argument_listContext,
ArgumentContext,
+ AssignableContext,
Assignment_segmentContext,
BlockquoteContext,
- Compound_condition_andContext,
- Compound_condition_orContext,
+ ComparisonExpressionContext,
Conditional_sectionContext,
- ConditionContext,
Else_if_clauseContext,
Else_if_sectionContext,
Else_sectionContext,
ExpressionContext,
Function_call_segmentContext,
Function_callContext,
+ FunctionCallExpressionContext,
+ Identifier_listContext,
+ IdentifierExpressionContext,
If_clauseContext,
If_sectionContext,
InputContext,
+ LiteralContext,
+ LiteralExpressionContext,
+ MemberExpressionContext,
Mention_attributesContext,
MentionContext,
- Multiplicative_numeric_expressionContext,
- Negated_unary_conditionContext,
+ MultiplicativeExpressionContext,
+ Numeric_literalContext,
ParagraphContext,
+ ParenthesizedExpressionContext,
Script_sectionContext,
- Signed_unary_numeric_expressionContext,
Statement_assignmentContext,
- Unary_conditionContext,
- Unary_numeric_expressionContext,
- Variable_listContext,
- Void_function_callContext,
+ UnaryExpressionContext,
} from './Generated/ArcscriptParser.js';
+import ArcscriptVariable from './ArcscriptVariable.js';
+import ArcscriptParserBase from './Generated/ArcscriptParserBase.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default class ArcscriptVisitor extends ArcscriptParserVisitor {
@@ -71,7 +74,9 @@ export default class ArcscriptVisitor extends ArcscriptParserVisitor {
}
return {
- condition: this.visitCompound_condition_or(ctx.compound_condition_or()),
+ condition: this.visitComparisonExpression(
+ ctx.expression() as ComparisonExpressionContext
+ ),
};
};
@@ -181,38 +186,34 @@ export default class ArcscriptVisitor extends ArcscriptParserVisitor {
return this.visitChildren(ctx.script());
};
- visitIf_clause = (ctx: If_clauseContext) => {
- return this.visitCompound_condition_or(ctx.compound_condition_or());
+ visitIf_clause = (ctx: If_clauseContext): boolean => {
+ return this.visitExpression(ctx.expression()) as boolean;
};
- visitElse_if_clause = (ctx: Else_if_clauseContext) => {
- return this.visitCompound_condition_or(ctx.compound_condition_or());
+ visitElse_if_clause = (ctx: Else_if_clauseContext): boolean => {
+ return this.visitExpression(ctx.expression()) as boolean;
};
visitStatement_assignment = (ctx: Statement_assignmentContext) => {
- const variableName = ctx.VARIABLE().getText();
- const variableObject = this.state.getVar(variableName);
- if (!variableObject) {
- throw new RuntimeError(`Variable ${variableName} not found`);
- }
+ const assignable = this.visitAssignable(ctx.assignable());
+
+ let variableValue = this.state.getVarValue(assignable.id);
+ let expressionValue = this.visitExpression(ctx.expression());
- let variableValue = this.state.getVarValue(variableObject.id);
- let compound_condition_or = this.visitCompound_condition_or(
- ctx.compound_condition_or()
- );
let result: VarValue = 0;
if (
typeof variableValue === 'string' ||
- typeof compound_condition_or === 'string'
+ typeof expressionValue === 'string'
) {
if (ctx.ASSIGNADD()) {
- result = ((variableValue as string) + compound_condition_or) as string;
+ result = ((variableValue as string) + expressionValue) as string;
} else if (ctx.ASSIGN()) {
- result = compound_condition_or;
+ result = expressionValue;
} else {
throw new RuntimeError('Invalid operation with string');
}
- this.state.setVarValues([variableObject.id], [result]);
+
+ this.state.setVarValues([assignable.id], [result]);
return null;
}
if (
@@ -225,352 +226,294 @@ export default class ArcscriptVisitor extends ArcscriptParserVisitor {
if (typeof variableValue === 'boolean') {
variableValue = variableValue ? 1 : 0;
}
- if (typeof compound_condition_or === 'boolean') {
- compound_condition_or = compound_condition_or ? 1 : 0;
+ if (typeof expressionValue === 'boolean') {
+ expressionValue = expressionValue ? 1 : 0;
}
}
+
+ const leftValue = new BigNumber(variableValue as number);
+ const rightValue = new BigNumber(expressionValue as number);
+
if (ctx.ASSIGNADD()) {
- result = new BigNumber(variableValue as number)
- .plus(new BigNumber(compound_condition_or as number))
- .toNumber();
+ result = leftValue.plus(rightValue).toNumber();
} else if (ctx.ASSIGNSUB()) {
- result = new BigNumber(variableValue as number)
- .minus(new BigNumber(compound_condition_or as number))
- .toNumber();
+ result = leftValue.minus(rightValue).toNumber();
} else if (ctx.ASSIGNMUL()) {
- result = new BigNumber(variableValue as number)
- .multipliedBy(new BigNumber(compound_condition_or as number))
- .toNumber();
+ result = leftValue.multipliedBy(rightValue).toNumber();
} else if (ctx.ASSIGNDIV()) {
- result = new BigNumber(variableValue as number)
- .dividedBy(new BigNumber(compound_condition_or as number))
- .toNumber();
+ result = leftValue.dividedBy(rightValue).toNumber();
if (!Number.isFinite(result)) {
throw new RuntimeError(`Invalid division by zero`);
}
} else if (ctx.ASSIGNMOD()) {
- result = new BigNumber(variableValue as number)
- .modulo(new BigNumber(compound_condition_or as number))
- .toNumber();
+ result = leftValue.modulo(rightValue).toNumber();
if (!Number.isFinite(result)) {
throw new RuntimeError(`Invalid division by zero`);
}
} else if (ctx.ASSIGN()) {
- result = compound_condition_or;
+ result = expressionValue;
}
- this.state.setVarValues([variableObject.id], [result]);
+ this.state.setVarValues([assignable.id], [result]);
return null;
};
- visitVoid_function_call = (ctx: Void_function_callContext) => {
- let argument_list: (VarValue | MentionResult)[] = [];
- let fname: ArcscriptVoidFunctionKeys;
- if (ctx.VFNAME()) {
- fname = ctx.VFNAME().getText() as ArcscriptVoidFunctionKeys;
- if (ctx.argument_list()) {
- argument_list = this.visitArgument_list(ctx.argument_list());
- }
- }
- if (ctx.VFNAMEVARS()) {
- fname = ctx.VFNAMEVARS().getText() as ArcscriptVoidFunctionKeys;
- if (ctx.variable_list()) {
- argument_list = this.visitVariable_list(ctx.variable_list());
- }
+ visitAssignable = (ctx: AssignableContext): ArcscriptVariable => {
+ const identifier = ctx.IDENTIFIER().getText();
+ const variableObject = this.state.getVar(identifier);
+ if (!variableObject) {
+ throw new RuntimeError(`Variable ${identifier} not found`);
}
-
- this.functions[fname!](...argument_list);
+ return variableObject;
};
- visitVariable_list = (ctx: Variable_listContext): string[] => {
- return ctx.VARIABLE_list().map(variable => variable.getText());
- };
-
- visitCompound_condition_or = (
- ctx: Compound_condition_orContext
- ): VarValue => {
- const compound_condition_and = this.visitCompound_condition_and(
- ctx.compound_condition_and()
- );
-
- if (ctx.compound_condition_or()) {
- return (
- compound_condition_and ||
- this.visitCompound_condition_or(ctx.compound_condition_or())
- );
+ visitExpression(ctx: ExpressionContext): VarValue {
+ if (ctx.constructor === ComparisonExpressionContext) {
+ return this.visitComparisonExpression(ctx);
}
-
- return compound_condition_and;
- };
-
- visitCompound_condition_and = (
- ctx: Compound_condition_andContext
- ): VarValue => {
- const negated_unary_condition = this.visitNegated_unary_condition(
- ctx.negated_unary_condition()
- );
-
- if (ctx.compound_condition_and()) {
- return (
- negated_unary_condition &&
- this.visitCompound_condition_and(ctx.compound_condition_and())
- );
+ if (ctx.constructor === UnaryExpressionContext) {
+ return this.visitUnaryExpression(ctx);
}
-
- return negated_unary_condition;
- };
-
- visitNegated_unary_condition = (ctx: Negated_unary_conditionContext) => {
- const unary_condition = this.visitUnary_condition(ctx.unary_condition());
-
- if (ctx.NEG() || ctx.NOTKEYWORD()) {
- return !unary_condition;
+ if (ctx.constructor === MultiplicativeExpressionContext) {
+ return this.visitMultiplicativeExpression(ctx);
}
-
- return unary_condition;
- };
-
- visitUnary_condition = (ctx: Unary_conditionContext) => {
- return this.visitCondition(ctx.condition());
- };
-
- visitCondition = (ctx: ConditionContext): VarValue => {
- if (ctx.expression_list().length === 1) {
- return this.visitExpression(ctx.expression(0));
+ if (ctx.constructor === AdditiveExpressionContext) {
+ return this.visitAdditiveExpression(ctx);
}
- const conditional_operator = ctx.conditional_operator();
- const exp0 = this.visitExpression(ctx.expression(0));
- const exp1 = this.visitExpression(ctx.expression(1));
-
- if (conditional_operator.GT()) {
- return exp0 > exp1;
+ if (ctx.constructor === ParenthesizedExpressionContext) {
+ return this.visitParenthesizedExpression(ctx);
}
- if (conditional_operator.GE()) {
- return exp0 >= exp1;
+ if (ctx.constructor === MemberExpressionContext) {
+ return this.visitMemberExpression(ctx);
}
- if (conditional_operator.LT()) {
- return exp0 < exp1;
+ if (ctx.constructor === FunctionCallExpressionContext) {
+ return this.visitFunctionCallExpression(ctx);
}
- if (conditional_operator.LE()) {
- return exp0 <= exp1;
+ if (ctx.constructor === LiteralExpressionContext) {
+ return this.visitLiteralExpression(ctx);
}
- if (conditional_operator.EQ()) {
- return exp0 === exp1;
+ if (ctx.constructor === IdentifierExpressionContext) {
+ return this.visitIdentifierExpression(ctx);
}
- if (conditional_operator.NE()) {
- return exp0 !== exp1;
+ throw new RuntimeError(
+ 'Invalid expression: Constructor = ' + ctx.constructor.name
+ );
+ }
+
+ visitComparisonExpression = (ctx: ComparisonExpressionContext): boolean => {
+ const left = this.visitExpression(ctx.expression(0));
+ if (ctx.AND() || ctx.ANDKEYWORD()) {
+ if (!left) {
+ return false;
+ }
+ const right = this.visitExpression(ctx.expression(1));
+ return Boolean(left) && Boolean(right);
}
- if (conditional_operator.ISKEYWORD()) {
- if (conditional_operator.NOTKEYWORD()) {
- return exp0 !== exp1;
+ if (ctx.OR() || ctx.ORKEYWORD()) {
+ if (left) {
+ return true;
}
- return exp0 === exp1;
+ const right = this.visitExpression(ctx.expression(1));
+ return Boolean(left) || Boolean(right);
}
+ const right = this.visitExpression(ctx.expression(1));
- return this.visitChildren(ctx) as VarValue;
+ if (ctx.EQ() || (ctx.ISKEYWORD() && !ctx.NOTKEYWORD())) {
+ return left === right;
+ }
+ if (ctx.NE() || (ctx.ISKEYWORD() && ctx.NOTKEYWORD())) {
+ return left !== right;
+ }
+ if (ctx.LT()) {
+ return left < right;
+ }
+ if (ctx.GT()) {
+ return left > right;
+ }
+ if (ctx.LE()) {
+ return left <= right;
+ }
+ if (ctx.GE()) {
+ return left >= right;
+ }
+ throw new RuntimeError('Invalid comparison operator');
};
- visitExpression = (ctx: ExpressionContext): VarValue => {
- if (ctx.STRING()) {
- let result = ctx.STRING().getText();
- result = result.replace(/^['"](.*)['"]$/, '$1');
- return result;
+ visitUnaryExpression = (ctx: UnaryExpressionContext) => {
+ if (ctx.NOTKEYWORD() || ctx.NEG()) {
+ return !this.visitExpression(ctx.expression());
}
- if (ctx.BOOLEAN()) {
- if (ctx.BOOLEAN().getText() === 'true') {
- return true;
- }
- return false;
+ if (ctx.ADD()) {
+ return this.visitExpression(ctx.expression());
}
-
- return this.visitAdditive_numeric_expression(
- ctx.additive_numeric_expression()
- );
+ if (ctx.SUB()) {
+ return -this.visitExpression(ctx.expression());
+ }
+ throw new RuntimeError('Invalid unary operator');
};
- visitAdditive_numeric_expression = (
- ctx: Additive_numeric_expressionContext
- ): VarValue => {
- if (ctx.additive_numeric_expression()) {
- let additive_numeric_expression = this.visitAdditive_numeric_expression(
- ctx.additive_numeric_expression()
- );
-
- let multiplicative_numeric_expression =
- this.visitMultiplicative_numeric_expression(
- ctx.multiplicative_numeric_expression()
- );
-
- let hasString = typeof multiplicative_numeric_expression === 'string';
-
- hasString = hasString || typeof additive_numeric_expression === 'string';
-
- if (!hasString) {
- if (typeof multiplicative_numeric_expression === 'boolean') {
- multiplicative_numeric_expression = multiplicative_numeric_expression
- ? 1
- : 0;
- }
- if (typeof additive_numeric_expression === 'boolean') {
- additive_numeric_expression = additive_numeric_expression ? 1 : 0;
- }
- }
+ visitMultiplicativeExpression = (ctx: MultiplicativeExpressionContext) => {
+ const left = this.visitExpression(ctx.expression(0));
+ const right = this.visitExpression(ctx.expression(1));
+ if (typeof left === 'string' || typeof right === 'string') {
+ throw new RuntimeError('Invalid operation with string');
+ }
+ const leftValue = new BigNumber(left as number);
+ const rightValue = new BigNumber(right as number);
- if (ctx.ADD()) {
- if (hasString) {
- return ((additive_numeric_expression as string) +
- multiplicative_numeric_expression) as string;
- }
- return new BigNumber(additive_numeric_expression as number)
- .plus(new BigNumber(multiplicative_numeric_expression as number))
- .toNumber();
+ if (ctx.MUL()) {
+ return leftValue.multipliedBy(rightValue).toNumber();
+ }
+ if (ctx.DIV()) {
+ if (rightValue.isZero()) {
+ throw new RuntimeError('Invalid division by zero');
}
- if (hasString) {
- throw new RuntimeError('Invalid subtraction with string');
+ return leftValue.dividedBy(rightValue).toNumber();
+ }
+ if (ctx.MOD()) {
+ if (rightValue.isZero()) {
+ throw new RuntimeError('Invalid division by zero');
}
- return new BigNumber(additive_numeric_expression as number)
- .minus(new BigNumber(multiplicative_numeric_expression as number))
- .toNumber();
+ return leftValue.modulo(rightValue).toNumber();
}
-
- const multiplicative_numeric_expression =
- this.visitMultiplicative_numeric_expression(
- ctx.multiplicative_numeric_expression()
- );
-
- return multiplicative_numeric_expression;
+ throw new RuntimeError('Invalid multiplicative operator');
};
- visitMultiplicative_numeric_expression = (
- ctx: Multiplicative_numeric_expressionContext
- ): VarValue => {
- if (ctx.multiplicative_numeric_expression()) {
- let multiplicative_numeric_expression =
- this.visitMultiplicative_numeric_expression(
- ctx.multiplicative_numeric_expression()
- );
- let signed_unary_numeric_expression =
- this.visitSigned_unary_numeric_expression(
- ctx.signed_unary_numeric_expression()
- );
- if (typeof signed_unary_numeric_expression === 'boolean') {
- signed_unary_numeric_expression = signed_unary_numeric_expression
- ? 1
- : 0;
- }
- if (typeof multiplicative_numeric_expression === 'boolean') {
- multiplicative_numeric_expression = multiplicative_numeric_expression
- ? 1
- : 0;
- }
- if (ctx.MUL()) {
- return new BigNumber(multiplicative_numeric_expression)
- .multipliedBy(new BigNumber(signed_unary_numeric_expression))
- .toNumber();
- }
- if (ctx.DIV()) {
- const result = new BigNumber(multiplicative_numeric_expression)
- .dividedBy(new BigNumber(signed_unary_numeric_expression))
- .toNumber();
- if (!Number.isFinite(result)) {
- throw new RuntimeError(`Invalid division by zero`);
- }
- return result;
- }
- // else MOD
- const result = new BigNumber(multiplicative_numeric_expression)
- .modulo(new BigNumber(signed_unary_numeric_expression))
- .toNumber();
- if (!Number.isFinite(result)) {
- throw new RuntimeError(`Invalid division by zero`);
+ visitAdditiveExpression = (ctx: AdditiveExpressionContext) => {
+ let left = this.visitExpression(ctx.expression(0));
+ let right = this.visitExpression(ctx.expression(1));
+ if (typeof left === 'string' || typeof right === 'string') {
+ if (ctx.ADD()) {
+ return (left as string) + (right as string);
}
- return result;
+ throw new RuntimeError('Invalid operation with string');
}
+ left = Number(left);
+ right = Number(right);
- const signed_unary_numeric_expression =
- this.visitSigned_unary_numeric_expression(
- ctx.signed_unary_numeric_expression()
- );
+ const leftValue = new BigNumber(left as number);
+ const rightValue = new BigNumber(right as number);
- return signed_unary_numeric_expression;
+ if (ctx.ADD()) {
+ return leftValue.plus(rightValue).toNumber();
+ }
+ if (ctx.SUB()) {
+ return leftValue.minus(rightValue).toNumber();
+ }
+ throw new RuntimeError('Invalid additive operator');
};
- visitSigned_unary_numeric_expression = (
- ctx: Signed_unary_numeric_expressionContext
- ): VarValue => {
- const unary_numeric_expression = this.visitUnary_numeric_expression(
- ctx.unary_numeric_expression()
- );
+ visitParenthesizedExpression = (ctx: ParenthesizedExpressionContext) => {
+ return this.visitExpression(ctx.expression());
+ };
- const sign = ctx.sign();
- if (sign) {
- if (sign.ADD()) {
- return +unary_numeric_expression;
- }
- return -unary_numeric_expression;
- }
- return unary_numeric_expression;
+ visitMemberExpression = (ctx: MemberExpressionContext) => {
+ throw new RuntimeError('Member expressions are not supported');
};
- visitUnary_numeric_expression = (
- ctx: Unary_numeric_expressionContext
- ): VarValue => {
- if (ctx.FLOAT()) {
- return parseFloat(ctx.FLOAT().getText());
+ visitIdentifierExpression = (ctx: IdentifierExpressionContext) => {
+ const identifier = ctx.IDENTIFIER().getText();
+ const variableObject = this.state.getVar(identifier);
+ if (!variableObject) {
+ throw new RuntimeError(`Variable ${identifier} not found`);
}
- if (ctx.INTEGER()) {
- return parseInt(ctx.INTEGER().getText(), 10);
+
+ if (identifier === 'w') {
+ console.log(
+ 'Getting variable',
+ identifier,
+ 'with value',
+ variableObject.getValue()
+ ); // --- IGNORE ---
}
- if (ctx.VARIABLE()) {
- const variableName = ctx.VARIABLE().getText();
- const variableObject = this.state.getVar(variableName);
- if (!variableObject) {
- throw new RuntimeError(`Variable ${variableName} not found`);
+ return variableObject.getValue();
+ };
+
+ visitLiteralExpression = (ctx: LiteralExpressionContext) => {
+ return this.visitLiteral(ctx.literal());
+ };
+
+ visitFunctionCallExpression = (ctx: FunctionCallExpressionContext) => {
+ return this.visitFunction_call(ctx.function_call());
+ };
+
+ visitLiteral = (ctx: LiteralContext) => {
+ if (ctx.BOOLEAN()) {
+ if (ctx.BOOLEAN().getText() === 'true') {
+ return true;
}
- return this.state.getVarValue(variableObject.id);
+ return false;
}
if (ctx.STRING()) {
let result = ctx.STRING().getText();
result = result.replace(/^['"](.*)['"]$/, '$1');
return result;
}
- if (ctx.BOOLEAN()) {
- if (ctx.BOOLEAN().getText() === 'true') {
- return true;
- }
- return false;
- }
- if (ctx.function_call()) {
- return this.visitFunction_call(ctx.function_call());
+ return this.visitNumeric_literal(ctx.numeric_literal());
+ };
+
+ visitNumeric_literal = (ctx: Numeric_literalContext) => {
+ if (ctx.FLOAT()) {
+ return parseFloat(ctx.FLOAT().getText());
}
- return this.visitCompound_condition_or(ctx.compound_condition_or());
+ return parseInt(ctx.INTEGER().getText(), 10);
};
visitFunction_call = (ctx: Function_callContext) => {
- let argument_list: (VarValue | MentionResult)[] = [];
+ let argument_list: (VarValue | MentionResult | ArcscriptVariable)[] = [];
+ const function_name = ctx.FNAME().getText() as ArcscriptNonVoidFunctionKeys;
if (ctx.argument_list()) {
argument_list = this.visitArgument_list(ctx.argument_list());
}
+ if (ctx.identifier_list()) {
+ argument_list = this.visitIdentifier_list(ctx.identifier_list());
+ if (
+ ArcscriptParserBase.arcscriptFunctionsInfo[function_name].argType ===
+ 'variable'
+ ) {
+ argument_list = argument_list.map(variable => {
+ if (variable instanceof ArcscriptVariable) {
+ return variable.name;
+ }
+ throw new RuntimeError('Expected a variable');
+ });
+ } else {
+ argument_list = argument_list.map(variable => {
+ if (variable instanceof ArcscriptVariable) {
+ return variable.getValue();
+ }
+ throw new RuntimeError('Expected a variable');
+ });
+ }
+ }
- const function_name = ctx.FNAME().getText() as ArcscriptNonVoidFunctionKeys;
+ // TODO: remove non void function keys
return this.functions[function_name](...argument_list);
};
+ visitIdentifier_list = (ctx: Identifier_listContext): ArcscriptVariable[] => {
+ return ctx.IDENTIFIER_list().map(identifier => {
+ const name = identifier.getText();
+ const variableObject = this.state.getVar(name);
+ if (!variableObject) {
+ throw new RuntimeError(`Variable ${name} not found`);
+ }
+ return variableObject;
+ });
+ };
+
visitArgument_list = (ctx: Argument_listContext) => {
return ctx.argument_list().map(argument => this.visitArgument(argument));
};
visitArgument = (ctx: ArgumentContext): VarValue | MentionResult => {
- if (ctx.STRING()) {
- let result = ctx.STRING().getText();
- result = result.replace(/^['"](.*)['"]$/, '$1');
- return result;
+ if (ctx.expression()) {
+ return this.visitExpression(ctx.expression());
}
if (ctx.mention()) {
return this.visitMention(ctx.mention());
}
- return this.visitAdditive_numeric_expression(
- ctx.additive_numeric_expression()
- );
+
+ throw new RuntimeError('Invalid argument');
};
visitMention = (ctx: MentionContext): MentionResult => {
diff --git a/TypeScript/src/Generated/ArcscriptParserBase.ts b/TypeScript/src/Generated/ArcscriptParserBase.ts
index 0a72a3d..159f66b 100644
--- a/TypeScript/src/Generated/ArcscriptParserBase.ts
+++ b/TypeScript/src/Generated/ArcscriptParserBase.ts
@@ -1,8 +1,9 @@
import { Parser, TerminalNode, Token, TokenStream } from 'antlr4';
import ArcscriptParser, {
Argument_listContext,
+ Function_callContext,
+ Identifier_listContext,
Mention_attributesContext,
- Variable_listContext,
} from './ArcscriptParser.js';
import { VarObject } from '../types.js';
@@ -16,6 +17,7 @@ type ArcscriptFunctionInfo = {
minArgs: number;
maxArgs: number | null;
returnType?: 'number' | 'void';
+ argType?: 'number' | 'variable' | 'mention';
};
export default class ArcscriptParserBase extends Parser {
@@ -30,8 +32,18 @@ export default class ArcscriptParserBase extends Parser {
sqrt: { minArgs: 1, maxArgs: 1, returnType: 'number' },
visits: { minArgs: 0, maxArgs: 1, returnType: 'number' },
show: { minArgs: 1, maxArgs: null, returnType: 'void' },
- reset: { minArgs: 1, maxArgs: null, returnType: 'void' },
- resetAll: { minArgs: 0, maxArgs: null, returnType: 'void' },
+ reset: {
+ minArgs: 1,
+ maxArgs: null,
+ returnType: 'void',
+ argType: 'variable',
+ },
+ resetAll: {
+ minArgs: 0,
+ maxArgs: null,
+ returnType: 'void',
+ argType: 'variable',
+ },
resetVisits: { minArgs: 0, maxArgs: 0, returnType: 'void' },
};
@@ -57,23 +69,24 @@ export default class ArcscriptParserBase extends Parser {
assertFunctionArguments(
token: Token,
- argListCtx: Argument_listContext | Variable_listContext | null = null
+ argListCtx: Argument_listContext | Identifier_listContext | null = null
) {
let argListLength = 0;
if (argListCtx) {
if (argListCtx instanceof Argument_listContext) {
argListLength = argListCtx.argument_list().length;
- } else if (argListCtx instanceof Variable_listContext) {
- argListLength = argListCtx.VARIABLE_list().length;
+ } else if (argListCtx instanceof Identifier_listContext) {
+ argListLength = argListCtx.IDENTIFIER_list().length;
}
}
- const min = ArcscriptParserBase.arcscriptFunctionsInfo[token.text].minArgs;
- const max = ArcscriptParserBase.arcscriptFunctionsInfo[token.text].maxArgs;
+
+ const { minArgs, maxArgs, argType } =
+ ArcscriptParserBase.arcscriptFunctionsInfo[token.text];
if (
- (min !== null && argListLength < min) ||
- (max !== null && argListLength > max)
+ (minArgs !== null && argListLength < minArgs) ||
+ (maxArgs !== null && argListLength > maxArgs)
) {
this.notifyErrorListeners(
`Invalid number of arguments for function ${token.text}`,
@@ -81,6 +94,43 @@ export default class ArcscriptParserBase extends Parser {
undefined
);
}
+
+ if (argType && argListCtx) {
+ if (argType === 'variable') {
+ if (!(argListCtx instanceof Identifier_listContext)) {
+ this.notifyErrorListeners(
+ `Invalid argument type for function ${token.text}. Expected variables.`,
+ token,
+ undefined
+ );
+ }
+ }
+ }
+ }
+
+ assertFunctionReturnValue(
+ fncCtx: Function_callContext,
+ hasReturnValue = true
+ ) {
+ const functionName = fncCtx.FNAME().getText();
+ const token = fncCtx.FNAME().symbol;
+ const { returnType } =
+ ArcscriptParserBase.arcscriptFunctionsInfo[functionName];
+ if (hasReturnValue) {
+ if (returnType === 'void') {
+ this.notifyErrorListeners(
+ `The function ${functionName} does not return a value`,
+ token,
+ undefined
+ );
+ }
+ } else if (returnType !== 'void') {
+ this.notifyErrorListeners(
+ `The function ${functionName} returns a value that is not being used`,
+ token,
+ undefined
+ );
+ }
}
assertVariable(variableToken: Token) {
diff --git a/TypeScript/src/antlr4.interpreter.ts b/TypeScript/src/antlr4.interpreter.ts
index b5e042b..11aad50 100644
--- a/TypeScript/src/antlr4.interpreter.ts
+++ b/TypeScript/src/antlr4.interpreter.ts
@@ -99,7 +99,7 @@ export default class Interpreter {
const tokenTypeNames = lexer.getSymbolicNames();
const allTokens = lexer.getAllTokens();
const variableTokens = allTokens.filter(
- token => tokenTypeNames[token.type] === 'VARIABLE'
+ token => tokenTypeNames[token.type] === 'IDENTIFIER'
);
const tokenIdMap = new Map