From 86e1fc155e0bc663c83b5a8ad01b9d381e9d8ae0 Mon Sep 17 00:00:00 2001 From: Jeremy Walker Date: Wed, 22 Jan 2025 21:17:40 +0000 Subject: [PATCH] Remove visitor pattern and add descriptions (#7361) * WIP * Remove Javascript interpreter * Add tests for all exercises * Tidy imports * Tidy imports * Remove visitor pattern and add descriptions * Improve descriptions further * Keep simplifying --- .../components/editor-information-tooltip.css | 7 +- .../information-widget.ts | 2 +- app/javascript/interpreter/error.ts | 2 +- .../interpreter/evaluation-result.ts | 32 +- app/javascript/interpreter/executor.ts | 317 ++++++++--------- app/javascript/interpreter/expression.ts | 143 ++------ app/javascript/interpreter/frames.ts | 75 ++-- .../interpreter/locales/en/translation.json | 1 - .../interpreter/locales/nl/translation.json | 1 - .../locales/system/translation.json | 1 - app/javascript/interpreter/parser.ts | 138 +++----- app/javascript/interpreter/scanner.ts | 10 +- app/javascript/interpreter/statement.ts | 96 ++--- app/javascript/interpreter/token.ts | 4 +- .../interpreter/concepts/arrays.test.ts | 164 +++++++++ .../interpreter/concepts/dictionaries.test.ts | 126 +++++++ .../concepts/template_literals.test.ts | 123 +++++++ .../interpreter/concepts/while.test.ts | 61 ++++ .../expression_descriptions.test.ts | 44 +++ .../interpreter/interpreter.test.ts | 334 +----------------- test/javascript/interpreter/parser.test.ts | 196 +++------- test/javascript/interpreter/scanner.test.ts | 8 +- .../statement_descriptions.test.ts | 37 ++ 23 files changed, 930 insertions(+), 992 deletions(-) create mode 100644 test/javascript/interpreter/concepts/arrays.test.ts create mode 100644 test/javascript/interpreter/concepts/dictionaries.test.ts create mode 100644 test/javascript/interpreter/concepts/template_literals.test.ts create mode 100644 test/javascript/interpreter/concepts/while.test.ts create mode 100644 test/javascript/interpreter/expression_descriptions.test.ts create mode 100644 test/javascript/interpreter/statement_descriptions.test.ts diff --git a/app/css/bootcamp/components/editor-information-tooltip.css b/app/css/bootcamp/components/editor-information-tooltip.css index 623220fe2a..a70ee8ccd9 100644 --- a/app/css/bootcamp/components/editor-information-tooltip.css +++ b/app/css/bootcamp/components/editor-information-tooltip.css @@ -10,11 +10,14 @@ @apply bg-thick-border-blue px-[6px] py-[1px] rounded-5; } + pre + p { + @apply mt-6; + } pre { - @apply mt-4; + @apply mt-2; } - p:not(:last-of-type) { + p:not(:last-child) { @apply mb-10; } ul { diff --git a/app/javascript/components/bootcamp/SolveExercisePage/CodeMirror/extensions/end-line-information/information-widget.ts b/app/javascript/components/bootcamp/SolveExercisePage/CodeMirror/extensions/end-line-information/information-widget.ts index 73f830a418..591b9327e2 100644 --- a/app/javascript/components/bootcamp/SolveExercisePage/CodeMirror/extensions/end-line-information/information-widget.ts +++ b/app/javascript/components/bootcamp/SolveExercisePage/CodeMirror/extensions/end-line-information/information-widget.ts @@ -90,7 +90,7 @@ export class InformationWidget extends WidgetType { if (!this.tooltip) return this.arrowElement = document.createElement('div') this.arrowElement.classList.add('tooltip-arrow') - this.tooltip.appendChild(this.arrowElement) + this.tooltip.prepend(this.arrowElement) } // @ts-ignore diff --git a/app/javascript/interpreter/error.ts b/app/javascript/interpreter/error.ts index 09c41f78d6..44598400d6 100644 --- a/app/javascript/interpreter/error.ts +++ b/app/javascript/interpreter/error.ts @@ -59,7 +59,6 @@ export type SyntaxErrorType = | 'MissingColonAfterKey' | 'MissingFieldNameOrIndexAfterOpeningBracket' | 'InvalidTemplateLiteral' - | 'MissingColonAfterThenBranchOfTernaryOperator' | 'NumberEndsWithDecimalPoint' | 'NumberWithMultipleDecimalPoints' | 'NumberContainsAlpha' @@ -108,6 +107,7 @@ export type RuntimeErrorType = | 'InvalidIndexSetterTarget' | 'UnexpectedEqualsForEquality' | 'VariableAlreadyDeclared' + | 'VariableNotDeclared' export type StaticErrorType = | DisabledLanguageFeatureErrorType diff --git a/app/javascript/interpreter/evaluation-result.ts b/app/javascript/interpreter/evaluation-result.ts index ac2046b508..8186b6c327 100644 --- a/app/javascript/interpreter/evaluation-result.ts +++ b/app/javascript/interpreter/evaluation-result.ts @@ -1,28 +1,12 @@ import type { TokenType } from './token' -export type EvaluationResultVariableStatement = { - type: 'VariableStatement' +export type EvaluationResultSetVariableStatement = { + type: 'SetVariableStatement' value: any name: string data?: Record } -export type EvaluationResultTernaryExpression = { - type: 'TernaryExpression' - value: any - condition: EvaluationResult - data?: Record -} - -export type EvaluationResultUpdateExpression = { - type: 'UpdateExpression' - operand: any - operator: any - value: any - newValue: any - data?: Record -} - export type EvaluationResultIfStatement = { type: 'IfStatement' value: any @@ -105,11 +89,10 @@ export type EvaluationResultConstantStatement = { data?: Record } -export type EvaluationResultAssignExpression = { - type: 'AssignExpression' +export type EvaluationResultChangeVariableStatement = { + type: 'ChangeVariableStatement' name: string - operator: TokenType - value: any + oldValue: any newValue: any data?: Record } @@ -171,10 +154,9 @@ export type EvaluationResultCallExpression = { } export type EvaluationResult = - | EvaluationResultVariableStatement + | EvaluationResultSetVariableStatement | EvaluationResultUpdateExpression | EvaluationResultConstantStatement - | EvaluationResultTernaryExpression | EvaluationResultIfStatement | EvaluationResultExpressionStatement | EvaluationResultForeachStatement @@ -188,7 +170,7 @@ export type EvaluationResult = | EvaluationResultBinaryExpression | EvaluationResultUnaryExpression | EvaluationResultGroupingExpression - | EvaluationResultAssignExpression + | EvaluationResultChangeVariableStatement | EvaluationResultGetExpression | EvaluationResultSetExpression | EvaluationResultTemplateTextExpression diff --git a/app/javascript/interpreter/executor.ts b/app/javascript/interpreter/executor.ts index 969e7179f3..751570bd93 100644 --- a/app/javascript/interpreter/executor.ts +++ b/app/javascript/interpreter/executor.ts @@ -4,12 +4,11 @@ import { Environment } from './environment' import { RuntimeError, type RuntimeErrorType, isRuntimeError } from './error' import { ArrayExpression, - AssignExpression, + ChangeVariableStatement, BinaryExpression, CallExpression, DictionaryExpression, Expression, - type ExpressionVisitor, GetExpression, GroupingExpression, LiteralExpression, @@ -18,7 +17,6 @@ import { TemplateLiteralExpression, TemplatePlaceholderExpression, TemplateTextExpression, - TernaryExpression, UnaryExpression, UpdateExpression, VariableExpression, @@ -36,12 +34,14 @@ import { RepeatUntilGameOverStatement, ReturnStatement, Statement, - type StatementVisitor, - VariableStatement, + SetVariableStatement, WhileStatement, } from './statement' import type { Token } from './token' -import type { EvaluationResult } from './evaluation-result' +import type { + EvaluationResult, + EvaluationResultChangeVariableStatement, +} from './evaluation-result' import { translate } from './translator' import cloneDeep from 'lodash.clonedeep' import type { LanguageFeatures } from './interpreter' @@ -68,9 +68,7 @@ export type ExternalFunction = { class LogicError extends Error {} -export class Executor - implements ExpressionVisitor, StatementVisitor -{ +export class Executor { private frames: Frame[] = [] private frameTime: number = 0 private location: Location | null = null @@ -80,9 +78,13 @@ export class Executor private readonly globals = new Environment() private environment = this.globals + // This tracks variables for each statement, so we can output + // the changes in the frame descriptions + private statementStartingVariables: Record = {} + constructor( private readonly sourceCode: string, - private languageFeatures: LanguageFeatures = {}, + private languageFeatures: LanguageFeatures, private externalFunctions: ExternalFunction[], private externalState: Record = {} ) { @@ -145,6 +147,7 @@ export class Executor }) } + // TODO: Also start/end the statement management const result = this.evaluate(statement.expression) return { value: result.value, frames: this.frames, error: null } } catch (error) { @@ -210,7 +213,7 @@ export class Executor }) } - public visitVariableStatement(statement: VariableStatement): void { + public visitSetVariableStatement(statement: SetVariableStatement): void { this.executeFrame(statement, () => { if (this.environment.inScope(statement.name.lexeme)) { this.error('VariableAlreadyDeclared', statement.location, { @@ -227,24 +230,39 @@ export class Executor } ) } + this.environment.define(statement.name.lexeme, value) return { - type: 'VariableStatement', + type: 'SetVariableStatement', name: statement.name.lexeme, value: value, } }) } - public visitConstantStatement(statement: ConstantStatement): void { + public visitChangeVariableStatement( + statement: ChangeVariableStatement + ): void { this.executeFrame(statement, () => { - const result = this.evaluate(statement.initializer) - this.environment.define(statement.name.lexeme, result.value) + // Ensure the variable exists + if (!this.environment.inScope(statement.name.lexeme)) { + this.error('VariableNotDeclared', statement.location, { + name: statement.name.lexeme, + }) + } + + const value = this.evaluate(statement.value) + + this.updateVariable(statement.name, value.value, statement) + + const oldValue = this.statementStartingVariables[statement.name.lexeme] + return { - type: 'ConstantStatement', + type: 'ChangeVariableStatement', name: statement.name.lexeme, - value: result.value, + oldValue, + newValue: value, } }) } @@ -329,23 +347,6 @@ export class Executor } } - public visitWhileStatement(statement: WhileStatement): void { - while ( - this.executeFrame(statement, () => this.evaluate(statement.condition)) - ) - this.executeBlock(statement.body, this.environment) - } - - public visitDoWhileStatement(statement: DoWhileStatement): void { - do { - this.executeBlock(statement.body, this.environment) - } while ( - this.executeFrame(statement, () => - this.evaluate(statement.condition) - ) - ) - } - public visitBlockStatement(statement: BlockStatement): void { // Change this to allow scoping // this.executeBlock(statement.statements, new Environment(this.environment)) @@ -377,100 +378,102 @@ export class Executor throw new ReturnValue(evaluationResult.value) } - visitForeachStatement(statement: ForeachStatement): void { - const iterable = this.evaluate(statement.iterable) - if (!isArray(iterable.value) || iterable.value?.length === 0) { - this.executeFrame(statement, () => { - return { - type: 'ForeachStatement', - value: undefined, - iterable, - elementName: statement.elementName.lexeme, - } - }) - } - - for (const value of iterable.value) { - this.executeFrame(statement, () => { - return { - type: 'ForeachStatement', - value, - iterable, - elementName: statement.elementName.lexeme, - } - }) - - // TODO: Think about this. Currently it creates a new environment for each loop iteration - // with the element in. But that's maybe not what we want as it'll be a new scope - // and the rest of Jiki is currently not scoped on a block basis. - // Consider a `finally` to unset the variable instead? - const loopEnvironment = new Environment(this.environment) - loopEnvironment.define(statement.elementName.lexeme, value) - this.executeBlock(statement.body, loopEnvironment) - } - } - - visitTernaryExpression(expression: TernaryExpression): EvaluationResult { - const condition = this.evaluate(expression.condition) - this.verifyBooleanOperand(condition.value, expression.condition.location) - - const result = condition.value - ? this.evaluate(expression.thenBranch) - : this.evaluate(expression.elseBranch) - - return { - type: 'TernaryExpression', - value: result.value, - condition: condition, - } - } - - visitTemplateLiteralExpression( - expression: TemplateLiteralExpression - ): EvaluationResult { - return { - type: 'TemplateLiteralExpression', - value: expression.parts - .map((part) => this.evaluate(part).value.toString()) - .join(''), - } - } - - visitTemplatePlaceholderExpression( - expression: TemplatePlaceholderExpression - ): EvaluationResult { - return { - type: 'TemplatePlaceholderExpression', - value: this.evaluate(expression.inner).value, - } - } - - visitTemplateTextExpression( - expression: TemplateTextExpression - ): EvaluationResult { - return { - type: 'TemplateTextExpression', - value: expression.text.literal, - } - } - - visitArrayExpression(expression: ArrayExpression): EvaluationResult { - return { - type: 'ArrayExpression', - value: expression.elements.map((element) => this.evaluate(element).value), - } - } - - visitDictionaryExpression( - expression: DictionaryExpression - ): EvaluationResult { - let dict: Record = {} - - for (const [key, value] of expression.elements) - dict[key] = this.evaluate(value).value - - return { type: 'DictionaryExpression', value: dict } - } + // visitForeachStatement(statement: ForeachStatement): void { + // const iterable = this.evaluate(statement.iterable) + // if (!isArray(iterable.value) || iterable.value?.length === 0) { + // this.executeFrame(statement, () => { + // return { + // type: 'ForeachStatement', + // value: undefined, + // iterable, + // elementName: statement.elementName.lexeme, + // } + // }) + // } + + // for (const value of iterable.value) { + // this.executeFrame(statement, () => { + // return { + // type: 'ForeachStatement', + // value, + // iterable, + // elementName: statement.elementName.lexeme, + // } + // }) + + // // TODO: Think about this. Currently it creates a new environment for each loop iteration + // // with the element in. But that's maybe not what we want as it'll be a new scope + // // and the rest of Jiki is currently not scoped on a block basis. + // // Consider a `finally` to unset the variable instead? + // const loopEnvironment = new Environment(this.environment) + // loopEnvironment.define(statement.elementName.lexeme, value) + // this.executeBlock(statement.body, loopEnvironment) + // } + // } + + // public visitWhileStatement(statement: WhileStatement): void { + // while ( + // this.executeFrame(statement, () => this.evaluate(statement.condition)) + // ) + // this.executeBlock(statement.body, this.environment) + // } + + // public visitDoWhileStatement(statement: DoWhileStatement): void { + // do { + // this.executeBlock(statement.body, this.environment) + // } while ( + // this.executeFrame(statement, () => + // this.evaluate(statement.condition) + // ) + // ) + // } + + // visitTemplateLiteralExpression( + // expression: TemplateLiteralExpression + // ): EvaluationResult { + // return { + // type: 'TemplateLiteralExpression', + // value: expression.parts + // .map((part) => this.evaluate(part).value.toString()) + // .join(''), + // } + // } + + // visitTemplatePlaceholderExpression( + // expression: TemplatePlaceholderExpression + // ): EvaluationResult { + // return { + // type: 'TemplatePlaceholderExpression', + // value: this.evaluate(expression.inner).value, + // } + // } + + // visitTemplateTextExpression( + // expression: TemplateTextExpression + // ): EvaluationResult { + // return { + // type: 'TemplateTextExpression', + // value: expression.text.literal, + // } + // } + + // visitArrayExpression(expression: ArrayExpression): EvaluationResult { + // return { + // type: 'ArrayExpression', + // value: expression.elements.map((element) => this.evaluate(element).value), + // } + // } + + // visitDictionaryExpression( + // expression: DictionaryExpression + // ): EvaluationResult { + // let dict: Record = {} + + // for (const [key, value] of expression.elements) + // dict[key] = this.evaluate(value).value + + // return { type: 'DictionaryExpression', value: dict } + // } public visitCallExpression(expression: CallExpression): EvaluationResult { let callee: any @@ -630,23 +633,14 @@ export class Executor } switch (expression.operator.type) { - case 'STRICT_INEQUALITY': - // TODO: throw error when types are not the same? - return { ...result, value: left.value !== right.value } case 'INEQUALITY': // TODO: throw error when types are not the same? - return { ...result, value: left.value != right.value } - case 'STRICT_EQUALITY': - // TODO: throw error when types are not the same? - return { - ...result, - value: left.value === right.value, - } + return { ...result, value: left.value !== right.value } case 'EQUALITY': // TODO: throw error when types are not the same? return { ...result, - value: left.value == right.value, + value: left.value === right.value, } case 'GREATER': this.verifyNumberOperands(expression.operator, left.value, right.value) @@ -797,42 +791,6 @@ export class Executor } } - public visitAssignExpression(expression: AssignExpression): EvaluationResult { - // Ensure the variable resolves if we're updating - // and doesn't resolve if we're declaring - if (expression.updating) { - if (!this.environment.inScope(expression.name.lexeme)) { - this.error('VariableNotDeclared', expression.location, { - name: expression.name.lexeme, - }) - } - } - - const value = this.evaluate(expression.value) - const newValue = - expression.operator.type === 'EQUAL' || expression.operator.type === 'TO' - ? value.value - : expression.operator.type === 'PLUS_EQUAL' - ? this.lookupVariable(expression.name, expression) + value.value - : expression.operator.type === 'MINUS_EQUAL' - ? this.lookupVariable(expression.name, expression) - value.value - : expression.operator.type === 'STAR_EQUAL' - ? this.lookupVariable(expression.name, expression) * value.value - : expression.operator.type === 'SLASH_EQUAL' - ? this.lookupVariable(expression.name, expression) / value.value - : null - - this.updateVariable(expression.name, newValue, expression) - - return { - type: 'AssignExpression', - name: expression.name.lexeme, - operator: expression.operator.type, - value, - newValue, - } - } - public visitUpdateExpression(expression: UpdateExpression): EvaluationResult { let value let newValue @@ -974,11 +932,15 @@ export class Executor } public executeStatement(statement: Statement): void { - statement.accept(this) + this.statementStartingVariables = cloneDeep(this.environment.variables()) + + const method = `visit${statement.constructor.name}` + this[method](statement) } public evaluate(expression: Expression): EvaluationResult { - return expression.accept(this) + const method = `visit${expression.constructor.name}` + return this[method](expression) } private lookupVariable(name: Token, expression: Expression): any { @@ -1017,6 +979,7 @@ export class Executor status, result, error, + priorVariables: this.statementStartingVariables, variables: this.environment.variables(), functions: this.environment.functions(), time: this.frameTime, diff --git a/app/javascript/interpreter/expression.ts b/app/javascript/interpreter/expression.ts index e046d985a4..7a9030f4b0 100644 --- a/app/javascript/interpreter/expression.ts +++ b/app/javascript/interpreter/expression.ts @@ -1,81 +1,52 @@ import type { Token } from './token' import { Location } from './location' +import { FrameWithResult } from './frames' +import { EvaluationResult } from './evaluation-result' -export interface ExpressionVisitor { - visitCallExpression(expression: CallExpression): T - visitLiteralExpression(expression: LiteralExpression): T - visitVariableExpression(expression: VariableExpression): T - visitUnaryExpression(expression: UnaryExpression): T - visitBinaryExpression(expression: BinaryExpression): T - visitLogicalExpression(expression: LogicalExpression): T - visitGroupingExpression(expression: GroupingExpression): T - visitTemplateLiteralExpression(expression: TemplateLiteralExpression): T - visitTemplatePlaceholderExpression( - expression: TemplatePlaceholderExpression - ): T - visitTemplateTextExpression(expression: TemplateTextExpression): T - visitAssignExpression(expression: AssignExpression): T - visitUpdateExpression(expression: UpdateExpression): T - visitArrayExpression(expression: ArrayExpression): T - visitDictionaryExpression(expression: DictionaryExpression): T - visitGetExpression(expression: GetExpression): T - visitSetExpression(expression: SetExpression): T - visitTernaryExpression(expression: TernaryExpression): T +function quoteLiteral(value: any): string { + if (typeof value === 'string') { + return `"${value}"` + } + return value } export abstract class Expression { - abstract accept(visitor: ExpressionVisitor): T abstract location: Location } -export class CallExpression extends Expression { - constructor( - public callee: VariableExpression, - public paren: Token, - public args: Expression[], - public location: Location - ) { +export class LiteralExpression extends Expression { + constructor(public value: any, public location: Location) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitCallExpression(this) + public description() { + return `${quoteLiteral(this.value)}` } } -export class TernaryExpression extends Expression { - constructor( - public condition: Expression, - public thenBranch: Expression, - public elseBranch: Expression, - public location: Location - ) { +export class VariableExpression extends Expression { + constructor(public name: Token, public location: Location) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitTernaryExpression(this) + public description() { + return `the ${this.name.lexeme} variable` } } -export class LiteralExpression extends Expression { - constructor(public value: any, public location: Location) { +export class CallExpression extends Expression { + constructor( + public callee: VariableExpression, + public paren: Token, + public args: Expression[], + public location: Location + ) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitLiteralExpression(this) - } } export class ArrayExpression extends Expression { constructor(public elements: Expression[], public location: Location) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitArrayExpression(this) - } } export class DictionaryExpression extends Expression { @@ -85,20 +56,6 @@ export class DictionaryExpression extends Expression { ) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitDictionaryExpression(this) - } -} - -export class VariableExpression extends Expression { - constructor(public name: Token, public location: Location) { - super() - } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitVariableExpression(this) - } } export class BinaryExpression extends Expression { @@ -110,10 +67,6 @@ export class BinaryExpression extends Expression { ) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitBinaryExpression(this) - } } export class LogicalExpression extends Expression { @@ -125,10 +78,6 @@ export class LogicalExpression extends Expression { ) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitLogicalExpression(this) - } } export class UnaryExpression extends Expression { @@ -139,66 +88,30 @@ export class UnaryExpression extends Expression { ) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitUnaryExpression(this) - } } export class GroupingExpression extends Expression { constructor(public inner: Expression, public location: Location) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitGroupingExpression(this) - } } export class TemplatePlaceholderExpression extends Expression { constructor(public inner: Expression, public location: Location) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitTemplatePlaceholderExpression(this) - } } export class TemplateTextExpression extends Expression { constructor(public text: Token, public location: Location) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitTemplateTextExpression(this) - } } export class TemplateLiteralExpression extends Expression { constructor(public parts: Expression[], public location: Location) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitTemplateLiteralExpression(this) - } -} - -export class AssignExpression extends Expression { - constructor( - public name: Token, - public operator: Token, - public value: Expression, - public updating: boolean, - public location: Location - ) { - super() - } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitAssignExpression(this) - } } export class UpdateExpression extends Expression { @@ -209,10 +122,6 @@ export class UpdateExpression extends Expression { ) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitUpdateExpression(this) - } } export class GetExpression extends Expression { @@ -223,10 +132,6 @@ export class GetExpression extends Expression { ) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitGetExpression(this) - } } export class SetExpression extends Expression { @@ -238,8 +143,4 @@ export class SetExpression extends Expression { ) { super() } - - accept(visitor: ExpressionVisitor): T { - return visitor.visitSetExpression(this) - } } diff --git a/app/javascript/interpreter/frames.ts b/app/javascript/interpreter/frames.ts index a004ffc53d..7f4ed4143c 100644 --- a/app/javascript/interpreter/frames.ts +++ b/app/javascript/interpreter/frames.ts @@ -2,7 +2,10 @@ import { type Callable } from './functions' import { RuntimeError } from './error' export type FrameExecutionStatus = 'SUCCESS' | 'ERROR' -import type { EvaluationResult } from './evaluation-result' +import type { + EvaluationResult, + EvaluationResultChangeVariableStatement, +} from './evaluation-result' import type { ExternalFunction } from './executor' import { BinaryExpression, @@ -12,8 +15,13 @@ import { LogicalExpression, VariableExpression, } from './expression' -import { IfStatement, Statement } from './statement' -import exp from 'constants' +import { + ExpressionStatement, + IfStatement, + SetVariableStatement, + Statement, + ChangeVariableStatement, +} from './statement' export type FrameType = 'ERROR' | 'REPEAT' | 'EXPRESSION' @@ -22,6 +30,7 @@ export type Frame = { code: string status: FrameExecutionStatus error?: RuntimeError + priorVariables: Record variables: Record functions: Record time: number @@ -53,12 +62,12 @@ export function describeFrame( return '

There is no information available for this line.

' } switch (frame.result.type) { - case 'VariableStatement': - return describeVariableStatement(frame) + case 'SetVariableStatement': + return describeSetVariableStatement(frame) case 'ForeachStatement': return describeForeachStatement(frame) - case 'AssignExpression': - return describeAssignExpression(frame) + case 'ChangeVariableStatement': + return describeChangeVariableStatement(frame) case 'IfStatement': return describeIfStatement(frame) case 'ReturnStatement': @@ -79,23 +88,25 @@ function addExtraAssignInfo(frame: Frame, output: string) { return output } -function describeVariableStatement(frame: FrameWithResult) { - let output - if (frame.result.data?.updating) { - output = `

This updated the ${frame.result.name} variable to ${frame.result.value}.

` - } else { - output = `

This created a new variable called ${frame.result.name} and sets it to be equal to ${frame.result.value}.

` +function describeSetVariableStatement(frame: FrameWithResult) { + const context = frame.context as SetVariableStatement + if (context === undefined) { + return '' } - output = addExtraAssignInfo(frame, output) - - return output + return context.description(frame.result) } -function describeAssignExpression(frame: FrameWithResult) { - let output = `

This updated the variable called ${frame.result.name} to ${frame.result.value.value}.

` - output = addExtraAssignInfo(frame, output) +function describeChangeVariableStatement(frame: FrameWithResult): string { + if (!frame.context === null) { + return '' + } + if (!(frame.context instanceof ChangeVariableStatement)) { + return '' + } - return output + return frame.context.description( + frame.result as EvaluationResultChangeVariableStatement + ) } function describeForeachStatement(frame: FrameWithResult) { @@ -115,10 +126,10 @@ function describeForeachStatement(frame: FrameWithResult) { function describeExpression(expression: Expression) { if (expression instanceof VariableExpression) { - return describeVariableExpression(expression) + return expression.description() } if (expression instanceof LiteralExpression) { - return describeLiteralExpression(expression) + return expression.description() } if (expression instanceof GroupingExpression) { return describeGroupingExpression(expression) @@ -132,18 +143,6 @@ function describeExpression(expression: Expression) { return '' } -function describeVariableExpression(expression: VariableExpression): string { - return `the ${expression.name.lexeme} variable` -} - -function describeLiteralExpression(expression: LiteralExpression): string { - let value = expression.value - if (typeof expression.value === 'string') { - value = '"' + expression.value + '"' - } - - return `${value}` -} function describeOperator(operator: string): string { switch (operator) { case 'GREATER': @@ -154,9 +153,9 @@ function describeOperator(operator: string): string { return 'greater than or equal to' case 'LESS_EQUAL': return 'less than or equal to' - case 'STRICT_EQUALITY': + case 'EQUALITY': return 'equal to' - case 'STRICT_INEQUALITY': + case 'INEQUALITY': return 'not equal to' case 'MINUS': return 'minus' @@ -234,8 +233,8 @@ function describeCallExpression( function isEqualityOperator(operator: string): boolean { return [ - 'STRICT_EQUALITY', - 'STRICT_INEQUALITY', + 'EQUALITY', + 'INEQUALITY', 'GREATER', 'LESS', 'GREATER_EQUAL', diff --git a/app/javascript/interpreter/locales/en/translation.json b/app/javascript/interpreter/locales/en/translation.json index b096a9f541..d231ff5e28 100644 --- a/app/javascript/interpreter/locales/en/translation.json +++ b/app/javascript/interpreter/locales/en/translation.json @@ -8,7 +8,6 @@ "MissingBacktickToTerminateTemplateLiteral": "Missing backtick ('`') to terminate template literal.", "MissingConditionAfterIf": "Did you forget to add a condition to your if statement?", "MissingCommaBetweenParameters": "Did you forget to add a command after the `{{parameter}}` parameter?", - "MissingColonAfterThenBranchOfTernaryOperator": "Expect ':' after then branch of ternary operator.", "MissingConstantName": "Expect constant name.", "MissingDoToStartBlock": "Are you missing a `do` to start the {{type}} body?", "MissingDoubleQuoteToStartString": "Did you forget the start quote for the \"{{string}}\" string?", diff --git a/app/javascript/interpreter/locales/nl/translation.json b/app/javascript/interpreter/locales/nl/translation.json index 3cb7e1ac26..2cc64fb52b 100644 --- a/app/javascript/interpreter/locales/nl/translation.json +++ b/app/javascript/interpreter/locales/nl/translation.json @@ -10,7 +10,6 @@ "MissingLeftParenthesisAfterFunctionCall": "Ben je het beginnende haakje vergeten om de {{function}} functie aan te roepen?", "ExceededMaximumNumberOfParameters": "Een function kan niet meer dan 255 parameters hebben.", "InvalidAssignmentTarget": "Ongeldige toewijzingsdoel.", - "MissingColonAfterThenBranchOfTernaryOperator": "Ontbrekende ':' na de then branch van de ternaire operator.", "MissingFieldNameOrIndexAfterLeftBracket": "Ontbrekende veld naam of index na '['.", "MissingRightBracketAfterFieldNameOrIndex": "Ontbrekende ']' na veld naam of index", "MissingRightParenthesisAfterExpression": "Ontbrekende ')' na expressie.", diff --git a/app/javascript/interpreter/locales/system/translation.json b/app/javascript/interpreter/locales/system/translation.json index 2f38b5d2b3..bcfd56862f 100644 --- a/app/javascript/interpreter/locales/system/translation.json +++ b/app/javascript/interpreter/locales/system/translation.json @@ -7,7 +7,6 @@ "InvalidTemplateLiteral": "InvalidTemplateLiteral", "MissingBacktickToTerminateTemplateLiteral": "MissingBacktickToTerminateTemplateLiteral", "MissingConditionAfterIf": "MissingConditionAfterIf", - "MissingColonAfterThenBranchOfTernaryOperator": "MissingColonAfterThenBranchOfTernaryOperator", "MissingCommaBetweenParameters": "MissingCommaBetweenParameters: parameter: {{parameter}}", "MissingConstantName": "MissingConstantName", "MissingDoToStartBlock": "MissingDoToStartBlock: type: {{type}}", diff --git a/app/javascript/interpreter/parser.ts b/app/javascript/interpreter/parser.ts index 49de01ff88..78fdc3c626 100644 --- a/app/javascript/interpreter/parser.ts +++ b/app/javascript/interpreter/parser.ts @@ -2,7 +2,6 @@ import { SyntaxError } from './error' import { type SyntaxErrorType } from './error' import { ArrayExpression, - AssignExpression, BinaryExpression, CallExpression, Expression, @@ -32,8 +31,9 @@ import { RepeatUntilGameOverStatement, ReturnStatement, Statement, - VariableStatement, + SetVariableStatement, WhileStatement, + ChangeVariableStatement, } from './statement' import type { Token, TokenType } from './token' import { translate } from './translator' @@ -157,8 +157,8 @@ export class Parser { } private statement(): Statement { - if (this.match('SET')) return this.setStatement() - if (this.match('CHANGE')) return this.changeStatement() + if (this.match('SET')) return this.setVariableStatement() + if (this.match('CHANGE')) return this.changeVariableStatement() if (this.match('IF')) return this.ifStatement() if (this.match('RETURN')) return this.returnStatement() if (this.match('REPEAT')) return this.repeatStatement() @@ -176,99 +176,63 @@ export class Parser { return this.expressionStatement() } - private setStatement(): Statement { + private setupVariableStatement(): Statement { const setToken = this.previous() - if (this.peek(2).type == 'LEFT_BRACKET') { - const assignment = this.assignment() - this.consumeEndOfLine() - - return new ExpressionStatement( - assignment, - Location.between(setToken, assignment) - ) - } else { - let name - try { - name = this.consume('IDENTIFIER', 'MissingVariableName') - } catch (e) { - const nameLexeme = this.peek().lexeme - if (nameLexeme.match(/[0-9]/)) { - this.error('InvalidNumericVariableName', this.peek().location, { - name: nameLexeme, - }) - } else { - throw e - } - } - if ( - (this.peek().type == 'IDENTIFIER' || this.peek().type == 'STRING') && - this.peek(2).type == 'TO' - ) { - const errorLocation = Location.between(this.previous(), this.peek()) - this.error('UnexpectedSpaceInIdentifier', errorLocation, { - first_half: name.lexeme, - second_half: this.peek().lexeme, + let name + try { + name = this.consume('IDENTIFIER', 'MissingVariableName') + } catch (e) { + const nameLexeme = this.peek().lexeme + if (nameLexeme.match(/[0-9]/)) { + this.error('InvalidNumericVariableName', this.peek().location, { + name: nameLexeme, }) + } else { + throw e } + } - // Guard mistaken equals sign for assignment - this.guardEqualsSignForAssignment(this.peek()) - - this.consume('TO', 'MissingToAfterVariableNameToInitializeValue', { - name: name.lexeme, + if ( + (this.peek().type == 'IDENTIFIER' || this.peek().type == 'STRING') && + this.peek(2).type == 'TO' + ) { + const errorLocation = Location.between(this.previous(), this.peek()) + this.error('UnexpectedSpaceInIdentifier', errorLocation, { + first_half: name.lexeme, + second_half: this.peek().lexeme, }) + } - const initializer = this.expression() - this.consumeEndOfLine() + // Guard mistaken equals sign for assignment + this.guardEqualsSignForAssignment(this.peek()) - return new VariableStatement( - name, - initializer, - Location.between(setToken, initializer) - ) - } - } - private changeStatement(): Statement { - const setToken = this.previous() - // if (this.peek(2).type == 'LEFT_BRACKET') { - // const assignment = this.assignment() - // this.consumeEndOfLine() - - // return new ExpressionStatement( - // assignment, - // Location.between(setToken, assignment) - // ) - // } else { - const name = this.consume('IDENTIFIER', 'MissingVariableName') - - const token = this.consume( - 'TO', - 'MissingToAfterVariableNameToInitializeValue', - { - name, - } - ) + this.consume('TO', 'MissingToAfterVariableNameToInitializeValue', { + name: name.lexeme, + }) const initializer = this.expression() this.consumeEndOfLine() - // return new VariableStatement( - // name, - // initializer, - // Location.between(setToken, initializer) - // ) - return new ExpressionStatement( - new AssignExpression( - name, - token, - initializer, - true, - Location.between(setToken, initializer) - ), + return [name, initializer, setToken] + } + + private setVariableStatement(): Statement { + const [name, initializer, setToken] = this.setupVariableStatement() + return new SetVariableStatement( + name, + initializer, Location.between(setToken, initializer) ) - // } + } + + private changeVariableStatement(): Statement { + const [name, initializer, changeToken] = this.setupVariableStatement() + return new ChangeVariableStatement( + name, + initializer, + Location.between(changeToken, initializer) + ) } private ifStatement(): Statement { @@ -469,7 +433,7 @@ export class Parser { const value = this.assignment() if (expr instanceof VariableExpression) { - return new AssignExpression( + return new ChangeVariableStatement( expr.name, operator, value, @@ -534,7 +498,7 @@ export class Parser { private equality(): Expression { let expr = this.comparison() - while (this.match('STRICT_EQUALITY')) { + while (this.match('EQUALITY')) { let operator = this.previous() const right = this.comparison() expr = new BinaryExpression( @@ -559,8 +523,8 @@ export class Parser { 'GREATER_EQUAL', 'LESS', 'LESS_EQUAL', - 'STRICT_EQUALITY', - 'STRICT_INEQUALITY' + 'EQUALITY', + 'INEQUALITY' ) ) { const operator = this.previous() diff --git a/app/javascript/interpreter/scanner.ts b/app/javascript/interpreter/scanner.ts index 7ef929a57c..2127ffbc2c 100644 --- a/app/javascript/interpreter/scanner.ts +++ b/app/javascript/interpreter/scanner.ts @@ -47,7 +47,7 @@ export class Scanner { function: 'FUNCTION', if: 'IF', in: 'IN', - is: 'STRICT_EQUALITY', + is: 'EQUALITY', null: 'NULL', not: 'NOT', or: 'OR', @@ -146,11 +146,11 @@ export class Scanner { * or do simple checks for the next characters (e.g. "++") */ private tokenizeBang() { - this.addToken(this.match('=') ? 'STRICT_INEQUALITY' : 'NOT') + this.addToken(this.match('=') ? 'INEQUALITY' : 'NOT') } private tokenizeEqual() { - this.addToken(this.match('=') ? 'STRICT_EQUALITY' : 'EQUAL') + this.addToken(this.match('=') ? 'EQUALITY' : 'EQUAL') } private tokenizeLeftParanthesis() { this.addToken('LEFT_PAREN') @@ -346,10 +346,10 @@ export class Scanner { private tokenForLexeme(lexeme: string): string { if (lexeme == 'is') { - return 'STRICT_EQUALITY' + return 'EQUALITY' } if (lexeme == 'equals') { - return 'STRICT_EQUALITY' + return 'EQUALITY' } return Scanner.keywords[this.lexeme()] diff --git a/app/javascript/interpreter/statement.ts b/app/javascript/interpreter/statement.ts index f9226b1c33..009c2c9fb9 100644 --- a/app/javascript/interpreter/statement.ts +++ b/app/javascript/interpreter/statement.ts @@ -1,24 +1,19 @@ +import { + EvaluationResult, + EvaluationResultChangeVariableStatement, + EvaluationResultSetVariableStatement, +} from './evaluation-result' import { Expression } from './expression' import { Location } from './location' import type { Token } from './token' -export interface StatementVisitor { - visitExpressionStatement(statement: ExpressionStatement): T - visitVariableStatement(statement: VariableStatement): T - visitConstantStatement(statement: ConstantStatement): T - visitIfStatement(statement: IfStatement): T - visitRepeatStatement(statement: RepeatStatement): T - visitRepeatUntilGameOverStatement(statement: RepeatUntilGameOverStatement): T - visitBlockStatement(statement: BlockStatement): T - visitFunctionStatement(statement: FunctionStatement): T - visitReturnStatement(statement: ReturnStatement): T - visitForeachStatement(statement: ForeachStatement): T - visitWhileStatement(statement: WhileStatement): T - visitDoWhileStatement(statement: DoWhileStatement): T +function quoteLiteral(value: any): string { + if (typeof value === 'string') { + return `"${value}"` + } + return value } - export abstract class Statement { - abstract accept(visitor: StatementVisitor): T abstract location: Location } @@ -26,23 +21,42 @@ export class ExpressionStatement extends Statement { constructor(public expression: Expression, public location: Location) { super() } +} + +export class SetVariableStatement extends Statement { + constructor( + public name: Token, + public initializer: Expression, + public location: Location + ) { + super() + } - public accept(visitor: StatementVisitor): T { - return visitor.visitExpressionStatement(this) + public description(result: EvaluationResultSetVariableStatement) { + return `

This created a new variable called ${ + result.name + } and sets its value to ${quoteLiteral( + result.value + )}.

` } } -export class VariableStatement extends Statement { +export class ChangeVariableStatement extends Statement { constructor( public name: Token, - public initializer: Expression, + public value: Expression, public location: Location ) { super() } - public accept(visitor: StatementVisitor): T { - return visitor.visitVariableStatement(this) + public description(result: EvaluationResultChangeVariableStatement) { + let output = `

This updated the variable called ${result.name} from...

` + output += `
${quoteLiteral(result.oldValue)}
` + output += `

to...

${quoteLiteral(
+      result.newValue.value
+    )}
` + return output } } @@ -54,10 +68,6 @@ export class ConstantStatement extends Statement { ) { super() } - - public accept(visitor: StatementVisitor): T { - return visitor.visitConstantStatement(this) - } } export class IfStatement extends Statement { @@ -69,10 +79,6 @@ export class IfStatement extends Statement { ) { super() } - - public accept(visitor: StatementVisitor): T { - return visitor.visitIfStatement(this) - } } export class RepeatStatement extends Statement { @@ -83,20 +89,12 @@ export class RepeatStatement extends Statement { ) { super() } - - public accept(visitor: StatementVisitor): T { - return visitor.visitRepeatStatement(this) - } } export class RepeatUntilGameOverStatement extends Statement { constructor(public body: Statement[], public location: Location) { super() } - - public accept(visitor: StatementVisitor): T { - return visitor.visitRepeatUntilGameOverStatement(this) - } } export class WhileStatement extends Statement { @@ -107,10 +105,6 @@ export class WhileStatement extends Statement { ) { super() } - - public accept(visitor: StatementVisitor): T { - return visitor.visitWhileStatement(this) - } } export class DoWhileStatement extends Statement { @@ -121,20 +115,12 @@ export class DoWhileStatement extends Statement { ) { super() } - - public accept(visitor: StatementVisitor): T { - return visitor.visitDoWhileStatement(this) - } } export class BlockStatement extends Statement { constructor(public statements: Statement[], public location: Location) { super() } - - public accept(visitor: StatementVisitor): T { - return visitor.visitBlockStatement(this) - } } export class FunctionParameter { @@ -150,10 +136,6 @@ export class FunctionStatement extends Statement { ) { super() } - - public accept(visitor: StatementVisitor): T { - return visitor.visitFunctionStatement(this) - } } export class ReturnStatement extends Statement { @@ -164,10 +146,6 @@ export class ReturnStatement extends Statement { ) { super() } - - public accept(visitor: StatementVisitor): T { - return visitor.visitReturnStatement(this) - } } export class ForeachStatement extends Statement { @@ -179,8 +157,4 @@ export class ForeachStatement extends Statement { ) { super() } - - public accept(visitor: StatementVisitor): T { - return visitor.visitForeachStatement(this) - } } diff --git a/app/javascript/interpreter/token.ts b/app/javascript/interpreter/token.ts index df1a09e854..4de6ab276f 100644 --- a/app/javascript/interpreter/token.ts +++ b/app/javascript/interpreter/token.ts @@ -59,8 +59,8 @@ export type TokenType = | 'WITH' // Grouping tokens - | 'STRICT_EQUALITY' - | 'STRICT_INEQUALITY' + | 'EQUALITY' + | 'INEQUALITY' // Invisible tokens | 'EOL' // End of statement diff --git a/test/javascript/interpreter/concepts/arrays.test.ts b/test/javascript/interpreter/concepts/arrays.test.ts new file mode 100644 index 0000000000..4722a56264 --- /dev/null +++ b/test/javascript/interpreter/concepts/arrays.test.ts @@ -0,0 +1,164 @@ +import { + Interpreter, + interpret, + evaluateFunction, +} from '@/interpreter/interpreter' +import type { ExecutionContext } from '@/interpreter/executor' +import { changeLanguage } from '@/interpreter/translator' + +beforeAll(() => { + changeLanguage('system') +}) + +afterAll(() => { + changeLanguage('en') +}) + +describe.skip('arrays', () => { + describe('set', () => { + test('single index', () => { + const { frames } = interpret(` + set scores to [7, 3, 10] + set scores[2] to 5 + `) + expect(frames).toBeArrayOfSize(2) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ + scores: [7, 3, 10], + }) + expect(frames[1].status).toBe('SUCCESS') + expect(frames[1].variables).toMatchObject({ + scores: [7, 3, 5], + }) + }) + + test('chained', () => { + const { frames } = interpret(` + set scoreMinMax to [[3, 7], [1, 6]] + set scoreMinMax[1][0] to 4 + `) + expect(frames).toBeArrayOfSize(2) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ + scoreMinMax: [ + [3, 7], + [1, 6], + ], + }) + expect(frames[1].status).toBe('SUCCESS') + expect(frames[1].variables).toMatchObject({ + scoreMinMax: [ + [3, 7], + [4, 6], + ], + }) + }) + }) + + describe('get', () => { + test('single index', () => { + const { frames } = interpret(` + set scores to [7, 3, 10] + set latest to scores[2] + `) + expect(frames).toBeArrayOfSize(2) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ + scores: [7, 3, 10], + }) + expect(frames[1].status).toBe('SUCCESS') + expect(frames[1].variables).toMatchObject({ + scores: [7, 3, 10], + latest: 10, + }) + }) + + test('chained', () => { + const { frames } = interpret(` + set scoreMinMax to [[3, 7], [1, 6]] + set secondMin to scoreMinMax[1][0] + `) + expect(frames).toBeArrayOfSize(2) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ + scoreMinMax: [ + [3, 7], + [1, 6], + ], + }) + expect(frames[1].status).toBe('SUCCESS') + expect(frames[1].variables).toMatchObject({ + scoreMinMax: [ + [3, 7], + [1, 6], + ], + secondMin: 1, + }) + }) + }) + + describe.skip('foreach', () => { + test('empty iterable', () => { + const echos: string[] = [] + const context = { + externalFunctions: [ + { + name: 'echo', + func: (_: any, n: any) => { + echos.push(n.toString()) + }, + description: '', + }, + ], + } + + const { frames } = interpret( + ` + foreach num in [] do + echo(num) + end + `, + context + ) + expect(frames).toBeArrayOfSize(1) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toBeEmpty() + expect(echos).toBeEmpty() + }) + test('multiple times', () => { + const echos: string[] = [] + const context = { + externalFunctions: [ + { + name: 'echo', + func: (_: any, n: any) => { + echos.push(n.toString()) + }, + description: '', + }, + ], + } + + const { frames } = interpret( + ` + foreach num in [1, 2, 3] do + echo(num) + end + `, + context + ) + expect(frames).toBeArrayOfSize(6) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toBeEmpty() + expect(frames[1].status).toBe('SUCCESS') + expect(frames[1].variables).toMatchObject({ num: 1 }) + expect(frames[2].status).toBe('SUCCESS') + expect(frames[3].status).toBe('SUCCESS') + expect(frames[3].variables).toMatchObject({ num: 2 }) + expect(frames[4].status).toBe('SUCCESS') + expect(frames[5].status).toBe('SUCCESS') + expect(frames[5].variables).toMatchObject({ num: 3 }) + expect(echos).toEqual(['1', '2', '3']) + }) + }) +}) diff --git a/test/javascript/interpreter/concepts/dictionaries.test.ts b/test/javascript/interpreter/concepts/dictionaries.test.ts new file mode 100644 index 0000000000..4688bd41d1 --- /dev/null +++ b/test/javascript/interpreter/concepts/dictionaries.test.ts @@ -0,0 +1,126 @@ +import { + Interpreter, + interpret, + evaluateFunction, +} from '@/interpreter/interpreter' +import type { ExecutionContext } from '@/interpreter/executor' +import { changeLanguage } from '@/interpreter/translator' + +beforeAll(() => { + changeLanguage('system') +}) + +afterAll(() => { + changeLanguage('en') +}) + +describe.skip('dictionary', () => { + describe('set', () => { + test('single field', () => { + const { frames } = interpret(` + set movie to {"title": "The Matrix"} + set movie["title"] to "Gladiator" + `) + expect(frames).toBeArrayOfSize(2) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ + movie: { title: 'The Matrix' }, + }) + expect(frames[1].status).toBe('SUCCESS') + expect(frames[1].variables).toMatchObject({ + movie: { title: 'Gladiator' }, + }) + }) + + test('chained', () => { + const { frames } = interpret(` + set movie to {"director": {"name": "Peter Jackson"}} + set movie["director"]["name"] to "James Cameron" + `) + expect(frames).toBeArrayOfSize(2) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ + movie: { director: { name: 'Peter Jackson' } }, + }) + expect(frames[1].status).toBe('SUCCESS') + expect(frames[1].variables).toMatchObject({ + movie: { director: { name: 'James Cameron' } }, + }) + }) + }) + + describe('get', () => { + test('single field', () => { + const { frames } = interpret(` + set movie to {"title": "The Matrix"} + set title to movie["title"] + `) + expect(frames).toBeArrayOfSize(2) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ + movie: { title: 'The Matrix' }, + }) + expect(frames[1].status).toBe('SUCCESS') + expect(frames[1].variables).toMatchObject({ + movie: { title: 'The Matrix' }, + title: 'The Matrix', + }) + }) + + test('chained', () => { + const { frames } = interpret(` + set movie to {"director": {"name": "Peter Jackson"}} + set name to movie["director"]["name"] + `) + expect(frames).toBeArrayOfSize(2) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ + movie: { director: { name: 'Peter Jackson' } }, + }) + expect(frames[1].status).toBe('SUCCESS') + expect(frames[1].variables).toMatchObject({ + movie: { director: { name: 'Peter Jackson' } }, + name: 'Peter Jackson', + }) + }) + }) + + describe('parsing', () => { + test('single field', () => { + const stmts = parse(` + set movie to {"title": "The Matrix"} + set movie["title"] to "Gladiator" + `) + expect(stmts).toBeArrayOfSize(2) + expect(stmts[0]).toBeInstanceOf(SetVariableStatement) + expect(stmts[1]).toBeInstanceOf(ExpressionStatement) + const exprStmt = stmts[1] as ExpressionStatement + expect(exprStmt.expression).toBeInstanceOf(SetExpression) + const setExpr = exprStmt.expression as SetExpression + expect(setExpr.field.literal).toBe('title') + expect(setExpr.obj).toBeInstanceOf(VariableExpression) + expect((setExpr.obj as VariableExpression).name.lexeme).toBe('movie') + }) + + test('chained', () => { + const stmts = parse(` + set movie to {"director": {"name": "Peter Jackson"}} + set movie["director"]["name"] to "James Cameron" + `) + expect(stmts).toBeArrayOfSize(2) + expect(stmts[0]).toBeInstanceOf(SetVariableStatement) + expect(stmts[1]).toBeInstanceOf(ExpressionStatement) + const exprStmt = stmts[1] as ExpressionStatement + expect(exprStmt.expression).toBeInstanceOf(SetExpression) + const setExpr = exprStmt.expression as SetExpression + expect(setExpr.value).toBeInstanceOf(LiteralExpression) + expect((setExpr.value as LiteralExpression).value).toBe('James Cameron') + expect(setExpr.field.literal).toBe('name') + expect(setExpr.obj).toBeInstanceOf(GetExpression) + const getExpr = setExpr.obj as GetExpression + expect(getExpr.obj).toBeInstanceOf(VariableExpression) + expect((getExpr.obj as VariableExpression).name.lexeme).toBe('movie') + expect(getExpr.field.literal).toBe('director') + }) + }) +}) diff --git a/test/javascript/interpreter/concepts/template_literals.test.ts b/test/javascript/interpreter/concepts/template_literals.test.ts new file mode 100644 index 0000000000..68619d6b59 --- /dev/null +++ b/test/javascript/interpreter/concepts/template_literals.test.ts @@ -0,0 +1,123 @@ +import { + Interpreter, + interpret, + evaluateFunction, +} from '@/interpreter/interpreter' +import type { ExecutionContext } from '@/interpreter/executor' +import { changeLanguage } from '@/interpreter/translator' + +beforeAll(() => { + changeLanguage('system') +}) + +afterAll(() => { + changeLanguage('en') +}) + +describe.skip('template literals', () => { + test('text only', () => { + const { frames } = interpret('set x to `hello`') + expect(frames).toBeArrayOfSize(1) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ x: 'hello' }) + }) + + test('placeholder only', () => { + const { frames } = interpret('set x to `${3*4}`') + expect(frames).toBeArrayOfSize(1) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ x: '12' }) + }) + + test('string', () => { + const { frames } = interpret('set x to `hello ${"there"}`') + expect(frames).toBeArrayOfSize(1) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ x: 'hello there' }) + }) + + test('variable', () => { + const { frames } = interpret(` + set x to 1 + set y to \`x is \${x}\` + `) + expect(frames).toBeArrayOfSize(2) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ x: 1 }) + expect(frames[1].status).toBe('SUCCESS') + expect(frames[1].variables).toMatchObject({ x: 1, y: 'x is 1' }) + }) + + test('expression', () => { + const { frames } = interpret('set x to `2 + 3 = ${2 + 3}`') + expect(frames).toBeArrayOfSize(1) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ x: '2 + 3 = 5' }) + }) + + test('complex', () => { + const { frames } = interpret('set x to `${2} + ${"three"} = ${2 + 9 / 3}`') + expect(frames).toBeArrayOfSize(1) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ x: '2 + three = 5' }) + }) + + describe('parsing', () => { + test('empty', () => { + const stmts = parse('``') + expect(stmts).toBeArrayOfSize(1) + expect(stmts[0]).toBeInstanceOf(ExpressionStatement) + const exprStmt = stmts[0] as ExpressionStatement + expect(exprStmt.expression).toBeInstanceOf(TemplateLiteralExpression) + const templateExpr = exprStmt.expression as TemplateLiteralExpression + expect(templateExpr.parts).toBeEmpty() + }) + + test('no placeholders', () => { + const stmts = parse('`hello there`') + expect(stmts).toBeArrayOfSize(1) + expect(stmts[0]).toBeInstanceOf(ExpressionStatement) + const exprStmt = stmts[0] as ExpressionStatement + expect(exprStmt.expression).toBeInstanceOf(TemplateLiteralExpression) + const templateExpr = exprStmt.expression as TemplateLiteralExpression + expect(templateExpr.parts).toBeArrayOfSize(1) + expect(templateExpr.parts[0]).toBeInstanceOf(TemplateTextExpression) + const textExpr = templateExpr.parts[0] as TemplateTextExpression + expect(textExpr.text.literal).toBe('hello there') + }) + + test('placeholders', () => { + const stmts = parse('`sum of ${2} + ${1+3*5} = ${2+(1+4*5)}`') + expect(stmts).toBeArrayOfSize(1) + expect(stmts[0]).toBeInstanceOf(ExpressionStatement) + const exprStmt = stmts[0] as ExpressionStatement + expect(exprStmt.expression).toBeInstanceOf(TemplateLiteralExpression) + const templateExpr = exprStmt.expression as TemplateLiteralExpression + expect(templateExpr.parts).toBeArrayOfSize(6) + expect(templateExpr.parts[0]).toBeInstanceOf(TemplateTextExpression) + expect(templateExpr.parts[1]).toBeInstanceOf( + TemplatePlaceholderExpression + ) + expect(templateExpr.parts[2]).toBeInstanceOf(TemplateTextExpression) + expect(templateExpr.parts[3]).toBeInstanceOf( + TemplatePlaceholderExpression + ) + expect(templateExpr.parts[4]).toBeInstanceOf(TemplateTextExpression) + expect(templateExpr.parts[5]).toBeInstanceOf( + TemplatePlaceholderExpression + ) + const part0Expr = templateExpr.parts[0] as TemplateTextExpression + expect(part0Expr.text.lexeme).toBe('sum of ') + const part1Expr = templateExpr.parts[1] as TemplatePlaceholderExpression + expect(part1Expr.inner).toBeInstanceOf(LiteralExpression) + const part2Expr = templateExpr.parts[2] as TemplateTextExpression + expect(part2Expr.text.lexeme).toBe(' + ') + const part3Expr = templateExpr.parts[3] as TemplatePlaceholderExpression + expect(part3Expr.inner).toBeInstanceOf(BinaryExpression) + const part4Expr = templateExpr.parts[4] as TemplateTextExpression + expect(part4Expr.text.lexeme).toBe(' = ') + const part5Expr = templateExpr.parts[5] as TemplatePlaceholderExpression + expect(part5Expr.inner).toBeInstanceOf(BinaryExpression) + }) + }) +}) diff --git a/test/javascript/interpreter/concepts/while.test.ts b/test/javascript/interpreter/concepts/while.test.ts new file mode 100644 index 0000000000..4417a0b8d4 --- /dev/null +++ b/test/javascript/interpreter/concepts/while.test.ts @@ -0,0 +1,61 @@ +import { + Interpreter, + interpret, + evaluateFunction, +} from '@/interpreter/interpreter' +import type { ExecutionContext } from '@/interpreter/executor' +import { changeLanguage } from '@/interpreter/translator' + +beforeAll(() => { + changeLanguage('system') +}) + +afterAll(() => { + changeLanguage('en') +}) + +describe.skip('while', () => { + test('once', () => { + const { frames } = interpret(` + set x to 1 + while (x > 0) do + change x to x - 1 + end + `) + expect(frames).toBeArrayOfSize(4) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ x: 1 }) + expect(frames[1].status).toBe('SUCCESS') + expect(frames[1].variables).toMatchObject({ x: 1 }) + expect(frames[2].status).toBe('SUCCESS') + expect(frames[2].variables).toMatchObject({ x: 0 }) + expect(frames[3].status).toBe('SUCCESS') + expect(frames[3].variables).toMatchObject({ x: 0 }) + }) + + test('multiple times', () => { + const { frames } = interpret(` + set x to 3 + while x > 0 do + change x to x - 1 + end + `) + expect(frames).toBeArrayOfSize(8) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].variables).toMatchObject({ x: 3 }) + expect(frames[1].status).toBe('SUCCESS') + expect(frames[1].variables).toMatchObject({ x: 3 }) + expect(frames[2].status).toBe('SUCCESS') + expect(frames[2].variables).toMatchObject({ x: 2 }) + expect(frames[3].status).toBe('SUCCESS') + expect(frames[3].variables).toMatchObject({ x: 2 }) + expect(frames[4].status).toBe('SUCCESS') + expect(frames[4].variables).toMatchObject({ x: 1 }) + expect(frames[5].status).toBe('SUCCESS') + expect(frames[5].variables).toMatchObject({ x: 1 }) + expect(frames[6].status).toBe('SUCCESS') + expect(frames[6].variables).toMatchObject({ x: 0 }) + expect(frames[7].status).toBe('SUCCESS') + expect(frames[7].variables).toMatchObject({ x: 0 }) + }) +}) diff --git a/test/javascript/interpreter/expression_descriptions.test.ts b/test/javascript/interpreter/expression_descriptions.test.ts new file mode 100644 index 0000000000..afee3f8755 --- /dev/null +++ b/test/javascript/interpreter/expression_descriptions.test.ts @@ -0,0 +1,44 @@ +import { interpret } from '@/interpreter/interpreter' +import type { ExecutionContext } from '@/interpreter/executor' +import { LiteralExpression, VariableExpression } from '@/interpreter/expression' +import { Location } from '@/interpreter/location' +import { Span } from '@/interpreter/location' +import { type Token, TokenType } from '@/interpreter/token' + +const location = new Location(0, new Span(0, 0), new Span(0, 0)) + +describe('LiteralExpression', () => { + describe('description', () => { + test('number', () => { + const expr = new LiteralExpression(1, location) + const actual = expr.description() + expect(actual).toBe('1') + }) + test('boolean', () => { + const expr = new LiteralExpression(true, location) + const actual = expr.description() + expect(actual).toBe('true') + }) + test('string', () => { + const expr = new LiteralExpression('hello', location) + const actual = expr.description() + expect(actual).toBe('"hello"') + }) + }) +}) + +describe('VariableExpression', () => { + describe('description', () => { + test('number', () => { + const token: Token = { + lexeme: 'name', + type: 'NUMBER', + literal: 'name', + location: location, + } + const expr = new VariableExpression(token, location) + const actual = expr.description() + expect(actual).toBe('the name variable') + }) + }) +}) diff --git a/test/javascript/interpreter/interpreter.test.ts b/test/javascript/interpreter/interpreter.test.ts index baba1cf83f..b0d8d5b9b4 100644 --- a/test/javascript/interpreter/interpreter.test.ts +++ b/test/javascript/interpreter/interpreter.test.ts @@ -139,7 +139,7 @@ describe('statements', () => { describe("truthiness doesn't exit", () => { test('and', () => { - const { frames } = interpret('set x to true and []') + const { frames } = interpret('set x to true and "asd"') expect(frames).toBeArrayOfSize(1) expect(frames[0].status).toBe('ERROR') }) @@ -160,213 +160,6 @@ describe('statements', () => { expect(frames[0].variables).toMatchObject({ x: 'sweet' }) }) }) - - describe('template literals', () => { - test('text only', () => { - const { frames } = interpret('set x to `hello`') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 'hello' }) - }) - - test('placeholder only', () => { - const { frames } = interpret('set x to `${3*4}`') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: '12' }) - }) - - test('string', () => { - const { frames } = interpret('set x to `hello ${"there"}`') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 'hello there' }) - }) - - test('variable', () => { - const { frames } = interpret(` - set x to 1 - set y to \`x is \${x}\` - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 1 }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 1, y: 'x is 1' }) - }) - - test('expression', () => { - const { frames } = interpret('set x to `2 + 3 = ${2 + 3}`') - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: '2 + 3 = 5' }) - }) - - test('complex', () => { - const { frames } = interpret( - 'set x to `${2} + ${"three"} = ${2 + 9 / 3}`' - ) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: '2 + three = 5' }) - }) - }) - }) - - describe('get', () => { - describe('dictionary', () => { - test('single field', () => { - const { frames } = interpret(` - set movie to {"title": "The Matrix"} - set title to movie["title"] - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ - movie: { title: 'The Matrix' }, - }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ - movie: { title: 'The Matrix' }, - title: 'The Matrix', - }) - }) - - test('chained', () => { - const { frames } = interpret(` - set movie to {"director": {"name": "Peter Jackson"}} - set name to movie["director"]["name"] - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ - movie: { director: { name: 'Peter Jackson' } }, - }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ - movie: { director: { name: 'Peter Jackson' } }, - name: 'Peter Jackson', - }) - }) - - describe('array', () => { - test('single index', () => { - const { frames } = interpret(` - set scores to [7, 3, 10] - set latest to scores[2] - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ - scores: [7, 3, 10], - }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ - scores: [7, 3, 10], - latest: 10, - }) - }) - - test('chained', () => { - const { frames } = interpret(` - set scoreMinMax to [[3, 7], [1, 6]] - set secondMin to scoreMinMax[1][0] - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ - scoreMinMax: [ - [3, 7], - [1, 6], - ], - }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ - scoreMinMax: [ - [3, 7], - [1, 6], - ], - secondMin: 1, - }) - }) - }) - }) - }) - - describe('set', () => { - describe('dictionary', () => { - test('single field', () => { - const { frames } = interpret(` - set movie to {"title": "The Matrix"} - set movie["title"] to "Gladiator" - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ - movie: { title: 'The Matrix' }, - }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ - movie: { title: 'Gladiator' }, - }) - }) - - test('chained', () => { - const { frames } = interpret(` - set movie to {"director": {"name": "Peter Jackson"}} - set movie["director"]["name"] to "James Cameron" - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ - movie: { director: { name: 'Peter Jackson' } }, - }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ - movie: { director: { name: 'James Cameron' } }, - }) - }) - }) - - describe('array', () => { - test('single index', () => { - const { frames } = interpret(` - set scores to [7, 3, 10] - set scores[2] to 5 - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ - scores: [7, 3, 10], - }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ - scores: [7, 3, 5], - }) - }) - - test('chained', () => { - const { frames } = interpret(` - set scoreMinMax to [[3, 7], [1, 6]] - set scoreMinMax[1][0] to 4 - `) - expect(frames).toBeArrayOfSize(2) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ - scoreMinMax: [ - [3, 7], - [1, 6], - ], - }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ - scoreMinMax: [ - [3, 7], - [4, 6], - ], - }) - }) - }) }) describe('assignment', () => { @@ -666,118 +459,6 @@ describe('statements', () => { }) }) - describe('while', () => { - test('once', () => { - const { frames } = interpret(` - set x to 1 - while (x > 0) do - change x to x - 1 - end - `) - expect(frames).toBeArrayOfSize(4) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 1 }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 1 }) - expect(frames[2].status).toBe('SUCCESS') - expect(frames[2].variables).toMatchObject({ x: 0 }) - expect(frames[3].status).toBe('SUCCESS') - expect(frames[3].variables).toMatchObject({ x: 0 }) - }) - - test('multiple times', () => { - const { frames } = interpret(` - set x to 3 - while x > 0 do - change x to x - 1 - end - `) - expect(frames).toBeArrayOfSize(8) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toMatchObject({ x: 3 }) - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ x: 3 }) - expect(frames[2].status).toBe('SUCCESS') - expect(frames[2].variables).toMatchObject({ x: 2 }) - expect(frames[3].status).toBe('SUCCESS') - expect(frames[3].variables).toMatchObject({ x: 2 }) - expect(frames[4].status).toBe('SUCCESS') - expect(frames[4].variables).toMatchObject({ x: 1 }) - expect(frames[5].status).toBe('SUCCESS') - expect(frames[5].variables).toMatchObject({ x: 1 }) - expect(frames[6].status).toBe('SUCCESS') - expect(frames[6].variables).toMatchObject({ x: 0 }) - expect(frames[7].status).toBe('SUCCESS') - expect(frames[7].variables).toMatchObject({ x: 0 }) - }) - }) - - describe('foreach', () => { - test('empty iterable', () => { - const echos: string[] = [] - const context = { - externalFunctions: [ - { - name: 'echo', - func: (_: any, n: any) => { - echos.push(n.toString()) - }, - description: '', - }, - ], - } - - const { frames } = interpret( - ` - foreach num in [] do - echo(num) - end - `, - context - ) - expect(frames).toBeArrayOfSize(1) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toBeEmpty() - expect(echos).toBeEmpty() - }) - - test('multiple times', () => { - const echos: string[] = [] - const context = { - externalFunctions: [ - { - name: 'echo', - func: (_: any, n: any) => { - echos.push(n.toString()) - }, - description: '', - }, - ], - } - - const { frames } = interpret( - ` - foreach num in [1, 2, 3] do - echo(num) - end - `, - context - ) - expect(frames).toBeArrayOfSize(6) - expect(frames[0].status).toBe('SUCCESS') - expect(frames[0].variables).toBeEmpty() - expect(frames[1].status).toBe('SUCCESS') - expect(frames[1].variables).toMatchObject({ num: 1 }) - expect(frames[2].status).toBe('SUCCESS') - expect(frames[3].status).toBe('SUCCESS') - expect(frames[3].variables).toMatchObject({ num: 2 }) - expect(frames[4].status).toBe('SUCCESS') - expect(frames[5].status).toBe('SUCCESS') - expect(frames[5].variables).toMatchObject({ num: 3 }) - expect(echos).toEqual(['1', '2', '3']) - }) - }) - describe('block', () => { test('non-nested', () => { const { frames } = interpret(` @@ -842,6 +523,16 @@ describe('frames', () => { expect(frames[0].error).toBeNil() expect(frames[0].variables).toBeEmpty() }) + + test('variable', () => { + const { frames } = interpret('set x to 1') + expect(frames).toBeArrayOfSize(1) + expect(frames[0].line).toBe(1) + expect(frames[0].status).toBe('SUCCESS') + expect(frames[0].code).toBe('set x to 1') + expect(frames[0].error).toBeNil() + expect(frames[0].variables).toMatchObject({ x: 1 }) + }) }) describe('multiple statements', () => { @@ -1019,7 +710,8 @@ describe('evaluateFunction', () => { expect(frames).toBeArrayOfSize(1) }) - test('with complex arguments', () => { + // TODO: Add when dictionaries and arrays are back + test.skip('with complex arguments', () => { const { value, frames } = evaluateFunction( ` function move with car, speeds do diff --git a/test/javascript/interpreter/parser.test.ts b/test/javascript/interpreter/parser.test.ts index 87954d52a2..dc20c00fbe 100644 --- a/test/javascript/interpreter/parser.test.ts +++ b/test/javascript/interpreter/parser.test.ts @@ -22,7 +22,7 @@ import { IfStatement, RepeatStatement, ReturnStatement, - VariableStatement, + SetVariableStatement, WhileStatement, } from '@/interpreter/statement' import { parse } from '@/interpreter/parser' @@ -39,8 +39,8 @@ describe('comments', () => { test('comment after statement', () => { const stmts = parse('set a to 5 // this (is) a. comme,nt do') expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - const varStmt = stmts[0] as VariableStatement + expect(stmts[0]).toBeInstanceOf(SetVariableStatement) + const varStmt = stmts[0] as SetVariableStatement expect(varStmt.name.lexeme).toBe('a') expect(varStmt.initializer).toBeInstanceOf(LiteralExpression) expect((varStmt.initializer as LiteralExpression).value).toBe(5) @@ -221,8 +221,8 @@ describe('dictionary', () => { test('empty', () => { const stmts = parse('set empty to {}') expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - const varStmt = stmts[0] as VariableStatement + expect(stmts[0]).toBeInstanceOf(SetVariableStatement) + const varStmt = stmts[0] as SetVariableStatement expect(varStmt.initializer).toBeInstanceOf(DictionaryExpression) const mapExpr = varStmt.initializer as DictionaryExpression expect(mapExpr.elements).toBeEmpty() @@ -231,8 +231,8 @@ describe('dictionary', () => { test('single element', () => { const stmts = parse('set movie to {"title": "Jurassic Park"}') expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - const varStmt = stmts[0] as VariableStatement + expect(stmts[0]).toBeInstanceOf(SetVariableStatement) + const varStmt = stmts[0] as SetVariableStatement expect(varStmt.initializer).toBeInstanceOf(DictionaryExpression) const mapExpr = varStmt.initializer as DictionaryExpression expect(mapExpr.elements.size).toBe(1) @@ -245,8 +245,8 @@ describe('dictionary', () => { test('multiple elements', () => { const stmts = parse('set movie to {"title": "Jurassic Park", "year": 1993}') expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - const varStmt = stmts[0] as VariableStatement + expect(stmts[0]).toBeInstanceOf(SetVariableStatement) + const varStmt = stmts[0] as SetVariableStatement expect(varStmt.initializer).toBeInstanceOf(DictionaryExpression) const mapExpr = varStmt.initializer as DictionaryExpression expect(mapExpr.elements.size).toBe(2) @@ -263,8 +263,8 @@ describe('dictionary', () => { 'set movie to {"title": "Jurassic Park", "director": { "name": "Steven Spielberg" } }' ) expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - const varStmt = stmts[0] as VariableStatement + expect(stmts[0]).toBeInstanceOf(SetVariableStatement) + const varStmt = stmts[0] as SetVariableStatement expect(varStmt.initializer).toBeInstanceOf(DictionaryExpression) const mapExpr = varStmt.initializer as DictionaryExpression expect(mapExpr.elements.size).toBe(2) @@ -290,8 +290,8 @@ describe('variable', () => { test('single-character name', () => { const statements = parse('set x to 1') expect(statements).toBeArrayOfSize(1) - expect(statements[0]).toBeInstanceOf(VariableStatement) - const varStatement = statements[0] as VariableStatement + expect(statements[0]).toBeInstanceOf(SetVariableStatement) + const varStatement = statements[0] as SetVariableStatement expect(varStatement.name.lexeme).toBe('x') const literalExpr = varStatement.initializer as LiteralExpression expect(literalExpr.value).toBe(1) @@ -300,8 +300,8 @@ describe('variable', () => { test('multi-character name', () => { const statements = parse('set fooBar to "abc"') expect(statements).toBeArrayOfSize(1) - expect(statements[0]).toBeInstanceOf(VariableStatement) - const varStatement = statements[0] as VariableStatement + expect(statements[0]).toBeInstanceOf(SetVariableStatement) + const varStatement = statements[0] as SetVariableStatement expect(varStatement.name.lexeme).toBe('fooBar') const literalExpr = varStatement.initializer as LiteralExpression expect(literalExpr.value).toBe('abc') @@ -315,8 +315,8 @@ describe('assignment', () => { set x to 2 `) expect(statements).toBeArrayOfSize(2) - expect(statements[1]).toBeInstanceOf(VariableStatement) - const varStatement = statements[1] as VariableStatement + expect(statements[1]).toBeInstanceOf(SetVariableStatement) + const varStatement = statements[1] as SetVariableStatement expect(varStatement.name.lexeme).toBe('x') const literalExpr = varStatement.initializer as LiteralExpression expect(literalExpr.value).toBe(2) @@ -369,9 +369,9 @@ describe('get', () => { set title to movie["title"] `) expect(stmts).toBeArrayOfSize(2) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - expect(stmts[1]).toBeInstanceOf(VariableStatement) - const varStmtWithGet = stmts[1] as VariableStatement + expect(stmts[0]).toBeInstanceOf(SetVariableStatement) + expect(stmts[1]).toBeInstanceOf(SetVariableStatement) + const varStmtWithGet = stmts[1] as SetVariableStatement expect(varStmtWithGet.initializer).toBeInstanceOf(GetExpression) const getExpr = varStmtWithGet.initializer as GetExpression expect(getExpr.field.literal).toBe('title') @@ -385,9 +385,9 @@ describe('get', () => { set director to movie["director"]["name"] `) expect(stmts).toBeArrayOfSize(2) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - expect(stmts[1]).toBeInstanceOf(VariableStatement) - const varStmtWithGet = stmts[1] as VariableStatement + expect(stmts[0]).toBeInstanceOf(SetVariableStatement) + expect(stmts[1]).toBeInstanceOf(SetVariableStatement) + const varStmtWithGet = stmts[1] as SetVariableStatement expect(varStmtWithGet.initializer).toBeInstanceOf(GetExpression) const getExpr = varStmtWithGet.initializer as GetExpression expect(getExpr.field.literal).toBe('name') @@ -408,9 +408,9 @@ describe('get', () => { set latest to scores[2] `) expect(stmts).toBeArrayOfSize(2) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - expect(stmts[1]).toBeInstanceOf(VariableStatement) - const varStmtWithGet = stmts[1] as VariableStatement + expect(stmts[0]).toBeInstanceOf(SetVariableStatement) + expect(stmts[1]).toBeInstanceOf(SetVariableStatement) + const varStmtWithGet = stmts[1] as SetVariableStatement expect(varStmtWithGet.initializer).toBeInstanceOf(GetExpression) const getExpr = varStmtWithGet.initializer as GetExpression expect(getExpr.field.literal).toBe(2) @@ -424,9 +424,9 @@ describe('get', () => { set secondMin to scoreMinMax[1][0] `) expect(stmts).toBeArrayOfSize(2) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - expect(stmts[1]).toBeInstanceOf(VariableStatement) - const varStmtWithGet = stmts[1] as VariableStatement + expect(stmts[0]).toBeInstanceOf(SetVariableStatement) + expect(stmts[1]).toBeInstanceOf(SetVariableStatement) + const varStmtWithGet = stmts[1] as SetVariableStatement expect(varStmtWithGet.initializer).toBeInstanceOf(GetExpression) const getExpr = varStmtWithGet.initializer as GetExpression expect(getExpr.field.literal).toBe(0) @@ -441,102 +441,6 @@ describe('get', () => { }) }) -describe('set', () => { - describe('dictionary', () => { - test('single field', () => { - const stmts = parse(` - set movie to {"title": "The Matrix"} - set movie["title"] to "Gladiator" - `) - expect(stmts).toBeArrayOfSize(2) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - expect(stmts[1]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[1] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(SetExpression) - const setExpr = exprStmt.expression as SetExpression - expect(setExpr.field.literal).toBe('title') - expect(setExpr.obj).toBeInstanceOf(VariableExpression) - expect((setExpr.obj as VariableExpression).name.lexeme).toBe('movie') - }) - - test('chained', () => { - const stmts = parse(` - set movie to {"director": {"name": "Peter Jackson"}} - set movie["director"]["name"] to "James Cameron" - `) - expect(stmts).toBeArrayOfSize(2) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - expect(stmts[1]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[1] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(SetExpression) - const setExpr = exprStmt.expression as SetExpression - expect(setExpr.value).toBeInstanceOf(LiteralExpression) - expect((setExpr.value as LiteralExpression).value).toBe('James Cameron') - expect(setExpr.field.literal).toBe('name') - expect(setExpr.obj).toBeInstanceOf(GetExpression) - const getExpr = setExpr.obj as GetExpression - expect(getExpr.obj).toBeInstanceOf(VariableExpression) - expect((getExpr.obj as VariableExpression).name.lexeme).toBe('movie') - expect(getExpr.field.literal).toBe('director') - }) - }) - - // TODO: add set on arrays -}) - -describe('template literal', () => { - test('empty', () => { - const stmts = parse('``') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(TemplateLiteralExpression) - const templateExpr = exprStmt.expression as TemplateLiteralExpression - expect(templateExpr.parts).toBeEmpty() - }) - - test('no placeholders', () => { - const stmts = parse('`hello there`') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(TemplateLiteralExpression) - const templateExpr = exprStmt.expression as TemplateLiteralExpression - expect(templateExpr.parts).toBeArrayOfSize(1) - expect(templateExpr.parts[0]).toBeInstanceOf(TemplateTextExpression) - const textExpr = templateExpr.parts[0] as TemplateTextExpression - expect(textExpr.text.literal).toBe('hello there') - }) - - test('placeholders', () => { - const stmts = parse('`sum of ${2} + ${1+3*5} = ${2+(1+4*5)}`') - expect(stmts).toBeArrayOfSize(1) - expect(stmts[0]).toBeInstanceOf(ExpressionStatement) - const exprStmt = stmts[0] as ExpressionStatement - expect(exprStmt.expression).toBeInstanceOf(TemplateLiteralExpression) - const templateExpr = exprStmt.expression as TemplateLiteralExpression - expect(templateExpr.parts).toBeArrayOfSize(6) - expect(templateExpr.parts[0]).toBeInstanceOf(TemplateTextExpression) - expect(templateExpr.parts[1]).toBeInstanceOf(TemplatePlaceholderExpression) - expect(templateExpr.parts[2]).toBeInstanceOf(TemplateTextExpression) - expect(templateExpr.parts[3]).toBeInstanceOf(TemplatePlaceholderExpression) - expect(templateExpr.parts[4]).toBeInstanceOf(TemplateTextExpression) - expect(templateExpr.parts[5]).toBeInstanceOf(TemplatePlaceholderExpression) - const part0Expr = templateExpr.parts[0] as TemplateTextExpression - expect(part0Expr.text.lexeme).toBe('sum of ') - const part1Expr = templateExpr.parts[1] as TemplatePlaceholderExpression - expect(part1Expr.inner).toBeInstanceOf(LiteralExpression) - const part2Expr = templateExpr.parts[2] as TemplateTextExpression - expect(part2Expr.text.lexeme).toBe(' + ') - const part3Expr = templateExpr.parts[3] as TemplatePlaceholderExpression - expect(part3Expr.inner).toBeInstanceOf(BinaryExpression) - const part4Expr = templateExpr.parts[4] as TemplateTextExpression - expect(part4Expr.text.lexeme).toBe(' = ') - const part5Expr = templateExpr.parts[5] as TemplatePlaceholderExpression - expect(part5Expr.inner).toBeInstanceOf(BinaryExpression) - }) -}) - describe('grouping', () => { test('non-nested', () => { const stmts = parse('(1 + 2)') @@ -747,7 +651,7 @@ describe('if', () => { expect(expStmt.thenBranch).toBeInstanceOf(BlockStatement) const thenStmt = expStmt.thenBranch as BlockStatement expect(thenStmt.statements).toBeArrayOfSize(1) - expect(thenStmt.statements[0]).toBeInstanceOf(VariableStatement) + expect(thenStmt.statements[0]).toBeInstanceOf(SetVariableStatement) expect(expStmt.elseBranch).toBeNil() }) @@ -766,11 +670,11 @@ describe('if', () => { expect(expStmt.thenBranch).toBeInstanceOf(BlockStatement) const thenStmt = expStmt.thenBranch as BlockStatement expect(thenStmt.statements).toBeArrayOfSize(1) - expect(thenStmt.statements[0]).toBeInstanceOf(VariableStatement) + expect(thenStmt.statements[0]).toBeInstanceOf(SetVariableStatement) expect(expStmt.elseBranch).toBeInstanceOf(BlockStatement) const elseStmt = expStmt.elseBranch as BlockStatement expect(elseStmt.statements).toBeArrayOfSize(1) - expect(elseStmt.statements[0]).toBeInstanceOf(VariableStatement) + expect(elseStmt.statements[0]).toBeInstanceOf(SetVariableStatement) }) test('nested', () => { @@ -790,18 +694,22 @@ describe('if', () => { expect(expStmt.thenBranch).toBeInstanceOf(BlockStatement) const thenStmt = expStmt.thenBranch as BlockStatement expect(thenStmt.statements).toBeArrayOfSize(1) - expect(thenStmt.statements[0]).toBeInstanceOf(VariableStatement) + expect(thenStmt.statements[0]).toBeInstanceOf(SetVariableStatement) expect(expStmt.elseBranch).toBeInstanceOf(IfStatement) const elseIfStmt = expStmt.elseBranch as IfStatement expect(elseIfStmt.condition).toBeInstanceOf(BinaryExpression) expect(elseIfStmt.thenBranch).toBeInstanceOf(BlockStatement) const elseIfStmtThenBlock = elseIfStmt.thenBranch as BlockStatement expect(elseIfStmtThenBlock.statements).toBeArrayOfSize(1) - expect(elseIfStmtThenBlock.statements[0]).toBeInstanceOf(VariableStatement) + expect(elseIfStmtThenBlock.statements[0]).toBeInstanceOf( + SetVariableStatement + ) expect(elseIfStmt.elseBranch).toBeInstanceOf(BlockStatement) const elseIfStmtElseBlock = elseIfStmt.elseBranch as BlockStatement expect(elseIfStmtElseBlock.statements).toBeArrayOfSize(1) - expect(elseIfStmtElseBlock.statements[0]).toBeInstanceOf(VariableStatement) + expect(elseIfStmtElseBlock.statements[0]).toBeInstanceOf( + SetVariableStatement + ) }) }) @@ -817,7 +725,7 @@ describe('repeat', () => { const expStmt = stmts[0] as RepeatStatement expect(expStmt.count).toBeInstanceOf(LiteralExpression) expect(expStmt.body).toBeArrayOfSize(1) - expect(expStmt.body[0]).toBeInstanceOf(VariableStatement) + expect(expStmt.body[0]).toBeInstanceOf(SetVariableStatement) }) }) @@ -833,7 +741,7 @@ describe('while', () => { const expStmt = stmts[0] as WhileStatement expect(expStmt.condition).toBeInstanceOf(BinaryExpression) expect(expStmt.body).toBeArrayOfSize(1) - expect(expStmt.body[0]).toBeInstanceOf(VariableStatement) + expect(expStmt.body[0]).toBeInstanceOf(SetVariableStatement) }) }) @@ -851,7 +759,7 @@ describe('foreach', () => { expect(foreachStmt.elementName.lexeme).toBe('elem') expect(foreachStmt.iterable).toBeInstanceOf(ArrayExpression) expect(foreachStmt.body).toBeArrayOfSize(1) - expect(foreachStmt.body[0]).toBeInstanceOf(VariableStatement) + expect(foreachStmt.body[0]).toBeInstanceOf(SetVariableStatement) }) test('with multiple statements in body', () => { @@ -867,8 +775,8 @@ describe('foreach', () => { expect(foreachStmt.elementName.lexeme).toBe('elem') expect(foreachStmt.iterable).toBeInstanceOf(ArrayExpression) expect(foreachStmt.body).toBeArrayOfSize(2) - expect(foreachStmt.body[0]).toBeInstanceOf(VariableStatement) - expect(foreachStmt.body[1]).toBeInstanceOf(VariableStatement) + expect(foreachStmt.body[0]).toBeInstanceOf(SetVariableStatement) + expect(foreachStmt.body[1]).toBeInstanceOf(SetVariableStatement) }) }) @@ -884,8 +792,8 @@ describe('block', () => { expect(stmts[0]).toBeInstanceOf(BlockStatement) const blockStmt = stmts[0] as BlockStatement expect(blockStmt.statements).toBeArrayOfSize(2) - expect(blockStmt.statements[0]).toBeInstanceOf(VariableStatement) - expect(blockStmt.statements[1]).toBeInstanceOf(VariableStatement) + expect(blockStmt.statements[0]).toBeInstanceOf(SetVariableStatement) + expect(blockStmt.statements[1]).toBeInstanceOf(SetVariableStatement) }) test('nested', () => { @@ -901,7 +809,7 @@ describe('block', () => { expect(stmts[0]).toBeInstanceOf(BlockStatement) const blockStmt = stmts[0] as BlockStatement expect(blockStmt.statements).toBeArrayOfSize(2) - expect(blockStmt.statements[0]).toBeInstanceOf(VariableStatement) + expect(blockStmt.statements[0]).toBeInstanceOf(SetVariableStatement) expect(blockStmt.statements[1]).toBeInstanceOf(BlockStatement) }) }) @@ -985,9 +893,9 @@ describe('white space', () => { `) expect(stmts).toBeArrayOfSize(3) - expect(stmts[0]).toBeInstanceOf(VariableStatement) - expect(stmts[1]).toBeInstanceOf(VariableStatement) - expect(stmts[2]).toBeInstanceOf(VariableStatement) + expect(stmts[0]).toBeInstanceOf(SetVariableStatement) + expect(stmts[1]).toBeInstanceOf(SetVariableStatement) + expect(stmts[2]).toBeInstanceOf(SetVariableStatement) }) }) @@ -1008,8 +916,8 @@ describe('location', () => { test('variable', () => { const statements = parse('set x to 1') expect(statements).toBeArrayOfSize(1) - expect(statements[0]).toBeInstanceOf(VariableStatement) - const expressionStatement = statements[0] as VariableStatement + expect(statements[0]).toBeInstanceOf(SetVariableStatement) + const expressionStatement = statements[0] as SetVariableStatement expect(expressionStatement.location.line).toBe(1) expect(expressionStatement.location.relative.begin).toBe(1) expect(expressionStatement.location.relative.end).toBe(11) diff --git a/test/javascript/interpreter/scanner.test.ts b/test/javascript/interpreter/scanner.test.ts index 0756cab87b..18a449ce31 100644 --- a/test/javascript/interpreter/scanner.test.ts +++ b/test/javascript/interpreter/scanner.test.ts @@ -31,8 +31,8 @@ describe('one, two or three characters', () => { ['>=', 'GREATER_EQUAL'], ['<', 'LESS'], ['<=', 'LESS_EQUAL'], - ['!=', 'STRICT_INEQUALITY'], - ['==', 'STRICT_EQUALITY'], + ['!=', 'INEQUALITY'], + ['==', 'EQUALITY'], ])("'%s' token", (source: string, expectedType: string) => { const tokens = scan(source) expect(tokens[0].type).toBe(expectedType as TokenType) @@ -63,8 +63,8 @@ describe('keyword', () => { ['true', 'TRUE'], ['while', 'WHILE'], ['with', 'WITH'], - ['is', 'STRICT_EQUALITY'], - ['equals', 'STRICT_EQUALITY'], + ['is', 'EQUALITY'], + ['equals', 'EQUALITY'], ])("'%s' keyword", (source: string, expectedType: string) => { const tokens = scan(source) expect(tokens[0].type).toBe(expectedType as TokenType) diff --git a/test/javascript/interpreter/statement_descriptions.test.ts b/test/javascript/interpreter/statement_descriptions.test.ts new file mode 100644 index 0000000000..2220913069 --- /dev/null +++ b/test/javascript/interpreter/statement_descriptions.test.ts @@ -0,0 +1,37 @@ +import { interpret } from '@/interpreter/interpreter' +import type { ExecutionContext } from '@/interpreter/executor' +import { LiteralExpression, VariableExpression } from '@/interpreter/expression' +import { Location } from '@/interpreter/location' +import { Span } from '@/interpreter/location' +import { type Token, TokenType } from '@/interpreter/token' +import { SetVariableStatement } from '@/interpreter/statement' +import { describeFrame } from '@/interpreter/frames' + +const location = new Location(0, new Span(0, 0), new Span(0, 0)) + +describe('SetVariableStatement', () => { + describe('description', () => { + test('standard', () => { + const { frames } = interpret('set my_name to "Jeremy"') + const actual = describeFrame(frames[0], []) + expect(actual).toBe( + '

This created a new variable called my_name and sets its value to "Jeremy".

' + ) + }) + }) +}) + +describe('ChangeVariableStatement', () => { + describe('description', () => { + test('standard', () => { + const { frames } = interpret(` + set my_name to "Aron" + change my_name to "Jeremy" + `) + const actual = describeFrame(frames[1], []) + expect(actual).toBe( + '

This updated the variable called my_name from...

"Aron"

to...

"Jeremy"
' + ) + }) + }) +})